赞
踩
光照和反射是我们看到颜色的基础,一切效果从反射开始,这里我们整理了UnityShader入门精要光照方面一些个人认为比较重要的知识点。
完整的工程会上传到个人代码仓库(链接),与书籍代码类似,但是包含了大量的个人中文注释(不是照搬书上的解释)和一些理解,看起来会比书上更友好。
目录
首先标准光照模型会把进入摄像机内的光线分为四个部分。
PS:辐射量是光的量化单位,即,单位时间内穿过单位面积的光的能量多少。如果不好理解,可以暂时简单理解为,光的强度+颜色,更契合后续的学习内容。
首先提醒,漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布是随机的。但是入射光线的角度很重要。
首先是兰伯特定律,漫反射符合兰伯特定律:反射光线的强度与表面法线和光源方向之间的夹角的余弦成正比。
然后Valve公司后续改进了该公式,被称为半兰伯特定律(实际上半兰伯特定律更常用),该公式没有使用max操作来方式n*l点积为负值,而是对结果进行了先缩放α倍,然后再加上β的偏移(PS:一般情况下,α和β都是0.5)。虽然半兰伯特定律没有任何物理依据,但是大部分情况下往往实现的效果更理想。
半兰伯特定律的一般公式表示;更常用的0.5常用系数公式
。α和β都是0.5,刚好可以将n*l的范围[-1,1]映射到[0,1],这样背面就不会有纯黑的阴影,而是会有明显的明暗变化。
PS:由于片元着色器的实现的效果更好,这里尽量只贴出片元着色器中编写的部分关键反射代码。TIP:完整代码看文章头部的链接。
效果展示
兰伯特定律-片元
- //顶点着色器代码
- v2f vert(a2v v){
- v2f o;
- //将顶点坐标转换到裁剪空间
- o.pos = UnityObjectToClipPos(v.vertex);
- //获取顶点的法线方向,变换到世界坐标
- // Tip 使用顶点变换的逆转置矩阵来对法线进行变换,逆矩阵 _World2Object,转置 mul中调换参数位置
- // Tip 截取了unity_WorldToObject前3*3矩阵,因为法线只有方向
- 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);
- }

半兰伯特定律-顶点
- //顶点着色器代码
- v2f vert(a2v v){
- v2f o;
- //将顶点坐标转换到裁剪空间
- o.pos = UnityObjectToClipPos(v.vertex);
- //获取环境光设置
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- //获取顶点的法线方向,变换到世界坐标后,再归一化
- // Tip 使用顶点变换的逆转置矩阵来对法线进行变换,逆矩阵 _World2Object,转置 mul中调换参数位置
- // Tip 截取了unity_WorldToObject前3*3矩阵,因为法线只有方向
- fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
- // 获取光源的方向 //假设场景中只有单个平行光
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
- //半兰伯特光照公式
- fixed3 halfLambert = dot(worldNormal,worldLightDir) * 0.5 + 0.5;
- // 通过漫反射公式计算出,漫反射光颜色
- fixed3 diffuse = _LightColor0.rgb *_Diffuse.rgb * halfLambert;
- //环境光+漫反射光 得最终颜色
- o.color = ambient + diffuse;
- return o;
- }
-
- //最后直接使用片元输出颜色
- fixed4 frag(v2f i):SV_Target{
- return fixed4(i.color,1.0);
- }

首先明确一点,高光反射模型是一个经验模型。而我们常用的有Phong模型,和Blinn模型。 (书上更倾向于使用Blinn模型)
经验模型:通过经验积累试验出来的问题比较优秀的解,但是该解无理论依据。也许前辈们试验了很多公式,最终觉得某个效果比较好,慢慢推广,裨益整个行业。渲染原则之一:看起来是对的,那他就是对的!
首先上示意图,图中n是法线方向,v是视角方向,i是光源方向,r是反射方向。而v方向看到的颜色,就是我们要通过高光模型得到的解。
由于是经验公式,没有什么物理依据,所以直接放出各个方向对应的关系公式和公式对应的解析。
Blinn模型是对Phong模型的一个简单且有效的优化方案。它的基本思想是,避免计算反射方向r。为此它引入了一个新的矢量h,h相较于r计算成本更低,而且摄像机和光源距离足够远的情况下,硬件会认为v和I是恒定值,此时h将会是个常量,不用再反复计算。
PS:实际编写后看到的效果,Blinn比Phong生成的光斑更大一些。书上有提及,这不意味着Blinn的效果比Phong模型效果更差,甚至有些情况下,Blinn模型效果更符合试验结果。所以建议使用Blinn模型。
接下来是公式,公式与Phong模型的公式十分类似,只是将r替换了h。
phong高光-片元
- v2f vert (a2v v)
- {
- v2f o;
-
- o.pos = UnityObjectToClipPos(v.vertex);
- //这里要再次注意法向量的转换矩阵,是逆矩阵的结果的逆
- o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
- o.worldPos = normalize(mul(unity_ObjectToWorld,v.vertex));
-
- 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));
- //获取反射方向,用于计算高光反射 //反射方向等于 l - 2(n*l)n 而reflect 填入的是入射方向也就是l的反方向,可以直接返回反射方向
- fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
- //获取相机观察方向 //相机的位置 - 顶点的世界坐标位置 = 顶点位置指向相机的方向,即观察方向
- fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
- //计算高光反射公式 //(Clight*Mspaceular)*pow(max(0,v*r),Mgloss) ;Mgloss越大,高光点越小
- fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate( dot(reflectDir,viewDir)),_Gloss);
-
- return fixed4(ambient + diffuse + specular,1.0);
- }

blinn高光-片元
- v2f vert (a2v v)
- {
- v2f o;
-
- o.pos = UnityObjectToClipPos(v.vertex);
- //这里要再次注意法向量的转换矩阵,是逆矩阵的结果的逆
- o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
- o.worldPos = normalize(mul(unity_ObjectToWorld,v.vertex));
-
- 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 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
- //blinm算法引入的h向量
- fixed3 halfDir = normalize(worldLightDir+viewDir);
- //计算高光反射公式 //(Clight*Mspaceular)*pow(max(0,v*r),Mgloss) ;Mgloss越大,高光点越小
- fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate( dot(worldNormal,halfDir)),_Gloss);
-
- return fixed4(ambient + diffuse + specular,1.0);
- }

这里会导向另外一篇文章。TODO
常用的有前向,延迟,顶点照明(逐渐淘汰),三种光照渲染流程。
每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。对于每个逐像素光源,我们都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass, 每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。这也就是意味着,渲染物体越多,或者每个物体受影响的逐像素光源越多,性能消耗越多。所以引擎通常会限制每个物体的逐像素光照数目。
Unity前向渲染路径中,有3种处理光照。逐顶点处理,逐像素处理,球谐函数(SH)处理。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important)。
然后Unity会对场景中的光源做一定的自动选择:
先排序:当我们渲染一个物体时,Unity 会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、光源强度等)对这些光源进行个重要度排序。其中,一定数目的光源会按逐像素的方式处理,然后最多有4个光源按逐顶点的方式处理,剩下的光源可以按SH方式处理。Unity 使用的判断规则如下。
场景中最亮的平行光,一定是逐像素的。
前向渲染有两种Pass: BasePass和Additional Pass。通常来说,这两种Pass进行的标签和渲染设置以及常规光照计算如图。
简单的来说,渲染一个物体时每有一个逐像素光源将会执行一次Additional Pass。而BasePass只会在统一渲染逐像素平行光和其他光源时执行一次。
一些其他要注意的点:
延迟渲染主要包含了两个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。
因为这种机制,所以延迟渲染更适合多光源渲染和场景中物体较多时使用,更为节省性能。但是如果物体或光源较少,则不合适,因为延迟渲染毕竟至少有两个Pass。
实际物理世界中,不太强的光照是会随着穿透介质的距离,不断地削弱。
Unity使用两种方式来模拟这种效果。
阴影这一节,概念比较复杂,但是实现起来比较简单,因为代码基本都是使用内置的宏来实现的。
在实时渲染中,我们最常使用的是一种名为Shadow Map的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。
太多了,主要是阴影映射纹理,阴影,阴影图,屏幕空间,深度值,这些词汇组合在一起,一下子不好接受,也不好简化。但是仔细读一下,弄清楚之后,还是蛮好理解的。
简单的来说,就是如果一个可以被相机看到,但是却大于在同一位置的阴影映射纹理中的深度值,就说明他能被看到,但是被其他物体阻挡了。就要在它上面生成阴影。
当使用了屏幂空间的阴影映射技术时,Unity 首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
上面也说了,要出现阴影分为两个步骤,即计算遮挡物,和计算接受阴影物体。万幸的是,这些运算基本都Unity为我们做完了。大部分情况下,使用内置的宏即可。
接下来是代码
- //省略代码........ 完整代码请看https://download.csdn.net/download/lanazyit/13586309
-
- / /用于存入对阴影纹理采样的坐标
- //注意 这里没有";"
- SHADOW_COORDS(2)//如果上一个变量是TEXCOORD1,这里传入2,如果是TEXCOORD2,这里传入3
- //省略代码........ 完整代码请看https://download.csdn.net/download/lanazyit/13586309
-
-
- //SHADOW_COORDS,TRANSFER_SHADOW,SHADOW_ATTENUATION三个宏组合,Unity帮我们做了所有阴影所需要的计算
- //使用这些内置宏,需要a2v.vertex,v2f.pos,v等一些变量的命名必须要与现在保持统一,编写shader时最好遵从同样用途的变量使用一样的名字的习惯
- fixed shadow = SHADOW_ATTENUATION(i);
- //TODO 这里环境光计算一次之后,后面的AdditionalPass就不会在计算?
- // 后续解释,不要在看书时有一些误解,AdditionalPass不要再写环境光的代码就好了
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
- //计算漫反射公式 //注意这里Unity提供的_LightColor0已经是颜色和强度相乘后的结果
- fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
- //获取相机观察方向 //相机的位置 - 顶点的世界坐标位置 = 顶点位置指向相机的方向,即观察方向
- fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
- //blinm算法引入的h向量
- fixed3 halfDir = normalize(worldLightDir+viewDir);
- //计算高光反射公式 //(Clight*Mspaceular)*pow(max(0,v*r),Mgloss) ;Mgloss越大,高光点越小
- fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate( dot(worldNormal,halfDir)),_Gloss);
- //用内置的UNITY_LIGHT_ATTENUATION宏来帮我们统一完成衰减和阴影的计算
- //PS:这个宏定义在AutoLight.cginc,包含了Unity帮我们处理的各种平台的各种光照条件的绝大部分情况
- UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
- return fixed4(ambient + (diffuse + specular)*atten*shadow,1.0);
- //省略代码........ 完整代码请看https://download.csdn.net/download/lanazyit/13586309
-

透明物体的阴影与不透明的类似,主要是要裁减掉,透明部分的阴影,这里我们把Fallback替换成VertexLit,然后在透明度超过阈值的地方进行裁剪。
但需要注意的是,由于Transparent/Cutout/VertexLit中计算透明度测试时,使用了名为_ _Cutoff 的属性来进行透明度测试,因此,这要求我们的Shader 中也必须提供名为_ Cutoff 的属性。否则,同样无法得到正确的阴影结果。
AlphaTestShadow.shader
- //省略代码........ 完整代码请看https://download.csdn.net/download/lanazyit/13586309
- fixed4 frag(v2f i):SV_Target{
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
- fixed4 texColor = tex2D(_MainTex,i.uv);
- // 如果不满足clip的条件,该片元的代码会在这里中断,或者说return
- // 正是这里实现了,透明测试的效果,效果比较极端,要么不透明,要么完全透明
- // clip(texColor.a - _Cutoff);
- if (texColor.a - _Cutoff < 0.0){
- discard;
- }
- fixed3 albedo = texColor.rgb *_Color.rgb;
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
- fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
- UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
- return fixed4(ambient + diffuse,1.0);
- }
- ENDCG
- }
- }
- //要注意,这里设置的Fallback与前面不同
- Fallback "Transparent/Cutout/VertexLit"
- //省略代码........ 完整代码请看https://download.csdn.net/download/lanazyit/13586309

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。