Unity Shader ------ 复杂的光照(下)
Posted bfxymy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Shader ------ 复杂的光照(下)相关的知识,希望对你有一定的参考价值。
笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。
【Unity Shader】(三)------ 光照模型原理及漫反射和高光反射的实现 【Unity Shader】(四)------ 纹理之法线纹理、单张纹理和遮罩纹理的实现 【Unity Shader】(五) ------ 透明效果之半透明效果的原理及实现 【Unity Shader】(六)------ 复杂的光照(上)
目录
前言
本文承接上文【Unity Shader】(六) ------ 复杂的光照(上),介绍剩下的光照衰减和阴影部分,最后实现包含了对不同光照类型进行光照计算,光照衰减,阴影产生等部分的真正意义上的标准光照 shader 。因为本文会上文有所联系,所以个人建议读者阅读上文,以免在本文某些地方出现思路上的突兀。
一. 光照衰减
1.1 使用 LUT
前面说过,我们使用 LUT 来计算衰减,这种做法的优劣点如下:
- 优点:因为直接计算光照衰减会涉及大量且复杂的数学运算,使用 LUT 可以不依赖数学表达式的复杂性,只需一个参数去采样即可。
- 缺点 : ① 需要预处理得到纹理,纹理大小影响衰减的精度。② 不直观,且使用 LUT 后就无法使用其它数学公式来计算。
当然,Unity 默认这种方法也是因为其在一定程度上提升了性能且大部分情况下,得到的效果是良好的。
1.2 关于光照衰减纹理
Unity 内使用 _LightTexture0 的纹理来计算光照衰减,在之前的代码中,我们已经使用过了。通常情况下,我们只关心 _LightTexture0 对角线上的纹理颜色值,其代表了在光源空间下不同位置的点的衰减值。(0,0)表示与光源重合的点的衰减值,(1,1)表示距离最远的点的光照衰减值。
上面说过,需要用一个点对纹理采样,那么就要先知道该点在光源空间下位置信息。同样是空间转换,我们在这里需要用到的转换矩阵为 _LightMatrix0 。在 Unity 5.4 之后,这个矩阵更换为 unity_WorldToLight 了。
所以这里转换语句应该为
1 float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
然后使用这个坐标的摸的平方进行采样。当然,如果用距离值来计算就需要开方操作了,为了,避免这个繁琐的步骤,我们使用顶点距离的平方来采样
1 fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
其中宏 UNITY_ATTEN_CHANNEL 可以得到衰减值所在的分量。
1.3 关于光照衰减的总结
上述所说的知识足够读者应付大部分的光照计算中的光照衰减部分,如果读者着实不希望采用 LUT 的方法来计算衰减,也可以使用数学公式,只是这样需要对公式有更深入的理解。很遗憾的是,笔者并没有找到关于计算衰减的公式的资料,对于衰减方面的资料,着实所寻不多,日后如果我能找到相关知识,我会补充到这篇文章中。
二. 阴影
在许多游戏制作中,为了追求真实,光影效果是必不可少的。光我们之前介绍了,现在来介绍阴影。
2.1 阴影是如何实现出来的
想象一下,一条光线从远方射过来,当它遇到了第一个不透明的物体时,那么理所当然的是,这条射线就无法再去照亮别的物体了。同时,挡住这条光线的物体会向附近的物体投射阴影。也就是说,阴影是光线无法到达的区域。
在 Unity 的实时渲染中,我们采用的是 Shadow Map 技术。关于 Shadow Map,我们可以 WIKI 上看到它的解释。
大意为:Shadow Map 是 Lance Williams 先生在 1978 年提出的技术,从此之后,它常用于预渲染和实时场景中。在 Unity 中,就是先把摄像机的位置与光源重合,然后摄像机看不到的区域就是阴影。这样理解是不是很简单?
我们先来看看 Shadow Map 是如何定义其工作原理的:
Algorithm overview
Rendering a shadowed scene involves two major drawing steps. The first produces the shadow map itself, and the second applies it to the scene. Depending on the implementation (and number of lights), this may require two or more drawing passes.
简单地说就是 : ① 生成阴影纹理 ② 在场景中使用阴影纹理
比如:在前向渲染中,如果平行光开启了阴影(要注意需要手动开启,创建了一个新光源,默认是没有阴影的)
Unity 就会为这个平行光计算阴影映射纹理。这张阴影映射纹理实质就是一张深度纹理,记录着从光源出发,距离光源最近的表面信息。
通常情况下,是通过调用 Base Pass 和 Additional Pass 来更新深度信息,但我们之前也说过,这两个 Pass 中包含了各种光照计算。为了避免多余的光照计算所造成的性能损耗,Unity 选择使用另外一个特别的 Pass 来管理光源的映射纹理。这个 Pass 就是 LightMode 标签中设置为 Shadow Caster 的那个 Pass。这个 Pass 的渲染目标是深度纹理。所以当一个光源开启了阴影效果之后,引擎就会在当前渲染物体的 shader 中寻找这个 Pass ,如果找不到,就去 Fallback 里面找;还找不到,就去 Fallback 的 Fallback 里面找。如果这样都找不到,那么该物体就无法向其它物体投射阴影,但是可以接收来自其它物体的阴影。
文字有点多,总结一下:
- 如果想要一个物体接收其它的物体的阴影,就要在 shader 中对阴影映射纹理进行采样,把采样结果和光照结果相乘得到阴影效果。
- 如果想要一个物体向其它物体投射阴影,就要把该物体加入到阴影映射纹理之中,这一步骤是在 Shadow Pass 中实现的。
- 如果想要一个光源产生阴影效果,则需要手动选择阴影类型:No Shadows , Hard Shadows , Soft Shadows。Hard Shadows 相对于 Soft Shadows 计算量少一些,能满足大部分场景,边缘不平滑,锯齿明显。
2.2 普通非透明物体阴影的实现
这一节我们来实现对一个不透明的物体的阴影处理,包括让它投射阴影和接收阴影。
2.2.1 准备工作
创建场景,去掉默认的天空盒子;新建一个 Material 和 一个 shader,命名为 Shadow;创建一个 Cube 和两个 plane,位置摆放如下;开启平行光的阴影;新建的 shader 中使用我上一篇最后给出的前向渲染的代码。
看到上面的图,不知道读者有没有一种细思极恐的感觉,因为上图有两处诡异的地方。
- 前文说过,需要一个 ShadowCaster 的 Pass 来处理阴影,但是在上一篇中实现的前向渲染的代码中,我们并没有定义这样的一个 Pass,也没有做出对阴影处理的操作,那么为什么正方体会有阴影呢?
- 可以确定的是两个 plane 都开启了投射阴影和接收阴影,图中就可以看到地面上的 plane 接收了正方体的阴影,那么,为什么右边的 plane 没有投影呢?
其实这是两个需要注意的地方
- 前文说过,当 shader 中没有 ShadowCaster 的 Pass 时会去它的 Fallback 里面找,我们之前的 Fallback 为 Specular,Specular 中也没有这个 Pass,最后在某个角落找到了它。想看源码的读者,可以在 Unity 官方下载 内置着色器 ,解压之后,在 DefaultResourcesExtra 文件夹中的 Normal-VertexLit 这个 shader 中找到以下代码。
1 // Pass to render object as a shadow caster 2 Pass 3 { 4 Name "ShadowCaster" 5 Tags { "LightMode" = "ShadowCaster" } 6 7 CGPROGRAM 8 #pragma vertex vert 9 #pragma fragment frag 10 #pragma target 2.0 11 #pragma multi_compile_shadowcaster 12 #pragma multi_compile_instancing // allow instanced shadow pass for most of the shaders 13 #include "UnityCG.cginc" 14 15 struct v2f { 16 V2F_SHADOW_CASTER; 17 UNITY_VERTEX_OUTPUT_STEREO 18 }; 19 20 v2f vert( appdata_base v ) 21 { 22 v2f o; 23 UNITY_SETUP_INSTANCE_ID(v); 24 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); 25 TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) 26 return o; 27 } 28 29 float4 frag( v2f i ) : SV_Target 30 { 31 SHADOW_CASTER_FRAGMENT(i) 32 } 33 ENDCG 34 35 }
所以事实上,我们之前实现的 ForwardRendering 也是可以产生阴影效果的。
- 右边的 plane 确定是打开了 CasterShadow 的,而且 shader 为 Unity 内置标准shader。而它却无法投射阴影是因为在默认情况下,在计算光源的阴影映射纹理时剔除物体背面。而内置的平面其实只有一个面。不过我们可以设置为 Two Sided,就可以令物体的所有面都计算光照信息了。
这样就看到阴影了。不过还是有一个奇怪的地方,那就是正方体为什么没有接收右边平面的阴影?因为 ForwardRendering 里面没有对接收的阴影做出处理。我们接下来就要完善这一步了。
2.2.2 接收阴影
我们开始对 Shadow 改造
I. 添加一个头文件,我们计算阴影所需要的宏都是在这个文件中声明的
II. 在输出结构体添加一个内置宏
这个宏用于声明对阴影纹理采样的坐标,参数为下一个可用的插值寄存器的索引,在上面,我们在 worldNormal 和 worldPos 都使用了一个,所以此时这个宏的参数应该为2
III. 在顶点着色器中添加一个宏
TRANSFER_SHADOW 这个宏会计算上一步定义的阴影纹理坐标。我们可以在 AutoLight 中看到它的定义
IV. 在片元着色器中计算阴影,同样使用一个内置宏
V. 将得到的阴影值与漫反射颜色,高光反射颜色相乘
VI. 保存,查看效果
可以看到,正方体已经接收到了右边平面的阴影。
此时,读者可能会有疑惑,上面步骤中那些代码应该添加在哪里,因为前向渲染中我们定义了两个 Pass,Base Pass 和 Additional Pass。事实上,两个 Pass 对阴影处理的原理是一样的,上面的步骤,我只对 Base Pass 做了修改,但这是不够完善的,所以接下来,我们来介绍完整的阴影管理。
在这里,还需注意的是 SHADOW_COORDS,TRANSFER_SHADOW,SHADOW_ATTENUATION 这三个宏会根据不同的情况有不同的定义
1 // ---- Screen space direction light shadows helpers (any version) 2 #if defined (SHADOWS_SCREEN) 3 4 #if defined(UNITY_NO_SCREENSPACE_SHADOWS) 5 UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture); 6 #define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) ); 7 inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord) 8 { 9 #if defined(SHADOWS_NATIVE) 10 fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz); 11 shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r); 12 return shadow; 13 #else 14 unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy); 15 // tegra is confused if we use _LightShadowData.x directly 16 // with "ambiguous overloaded function reference max(mediump float, float)" 17 unityShadowCoord lightShadowDataX = _LightShadowData.x; 18 unityShadowCoord threshold = shadowCoord.z; 19 return max(dist > threshold, lightShadowDataX); 20 #endif 21 } 22 23 #else // UNITY_NO_SCREENSPACE_SHADOWS 24 UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture); 25 #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos); 26 inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord) 27 { 28 fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord); 29 return shadow; 30 } 31 32 #endif 33 34 #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; 35 #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) 36 #endif
当然,你可以不必过于深究,但需要注意的是,这三个宏是天生的三个好基友,如果关闭了阴影,那么 SHADOW_COORDS,TRANSFER_SHADOW 会不起作用,而 SHADOW_ATTENUATION 的值为 1 。那么漫反射颜色和高光反射颜色不受 shadow 影响。而且这些宏会使用 v.vertex 和 a.pos 等变量来计算,所以 a2v 顶点坐标变量必须为 vertex,输入结构体 a2v 必须命名为 v ,且 v2f 中顶点位置坐标为 pos。
2.2.3 完善的的光照衰减和阴影管理
在之前实现前向渲染的时候,我们为了得到光照衰减值,对不同光源做了判断,然后将得到的结果与反射颜色相乘,在这一点上,阴影的计算过程类似。而幸运的是,Unity 为我们提供了一个内置宏 UNITY_LIGHT_ATTENUATION 来同时得到光照衰减因子和阴影值。我们可以在 AutoLight 中找到它的定义
1 #define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) 2 unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; 3 fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); 4 fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow; 5 #endif
接下来,我们用它替换 Base Pass 和 Additional Pass 中对光照衰减和阴影的计算。代码和之前大部分都是一样的,所以这里就不分步讲解,给出完整代码。读者可以自行实现一下。
1 Shader "Unity/01-Shadow" 2 { 3 Properties 4 { 5 _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) 6 _Specular ("Specular", Color) = (1, 1, 1, 1) 7 _Gloss ("Gloss", Range(8.0, 256)) = 20 8 } 9 SubShader 10 { 11 Tags { "RenderType"="Opaque" } 12 13 Pass { 14 15 Tags { "LightMode"="ForwardBase" } 16 17 CGPROGRAM 18 19 #pragma multi_compile_fwdbase 20 21 #pragma vertex vert 22 #pragma fragment frag 23 24 #include "Lighting.cginc" 25 #include "AutoLight.cginc" 26 27 fixed4 _Diffuse; 28 fixed4 _Specular; 29 float _Gloss; 30 31 struct a2v { 32 float4 vertex : POSITION; 33 float3 normal : NORMAL; 34 }; 35 36 struct v2f { 37 float4 pos : SV_POSITION; 38 float3 worldNormal : TEXCOORD0; 39 float3 worldPos : TEXCOORD1; 40 SHADOW_COORDS(2) 41 }; 42 43 v2f vert(a2v v) { 44 v2f o; 45 o.pos = UnityObjectToClipPos(v.vertex); 46 47 o.worldNormal = UnityObjectToWorldNormal(v.normal); 48 49 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; 50 51 TRANSFER_SHADOW(o); 52 return o; 53 } 54 55 fixed4 frag(v2f i) : SV_Target { 56 fixed3 worldNormal = normalize(i.worldNormal); 57 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); 58 59 UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos); 60 61 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 62 63 fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); 64 65 fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); 66 fixed3 halfDir = normalize(worldLightDir + viewDir); 67 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); 68 69 70 71 return fixed4(ambient + (diffuse + specular) * atten, 1.0); 72 } 73 74 ENDCG 75 } 76 77 Pass { 78 79 Tags { "LightMode