Unity-Shader读书笔记(5)

Unity中的基础光照

关于光所需的知识点

  1. 光源

            我们将光源当做一个没有体积的点。在光学中我们通过辐照度(irradiance)来量化光。

  2. 吸收和散射

            光线由光源发射后于物体相交(我理解就是光照在了物体上)只有两种结果:散射和吸收。

            散射只改变光线的方向而不改变光线的密度和颜色。光线在物体表面经过散射后有两种大致的方向:向物体内部(这种现象又被称为折射),向物体外部(这种现象被称为反射)。对于不透明的物体,折射进入物体内部的光线还会对物体内部的颗粒进行相交,其中一部分光线会被物体吸收而另外一部分则发射出物体表面。我们为了区分这两种不同的散射方向,我们光照模型中使用了不同的部分来计他们:高光反射(specular)部分表示物体表面是如何反射光线,漫反射(diffuse)部分则表示有多少光线会被折射、吸收和散射出表面。根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,我们也会用出射度(exitance)来描述它。

            吸收只会改变光线的密度和颜色,而不改变光线的方向。

  3. 着色

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

  4. BRDF光照模型

    【图形学】光照模型-从最简单开始到BRDF实现_schlick-ggx_程序员菜鸟的博客-CSDN博客

    基于BRDF的光照模型_拳四郎的博客-CSDN博客

    https://zhuanlan.zhihu.com/p/490024846

    https://www.bilibili.com/video/BV1X7411F744/?spm_id_from=333.880.my_history.page.click

标准光照模型

        在1975年,著名学者Bui Tuong Phong提出了标准光照模型背后的基本理念。标准光照模型只关心直接光照(direct light),也就是直接从光源发出后照射到物体表面,然后经过物体表面一次反射后直接进入摄像机的光线。

        它的基本方法是把进入摄像机内的光线分成4个部分:自发光、高光反射、漫反射和环境光。每一个部分使用一种方法来计算它的贡献度。关于phong模型的知识点,我更推荐去看Games101 的第7集

  1. 环境光(ambient)

            虽然标准光照模型的重点在于描述直接光照,但在真实的世界中,物体可以被间接光照(indirect light)所照亮。在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。在标准光照模型中,环境光就只是一个简单全局变量。

  2. 自发光(emissive)

            物体本身所发的光可以直接进入摄像机而不需要进行任何的反射。所以这个值就是物体自发光的数值。

  3. 漫反射(diffuse)

            我们之前已经描述过什么是漫发射这里不多描述了。我们只需要知道漫反射满足兰伯特定律(Lambert's law):反射光线的强度与表面法线和光源方向之间的夹角的余弦值成正比。所以漫反射颜色值为:diffuseColor = lightColor * mDiffuse * max(0,Dot(normal,I))。

            diffuseColor是我们所要得到的漫反射颜色值,LightColor是光照的颜色,mDiffuse是这个材质的漫反射颜色。normal是材质这个点的表面法线而 I 是指光源的单位矢量。这里用max的原因是有些光线和法线点乘可能为负,我们这里就可以认为这部分光线在这时候被遮挡住了。那么它们的贡献度是0,但不为负。

  4. 高光反射(specular)

            这里的高光反射是一种经验模型,也就是说它并不符合真实世界中的高光反射现象。它可用于计算哪些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是有光泽的,比如金属。phong模型高光反射的公式为:

            specularColor = lightColor * mSpecular * max(0,Dot(view,reflect))^ mGloss。

            这里specularColor是我们要得到的高光颜色。LightColor是光照的颜色,mSpecular是这个材质高光反射的颜色,它控制材质对于高光反射的强度和颜色。view是观察方向(单位向量)而 Ireflect是光反射方向 (单位向量)。mGloss是材质的光泽度(gloss),也被称为反光度(shininess)。mGloss越大高光区域的亮点就越小。

  5. 逐像素和逐顶点

            我们在计算光照模型的时候,我们有两个选择:在片元着色器中计算就是逐像素,在顶点着色器中计算就是逐顶点。

            在逐像素的时候,我们会以每一个像素为基础得到它的法线(可以对顶点法线插值或是从法线纹理采样得到),然后对光照模型进行计算。这种在面片之间对顶点法线进行插值的技术被称为Phong着色(Phong Shading),也被称为Phong插值或法线插值着色技术。这个和上面的phong光照模型不一样。

            而相对的逐顶点光照也被称为高洛德着色(Gouraud Shading)。

逐顶点漫反射光照

        预备条件:场景中只有平行光,无天空盒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader "Learn/DiffuseVertexLevel"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

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

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

struct v2f
{
fixed3 color : COLOR;
float4 pos : SV_POSITION;
};

float4 _DiffuseColor;

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 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(worldNormal,worldLight));
// 返回的颜色是漫反射加上环境光
o.color = ambient + diffuse;
return o;
}

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

        在上述的漫反射中,我们知道漫反射的颜色值我们可以看做是:光的颜色乘上漫反射系数再乘上这一顶点的法线和光的点乘(值限定在0到1之间)。属性语义块中定义的的_DiffuseColor就是漫反射系数也就是mDiffuse。

        光的颜色我们可以从Unity 内置文件#include "Lighting.cginc"中得到_LightColor0以获取光源的颜色和强度信息。但是这里说明一下如果想得到正确的值那么就要定义合适的LightMode标签。关于LightMode更多信息可以查看官网的说明URP ShaderLab Pass 标签 | Universal RP | 12.1.1。(如果发现内容不对,你可以转成中文后找到着色器和材质,然后在其子目录下点击URP Shader​Lab Pass 标签,2023.3.28留)。

        光源方向我们可以通过_WorldSpaceLightPos0来得到,但是这里光源方向的取值方式并不具有普遍性。只是因为我们场景中只有一个光源且该光源还是平行光类型。如果不是这样使用_WorldSpaceLightPos0得到结果就可能不正确。此刻我们得到的光源方向是在世界坐标系下的方向。如果我们需要和法线点乘从而计算出光作用的大小。那么法线和光源方向要在同一个坐标系才有意义。我们从NORMAL语义中得到是模型空间下的法线坐标,所以我们将法线转到世界空间坐标系下让其与光源方向进行点乘(你也可以把光源方向转变到模型空间下)。我们使用UnityShader的内置变量unity_WorldToObject(内置着色器变量 - Unity 手册)矩阵来完成变化。因为法线只有xyz三个属性有用,所以我们也只要unity_WorldToObject的前三行和前三列。转换完成后我们再使用normalize函数进行归一化。然后我们计算其点乘的时候还要预防他们得到负值。因此使用saturate函数防止。saturate函数会将值锁定在[0,1]之间。

Ps: 这里大家可以发现在转换法线的时候,我们是unity_WorldToObject右乘法线。这个和我们之前的说法是相反。而且我们明明需要将法线从模型空间转变到世界空间,我们使用的还是unity_WorldToObject而不是unity_ObjectToWorld。关于这点大家可以看一下这篇文章【UnityShader】坐标变换之法线变换_小蜗牛zjt的博客-CSDN博客,简单概括一下就是法线的转换是特殊。

        环境光我们可以使用UnityShader的UNITY_LIGHTMODEL_AMBIENT变量获取。

        环境光ambient加上之前我们通过算式获取到的漫反射变量就得到了最终光照的结果。

逐像素版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader "Learn/DiffusePixelLevel"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

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

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

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

float4 _DiffuseColor;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
o.worldNormal = worldNormal;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(i.worldNormal,worldLight));
fixed3 color = ambient + diffuse;
return fixed4(color,1);
}
ENDCG
}
}
}

        逐像素版光照可以得到更加平滑的光照效果,但是它仍然会有问题:在光照无法到达的区域,模型的外观是全黑的。这是因为saturate(dot(i.worldNormal,worldLight)) 的计算结果是0。所以我们得到的颜色就是(0,0,0)黑色。这时候就没有任何的明暗变化,这使得模型的背光区域看起来就像是平面一样。

半兰伯特模型

        之前我们使用的漫反射光照模型(之前说明的漫反射计算公式)右被称为兰伯特模型。因为它符合兰伯特定律——在平面某点反射光的强度与该反射点的法向量和入射角度的余弦成正比。广义的半兰伯特模型公式为:

        colorDiffuse = colorLight * mDiffuse * (α * Dot(normal,I) + β)。

与之前的漫反射公式不一样的是,半兰伯特模型并没有对法线和光线方向的点乘用max做限制处理。而是对其进行放大α,并加上β值。不过半兰伯特模型是没有任何物理依据的,它仅仅是一个视觉加强的手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader "Learn/HalfLambertShader"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

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

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

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

float4 _DiffuseColor;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
o.worldNormal = worldNormal;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * (0.5 * dot(i.worldNormal,worldLight) + 0.5);
fixed3 color = ambient + diffuse;
return fixed4(color,1);
}
ENDCG
}
}
}

高光反射模型

        Unity Shader中提供了计算反射方向的函数reflect(i,n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Shader "Learn/AddSpecularShader"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
_SpecularColor ("SpecularColor",Color) = (1,1,1,1)
// 高光系数
_Gloss ("Gloss",Range(8,255)) = 20
}
SubShader
{
Tags { "LightMode"="ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

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

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

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

float4 _DiffuseColor;
float4 _SpecularColor;
fixed _Gloss;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
o.worldNormal = worldNormal;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 得到反射光
fixed3 reflectDir = reflect(-worldLight,i.worldNormal);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(i.worldNormal,worldLight));
// 计算得到高光值
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}

FallBack "SPECULAR"
}

Blinn-Phong模型

        计算反射是比较耗时的操作,但是我们可以计算视角方向的向量和光照方向的向量相加得到的半程向量h将其归一化后与法线点乘,以此来代替反射光方向和视角方向的点乘。