【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 3071|回复: 6

[Shader着色器] 《Unity Shaders and Effects Cookbook》第一节 概述及Diffuse Shading介绍

[复制链接]
发表于 2015-3-30 22:21:28 | 显示全部楼层 |阅读模式
本帖最后由 夜行的猫仔 于 2015-4-9 01:00 编辑

本系列主要参考《Unity Shaders and Effects Cookbook》一书(感谢原书作者),同时会加上一点个人理解或拓展。


这里是本书所有的插图。这里是本书所需的代码和资源(当然你也可以从官网下载)。

========================================== 分割线 ==========================================



概述

几个月以前我就一直在找关于Unity Shader相关的资料,大三的时候有一门叫《计算机图形学》,让我觉得如果游戏程序员要从beginner成长为advancer,就必须了解底层渲染的一些知识。

Unity3D无疑是一个非常出色的游戏引擎,它让很多独立开发者真正能够独立开发一款让自己满意的游戏,而不需要考虑那些烦人的细节,正如Unity宣称的那样,让他们来做那些令人抓狂的事情吧!但是,在我们制作游戏的时候,经常会让游戏带有明显的“made in Unity”的感觉:明显的锯齿,糟糕的游戏性能,不真实的场景等等。当然这不是Unity引擎的错误,我相信它提供了很多强大的优化支持,支持很多自学者没有接触和学习这些高级技巧,就制作出了那样画面粗糙的游戏。

像育碧、盛大这种大型游戏公司,还是非常看重程序员对计算机图形学方面的知识,因为他们面向的对象是广大PC机,对画面和性能都有非常高的要求。而对于手机游戏,虽然现在手机硬件对渲染的支持还不是非常广泛,但是我们有理由相信,在这个发展迅速的时代,手机硬件的支持已经不再是梦想,手机游戏对画质的要求也一定越来越高。掌握shaders的原理和技术,我想是一个想要成长为更高level的游戏程序员不可或缺的部分。

哈,就在前两天,我发现了一本书,《Unity Shaders and Effects Cookbook》,这是我目前发现的第一本系统讲述Unity Shaders的书籍,我当时高兴坏了,毕竟之前都只是查看手册和一些论坛里的留言,非常零散。看了评论也是非常不错,因此决定把这本书当成以后学习Unity Shader的资料,并在博客里记录下学习的体会。

这里是本书所有的插图,先看看过个瘾吧!

这里是本书所需的代码和资源。

在这篇文章里,先对Diffuse Shading开个头。


Diffuse Shading 介绍
可以说Diffuse Shading是所有高级Shader的基础。几乎每个讲计算机图形渲染的书的第一章都是这个。在过去的计算机图形硬件中,这是通过固定渲染流水线来完成的。学过OpenGL等类似语言的人都会知道,以前是通过一些固定接口设置渲染属性来调节渲染细节的。而现在,那种固定编程的模型基本已经弃用,我们可以通过Cg更灵活地控制渲染细节。
Diffuse,即漫反射,可以想象成太阳光照射到某一个粗糙平面上时,该平面向四面八方都会反射光线,这些光线中的某些光线进入我们的眼睛,从而使我们看到了这个平面。一个基本的漫反射光照模型,需要包括以下几个部分:一个反射颜色(emissive color,可以理解成平面本身的颜色), 一个环境光颜色(ambient color,可以理解成光源的颜色), 以及计算来自所有光源的光线和。
  • 创建一个基本的Surface Shader
  • 向Surface Shader添加properties
  • 在Surface Shader中使用properties
  • 创建一个自定义的diffuse lighting model(漫反射光照模型)
  • 创建一个Half Lambert lighting model(半兰伯特光照模型)
  • 创建一个ramp texture来控制diffuse shading
  • 使用2D ramp texture来创建一个假的BRDF(双向反射分布函数)

下面的章节中,首先讲解完整的漫反射光照模型,然后会讲述使用2D texture来实现更复杂的漫反射模型的技巧。


下面给出了本章中主要实现的渲染效果:

  • 最基本的漫反射光照模型(上述前四个小节):
  • 半兰伯特光照模型(第5节):
  • 使用一维渐变图控制光照模型(第6节):
  • 使用二维渐变图控制光照模型(第7节):



结束语
呼呼,希望自己能坚持下来,也希望对大家有所帮助。

 楼主| 发表于 2015-3-30 22:26:48 | 显示全部楼层

这一次的工作非常简单,我们主要是为了了解Unity Shaders的基本工作原理。



准备工作
  • 创建一个新的Unity Project
  • 在Assets目录下新建一个文件夹:Shaders
  • 在Assets目录下新建一个文件夹:Materials


实现
  • 在Shaders文件夹下,点击右键新建一个Shader,命名为BasicDiffuse,并打开它;
  • 在Materials文件夹下,创建一个Material,并使用我们之前创建的BasicDiffuse.shader;
  • 创建一个Sphere,并把之前创建的Material拖到它上面,查看效果;
  • 你还可以给Material添加一个texture。



解释打开BasicDiffuse.shader后,我们可以发现Unity实际上已经为我们写好了很多代码:
[mw_shl_code=applescript,true]Shader "Custom/BasicDiffuse" {  
    Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
         
        CGPROGRAM  
        #pragma surface surf Lambert  
  
        sampler2D _MainTex;  
  
        struct Input {  
            float2 uv_MainTex;  
        };  
  
        void surf (Input IN, inout SurfaceOutput o) {  
            half4 c = tex2D (_MainTex, IN.uv_MainTex);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
        ENDCG  
    }   
    FallBack "Diffuse"  
}  [/mw_shl_code]

  • 第一行表明了这个shader在Unity中的路径,它会出现在你选择某个Material的shader时的下拉列表里,并且可以随时更改;
  • 最后一行表明,当这个shader在当前环境中运行失败后,会默认调用Unity自带的Diffuse Shader;
  • 其他内容在下面的章节会讲到。
Unity内部使用的是Cg来实现的,它把这些实现细节封装了起来,提供我们一种基于构件的编写shader的方法,像调整图片的uv坐标、矩阵转换等工作它都帮你做好了。而以前,我们需要重复编写一些基本功能的代码,来从头创建一个shader。当你的经验逐渐丰富起来,自然而然就会想要了解Unity是怎样处理图像处理单元(GPU)的工作的。如果你想了解Unity是怎样调用Cg的,可以到Unity的安装目录Unity4\Editor\Data\CGIncludes  下查看。在靠后面的一些章节中,会讲到。


 楼主| 发表于 2015-3-30 22:30:48 | 显示全部楼层

这次是在上一篇的基础上,进一步学习Unity Shaders的Properties。


准备工作
我们假设你已经进行了上一篇里的工作:创建了一个BasicDiffuse.shader,并且看到了Unity自动为你创建的代码:
[mw_shl_code=applescript,true]Shader "Custom/BasicDiffuse" {  
    Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
         
        CGPROGRAM  
        #pragma surface surf Lambert  
  
        sampler2D _MainTex;  
  
        struct Input {  
            float2 uv_MainTex;  
        };  
  
        void surf (Input IN, inout SurfaceOutput o) {  
            half4 c = tex2D (_MainTex, IN.uv_MainTex);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
        ENDCG  
    }   
    FallBack "Diffuse"  
}  [/mw_shl_code]
实现
Unity Shaders的Properties指的是上述代码中第2至4行的内容。你可以看到它们被一个被标有Properties大括号所包含。这里面的每一项代表了将会出现在Unity Inspector中的GUI,例如一张可供拖放的2D Texture、一个Color等等。这些值将可以使用户可以方便地调整参数来实现不同的渲染效果。又一次,Unity帮我们做好了GUI的创建工作,我们只需要在这里添加相应的一行,就可以在Inspector界面里快速创建一个可供调整的GUI。

现在,Properties只包含了一项内容,即_MainTex。下面我们进行一些更改:
  • 将_MainTex这一行删除,在其位置添加新的代码:_EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
  • 保存后,进入Unity查看效果。Unity将自动编译该shader,随后就可以在Material的Inspector页面上看到新的GUI。
  • 继续添加两行代码,完整代码如下所示:
[mw_shl_code=applescript,true]Properties {  
    _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
    _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
    _MySliderValue ("This is a Slider", Range(0, 10)) = 2.5  
}  [/mw_shl_code]
  • 保存后,进入Unity查看编译结果。
  • 不出意外的话,你应该会看到下面的画面:
  • 现在,你可以尝试在Unity的Inspector里面调整你刚才添加的这些Properties了!



解释

通过上面的尝试,如果你仔细观察过上述代码的特征,应该可以自己总结出来这是怎么工作的。

如上图所示,Properties中的每一项共包含四个部分(缺一不可):
  • 首先,你需要定义变量名称,这些名称将会在shader后面的编写中方便你引用它们,这很像我们编写C#、C++等代码时定义的变量名;
  • 接下来,是GUI名称,这些将会出现在Unity的Inspector界面里,也就是用户会看到的名字,正如上一部分图中所示那样。这些名字和编写shader没有直接关系,只是为了方便用户理解;
  • 再下面是类型。例如,Unity自动生成的代码中的_MainTex类型为2D,表明这是一个2D贴图,我们之前定义的_EmissiveColor类型为Color。Unity支持的类型可见下表:
  • 最后,是默认值。这是不可省略的。
细心的你可能发现,在Unity自动创建的代码中,_MainTex还包含了一个花括号:

_MainTex ("Base (RGB)", 2D) = "white" {}

从Unity的官方文档里(见下),你可以了解到它的作用,在后面的章节中当然也会讲到。简单来讲,这些花括号仅针对类型为2D、Rect和Cube的变量,如果你在其他类型变量后面添加了这些括号会出现编译错误。
这部分的官方文档,请见这里





 楼主| 发表于 2015-3-30 22:35:44 | 显示全部楼层
我们已经向Surface Shader中添加了一些properties。在这篇教程里,我们将学习如何在Shader中访问和使用它们,以便通过调整Inspector中的变量来改变渲染效果。
准备工作
  • 在上一篇结束后,我们的shader代码如下:
  • [mw_shl_code=applescript,true]Shader "Custom/BasicDiffuse" {  
        Properties {  
            _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
            _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
            _MySliderValue ("This is a Slider", Range(0,10)) = 2.5  
        }  
        SubShader {  
            Tags { "RenderType"="Opaque" }  
            LOD 200  
             
            CGPROGRAM  
            #pragma surface surf Lambert  
      
            sampler2D _MainTex;  
      
            struct Input {  
                float2 uv_MainTex;  
            };  
      
            void surf (Input IN, inout SurfaceOutput o) {  
                half4 c = tex2D (_MainTex, IN.uv_MainTex);  
                o.Albedo = c.rgb;  
                o.Alpha = c.a;  
            }  
            ENDCG  
        }   
        FallBack "Diffuse"  
    }  [/mw_shl_code]

    2.因为之前我们移除了_MainTex属性,所以首先移除和它相关的代码。即上面代码的sampler2D _MainTex一行以及void surf函数中的half4 c这一行。


实现
  • 在原来sampler2D _MainTex的地方,添加如下代码:

[mw_shl_code=applescript,true]float4 _EmissiveColor;  
float4 _AmbientColor;  
float _MySliderValue  [/mw_shl_code]
2.下面,我们使用_EmissiveColor和_AmbientColor来计算surf函数中新的c的值:
[mw_shl_code=applescript,true]void surf (Input IN, inout SurfaceOutput o)  
{  
  float4 c;  
  c =  pow((_EmissiveColor + _AmbientColor),  _MySliderValue);  
  o.Albedo = c.rgb;  
  o.Alpha = c.a;  
}  [/mw_shl_code]
最后,我们的代码应该是这样的:
[mw_shl_code=applescript,true]Shader "Custom/BasicDiffuse" {  
    Properties {  
        _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
        _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
        _MySliderValue ("This is a Slider", Range(0,10)) = 2.5  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
        CGPROGRAM  
        #pragma surface surf Lambert  
         
        //We need to declare the properties variable type inside of the  
        //CGPROGRAM so we can access its value from the properties block.  
        float4 _EmissiveColor;  
        float4 _AmbientColor;  
        float _MySliderValue;  
         
        struct Input  
        {  
            float2 uv_MainTex;  
        };  
         
        void surf (Input IN, inout SurfaceOutput o)  
        {  
            //We can then use the properties values in our shader  
            float4 c;  
            c =  pow((_EmissiveColor + _AmbientColor), _MySliderValue);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
         
        ENDCG  
    }   
    FallBack "Diffuse"  
}[/mw_shl_code]

下图显示了使用本文中的shader,并适当调整参数后的结果:


其参数如下:



画面左边是我们自定义的shader,右边则是使用Unity默认的Diffuse Shader。


解释
  • 当我们在Properties块中声明一个新的变量时,就提供了在Inspector界面中改变它的值的方式。
  • 如果我们想要在SubShader中访问它,还需要在CGPROGRAM内部声明一个和Properties中名字相同的变量,这将自动建立一个连接,两者将操作同一个数据。
  • 除了声明一个相同名字的变量,你还需要声明它的类型,如上面的float4、float等,这和Properties中的属性是不同的。在后面我们将看到如何使用这些属性来优化代码。
  • pow()函数是一个内置函数。你可以访问Cg的网站,来查看更详细的信息,同时也可以学到更多关于Cg Shading Language的内容。







 楼主| 发表于 2015-4-9 00:50:28 | 显示全部楼层
我们学了怎样在surface shader(这里即指surf函数)中使用自己定义的Properties变量。而在之前的学习中,我们实际上使用的都是Unity内置的Diffuse Lighting Model,即漫反射光照模型。这一次,我们将学习如何让Unity使用我们自己定义的光照模型进行渲染。
准备工作
  • 使用上一篇结束时的shader代码即可。
[mw_shl_code=csharp,true]Shader "Custom/BasicDiffuse" {  
    Properties {  
        _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
        _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
        _MySliderValue ("This is a Slider", Range(0,10)) = 2.5  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
        CGPROGRAM  
        #pragma surface surf Lambert  
         
        //We need to declare the properties variable type inside of the  
        //CGPROGRAM so we can access its value from the properties block.  
        float4 _EmissiveColor;  
        float4 _AmbientColor;  
        float _MySliderValue;  
         
        struct Input  
        {  
            float2 uv_MainTex;  
        };  
         
        void surf (Input IN, inout SurfaceOutput o)  
        {  
            //We can then use the properties values in our shader  
            float4 c;  
            c =  pow((_EmissiveColor + _AmbientColor), _MySliderValue);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
         
        ENDCG  
    }   
    FallBack "Diffuse"  
}  [/mw_shl_code]

实现
  • 将上述代码的第11行,即#pragma surface surf Lambert一行,改为如下代码:


[mw_shl_code=csharp,true]#pragma surface surf BasicDiffuse [/mw_shl_code]

2. 向SubShader中添加如下函数(位置需在#pragma下面):
[mw_shl_code=csharp,true]inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)  
{  
    float difLight = max(0, dot (s.Normal, lightDir));  
    float4 col;  
    col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);  
    col.a = s.Alpha;  
    return col;  [/mw_shl_code]
3. 保存,进入Unity查看编译结果。
Unity编译成功后,你会发现Material并没有什么可视化变化。因为上面仅仅是将Unity自带的Surface Lighting Model——Lambert,换成了我们自定义的光照模型——BasicDiffuse。


解释
  • "#pragma surface"将直接告诉Shader使用哪个光照模型用于计算。当我们最初创建了一个Shader时,Untiy为我们指定了一个默认的光照模型即Lambert(在Lighting.cginc中定义)。因此我们一开始可以使用这个默认的模型进行渲染。而现在,我们告诉Shader,嘿,使用一个名叫BasicDiffuse的光照模型给我渲染哦!
  • 为了创建一个新的光照模型,我们需要声明一个新的光照模型函数。例如上面,我们声明了BasicDiffuse,并且定义了一个函数名叫LightingBasicDiffuse,如你所见,这两者之间的关系即为Lighting<自定义的光照模型名称>。下面有三种可供选择的光照模型函数:


[mw_shl_code=csharp,true]half4 LightingName (SurfaceOutput s, half3 lightDir, half atten){}  [/mw_shl_code]

这个函数被用于forward rendering(正向渲染),但是不需要考虑view direction(观察角度)时。
[mw_shl_code=csharp,true]half4 LightingName (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){}  [/mw_shl_code]

这个函数被用于forward rendering(正向渲染),并且需要考虑view direction(观察角度)时。
[mw_shl_code=csharp,true]half4 LightingName_PrePass (SurfaceOutput s, half4 light){}  [/mw_shl_code]
    这个函数被用于需要使用defferred rendering(延迟渲染)时。
    3. 观察我们定义的光照模型函数。dot函数是一个Cg的内置数学函数,可以被用于比较空间中两个向量的方向。若两个参数向量均为单位向量(一般如此),-1表示两向量平行但方向相反,1表示两向量平行且方向相同,0表示两向量垂直。
    4. 为了完成光照模型函数,我们还使用了一个Unity提供的数据——类型为SurfaceOutput的变量s。我们将s.Albedo(从surf函数中输出)和_LightColor0.rgb(Unity提供)相乘,结果再乘以(difLight * atten * 2),最后作为颜色值进行输出。
到这里,可能大家还会对LightingBasicDiffuse的代码不理解。下面再谈一下我的理解。

  • 首先是参数。s是上一个surf函数的输出。


[mw_shl_code=csharp,true]void surf (Input IN, inout SurfaceOutput o)  
{  
    //We can then use the properties values in our shader  
    float4 c;  
    c =  pow((_EmissiveColor + _AmbientColor), _MySliderValue);  
    o.Albedo = c.rgb;  
    o.Alpha = c.a;  
}  [/mw_shl_code]
2 . 由上可以看出,经过surf函数,计算输出了s的Albedo(反射率)和Alpha(透明度)。
3.   LightBasicDiffuse函数输出的将是一个surface上某一点的颜色值和透明度值。因此lightDir对应该点的光照方向。而atten表明了光线的衰减率。
4.   光照模型函数中的第一行代码通过dot函数和max函数,计算到达该点的光照值(由于dot函数的两个参数都是单位向量,也可以理解成是入射光线的角度的余弦值,角度越大,余弦值越小,进入人眼的光线也就越少,物体看起来也就越暗)。由于光线有可能从相反射入,因此通过dot得到的值有可能是负数。如果不用max加以限制,之后将会得到非预期的效果,如全黑等。
    接下来计算颜色值col。col的rgb由三个部分计算而得:第一个部分是surface本身的反射率,这很好理解,因为反射率越大,进入人眼的光线就越多,颜色也就越鲜亮;第二个是_LightColor0.rgb。_LightColor0是Unity内置变量,我们可以使用它得到场景中光源的颜色等;最后便是利用第一步中得到的光照值和衰减率的乘积。细心的童鞋可以发现,这里后面多乘了一个倍数2。按我的猜测,这里仅仅是根据需要自行修改的。例如,没有倍数2时,效果如下:

  • 乘以倍数2后效果如下:

结束语

更多关于Surface Shader光照模型函数参数的信息,可以参见Unity官方文档









 楼主| 发表于 2015-4-9 01:02:56 | 显示全部楼层

我们演示了如何使用自定义的光照模型进行渲染。这一次,我们将进一步看一下怎样对它做一些变化来得到更好的效果!

我们会列出两种方法:使用Half Lambert lighting model(半兰伯特光照模型)和使用一个ramp texture来控制diffuse shading。


准备工作
同样,我们需要你已经做好了上一篇文章中的内容,并得到了如下shader:
[mw_shl_code=csharp,true]Shader "Custom/BasicDiffuse" {  
    Properties {  
        _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
        _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
        _MySliderValue ("This is a Slider", Range(0,10)) = 2.5  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
        CGPROGRAM  
        #pragma surface surf BasicDiffuse  
         
        //We need to declare the properties variable type inside of the  
        //CGPROGRAM so we can access its value from the properties block.  
        float4 _EmissiveColor;  
        float4 _AmbientColor;  
        float _MySliderValue;  
         
        struct Input  
        {  
            float2 uv_MainTex;  
        };  
         
        void surf (Input IN, inout SurfaceOutput o)  
        {  
            //We can then use the properties values in our shader  
            float4 c;  
            c =  pow((_EmissiveColor + _AmbientColor), _MySliderValue);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
         
        inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)  
        {  
            float difLight = max(0, dot (s.Normal, lightDir));  
            float4 col;  
            col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);  
            col.a = s.Alpha;  
            return col;  
        }  
         
        ENDCG  
    }   
    FallBack "Diffuse"  
}  [/mw_shl_code]

创建一个Half Lambert lighting model(半兰伯特光照模型)
如果你看过之前的文章中,相信还记得Lambert这个名字。没错,它就是Unity默认的diffuse lighting model。简单来说,Lambert定律认为,在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比(即我们之前使用dot函数得到的结果)。Half Lambert最初是由Valve(游戏半条命2使用的引擎即是其开发的)提出来,用于提高物体在一些光线无法照射到的区域的亮度的。简单说来,它提高了漫反射光照的亮度,使得漫反射光线可以看起来照射到一个物体的各个表面。而Half Lambert最初也是被用于游戏半条命的画面渲染,为了防止某个物体的背光面丢失形状并且显得太过平面化。这个技术是完全没有基于任何物理原理的,而仅仅是一种感性的视觉增强(参考这里)。
好啦,说了这么多还是要演示一下,代码非常简单!我们只需要稍微更改上述的LightingBasicDiffuse函数:

[mw_shl_code=csharp,true]   inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)  
        {  
            float difLight = max(0, dot (s.Normal, lightDir));  
            // Add this line  
            float hLambert = difLight * 0.5 + 0.5;  
              
            float4 col;  
            // Modify this line  
            col.rgb = s.Albedo * _LightColor0.rgb * (hLambert * atten * 2);  
            col.a = s.Alpha;  
            return col;  
}  [/mw_shl_code]

由代码可以看出,我们定义了一个新的变量hLambert来替换difLight用于计算某点的颜色值。difLight的范围是0.0-1.0,而通过hLambert,我们将结果由0.0-1.0映射到了0.5-1.0,从而达到了增加亮度的目的。下图显示了这一变化:


我们可以通过对比来看一下Lambert和Half Lambert的渲染区别(分别对应左图和右图):


创建一个ramp texture来控制diffuse shading
下面介绍另一种简单实用的方法——使用一张ramp texture(渐变图)来控制漫反射光照的颜色。这允许你着重强调surface的颜色而减弱漫反射光线或者其他更高级光线的影响。 可以在很多卡通风格的游戏中看到这种技术,通常在这些情况下你需要一个更加艺术而非写实风格的画面,并且不需要很多的真实物理模拟的光照模型。
这个技术在Team Fortress 2(军团要塞2)中流行起来,这个技术也是由Valve提出来用于渲染他们的游戏角色的。他们发表了一个非常有名的论文,强烈建议你应该读一下它!这篇论文讲解了军团要塞2中使用的光照和渲染技术。
上代码!
我们重新修改LightingBasicDiffuse函数,增加一个新的变量ramp:

[mw_shl_code=csharp,true]inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)  
{  
    float difLight = max(0, dot (s.Normal, lightDir));  
    float hLambert = difLight * 0.5 + 0.5;  
    float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;  
      
    float4 col;  
    col.rgb = s.Albedo * _LightColor0.rgb * (ramp);  
    col.a = s.Alpha;  
    return col;  [/mw_shl_code]

其中,我们还需要一张texture,即_RampTex。即之前说到的渐变图。回忆一下,为了能够在Inspector中拖拽一个texture,并在shader中使用需要怎么做?首先,我们需要在Properties块中声明它,然后在SubShader中声明一个相同名字的变量,并制定它的类型,之后就可以在函数中访问它啦!忘记的请翻看之前的几篇文章。完整的代码如下:
[mw_shl_code=csharp,true]Shader "Custom/RampDiffuse" {  
    Properties {  
        _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
        _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
        _MySliderValue ("This is a Slider", Range(0,10)) = 2.5  
        // Add this line  
        _RampTex ("Ramp Texture", 2D) = "white"{}  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
         
        CGPROGRAM  
        #pragma surface surf BasicDiffuse  
         
        //We need to declare the properties variable type inside of the  
        //CGPROGRAM so we can access its value from the properties block.  
        float4 _EmissiveColor;  
        float4 _AmbientColor;  
        float _MySliderValue;  
        // Add this line  
        sampler2D _RampTex;  
         
        struct Input  
        {  
            float2 uv_MainTex;  
        };  
         
        void surf (Input IN, inout SurfaceOutput o)  
        {  
            //We can then use the properties values in our shader  
            float4 c;  
            c =  pow((_EmissiveColor + _AmbientColor), _MySliderValue);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
         
        inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)  
        {  
            float difLight = max(0, dot (s.Normal, lightDir));  
            float hLambert = difLight * 0.5 + 0.5;  
            // Add this line  
            float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;  
              
            float4 col;  
            // Modify this line  
            col.rgb = s.Albedo * _LightColor0.rgb * (ramp);  
            col.a = s.Alpha;  
            return col;  
        }  
         
        ENDCG  
    }   
    FallBack "Diffuse"  
}  
[/mw_shl_code]

使用的ramp texture(渐变图)如下:



其中最重要的代码只有一行:

[mw_shl_code=csharp,true]float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;[/mw_shl_code]

这行代码返回一个rgb值。tex2D函数接受两个参数:第一个参数是操作的texture,第二个参数是需要采样的UV坐标。这里,我们并不像使用一个vertex来代表一个UV坐标进行采样,而仅仅想使用一个漫反射浮点值(即hLambert)来映射到渐变图上的某一个颜色值。最后得到的结果便是,我们将会根据计算得到的Half Lambert光照值来决定光线照射到一个物体表面的颜色变化。

我们再来对比看一下Half Lambert和添加了ramp texture控制后的渲染区别(分别对应左图和右图):





结束语
Diffuse Shader还有最后一篇文章就会阶段性结束了。通过这一些文章,相信已经对Unity Shaders有了一个大致的了解。呼,作为一个初学者,现在的渲染结果可能看起来还狠简陋,但是一口气吃个胖子是不现实的!呼呼,加油!相信学习这些对游戏渲染还是很有帮助的,毕竟每一个出色游戏几乎全部都使用了自己编写的shader,希望自己以后也可以有所创新,可以为游戏增光添彩。



 楼主| 发表于 2015-4-9 01:07:43 | 显示全部楼层

终于到了Diffuse Shading一章的最后一篇了!回忆一下,在上一篇中,一共学习了两种改善漫反射光照的方法:一种是完全根据感性认识,使用Half Lembert方法改变了光照值区间,使得物体颜色整体提亮;一种是通过一张渐变图,来控制光照值。

这两种方法都只考虑到了入射光线和反射点所在的平面法向量的夹角,但是想象实际生活中我们观察一个物体,即便是在相同的光照下观察物体的同一点,如果我们观察位置有所改变,看到的结果也会不一样。因此,这一篇中,我们将引入这一新的参数:view direction——观察点方向。

为了达到这一目的,我们使用一张二维的渐变图来代替之前的一维渐变图(因为在之前的方法中,我们仅使用一个参数就决定了该图的采样位置),使用两个参数来决定采样的真正的UV坐标:一个参数由入射光线和平面法向量计算而得,一个由观察点方向和平面法向量计算而得。


BRDF
BRDF是bidirectional reflectance distribution function的简写。这名字很长,翻译过来就是双向反射分布函数。简单说,就是考虑光线是如何从一个入射角度(the light direction)在一个不透明平面上反射到某一个观察者的眼睛(the view direction)里的。

准备工作
  • 还是需要上一篇结束时的代码:
[mw_shl_code=csharp,true]Shader "Custom/RampDiffuse" {  
    Properties {  
        _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)  
        _AmbientColor  ("Ambient Color", Color) = (1,1,1,1)  
        _MySliderValue ("This is a Slider", Range(0,10)) = 2.5  
        _RampTex ("Ramp Texture", 2D) = "white"{}  
    }  
    SubShader {  
        Tags { "RenderType"="Opaque" }  
        LOD 200  
         
        CGPROGRAM  
        #pragma surface surf BasicDiffuse  
         
        float4 _EmissiveColor;  
        float4 _AmbientColor;  
        float _MySliderValue;  
        sampler2D _RampTex;  
         
        struct Input  
        {  
            float2 uv_MainTex;  
        };  
         
        void surf (Input IN, inout SurfaceOutput o)  
        {  
            float4 c;  
            c =  pow((_EmissiveColor + _AmbientColor), _MySliderValue);  
            o.Albedo = c.rgb;  
            o.Alpha = c.a;  
        }  
         
        inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)  
        {  
            float difLight = max(0, dot (s.Normal, lightDir));  
            float hLambert = difLight * 0.5 + 0.5;  
            float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;  
              
            float4 col;  
            col.rgb = s.Albedo * _LightColor0.rgb * (ramp);  
            col.a = s.Alpha;  
            return col;  
        }  
         
        ENDCG  
    }   
    FallBack "Diffuse"  
}  [/mw_shl_code]

  • 除此之外,我们还需要一张二维渐变图:

    大小为512*512:首先从左下角开始一个渐变色,直到图片右上角;再开始另一个渐变色,从左上角开始一直到图片中间;最后再开始一个渐变色,从右下角直到中间。


实现
  • 首先,给我们的光照函数LightingBasicDiffuse添加新的参数viewDir,表示观察方向:


[mw_shl_code=csharp,true]inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) [/mw_shl_code]
      这个参数将会由Unity内部提供,来得到当前摄像机的观察位置到观察点的方向向量。
2.  与计算入射光线和平面法向量类似,计算观察方向和平面法向量的夹角余弦值:
[mw_shl_code=csharp,true]float rimLight = max(0, dot (s.Normal, viewDir));[/mw_shl_code]

3. 使用Half Lambert方法改善rimLight的值(后面我们会对比一下如果不这么做会有什么区别):
[mw_shl_code=csharp,true]float rim_hLambert = rimLight * 0.5 + 0.5;[/mw_shl_code]

4. 最后,使用新的计算结果在_RampTex中采样:
[mw_shl_code=csharp,true]float3 ramp = tex2D(_RampTex, float2(dif_hLambert, rim_hLambert)).rgb;[/mw_shl_code]

5,。完成的代码如下
[mw_shl_code=csharp,true]inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)  
{  
    float difLight = max(0, dot (s.Normal, lightDir));  
    // Add this line  
    float rimLight = max(0, dot (s.Normal, viewDir));  
    // Modify this line  
    float dif_hLambert = difLight * 0.5 + 0.5;  
    // Add this line  
    float rim_hLambert = rimLight * 0.5 + 0.5;  
    // Modify this line  
    float3 ramp = tex2D(_RampTex, float2(dif_hLambert, rim_hLambert)).rgb;  
      
    float4 col;  
    col.rgb = s.Albedo * _LightColor0.rgb * (ramp);  
    col.a = s.Alpha;  
    return col;  [/mw_shl_code]

  • 最后得到的渲染结果如下:



解释
当使用了观察方向这个参数后,我们可以创建一个非常简单的衰减渲染结果。下图显示了观察方向和平面法向量进行dot运算后的结果:


而通过一张二维渐变图,我们可以考虑两个方向对我们观察结果的影响:




思考
在原书中,实际上在计算两个方向的dot值后,都没有对其和0值取max。而且,在得到rimLight后,也没有采用Half Lambert方法对其优化。实践是检验真理的唯一方法!我们最后就来看一下,究竟有什么区别。这次除了正面,我们还会看一下侧面观察效果有什么不同。


两个方向都不考虑max、不使用Half Lambert
代码如下:

[mw_shl_code=csharp,true]inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)  
{  
    float difLight = dot (s.Normal, lightDir);  
    float rimLight = dot (s.Normal, viewDir);  
    float3 ramp = tex2D(_RampTex, float2(difLight, rimLight)).rgb;  
      
    float4 col;  
    col.rgb = s.Albedo * _LightColor0.rgb * (ramp);  
    col.a = s.Alpha;  
    return col; [/mw_shl_code]

渲染结果如下:





上面两张图的特点有:两条明显的分割线,分割成了三块,有两块(第一张图中苹果的左边两个区域)的明暗变化是错误的。一条分割线是由入射光线引起的,在计算入射光线方向和法线的点乘时出现了负数,导致由最暗突变到了最亮。一条分割线是由观察角度引起的,道理类似。

分别对两个方向使用max
对入射光线方向的计算结果使用max操作:

[mw_shl_code=csharp,true]float difLight = max (0, dot (s.Normal, lightDir));  [/mw_shl_code]

结果如下:



结果是分界线仍然存在,但背光面的明暗变化对了。

对观察方向的结果使用max操作:

[mw_shl_code=csharp,true]float rimLight = max (0, dot (s.Normal, viewDir)); [/mw_shl_code]

结果如下:



通过和上面一种情况观察,可以发现max操作主要改善了正负交界处明暗的不正常变化的情况,防止了一些奇葩情况的出现,例如第一种情况下明暗的不正常变化。因此,如果你确保观察方向和入射方向都非常恰好的话,很有可能发现渲染结果没有变化。

分别使用Half Lambert修正
首先对入射光线的计算结果进行Half Lambert修正:

[mw_shl_code=csharp,true]      inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)  
        {  
            float difLight = max (0, dot (s.Normal, lightDir));  
            float rimLight = max (0, dot (s.Normal, viewDir));  
            float dif_hLambert = difLight * 0.5 + 0.5;  
            float3 ramp = tex2D(_RampTex, float2(dif_hLambert, rimLight)).rgb;  
              
            float4 col;  
            col.rgb = s.Albedo * _LightColor0.rgb * (ramp);  
            col.a = s.Alpha;  
            return col;  
} [/mw_shl_code]

结果如下:





这也是原书所得到的结果。可以发现,使用了Half Lambert修正后,除了整体变亮以外,由于入射光线产生的分割线也消失了,在背光面(即原来max操作后为0的区域)现在也有了合理而连续的颜色变化。
而由于观察方向计算结果还未修正,因此上面的侧向观察图中,仍旧没有合理的渐变结果。

在用Half Lambert继续对观察方向计算结果进行修正,即上文中的代码后,结果如下:






结束语
关于漫反射光照模型的学习,基本告一段落。下面一章里,主要会学习如何使用材质贴图来进行渲染!








您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-11-23 22:11

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表