n个月后重读《Unity Shader入门精要》(第六章
光与材质
首先要明白一件事情,物体的呈现的颜色不是物体的性质。物体的颜色取决于物体的材质和光。
而光有分为好几种,一是最简单的平行光,它没有所谓的光源,光强均等,是一种理想模型(例如狭义上的太阳光),二是点光源,例如基本上所有的灯光都是点光源,它的光强与它有限的距离成反比,还有其他衍生下去的光类型,不外乎这几种性质:照射距离,光的强度(颜色)。
物体的材质包括对各个颜色分量的处理程度,因此对某个颜色处理得更宽容,物体就偏向呈现那个颜色。还包括反射率,漫反射率,和环境光率,这些都直接影响到物体呈现的效果。
物体除了被光直接照亮,反射到摄像机中,还有其他的光线。例如密室中一个物体被聚光灯照亮,背部也不是完全的黑暗。这就是部分散射光和环境光的作用。总体来说,「从摄像机看到某位置的」光=反射光+散射光+环境光,这有一个具体的公式,有兴趣的可以查阅相关资料
标准光照模型(数学基础
裴祥风(Bui Tuong Phong)提1975年出的的标准光照模型,基本方法是把进入摄像机的光线分为四个部分,每个部分用各自的方法计算贡献度。由简单至复杂陈列如下:
环境光
环境光用于描述所有其他的间接光照,但在标准光照模型中,通常使用一个全局变量环境光来近似模拟间接光照:
自发光
光线可以由光源直接进入摄像机而不进行反射。自发光的表面并不会照亮其他物体,只是使自身看起来更亮而已。计算时直接使用材质的自发光颜色:
漫反射
漫反射光照符合兰伯特定律(Lambert’s law):反射光线强度与表面法线和光源方向间夹角的余弦成正比。因此漫反射部分计算如下:
其中$c_{light}$光源颜色,$m_{diffuse}$为材质的漫反射颜色,$n$为表面法线,$l$为指向光源的单位矢量
高光反射
这里的高光反射都是经验模型,不符合真实世界情况。它用于计算那些镜面反射的光线,使物体看起来比较有光泽。
已知表面法线$n$,光源方向$l$,视角方向$v$这三个矢量,可以推出第四个矢量反射方向$r$:
这样就可以利用Phone模型,计算高光反射部分如下
其中$c_{light}$为光源颜色和强度, $m_{specular}$为材质的高光反射颜色。$m_{gloss}$为材料的光泽度(gloss),也成为反光度(shininess),$m_{gloss}$越小,亮点就越大。
Blinn使用了一个更简单的修改方法来得到类似的效果,它的基本思想是避免计算反射方向$r$。因此引入一个新的矢量$h$,它是通过$v$和$l$的取平均后再归一得到的,即
然后使用$n$和$h$之间的夹角进行计算
在硬件实现时如果摄像机和光源足够近($v$和$l$都是定值,因此$h$是常量),Blinn模型会更快。如果$v$和$l$不是定值时Phong模型可能更快一点。Blinn方法由于简化计算从而在某些情况可能更快,而且在一些情况下更符合实验结果。但是注意,这两种光照模型都是经验模型,不应该认为Blinn是『正确的』Phong模型的近似。
标准光照模型是一个经验模型,因为裴祥风先提出、Blinn的方法简化计算,因此这种模型称为Blinn-Phone模型。
实现(Unity Shader
漫反射光照模型
上一节我们给出漫反射部分的计算公式:
逐顶点光照
DiffuseVertexLevel.shader
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
| Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed3 color : COLOR; }; v2f vert(a2v v) { v2f o; // Transform the vertex from object space to projection space o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // Transform the normal from object space to world space fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object)); // Get the light direction in world space fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); // Compute diffuse term 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" }
|
逐像素光照
DiffusePixelLevel.shader
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
| Shader "Unity Shaders Book/Chapter 6/Diffuse Pixel-Level" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; }; v2f vert(a2v v) { v2f o; // Transform the vertex from object space to projection space o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // Transform the normal from object space to world space o.worldNormal = mul(v.normal, (float3x3)_World2Object); return o; } fixed4 frag(v2f i) : SV_Target { // Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // Get the normal in world space fixed3 worldNormal = normalize(i.worldNormal); // Get the light direction in world space fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); fixed3 color = ambient + diffuse; return fixed4(color, 1.0); } ENDCG } } FallBack "Diffuse" }
|
半兰伯特模型
Valve在开发HF的时候在兰伯特模型(即漫反射光照模型)上进行修改,因此称为半兰伯特光照模型,广义的半兰伯特光照模型的公式如下
使用了一次函数来代替max函数防止负值,绝大多数情况下,$\alpha$和$\beta$的值为0.5,即
修改DiffusePixelLevel.shader中的片源着色器:
HalfLambert.shader
1 2 3 4 5 6 7 8 9 10 11
| fixed4 frag(v2f i) : SV_Target { ...... // Compute diffuse term fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert; fixed3 color = ambient + diffuse; return fixed4(color, 1.0); }
|
以下是三个光照的对比效果:(半兰伯特很明显能够使背光面照亮
高光反射模型
上一节我们给出高光反射部分的计算公式:
- reflect(i,n)函数用于计算反射方向(i:入射方向,n:法线方向)
逐顶点光照
SpecularVertexLevel.shader
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
| Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("Specular", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Diffuse; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed3 color : COLOR; }; v2f vert(a2v v) { v2f o; // Transform the vertex from object space to projection space o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // Transform the normal from object space to world space fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object)); // Get the light direction in world space fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); // Get the reflect direction in world space fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)); // Get the view direction in world space fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(_Object2World, v.vertex).xyz); // Compute specular term 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 } } FallBack "Specular" }
|
逐像素光照
SpecularPixelLevel.shader
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
| Shader "Unity Shaders Book/Chapter 6/Specular Pixel-Level" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("Specular", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Diffuse; fixed4 _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; // Transform the vertex from object space to projection space o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // Transform the normal from object space to world space o.worldNormal = mul(v.normal, (float3x3)_World2Object); // Transform the vertex from object spacet to world space o.worldPos = mul(_Object2World, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { // Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); // Get the reflect direction in world space fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)); // Get the view direction in world space fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // Compute specular term fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } FallBack "Specular" }
|
Blinn-Phong光照模型
上一节中提到了Phong光照模型的实习,但是我们提出来另一种Blinn光照模型,即引入一个矢量$h$来避免计算反射方向$r$:
然后使用$n$和$h$之间的夹角进行计算(Blinn模型):
修改SpecularPixelLevel.shader中的片源着色器:
BlinnPhong.shader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| fixed4 frag(v2f i) : SV_Target { ... // Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); // Get the view direction in world space fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // Get the half direction in world space fixed3 halfDir = normalize(worldLightDir + viewDir); // Compute specular term fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0); }
|
以下是三个高光的对比效果:(Blinn-Phong模型看起来更大更亮一些。
使用Unity内置函数
与BlinnPhong.shader中代码几乎一致,修改部分代码如下
BlinnPhongUseBuildInFunctions.shader
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
| ... v2f vert(a2v v) { v2f o; ... // Use the build-in funtion to compute the normal in world space o.worldNormal = UnityObjectToWorldNormal(v.normal); ... return o; } fixed4 frag(v2f i) : SV_Target { ... fixed3 worldNormal = normalize(i.worldNormal); // Use the build-in funtion to compute the light direction in world space // Remember to normalize the result fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); ... // Use the build-in funtion to compute the view direction in world space // Remember to normalize the result fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); ... } ...
|
UnityCG.cginc的常用函数
函数名 |
描述 |
float3 WorldSpaceViewDir(float4 v) |
模型空间的顶点位置->世界空间中该点到摄像机的观察方向(内部使用UnityWorldSpaceViewDir) |
float3 UnityWorldSpaceViewDir(float4 v) |
世界空间的顶点位置->世界空间中该点到摄像机的观察方向 |
float3 ObjSapceViewDir(float4 v) |
模型空间的顶点位置->模型空间中该点到摄像机的观察方向 |
float3 WorldSpaceLightDir(float4 v) |
前向渲染中,模型空间的顶点位置->世界空间中该点到摄像机的光照方向。没有归一化(内部使用UnityWorldSpaceLightDir) |
float3 UnityWorldSpaceLightDir(float4 v) |
前向渲染中,世界空间的顶点位置->模型空间中该点到摄像机的光照方向。没有归一化 |
float3 ObjSpaceLightDir(float4 v) |
前向渲染中,模型空间的顶点位置->模型空间中该点到摄像机的光照方向。没有归一化 |
float3 UnityObjectToWorldNormal(float3 norm) |
法线方向,模型空间->世界空间 |
float3 UnityObjectToWorldDir(float3 dir) |
方向矢量,模型空间->世界空间 |
float3 UnityWorldToObjectDir(float3 dir) |
方向矢量,世界空间->模型空间 |