• Unity Shader—04 Unity中的基础光照


    一、标准光照模型

    1.1 光的散射和吸收

    光线从光源中发射出来后,与物体表面相交,结果通常有两个:「散射」和「吸收」。
    散射只会改变光线的方向,而不改变光线的颜色和密度。吸收则相反。光线在物体表面散射后,一种会散射到物体外部,也就是「反射」;另一种会散射到物体内部,也就是「折射」。对于不透明的物体,折射入内部的光线还会继续与物体内部颗粒相交,一部分最后又会重新发射出表面。另一部分则被物体吸收。

    我们在光照模型中使用「高光反射」表示物体表面如何反射光线;使用「漫反射」表示有多少光线被折射、吸收、散射出表面。

    1.2 标准光照模型

    标准光照模型只关心直接光照,也就是从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。
    它将进入摄像机内的光线分成了四个部分,每个部分用一种方法来计算它的贡献度。它们分别是:

    • 自发光:用 c e m i s s i v e c_{emissive} cemissive表示,用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。如果没有使用全局光照技术,这些自发光的表面并不会真的照亮周围的物体,只是本身看起来更亮而已。
    • 高光反射:用 c s p e c u l a r c_{specular} cspecular表示,用于描述当光线照射到模型表面时,该表面会在「完全镜面反射」方向散射多少辐射量。
    • 漫反射:用 c d i f f u s e c_{diffuse} cdiffuse表示,用于描述当光线照射到模型表面时,该表面会向每个方向散射多少辐射量。
    • 环境光:用 c a m b i e n t c_{ambient} cambient表示,用于描述其他所有间接光照。

    环境光

    在标准光照模型中,环境光通常是一个全局变量 g a m b i e n t g_{ambient} gambient,也就是说场景中所有的物体都使用这个环境光。即
    c a m b i e n t = g a m b i e n t c_{ambient} = g_{ambient} cambient=gambient

    自发光

    标准光照模型直接使用光源材质的自发光颜色 m e m i s s i v e m_{emissive} memissive作为光源的自发光辐射量
    c e m i s s i v e = m e m i s s i v e c_{emissive} = m_{emissive} cemissive=memissive

    漫反射

    在漫反射中,视角的位置并不重要,因为反射是完全随机的,可以认为反射在任何方向上的分布都是一样的。但入射光线的角度很重要。

    根据兰伯特定律:反射光线的强度与表面法线和光源之间夹角的余弦值成正比。我们假设 n n n是表面法线, l l l是指向光源的单位矢量,则 c o s θ = n ⋅ l cosθ=n·l cosθ=nl。则根据定律可得
    c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) m a x ( 0 , n ⋅ l ) c_{diffuse}=(c_{light}·m_{diffuse})max(0,n·l) cdiffuse=(clightmdiffuse)max(0,nl)
    其中 m d i f f u s e m_{diffuse} mdiffuse是材质的漫反射颜色, c l i g h t c_{light} clight是光源颜色。之所以使用 m a x max max是为了防止法线与光源方向点乘的结果为负值,也就是光源从背面照射的情况。

    高光反射

    高光反射计算就要复杂一些,需要知道法线、视角方向、光源方向、反射方向(假设都是单位矢量)。这其中的反射方向可以通过入射方向和法线两个矢量计算得来
    由   l + r = 2 ∣ l ∣ c o s θ   n   得  r = 2 ( n ⋅ l ) n − l 由 \ \ l+r=2|l|cosθ\ n \ \ 得 \ r=2(n·l)n - l   l+r=2∣lcosθ n   r=2(nl)nl
    利用Phong模型就可以计算出高光反射部分:
    c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , r ⋅ v ) m g l o s s = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , ( 2 ( n ⋅ l ) n − l ) ⋅ v ) m g l o s s

    cspecular=(clight·mspecular)max(0,r·v)mgloss=(clight·mspecular)max(0,(2(n·l)nl)·v)mgloss" role="presentation" style="position: relative;">cspecular=(clight·mspecular)max(0,r·v)mgloss=(clight·mspecular)max(0,(2(n·l)nl)·v)mgloss
    cspecular=(clightmspecular)max(0,rv)mgloss=(clightmspecular)max(0,(2(nl)nl)v)mgloss
    其中 m g l o s s m_{gloss} mgloss代表材质的光泽度,它用于控制高光区域的亮点有多宽。 m g l o s s m_{gloss} mgloss越大,亮点越小。 m s p e c u l a r m_{specular} mspecular代表材质的高光反射颜色,用于控制该材质对于高光反射的强度和颜色。

    Blinn模型对上述模型进行了一点修改。它引入了一个 h h h向量,用来避免计算反射方向 r r r h h h是由 v v v l l l取平均后再归一化得来
    h = v + l ∣ v + l ∣ h = \frac {v+l} {|v+l|} h=v+lv+l
    然后再使用 n n n h h h之间的夹角进行计算。
    c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , n ⋅ h ) m g l o s s c_{specular}=(c_{light}·m_{specular})max(0,n·h)^{m_{gloss}} cspecular=(clightmspecular)max(0,nh)mgloss

    二、在Unity中实现漫反射

    2.1 逐顶点光照

    新建一个Unity Shader,其基本结构如下

    Shader "Unlit/VertDiffuseShader"
    {
        Properties
        {
            
        }
        SubShader
        {
            Tags { "LightMode"="ForwardBase" }
            LOD 100
    
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                ENDCG
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    首先定义一个Color类型的属性,用于控制材质的漫反射颜色

    Properties  
    {  
        _Diffuse("Diffuse",Color) = (1,1,1,1)  
    }
    
    • 1
    • 2
    • 3
    • 4

    在Pass中也定义一个与之匹配的变量

    fixed4 _Diffuse;
    
    • 1

    定义一个输出结构体

    struct v2f
    {
    	float4 vertex: SV_POSITION;
    	fixed3 color: COLOR;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    顶点着色器的输入参数,我们可以直接使用「UnityCG.cginc」库中的「appdata_base」,它的结构如下所示

    struct appdata_base {  
        float4 vertex : POSITION;  
        float3 normal : NORMAL;  
        float4 texcoord : TEXCOORD0;  
        UNITY_VERTEX_INPUT_INSTANCE_ID  
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来编写顶点着色器。根据前面的公式,我们需要获取到法线和指向光源方向的单位向量,以及材质的漫反射颜色和光源颜色。

    v2f vert(appdata_base v)
    {
    	v2f o;
    	// 将顶点坐标从物体空间转换到裁剪空间
    	o.vertex = UnityObjectToClipPos(v.vertex);
    	// 将法线从物体空间转换到世界空间
    	const fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    	// 获取归一化的光源方向
    	const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
    	// 计算漫反射
    	const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
    	// 环境光
    	const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    	o.color = diffuse + ambient;
    	return o;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在片元着色器中直接输出顶点颜色

    fixed4 frag(v2f i):SV_Target
    {
    	return fixed4(i.color,1);
    }
    
    • 1
    • 2
    • 3
    • 4

    最后挂载到一个胶囊体上,效果如下:

    2.2 逐像素光照

    逐像素光照只需要对前面的逐顶点光照进行一点修改即可。首先我们定义的输出结构体需要将原来的color改为worldNormal,也就是将法线的世界坐标传入片元着色器

    struct v2f
    {
    	float4 vertex: SV_POSITION;
    	fixed3 worldNormal: TEXCOORD0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后修改顶点着色器,只需要传入转换后的法线和顶点坐标即可

    v2f vert(appdata_base v)
    {
    	v2f o;
    	// 将顶点坐标从物体空间转换到裁剪空间
    	o.vertex = UnityObjectToClipPos(v.vertex);
    	// 将法线从物体空间转换到世界空间
    	const fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    	o.worldNormal = worldNormal;
    	return o;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    将漫反射光照模型的计算放到片元着色器中

    fixed4 frag(v2f i):SV_Target
    {
    	// 获取归一化的光源方向
    	const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
    	// 环境光
    	const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    	// 计算漫反射
    	const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal,worldLight));
    	fixed3 color = ambient + diffuse;
    	return fixed4(color,1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    效果如下

    2.3 逐顶点还是逐像素

    在前面的效果图中,逐顶点光照和逐像素光照看起来似乎并没有什么区别,但是放大后就会看出端倪。

    很明显,逐顶点光照的阴影部分有明显的棱角,而逐像素光照就显得很细腻。这是因为在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。因为顶点数目远远小于像素数目,因而逐顶点光照计算量要远远小于逐像素光照。但是由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,因而渲染图元内部的颜色总是暗于顶点处的最高颜色值,从而会出现明显的棱角现象。另外,由于是线性插值,当光照模型中有非线性计算(如高光反射)时,逐顶点光照就会出现问题。

    2.4 半兰伯特光照模型

    在前面我们使用的漫反射模型也被称为「兰伯特光照模型」,因为它符合兰伯特定律。但是不管是逐顶点还是逐像素光照,在光照无法到达的区域,模型的外观近乎全黑,没有任何明暗变化。这会使背光区域看起来像个平面一样。为了解决这一问题,人们提出了半兰伯特光照模型。

    半兰伯特光照模型实际上是对兰伯特光照模型进行了一个简单的修改:
    c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ( α ( n ⋅ l ) + β ) c_{diffuse}=(c_{light}·m_{diffuse})(α(n·l)+β) cdiffuse=(clightmdiffuse)(α(nl)+β)
    大多数情况下 α 、 β α、β αβ都会取0.5,即
    c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ( 0.5 ( n ⋅ l ) + 0.5 ) c_{diffuse}=(c_{light}·m_{diffuse})(0.5(n·l)+0.5) cdiffuse=(clightmdiffuse)(0.5(nl)+0.5)
    在原兰伯特模型中,对于模型的背光面, n ∗ l n*l nl的结果都将被映射为0;而在半兰伯特模型中,背光面映射的值也会有不同结果。这就使背光面也可以产生明暗变化。

    代码修改如下

    fixed4 frag(v2f i):SV_Target
    {
    	// 获取归一化的光源方向
    	const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
    	// 环境光
    	const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    	// 计算漫反射,使用半兰伯特模型
    	const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(i.worldNormal, worldLight) * 0.5 + 0.5);
    	fixed3 color = ambient + diffuse;
    	return fixed4(color, 1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    结果如下

    三、在Unity中实现高光反射

    3.1 逐顶点光照

    为了更好的展现高光反射与漫反射混合后的效果,我们可以直接复制一份之前写的漫反射逐顶点光照Shader,并在此基础上进行修改。

    首先增加两个属性,用来控制高光反射的颜色和范围。然后在Pass中定义与之匹配的变量

    _Specular("Specular",Color) = (1,1,1,1)  
    _Gloss("Gloss",Range(8.0,256)) = 20
    
    • 1
    • 2

    接下来在顶点着色器中计算高光反射。我们首先要得到反射光向量和视角方向向量,幸运的是Unity提供了一个「reflect」函数,可以直接根据入射光向量和法线计算出反射光向量。不过需要注意一点,Unity中的入射光向量是从入射点指向光源的,而这个函数需要传入从光源指向入射点的向量,所以需要对方向取反。

    v2f vert(appdata_base v)
    {
    	v2f o;
    	// 将顶点坐标从物体空间转换到裁剪空间
    	o.vertex = UnityObjectToClipPos(v.vertex);
    	// 将法线从物体空间转换到世界空间
    	const fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    	// 获取归一化的光源方向
    	const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
    	// 计算漫反射
    	const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
    	// 环境光
    	const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
    	// 反射光向量
    	const fixed3 reflectDir = normalize(reflect(-worldLight, worldNormal));
    	// 视角方向向量 摄像机位置-顶点位置
    	const fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex));
    	// 计算高光反射
    	const fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
    	o.color = diffuse + ambient + specular;
    	return o;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    效果如下

    3.2 逐像素光照

    首先复制一份逐像素漫反射光照Shader,然后定义如下属性

    Properties
    {
    	_Diffuse("Diffuse",Color) = (1,1,1,1)
    	_Specular("Specular",Color) = (1,1,1,1)
    	_Gloss("Gloss",Range(1,256)) = 20
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    由于计算高光反射需要用到顶点的世界坐标,而在片元着色器中我们无法取得这个值,所以在传入参数中添加一个变量用于缓存

    struct v2f
    {
    	float4 vertex: SV_POSITION;
    	fixed3 worldNormal: TEXCOORD0;
    	fixed3 worldVert: TEXCOORD1;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后在顶点着色器中缓存顶点世界坐标的值

    v2f vert(appdata_base v)
    {
    	v2f o;
    	// 将顶点坐标从物体空间转换到裁剪空间
    	o.vertex = UnityObjectToClipPos(v.vertex);
    	// 将法线从物体空间转换到世界空间
    	const fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    	o.worldNormal = worldNormal;
    	// 缓存顶点的世界坐标
    	o.worldVert = mul(unity_ObjectToWorld, v.vertex);
    	return o;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    然后在片元着色器中计算高光反射。计算过程与上一节相同

    fixed4 frag(v2f i):SV_Target
    {
    	// 获取归一化的光源方向
    	const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
    	// 环境光
    	const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    	// 计算漫反射
    	const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal,worldLight));
    
    	// 反射光向量
    	const fixed3 reflectDir = normalize(reflect(-worldLight, i.worldNormal));
    	// 视角方向向量 摄像机位置-顶点位置
    	const fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldVert);
    	// 计算高光反射
    	const fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
    	
    	fixed3 color = ambient + diffuse+specular;
    	return fixed4(color,1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    效果如下,可以明显看出效果比逐顶点光照细腻

    3.3 Blinn-Phong光照模型

    我们前面使用的都是Phong光照模型,下面来实现一下另一种高光反射模型——Blinn-Phong模型。

    与Phong模型相比,Blinn-Phong模型只是将 r ⋅ v r·v rv换成了 n ⋅ h n·h nh。所以可以直接复制一份上一节的Shader,然后稍微修改一下片元着色器

    fixed4 frag(v2f i):SV_Target
    {
    	// 获取归一化的光源方向
    	const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
    	// 环境光
    	const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    	// 计算漫反射
    	const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLight));
    
    	// 视角方向向量 摄像机位置-顶点位置
    	const fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldVert);
    	const fixed3 halfDir = normalize(viewDir + worldLight);
    	// 计算高光反射
    	const fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, i.worldNormal)), _Gloss);
    	fixed3 color = ambient + diffuse + specular;
    	return fixed4(color, 1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    效果如下,可以发现在相同参数的情况下Blinn-Phong高光反射看起来高光范围要更大、更亮一些

  • 相关阅读:
    如何基于three.js(webgl)引擎架构,研发一套通过配置就能自动生成的3D机房系统
    2.3 带头结点的单链表:理论+编程实战(C语言详细)
    Codeforces Round 900 (Div. 3)(A-F)
    Task.Run(), Task.Factory.StartNew() 和 New Task() 的行为不一致分析
    C#语言基础
    springboot项目打包成jar
    845. 八数码(Acwing)
    针孔相机模型原理&坐标系辨析&内参标定流程&内参变换
    关于面试题
    软件项目管理 6.7.参数估算法
  • 原文地址:https://blog.csdn.net/LWR_Shadow/article/details/126903749