Unity Shader入门精要——第6章 Unity中的基础光照

Unity Shader入门精要读书笔记系列

第1章 欢迎来到Shader的世界
第2章 渲染流水线
第3章 Unity Shader基础
第4章 学习Shader所需的数学基础
第5章 开始 Unity Shader 学习之旅
第6章 Unity中的基础光照



前言

上一章我们学习了了Unity中的Shader基本语法,和Unity内置的一些结构体、函数和宏。
接下来我们继续学习Unity中基础光照模型。

一、什么是光照模型

辐照度:用于量化光,对于平行光 l。辐照度指的是垂直于 l的单位面积内单位时间穿过的光的能量。在计算光照模型时,我们需要知道一个物体表面的辐照度。
不难想象,光的入射方向反方向越垂直于物体表面(或者说与表面法向量夹角越小)辐照度越大。

漫反射:光照在粗糙的表面上,由于各点的法线方向不一致,反射光向不同的方向无规则的反射。
高光反射:物体表面都有相对光滑的镜面,面积越小越光滑,反射光线相对越集中,高光就越强。

根据材质属性(如漫反射属性)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度。我们把这个等式称为光照模型(Lighting Model),这个过程叫做着色 (shading) 。

1.BRDF光照模型

BRDF (Bidirectional Reflectance Distribution Function) 双向反射分布函数。
对于某一点,当给出入射方向和辐照度后。可以通过BRDF计算出反射光在不同方向上的反射率。
对于人眼来说,即在不同方向看到这个点的颜色不同。

BDRF光照模型分为经验模型和基于物理的分析模型,本章主要是学习经验模型。
经验模型并不能真实地反映物体和光线之间的交互,只是看起来正确。

2.标准光照模型

color = 自发光 (emissive) + 环境光(ambient ) + 漫反射(diffuse ) + 高光反射(specular)

二、Unity中实现光照模型

1.漫反射

兰伯特(Lambert)模型

在这里插入图片描述

  • 逐顶点光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Unity Shaders Book/Chapter 6/Diffuse VertexLevel"
{
    
    
    Properties
    {
    
    
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100 //Level of detail 当LOD的值小于设定值时,相应的shader不会工作

        Pass
        {
    
    
            // 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量
            Tags{
    
    "LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            // 材质的漫反射属性
            fixed4 _Diffuse;

            struct a2v
            {
    
    
                // POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
                float4 vertex : POSITION;
                // NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
                float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                // SV_POSITION语义告诉Unity, pos里包含了顶点在裁剪空间中的位置信息
                // 顶点着色器的输出结构中,必须包含 SV_POSITION
                float4 pos : SV_POSITION;
                // COLORO语义可以用于存储颜色信息
                float3 color : COLOR0;
            };

            // 使用顶点着色器计算漫反射光照模型
            v2f vert (a2v v)
            {
    
    
                // 声明输出变量
                v2f o;
                // 将顶点位置从模型空间转换到裁剪空间(使用MVP矩阵右乘v.vertex)
                o.pos = UnityObjectToClipPos(v.vertex);

                // 使用内置变量 UNITY_LIGHTMODEL_AMBIENT 获取环境光部分
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                
                // 将法线从模型空间转换到世界空间,使用normalize函数归一化
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

                // 使用内置变量 _WorldSpaceLightPos0 获取光源方向
                fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

                // 兰伯特漫反射光照公式,使用内置变量 _LightColor0 获取光源颜色和强度信息
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

                o.color = ambient + diffuse;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
            	// 片元着色器直接输出顶点颜色
                return fixed4(i.color, 1.0);
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}

  • 逐像素光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Unity Shaders Book/Chapter 6/Diffuse PixelLevel"
{
    
    
    Properties
    {
    
    
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100 //Level of detail 当LOD的值小于设定值时,相应的shader不会工作

        Pass
        {
    
    
            // 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量
            Tags {
    
    "LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            // 材质的漫反射颜色
            fixed4 _Diffuse;

            struct a2v
            {
    
    
                // POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
                float4 vertex : POSITION;
                // NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
                float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                // SV_POSITION语义告诉Unity, pos里包含了顶点在裁剪空间中的位置信息
                // 顶点着色器的输出结构中,必须包含 SV_POSITION
                float4 pos : SV_POSITION;
                // 世界空间下的法线,自定义数据一般用 TEXCOORD0 语义
                float3 worldNormal : TEXCOORD0;
            };


            v2f vert (a2v v)
            {
    
    
                v2f o;

                // 将顶点位置从模型空间转换到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);
                // 将模型空间下的法线转换到世界空间下
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

                return o;
            }

            // 使用片元着色器计算漫反射光照模型
            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                
                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                fixed3 color = ambient + diffuse;

                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }

    FallBack "Diffuse"
}

半兰伯特(Half Lambert)模型

在兰伯特模型中,漫反射颜色在[-1,1]。通过saturate函数将负数全部改为0,即背光区域全部为黑色,效果不好。于是半兰伯特技术被提出。
在这里插入图片描述
大多数情况下α,β的值为0.5,他将漫反射颜色[-1,1]映射到[0,1]区间内。
这样即使是背光区域,也有了明暗变化,不会呈现完全死黑的现象。

// 在片元着色其中修改漫反射的计算公式
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLightDir) * 0.5 + 0.5);

效果对比

从左到右依次为逐顶点、逐像素、半兰伯特光照。
在这里插入图片描述

逐顶点光照(per-vertex lighting)
逐顶点光照也被称为高洛德着色(Gouraud shading)。在每个顶点上进行光照计算,然后在渲染图元内部进行线性插值,最后输出成像素颜色。
由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。
但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射时)时,逐顶点光照就会出问题。
并且图元内部的颜色总是暗于顶点处的最高颜色值,某些情况下会产生明显的棱角现象。

逐像素光照(per-pixel lighting)
逐像素光照中以每个像素为基础,得到它的法线(对顶点法线插值得到或者从法线纹理中采样得到),然后进行光照计算。
这种着色技术称为Phong着色(Phong shading),或者Phong插值或法线插值着色技术。(不同于Phong光照模型)。
一般来说逐像素着色比逐顶点着色更加丝滑,同时计算量更大。

2.高光反射

Phone模型

在这里插入图片描述
max函数是为了防止视角方向v与反射方向r点乘出现负数。

  • 逐顶点光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unity Shaders Book/Chapter 6/SpecularVertexLevel"
{
    
    
    Properties
    {
    
    
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) 

        // 用于控制材质的高光反射颜色
        _Specular ("Specular", Color) = (1, 1, 1, 1) 

        // 用于控制高光区域的大小
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            // 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量,例如 _LightColorO
            Tags{
    
    "LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
    
    
                // POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
                float4 vertex : POSITION;
                // NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
                float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                float4 pos : SV_POSITION;
                // COLORO语义可以用于存储颜色信息
                float3 color : COLOR;
            };

            // 使用顶点着色器计算高光反射光照模型
            v2f vert (a2v v)
            {
    
    
                v2f o;
                // 将顶点位置从模型空间转换到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);
                // 环境光部分
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // 将法线从模型空间转换到世界空间
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
                // 光源方向
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 漫反射部分 _LightColor0 光源颜色和强度信息
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
                // 使用 reflect 计算反射方向
                fixed3 reflectDir = normalize(reflect(- worldLightDir, worldNormal));
                // 视角方向,世界空间下的摄像机坐标减去顶点坐标
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
                // 高光反射计算公式
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
                o.color = ambient + diffuse + specular;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                return fixed4(i.color, 1.0);
            }

            ENDCG
        }
    }
}

  • 逐像素光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unity Shaders Book/Chapter 6/SpecularPixelLevel"
{
    
    
    Properties
    {
    
    
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) 

        // 用于控制材质的高光反射颜色
        _Specular ("Specular", Color) = (1, 1, 1, 1) 

        // 用于控制高光区域的大小
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            // 只有定义了正确的 LightMode 才能得到一些 Unity 的内置光照变量,例如 _LightColorO
            Tags{
    
    "LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            // _Gloss范围较大,使用float存储
            float _Gloss;

            struct a2v
            {
    
    
                // POSITION语义告诉Unity, 用模型空间的顶点坐标填充vertex变量
                float4 vertex : POSITION;
                // NORMAL语义告诉Unity, 用模型空间的法线方向填充normal变量
                float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                // 顶点着色器必须输出裁剪空间下的pos
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            // 顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器即可
            v2f vert (a2v v)
            {
    
    
                v2f o;
                // 将顶点位置从模型空间转换到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);
                // 将法线从模型空间转换到世界空间
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            // 使用片元着色器计算高光反射光照模型
            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                // 高光反射公式
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
}

Blinn-Phong模型

在这里插入图片描述

修改片元着色器中高光反射的计算

fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

可以想象,看到当视角与光反射方向越贴近时,视角与光方向的半角h与法线n夹角越小,高光就越亮。

效果对比

从左到右分别是逐顶点高光、逐像素高光、BlinnPhong高光。
在这里插入图片描述
由于逐顶点中使用了线性插值,可以看到效果不是很好。

三、使用Unity内置的函数

从上面的代码中可以看出,手动计算光源信息比较麻烦。Unity提供了一些内置函数来帮助我们计算这些信息。

使用内置函数改写Blinn-Phong

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unity Shaders Book/Chapter 6/BlinnPhongUseBuildInFunction"
{
    
    
    Properties
    {
    
    
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) 
        _Specular ("Specular", Color) = (1, 1, 1, 1) 
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            Tags{
    
    "LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            float4 _Diffuse;
            float4 _Specular;
            float _Gloss;

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

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


            v2f vert (a2v v)
            {
    
    
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal); // 使用内置函数
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 worldNormal = normalize(i.worldNormal);

                //fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);   //_WorldSpaceLightPos0只能用于平行光
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 使用内置函数,注意归一化
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                //fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // 使用内置函数

                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41044598/article/details/126366620