UnityShader-复杂光照 渲染路径(Rendering Path) 渲染路径决定了光照如何应用到UnityShader中,Unity包含3中渲染路径:
前向渲染路径(Forward Rendering Path)
延迟渲染路径(Deffered Rendering Path)
顶点照明路径(Vertex Lit Rendering Path)
大多数情况下,一个项目只使用一种渲染路径,如果在摄像机中额外设置,则会覆盖掉全局渲染路径
可以在Pass中使用标签来指定使用的渲染路径,如果没有指定任何渲染路径,那么一些光照变量很可能无法被正确赋值,计算结果也会出现偏差
前向渲染路径 最常用的一种渲染路径,每进行一次完整的前向渲染,需要渲染该对象的渲染图元,并计算两个缓冲区信息:颜色缓冲区和深度缓冲区
前向渲染可以用以下伪代码表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Pass { for (each primitive in this model) { for (each fragment covered by this primitive) { if (failed in depth test) { discard ; } else { float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir); writeFrameBuffer(fragment, color); } } } }
假设场景中有N个物体,每个物体受到M个光源的影响,那么要渲染整个场景一共需要N*M个Pass
在Unity中,前向渲染路径有3中处理光照方式:
逐顶点处理
逐像素处理
球谐函数(Spherical Harmonics, SH)处理
决定一个光源使用哪种处理模式取决于光源类型 和渲染模式
光源类型:该光源是平行光、点光源、聚光源等
渲染模式:该光源是否是重要的,如果设置为Important,则按照逐像素光源来处理 ,否则按照逐顶点处理,如果你在Base Pass中又没有计算逐顶点光照,那么该光源就不会被纳入计算了
Unity中会按照重要度对光源进行处理,一般来说,一定数目的光源按照逐像素处理,最多4个光源按照逐顶点方式处理,剩下光源按照SH方式处理,判断规则如下:
场景中最亮的平行光按照逐像素处理
渲染模式被设置成Not Important的光源,会按照逐顶点或者SH处理
渲染模式被设置成Important光源,会按照逐像素处理
如果以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量(Pixel Light Count),会有更多的光源按照逐像素的方式进行渲染
前向渲染分为两种Pass:Base Pass和Additional Pass,它们的设置如下:
说明:
渲染设置中的编译指令会影响光照变量的正确值
Additional Pass中需要使用编译指令来开启阴影效果
环境光和自发光在Base Pass中计算,如果在Additional Pass中计算可能被多次计算(每次计算逐像素光源都会重复计算一次环境光ambient)
在Additional Pass的渲染设置中开启和设置了混合模式 ,这样可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终的多个光照的渲染结果,否则只会得到最后一次光照结果
一个Unity Shader定义1个或多个Base Pass和一个Additional Pass,一个Base Pass只会执行一次,而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass
常用内置光照变量如下:
常用内置光照函数如下:
顶点照明渲染路径 顶点渲染路径是对硬件配置要求最少、运算性能最高,但同时也是效果最差的类型
不支持逐像素才能得到的效果,如阴影、法线映射、高精度的光照反射等
不可以使用逐像素光照变量(Unity只填充逐顶点相关光源变量)
可访问的内置变量和函数:
延迟渲染路径 当场景中包含大量实时光源时,前向渲染的性能会急速下降,因为场景中有多个光源时会执行多次Pass,而每个Pass都要重新渲染一遍物体,导致计算重复
延迟渲染除了使用颜色缓冲和深度缓冲之外,还会利用额外的缓冲区,称为G缓冲(G-buffer),用于存储我们所关心的表面(通常指离摄像机最近的表面)的其他信息,如表面法线、位置、用于光照计算的材质属性等
原理:
延迟渲染主要包含两个Pass:
第一个Pass中,不进行任何光照计算,仅仅计算哪些片元是可见的(通过深度缓冲技术实现),然后把它的相关信息放到G缓冲区中。具体来说,会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光、深度 等信息渲染到屏幕空间的G缓冲区中,对于每个物体而言只会执行一次
第二个Pass中,利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。此时,我们只需要对G缓冲中的每个屏幕像素执行一次昂贵的光照计算(相比于对每个物体上的每个像素进行计算节省了非常多的性能)
这里《Shader入门精要》讲得不是很详细,推荐链接:[延迟着色法 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/05 Advanced Lighting/08 Deferred 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 Pass1 { for (each primitive in this model) { for (each fragment covered by this primitive) { if (failed in depth test) discard ; else { writeGBuffer(materialInfo, pos, normal, lightDir, viewDir); } } } } Pass2 { for (each pixel in the screen) { if (the pixel is valid) { readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir); float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir); writeFrameBuffer(pixel, color); } } }
因此,延迟渲染使用的Pass数目通常就2个,这跟场景中包含的光源数目没有关系,只和屏幕空间的大小有关
对于延迟渲染路径来说,它最适合在场景中光源数目很多,如果使用前向渲染会造成性能瓶颈时使用,且每个光源都能按逐像素方式处理
缺点:
不支持真正的抗锯齿功能
不能处理半透明物体
对显卡有要求。必须支持MRT、Shader Mode3.0以上、深度渲染纹理以及双面的模板缓冲
可访问的内置变量和函数
光源类型 Unity支持4种光源类型:平行光、点光源、聚光灯和面光源
常用光源属性:光源位置、方向、颜色、强度、衰减
平行光 照亮范围没有限制,光源没有唯一位置,只有方向,光照强度不会衰减
点光源 照亮空间为一个球体,光照强度会随着距离衰减
聚光灯 照亮空间为一个锥形区域,在Unity面板中,半径由Range属性决定,张开角度由Spot Angle属性决定
一个用于多光源的Shader如下,其中的光照强度是使用数学公式计算的
如果场景中包含多个平行光,Unity会选择最亮的平行光传递给Base Pass进行逐像素处理 ,其他平行光会按照逐顶点或者在Additional Pass中按照逐像素的方式处理
对于Base Pass来说,它处理的逐像素光源类型一定是平行光
该过程可以在Unity的帧调试器(Frame Debugger)中实验
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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 Shader "Forward Rendering" { Properties { _Diffuse ("Diffuse", Color) = (1 , 1 , 1 , 1 ) _Specular ("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss ("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Tags { "RenderType"="Opaque" } Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #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; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize (i.worldNormal); fixed3 worldLightDir = normalize (_WorldSpaceLightPos0.xyz); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max (0 , dot (worldNormal, worldLightDir)); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize (worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow (max (0 , dot (worldNormal, halfDir)), _Gloss); fixed atten = 1.0 ; return fixed4(ambient + (diffuse + specular) * atten, 1.0 ); } ENDCG } Pass { Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM #pragma multi_compile_fwdadd #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.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; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize (i.worldNormal); #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLightDir = normalize (_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = normalize (_WorldSpaceLightPos0.xyz - i.worldPos.xyz); #endif fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max (0 , dot (worldNormal, worldLightDir)); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize (worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow (max (0 , dot (worldNormal, halfDir)), _Gloss); #ifdef USING_DIRECTIONAL_LIGHT fixed atten = 1.0 ; #else #if defined (POINT) float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1 )).xyz; fixed atten = tex2D(_LightTexture0, dot (lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #elif defined (SPOT) float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1 )); fixed atten = (lightCoord.z > 0 ) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5 ).w * tex2D(_LightTextureB0, dot (lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #else fixed atten = 1.0 ; #endif #endif return fixed4((diffuse + specular) * atten, 1.0 ); } ENDCG } } FallBack "Specular" }
由于光源衰减涉及到大量复杂计算,因此Unity选择使用一张纹理作为查找表(Lookup Table, LUT),以在片元着色器中得到光源的衰减。
默认情况下,一个物体可以接收除最亮的平行光之外的4个逐像素光照 (理解为存在平行光的情况下,执行4次Additional Pass)。可以通过Edit->Project Settings->Quality->Pixel Light Count修改默认值
光照衰减 Unity使用衰减纹理计算逐像素光照的衰减,相比于直接计算能够提高性能
缺点:
需要预处理得到采样纹理,纹理大小会影响衰减精度
不直观,不方便,一旦把数据存储到查找表中,就无法使用其他数学公式计算衰减
Unity默认使用纹理查找方式来计算逐像素的点光源和聚光源的衰减
用于光照衰减的纹理
Unity内部使用_LightTexture0纹理计算光源衰减,其对角线上的纹理颜色值表明在光源空间 中不同位置的点的衰减值,(0, 0)为最近点,(1, 1)为距离最远的点的衰减,可以通过_LightMatrix0把顶点从世界空间变换到光源空间
获取顶点在光源空间中的位置:
1 float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1 )).xyz;
使用坐标的模的平方对衰减纹理进行采样,获得衰减值:
1 2 3 4 fixed atten = tex2D(_LightTexture0, dot (lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
阴影 阴影实现 原理:当一个光源发射的一条光线遇到一个不透明物体时,这条光线就不能再继续照射其他物体,因此物体就会向旁边的物体投射阴影
在实时渲染中,最常用的是Shadow Map技术 。该技术首先把摄像机位置放在与光源重合的位置,那么场景中该光源的阴影区域就是那些摄像机看不到的地方
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算**阴影映射纹理(shadowmap)**。该纹理本质为一张深度图,记录了从该光源的位置出发、能看到的场景中距离它最近的表面的位置(具体来说记录了深度,有点类似深度测试)
Unity选择使用一个额外的Pass(相对于Base 和Additional Pass)来专门更新光源的阴影映射纹理,该Pass的LightMode标签为ShadowCaster ,该Pass渲染目标为阴影映射纹理(或深度纹理)。
当开启了光源的阴影效果后 ,渲染引擎首先在当前渲染物体的Unity Shader中找到LightMode为ShadowCaster的Pass,如果没有就在Fallback指定的Unity Shader中寻找,如果仍然没有找到,则该物体无法向其他物体投射阴影,但是可以接收来自其他物体投射的阴影
传统阴影映射纹理实现:
将顶点变换到光源空间,对阴影映射纹理进行采样,得到该位置的深度信息,如果该点深度值小于顶点深度值,说明该点位于阴影中
Unity中阴影实现:
采用屏幕空间的阴影映射技术(Screenspace Shadow Map)。Unity首先调用LightMode为ShadowCaster的Pass得到 可投射阴影的光源的阴影映射纹理 以及摄像机的深度纹理 。然后根据光源的阴影映射纹理和摄像机纹理得到屏幕空间的阴影图 。
如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,表面该表面通过了深度测试,是可见的,但是处在该光源的阴影之中。因此,阴影图包含了屏幕空间中所有阴影的区域。如果想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样
一个物体接收来自其他物体的阴影 :在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样 ,把采样结果和最后光照结果相乘来产生阴影效果
向其他物体投射阴影 :把该物体加入到光源的阴影映射纹理计算中,让其他物体在对该阴影映射纹理采样时可以得到该物体的相关信息,该过程通过为该物体执行LightMode为Shadow Caster的Pass来实现。该Pass通常在Fallback的回调Shader中实现
注意:该技术需要显卡支持MRT,有些移动平台不支持此特性
不透明物体阴影 选择让一个物体投射或者接收阴影:设置Mesh Renderer组件中的Cast Shadows和Receive Shadows属性来实现
默认情况下,在计算光源的阴影映射纹理时会剔除掉物体的背面,对于没有正面的物体而言(如内置的平面),需要将Casst Shadows设置为Two Sided来允许物体的所有面计算阴影信息
让其他物体接收阴影 :使用三个内置宏,可以在AutoLight.cginc中找到声明
SHADOW_COORDS:声明一个用于对阴影纹理采样的坐标,参数是下一个可用的插值寄存器的索引值
TRANSFER_SHADOW:在顶点着色器中计算上一步声明的阴影纹理坐标,不同平台有所差异
SHADOW_ATTENUATION:对相关纹理进行采样,得到阴影信息,在片元着色器中计算阴影值
为了确保宏能够正确工作,需要保证:
a2v中顶点坐标变量名为vertex
v2f中顶点位置变量名为pos
只是在Base Pass中实现阴影的代码如下:
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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 Shader "Shadow" { Properties { _Diffuse ("Diffuse", Color) = (1 , 1 , 1 , 1 ) _Specular ("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss ("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Tags { "RenderType"="Opaque" } Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "AutoLight.cginc" #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; SHADOW_COORDS(2 ) }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize (i.worldNormal); fixed3 worldLightDir = normalize (_WorldSpaceLightPos0.xyz); fixed shadow = SHADOW_ATTENUATION(i); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max (0 , dot (worldNormal, worldLightDir)); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize (worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow (max (0 , dot (worldNormal, halfDir)), _Gloss); fixed atten = 1.0 ; return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0 ); } ENDCG } Pass { Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM #pragma multi_compile_fwdadd #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.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; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize (i.worldNormal); #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLightDir = normalize (_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = normalize (_WorldSpaceLightPos0.xyz - i.worldPos.xyz); #endif fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max (0 , dot (worldNormal, worldLightDir)); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize (worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow (max (0 , dot (worldNormal, halfDir)), _Gloss); #ifdef USING_DIRECTIONAL_LIGHT fixed atten = 1.0 ; #else #if defined (POINT) float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1 )).xyz; fixed atten = tex2D(_LightTexture0, dot (lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #elif defined (SPOT) float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1 )); fixed atten = (lightCoord.z > 0 ) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5 ).w * tex2D(_LightTextureB0, dot (lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #else fixed atten = 1.0 ; #endif #endif return fixed4((diffuse + specular) * atten, 1.0 ); } ENDCG } } FallBack "Specular" }
Unity 绘制屏幕阴影的过程:
更新摄像机深度纹理
得到光源的阴影映射纹理
得到屏幕空间的阴影图
对阴影图采样并与其他光源混合
统一管理光照衰减和阴影 光照衰减和阴影对物体最终的渲染影响本质相同——都是把光照衰减因子和阴影值及光照结果相乘得到最终渲染结果,该过程用代码表示为:
1 fixed4 color = fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0 );
其中光照衰减和阴影的计算可以使用内置的UNITY_LIGHT_ATTENUATION宏实现,该宏接收3个参数:
atten,该宏会将光照衰减和阴影值相乘的结果存储到该变量中
结构体v2f
世界空间的坐标,该参数会用于计算光源空间下的坐标
使用方法:
1 2 3 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
如果希望在Additional Pass中添加阴影效果,需要使用#pragma multi_compile_fwdadd_fullshadows
编译指令
透明物体阴影 让使用透明度测试的物体得到阴影 :把Fallback设置为Transparent/Cutout/VertexLit
注意:由于该shader中计算透明度测试时,使用了_Cutoff属性来进行透明度测试,因此我们的Shader中也必须提供名为_Cutoff的属性。
同时,如果物体背面也会产生阴影,需要将Mesh Renderer组件中的Cast Shadows属性设置为Two Sided,强制Unity计算阴影映射纹理时计算所有面的深度信息
对于透明度混合的物体添加阴影比较复杂,在Unity中,所有内置的半透明Shader不会产生任何阴影效果。但是可以通过将Fallback设置为VertexLit、Diffuse这些不透明物体使用的Unity Shader来强制生成