最近3D项目需要调模型在游戏里面的效果,之前提供的一个只能调漫反射的强度的简单Shader不太够,需要支持更多的功能,比如高光
,环境光
,阴影
等。
这一块的Shader之前也都是到处乱翻的资料,没有系统学习过,趁此机会翻了下《Unity Shader入门精要》关于光照的知识并做个总结。
知识点
这里讲到的一些光照模型都是经验模型,不完全符合真实世界中的光学现象
漫反射
用来表示有多少光线会被折射、吸收和散射出表面,漫反射符合兰伯特定律:反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比,计算公式
cdiffuse=(clight · mdiffuse) max(0, n · l)
- n: 表面法线
- l: 指向光源的单位矢量,可以用Unity Shader中的函数
UnityWorldSpaceLightDir
, WorldSpaceLightDir
获取,这些函数返回的向量没有归一化,需要用normalize
进行归一化处理
- mdiffuse: 材质漫反射的颜色
- clight: 光源颜色
对应Unity Shader的代码
1 2 3 4 5 6 7 8
| // 灯光方向 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));
// 法线归一化 fixed3 worldNormal = normalize(i.worldNormal);
// 漫反射颜色 fixed3 diffuseColor = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
|
为了防止法线和光源方向点乘为负值,需要用取最大值进行截取
兰伯特定律的被光面因为光照无法到达,所以表现为全黑的效果,没有任何明暗变化。为了解决这个问题,有一个改善的技术半兰伯特模型,计算公式
cdiffuse=(clight · mdiffuse) (a(n · l) + b)
取消了max防止为负值,但对结果进行了缩放然后再便宜,绝大多数情况下a和b都为0.5
对应的Unity Shader代码
1 2 3 4 5 6 7 8
| // 灯光方向 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));
// 法线归一化 fixed3 worldNormal = normalize(i.worldNormal);
// 漫反射颜色 fixed3 diffuseColor = _LightColor0.rgb * _Diffuse.rgb * (0.5 * dot(worldNormal, worldLightDir)) + 0.5);
|
高光反射
用来表示物体表面是如何反射光的,高光反射也有两个常用经验模型
- Phong模型
需要知道几个信息,表面法线、视角方向、光源方向、反射方向,如图:
其中反射方向r可以通过其他信息计算得到:
r = 2(n · l)n - l (可以使用Unity Shader中的函数reflect
获得反射向量,因为参数的向量需要从反射点射出,所欲传入的灯光向量需要取负值,另外该函数返回的向量没有归一化)
得到r之后,我们可以带入Phone模型公式:
cspecular=(clight · mspecular) max(0, v · r)mgloss
- r: 反射方向
- v:点到摄像机方向的单位矢量,可以用Unity Shader中的函数
UnityWorldSpaceViewDir
、WorldSpaceViewDir
获取,这些函数返回没有进行归一化
- mspecular: 高光反射的颜色
- clight: 光源颜色
- mgloss: 材质的光泽度,用来控制高光区域的亮点有多宽,值越大,两点越小
对应的Unity Shader代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // 灯光方向 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));
// 法线归一化 fixed3 worldNormal = normalize(i.worldNormal);
// 摄像机方向 fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPosition));
// 反射方向 fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// 高光颜色 fixed3 specularColor = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, worldViewDir)), _Gloss);
|
- Blinn模型
跟Phong模型相比,它避免计算反射方向r,但引入的另外一个矢量h
h = (v + l) / |v + l| 对应Unity代码normalize(worldViewDir + worldLightDir)
模型公式:
cspecular=(clight · mspecular) max(0, n · h)mgloss
对应的Unity代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // 灯光方向 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));
// 法线归一化 fixed3 worldNormal = normalize(i.worldNormal);
// 摄像机方向 fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPosition));
// 向量h fixed3 halfDir = normalize(worldLightDir + worldNormal);
// 高光颜色 fixed3 specularColor = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldViewDir)), _Gloss);
|
在某些情况下(比如摄像机和光源距离模型足够远),Blinn模型会快于Phone模型
这两种模型都是经验模型,不能说谁对谁错。只是Blinn模型更符合实验结果
环境光
模拟来自于其他物体的间接光照,环境光公式比较简单就是一个全局变量:
cambient = gambient
对应的Unity代码
1
| fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
|
逐顶点光照和逐像素光照
取决于光照计算在顶点着色其还是片元着色其中
一般情况下光照计算都会在片元着色器中,因为这样效果会更好点。但是一般情况下像素点会远远超过顶点,所以计算量会比较大,当灯光比较多的时候,一些灯光会降级到顶点着色器处理。
完整的例子
增加了几个参数:
- 环境光、漫反射、高光强度设置
- 不受光照影响的比例(使用原贴图显示的比例)
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| Shader "Light" { Properties { _MainTex("主贴图", 2D) = "white" {} _MainColor("主颜色", Color) = (1, 1, 1, 1) _MainFactor("不受光照影响的比例", Range(0, 1)) = 1
_AmbientFactor("环境光强度", float) = 1 _DiffuseFactor("漫反射强度", float) = 1 _SpecularFactor("高光强度", float) = 1 _SpecularColor("高光颜色", Color) = (1, 1, 1, 1) _Gloss("光泽度", Range(8, 255)) = 20 }
SubShader { Tags { "RenderType" = "Qpaque" }
CGINCLUDE
#include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc"
sampler2D _MainTex; fixed4 _MainTex_ST; fixed4 _MainColor; fixed _MainFactor;
float _AmbientFactor; fixed _DiffuseFactor; fixed _SpecularFactor; fixed4 _SpecularColor; float _Gloss; ENDCG
Pass { Tags {"LightMode" = "ForwardBase"}
CGPROGRAM #pragma multi_compile_fwdbase
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 worldNormal : NORMAL; float3 worldVertex : TEXCOORD1; };
#pragma vertex vert #pragma fragment frag
v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldVertex = mul(unity_ObjectToWorld, v.vertex); return o; }
fixed4 frag(v2f i) : SV_TARGET0 { // 物体本身颜色 fixed3 mainColor = tex2D(_MainTex, i.uv) * _MainColor;
// 受环境光之后影响之后的颜色 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientFactor * mainColor;
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldVertex)); fixed3 worldNormal = normalize(i.worldNormal);
// 漫反射光颜色 fixed3 diffuseColor = max(0, dot(worldNormal, worldLightDir)) * mainColor.xyz * _LightColor0.rgb * _DiffuseFactor;
// 高光颜色 fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex)); fixed3 hDir = normalize(worldViewDir + worldLightDir); fixed3 specularColor = _SpecularColor.rgb * _LightColor0.rgb * pow(max(0, dot(hDir, worldNormal)), _Gloss) * _SpecularFactor;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);
// 受灯光影响之后的颜色 fixed3 lightColor = ambient + (diffuseColor + specularColor) * atten;
// 最终颜色 fixed4 color = fixed4(mainColor * _MainFactor + lightColor * (1 - _MainFactor), 1); return color; } ENDCG } } FallBack "Diffuse" }
|
疑问
为什么漫反射光照结果计算时,要使用归一化的法线以及单位光源方向?
涉及到一个概念就是辐照度,辐照度用来量化光,可以理解为辐照度越大,光越亮
辐照度在平行光中指的是单位面积上单位时间穿过的能量,如图:
在左图中,光是垂直照射到物体表面,因此光线之间的垂直距离保持不变;而在右图中,光是斜着照射到物体表面,在物体表面光线之间的距离是d/cosθ,因此单位面积上接收到的光线数目要少于左图
可以看到d/cosθ越大,光射到平面的间距就越大,辐照度就越小,也就是辐照度跟cosθ成正比。
而n · l = |n| * |l| * cosθ,为了计算cosθ,所以需要归一化法线和光源方向向量
Tags {"LightMode" = "ForwardBase"}
表示告诉Unity这个Pass是使用前向渲染中的ForwardBase路径,另外一种叫ForwardAdd路径。
#pragma multi_compile_fwdbase
是一个编译指令,可以为相应类型的Pass生成所有需要的Shader变种,这些变种会处理不同条件下的渲染逻辑,例如是否使用光照贴图、当前处理哪种光源类型、是否开启阴影等,同时Unity也会在背后将一些内置变量传递到Shader中,例如环境光变量UNITY_LIGHTMODEL_AMBIENT
这一块后续第二讲会继续介绍相关内容
为什么要max约束点成的结果
防止算出来的系数是一个负值,从而导致算出来的光照颜色是一个负值,这样与原始颜色相加之后,颜色就会变暗了