美文网首页
Unity Shader 入门到改行3——简单光照模型

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

作者: 太刀 | 来源:发表于2020-12-19 00:33 被阅读0次
    Pink Floyd 月之阴暗面

    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 自发光

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

    相关文章

      网友评论

          本文标题:Unity Shader 入门到改行3——简单光照模型

          本文链接:https://www.haomeiwen.com/subject/rfipgktx.html