Unity Shader 入门到改行3——简单光照模型

Pink Floyd 月之阴暗面

0.本文示例代码地址

GitHub

1. 标准光照模型

标准光照模型只关心直接光照,也就是那些直接从光源发射出来,照射到物体表面后,一次反射进入摄像机的光线。光照模型考虑4个部分的光线

  • 自发光(emissive)
    物体本身会往给定方向发射的光线,物体的自发光并不会对周围其它的物体产生照射,具有自发光的物体并不是一个光源
  • 漫反射(diffuse)
    光线从光源照射到物体时,物体向不同方向散射的光线。漫反射与观察点位置和角度无关
  • 高光反射(specular)
    光源从光源照射到物体时,物体在完全镜面反射方向发射的光线。
  • 环境光(ambient)
    对环境中所有间接光照效果的简单模拟。

2. 逐顶点计算还是逐像素计算

2.1 逐顶点计算 per-vertext lighting

上述的四个部分计算放在顶点着色器中,针对每个顶点计算光照后的颜色,然后在图元所覆盖的像素进行线性插值,得到每个像素的颜色

2.2 逐像素计算 per-pixel lighting

上述四个部分计算放在像素着色器中,每个像素的位置、法线等信息由定点进行线性插值得到。

2.3 各自的优缺点

  • 计算量:通常顶点数量远远小于像素数量,所以顶点光照的计算量比像素光照的计算量少
  • 效果:当然是像素光照的效果更好,并且逐顶点光照不能处理有非线性计算的光照模型,因为非线性光照模型通过线性插值获得的结果不正确
    这篇文章后面所涉及到的都是逐像素计算 shader,逐顶点的原理一样,只是计算放到了顶点着色函数中。

3. 漫反射部分的计算

3.1 兰伯特公式

在计算漫反射,只要物体和光源的位置和角度固定了,那么无论观察点的位置和角度怎么变化,物体表面某一点的漫反射颜色固定不变,也就是漫反射的结果与观察点无关。漫反射光照符合兰伯特定律(Lambert Law),在物体表面的任何一点,反射光线的强度与该点的表面法线和光源方向夹角的余弦值成正比。兰伯特公示如下:

兰伯特计算公式

其中,n是表面法线,I是指向光源的单位向量。Clight是光源颜色, Mdiffuse 是表面本身的颜色.

3.1.1 法线变换

我们对顶点进行坐标变换时,如果在后续的处理阶段中需要用到顶点法线信息,那么需要对顶点的法线也一并做变换,使用针对顶点的变换矩阵对法线进行变换时,有一种情况下得到的结果不正确:

当变换包括一个非等比缩放时,用同样的变换矩阵对法线变换,变换后的法线和平面不垂直。

解决方法(如何推导的先不用管):使用顶点变换矩阵的逆转置矩阵 来对法线进行变换。在兰伯特公式中,我们需要确保光源方向和法线方向在同一个坐标系才有意义。我们计算时采用世界坐标系,而顶点着色器中的顶点数据是模型坐标系的,因此我们需要将法线变换成世界坐标系。如何将顶点的法线(appdata传递过来的 NORMAL 是模型坐标系)转换到世界坐标系呢?
首先我们看看 Unity shader 中如何将顶点从模型坐标转换到世界坐标:使顶点右乘 一个内置的变换矩阵 unity_ObjectToWorld:

o.pos = mul(unity_ObjectToWorld, v.vertex);

那么如何进行法线变换呢?Unity 内置的 unity_WorldToObject 是 unity_ObjectToWorld 的逆矩阵,而左乘则相当于使用转置矩阵进行了右乘,所以法线变换的过程如下:

float4 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
3.1.2 兰伯特公式的顶点着色实现

我们新建一个 Lambert.shader,在片元着色器中计算漫反射(也可以在顶点着色器中计算,将对应代码移动到顶点着色器中就可以了),代码如下:

Shader "Shader_Examples/03_Lambert"
{
    Properties
    {       
        _MainColor ("MainColor", Color) = (1, 1, 1, 1)
    }
    SubShader
    {           
        Tags { "LightMode"="ForwardBase" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
                        
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;                         
            };

            fixed4 _MainColor;          
            
            v2f vert (appdata v)
            {
                v2f o;              
                o.pos = UnityObjectToClipPos(v.vertex);     // 顶点变换到裁剪空间                
                o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject)); // 法线变换到世界空间
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {       
                fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);                // 光源方向归一化
                fixed diffuse = saturate(dot(i.worldNormal, worldLight));                   // 计算漫反射强度
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * diffuse;
                return fixed4(color, 1.0);              
            }
            ENDCG
        }
    }
}


SubShader 一定要指定Tag : "LightMode" = "ForwardBase",否则使用内置变量 _WorldSpaceLightPos0 将不正确。

渲染的结果:


兰伯特渲染结果

3.2 半兰伯特公式

上面的 Lambert.shader计算光照时,法线和光源方向点乘时有可能得到负数,兰伯特光照模型使用 saturate 来确保最终得到的数为正数

saturate(x) = max(0, x);

将不会受到光照射的点(该点法线和光线夹角余弦为负数)显示为黑色。虽然是符合理解,但实际并不好看,例如上图中胶囊体和球体最左边的黑色部分。为了让画面更好看一些,提出了一种区间转化的取巧方法,将(-1,1)转化到(0,1),这个方法提升了画面整体亮度。相对于上面的 03_Lambert shader,半兰伯特光照模型只是在计算漫反射部分有细微的区别:

fixed diffuse = dot(i.worldNormal, worldLight) * 0.5 + 0.5;             // 计算漫反射强度

可以看一下兰伯特(上排)和半兰伯特(下排)的渲染结果对比


兰伯特和半兰伯特渲染结果

4. 高光部分的计算

4.1 Phong光照模型

高光反射用于计算那些完全镜面反射方向的光线,可以是物体看起来更接近镜面的光滑。计算高光反射,需要知道法线n、光源方向l、视角方向v和反射方向r。

高光反射

从不同的观察点看同一个点,高光结果肯定是不同的,所以高光与观察点也有关系,高光强度的计算公式为:


高光计算公式

如何计算反射方向r?把n、l、v都看成是单位向量,从 l 的终点作一条平行于r的向量,必然与n相交于一点,可以轻易的得到 r 的计算公式:


计算反射方向

在编写 shader 时,可以使用 Cg 提供的内置函数 reflect

 fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

注:公式中的I是光源方向,上述代码中的 reflect 的第一个参数是入射光方向,二者互为负向量。
gloss 是一个调节表面高光反射“集中度”的一个参数,shader写好之后可以调整这个值看直观效果。
到此,在上面的 Half_Lambert shader 的基础上,我们可以加入高光的计算。最终shader代码:

Shader "Shader_Examples/03_Phong"
{
    Properties
    {
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _SpecularColor ("SpecularColor", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag                       

            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {                           
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            float4 _MainColor;
            float4 _SpecularColor;
            float _Gloss;       
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));    // 法线变换
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;                        // 顶点世界坐标
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = i.worldNormal;
                fixed3 lightInDir = -normalize(_WorldSpaceLightPos0.xyz);               // 入射光线方向
                fixed3 reflectDir = normalize(reflect(lightInDir, worldNormal));        // 反射光线方向
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);  // 视线反方向(点到摄像机)         

                // 计算高光
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

                // 半兰伯特计算漫反射
                fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * (dot(-lightInDir, worldNormal) * 0.5 + 0.5);

                fixed4 col = fixed4(specular + diffuse, 1.0);
                return fixed4(col);
            }
            ENDCG
        }
    }
}

注1. 在进行法线变换时,因为法线是3维向量,需要将unity_WorldToObject转维3维矩阵

o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));  

注2. 一定要注意计算反射光线的函数 reflect 的第一个输入参数是入射方向而不是光线方向
渲染结果如下:

渲染结果

从上到下3排依次为:兰伯特漫反射、半拉伯特漫反射、半兰伯特+Phong高光,为了能比较明显的看到高光部分,我给灯光添加了一个颜色,高光颜色设置为白色。

4.2 Blinn-Phong

Blinn 在 Phong 光照模型上引入了“半程向量”的概念来进行模拟,不再需要计算反射方向,而使用光线方向l和视线方向v的“中间向量”来模拟。高光部分通过半程向量 h 和 表面法线 n 来确定。半程向量 h 通过对 l 和 v 平均后归一化得到。


半程向量计算

引入 半程向量后,Blinn-Phong 的光照计算公式为:


Blinn-Phong光照模型

将上述的 03_Phong 光照Shader中高光部分计算替换为如下的代码即可:
           fixed4 frag (v2f i) : SV_Target
           {
               fixed3 worldNormal = i.worldNormal;
               fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);              // 入射光线方向
               fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);  // 视线反方向(点到摄像机) 
               fixed3 halfDir = normalize(lightDir + viewDir); 

               // 计算高光
               fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

               // 半兰伯特计算漫反射
               fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * (dot(lightDir, worldNormal) * 0.5 + 0.5);

               fixed4 col = fixed4(specular + diffuse, 1.0);
               return fixed4(col);
           }

最终将得到如下的渲染结果:


渲染结果,依次为半兰伯特、兰伯特、Phong、Blinn-Phong

5. 环境光

环境光比较简单,直接在每个像素上面添加一个固定的颜色就可以了。

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed4 col = fixed4(specular + diffuse + ambient, 1.0);

在上面的 Blinn-Phong 代码中添加相应的环境光,并在 Unity 中设置一个偏黄的环境光,最后得到的渲染效果如下:


增加一个偏黄的环境光

如何在 Unity 中设置环境光颜色?
Unity 菜单 -> Window -> Lighting -> Settings,得到如下对话框,进行修改即可。


环境光设置

6 自发光

物体自发光也比较简单,直接给材质增加一个应颜色属性,在最终计算颜色时将这个颜色属性加上即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容