Unity-Shader读书笔记(8)
更复杂的光照
之前的Shader代码,我们都默认场景中只有一个平行光。在实际开发游戏的过程中(一般指大一点的3d游戏,我工作到现在做的项目都是只用一个平行光的。希望我能去做一个大一点的项目吧。2023.5.9),我们就要处理更加复杂的光源。更重要的是,在这里我们要得到物体的阴影。
Unity的渲染路径
在Unity中,渲染路径(Rendering
Path)觉得了光照是如何应用到Unity
Shader中的。因此,对于每一个Pass我们都要指定好它使用的渲染路径,这样引擎才会知道这个Pass所要的光源和处理后的光照信息。有关渲染路径的更多信息,大家可以查看Unity官网中的文章:内置渲染管线中的渲染路径
- Unity
手册 。渲染路径的设置可以在项目对全局的摄像机进行设置,也可以对摄像机单独设置(我不知道不同的Unity版本是否有差异,所以大家针对自己的Unity版本百度一下吧。比如2021.3LTS版本设置渲染路径的官网文章:Graphics
- Unity
手册 ,其实Unity'2019.4.0f1也是在这里设置的。而摄像机的设置就是在摄像机的inspector窗口下就有Rendering
Path的选项,在Unity官方对于Camera类的描述中也有rendering path的选项Camera
- Unity 脚本 API 。)。
完成上面的设置后,我们就可以在Pass中使用LightMode标签来指定Pass使用的渲染路径了。关于LightMode更多的信息,大家可以看Unity官网的内容:ShaderLab:为通道分配标签。
- Unity 手册 ,ShaderLab:内置渲染管线中的预定义通道标签
- Unity
手册 。当我们设定完后,Unity就会给这个Pass对应的光源信息。
前向渲染路径
前向渲染的原理:
每进行一个完整的前向渲染,我们需要渲染该对象的渲染图元。并计算两个缓冲区的信息:颜色缓冲区和深度缓冲区。深度缓冲区用来做深度测试决定这个片元是否可见,可见则更新颜色缓冲区的信息。对于每个逐像素光源,我们都需要进行上面一次完整的流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照的结果混合起来得到最终的颜色值。
Unity中的前向渲染
在Unity中,前向渲染路径有3钟处理光照(即照亮物体)的方式:逐顶点处理,逐像素处理,球谐函数处理。决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指其是平行光还是点光源等,而光源类型指的是该光源是否重要。如果我们设置为重要(在light组件的Inspector窗口中的render
mode中设置,默认是auto。),那么Unity就会当其是逐像素的光源来处理(引擎对逐像素的光源有数量上的限制)。更多的关于Unity中的前向渲染的介绍大家可以看一下官网中的描述:Forward
rendering path - Unity 手册 。
有几点是我们需要注意的:
我们除了要设置Pass标签外,我们还要设置#pragma
multi_compile_fwdbase给前向渲染的Base Pass和#pragma
multi_compile_fwdadd给前向渲染的Additional
Pass。我们这样做后才能得到光照的正确信息。(Declaring
and using shader keywords in HLSL - Unity
手册 。有关这两个编译指令,我在官网上只能查到这篇问题有说明。)
环境光和自发光是在Base
Pass中计算的。对于一个物体来说,环境光和自发只要计算一次就够了。如果在Additional
Pass中计算,则会造成多次叠加环境光和自发光。
在Additional
Pass中,我们还开启了混合模式。这是因为我们希望每个Additional
Pass可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终有多个光照的渲染效果。如果我们不开启,那么Additional
Pass就会覆盖之前的渲染结果,看起来就好像该物体就只有受到这一个光源的影响。通常情况下,我们旋转的混合模式是Blend
One One。
在前向渲染中,Unity Shader通常只会定义一个Base
Pass(双面渲染的时候就需要两个Base Pass)以及一个Additional
Pass。一个Base Pass只会执行一次,但是一个Additional
Pass执行的次数为影响该物体的逐像素光源的数目。
渲染路径的设置用于告诉Unity该Pass在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量。对于这些内置变量的介绍大家可以看官网中的描述内置着色器变量
- Unity
手册 (前向渲染内置变量的描述在关于光照变量的介绍中)。还有前向渲染中可以使用的内置光照函数:内置着色器
helper 函数 - Unity 手册 。
顶点照明渲染路径
顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。实际上,它仅仅是前向渲染路径的一个子集,也就是说,所有可以在顶点照明渲染路径中实现的功能都可以在前向渲染路径中完成。就如它的名字一样,顶点照明渲染路径只是使用了逐顶点的方式来计算光照,并没有什么神奇的地方。实际上,我们在上面的前向渲染路径中也可以计算一些逐顶点的光源。但如果选择使用顶点照明渲染路径,那么
Unity
会只填充那些逐顶点相关的光源变量,意味着我们不可以使用一些逐像素光照变量。(照抄原文)
Unity中的顶点照明渲染
顶点照明渲染只需一个Pass,在这个Pass中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的。虽然顶点照明渲是Unity中最快的渲染路径且被很多硬件支持,但是游戏机上不支持这种路径(这是书中的原话,不知道现在是否支持。我想这个比较low可能现在也不支持吧)。其可访问的内置变量和函数,大家可以看一下官网文章内置着色器变量
- Unity
手册 (内置变量的描述在关于光照变量的介绍中),内置光照函数:内置着色器
helper 函数 - Unity 手册 。
延迟渲染路径
前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如,如果我们在场景的某一块区域放置了多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,我们就需要为该区域内的每个物体执行多个Pass
来计算不同光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得到最终的光照。然而,每执行一个
Pass
我们都需要重新渲染一遍物体,但很多计算实际上是重复的。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为G缓冲(G-buffer),其中G是英文Geometry
的缩写。G缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。(再次照抄原文,其实之前也有啦,毕竟读书笔记嘛。后面很多也会照抄原文,因为很多都是概念上的东西)
延迟渲染的原理
延迟渲染主要包含了两个Pass。在第一个Pass
中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个
Pass
中,我们利用G缓冲区的各个片元信息例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。
Unity中的延迟渲染
延迟渲染的效率和场景中包含的光源数目是没有关系的。换句话说,延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张
2D 图像,我们的计算实际上就是在这些图像空间中进行的。
更多的信息大家可以看一下官网的描述:延迟着色渲染路径
- Unity 手册 。
关于其内置变量和函数大家可以看一下官网文章内置着色器变量
- Unity
手册 (内置变量的描述在关于光照变量的介绍中),内置光照函数:无。
Unity的光源类型
Unity一共支持4种光源类型:平行光、点光源、聚光灯和面光源(area
light,这里不做介绍)。官网的介绍文章:光源类型
- Unity 手册 。
光源类型介绍
平行光:平行光是最简单的光源,它的照亮范围是没有限制的。所以它通常是作为太阳这样的角色在场景中出现的。它之所以简单是位置对它而言毫无意义,只有旋转才会改变它产生的效果。正因如此平行光也不存在衰减的概念,即它的光照强度不会因为距离的改变而改变。
点光源:点光源照亮的空间是有限制,它可以表示由一个点发出向所有方向延伸的光。点光源是会衰减的。随着物体离点光源越远,物体接受到的光照强度也会减少。点光源中心光照强度最大,边界最小(值为0)。而其中的衰减值可以由一个函数定义。
聚光灯:聚光灯照亮的空间同样也是有限制的,但聚光灯照亮的区域是锥形区域。聚光灯中心光照强度最大,边界最小(值为0)。只是聚光灯的衰减值判断更加负责。
在Unity
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 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 Shader "Learn/ForwardRenderShader" { Properties { _DiffuseColor ("DiffuseColor", Color) = (1 ,1 ,1 ,1 ) _SpecularColor ("SpecularColor",Color) = (1 ,1 ,1 ,1 ) _Gloss ("Gloss",Range(8 ,255 )) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD0; }; float4 _DiffuseColor; float4 _SpecularColor; fixed _Gloss; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld,v.vertex); fixed3 worldNormal = normalize (mul(v.normal,(float3x3)unity_WorldToObject)); o.worldNormal = worldNormal; return o; } fixed4 frag (v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz); fixed3 reflectDir = reflect (-worldLight,i.worldNormal); reflectDir = normalize (reflectDir); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot (i.worldNormal,worldLight)); fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow (saturate(dot (viewDir,reflectDir)),_Gloss); fixed3 color = ambient + diffuse + specular; return fixed4(color,1 ); } ENDCG } Pass { Tags { "LightMode"="ForwardAdd" } Blend one one CGPROGRAM #pragma multi_compile_fwdadd #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD0; }; float4 _DiffuseColor; float4 _SpecularColor; fixed _Gloss; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld,v.vertex); fixed3 worldNormal = normalize (mul(v.normal,(float3x3)unity_WorldToObject)); o.worldNormal = worldNormal; return o; } fixed4 frag (v2f i) : SV_Target { #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz); fixed atten = 1.0 ; #else fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz - i.worldPos); float3 lightCoord = mul(unity_WorldToLight,float4(i.worldPos,1 )).xyz; fixed atten = tex2D(_LightTexture0,dot (lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL; #endif fixed3 reflectDir = reflect (-worldLight,i.worldNormal); reflectDir = normalize (reflectDir); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot (i.worldNormal,worldLight)); fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow (saturate(dot (viewDir,reflectDir)),_Gloss); fixed3 color = diffuse + specular; return fixed4(color * atten,1 ); } ENDCG } } FallBack "SPECULAR" }
代码中的第一个Pass就是前向渲染中的Base
Pass。这个是由我们设置的LightMode标签决定的。第一个Pass是ForwardBase,第二个Pass是ForwardAdd。所以第一个Pass是前向渲染的Base
Pass,第二个则是Additional Pass。
第一个Pass中我们使用了#pragma
multi_compile_fwdbase指令来得到Base
Pass正确的光照变量,第二个Pass中用#pragma
multi_compile_fwdadd指令来得到Additional Pass正确的光照变量。Base
Pass中的光照计算和我们之前的计算没有太多的差别,其实书中还是有点差别的。书中还是加入了atten变量,但是因为我们的主要光源是平行光atten必定为1。所以我觉得这个没必要加上去。如果场景中有多个平行光,Unity会选择最亮的平行光传递给Base
Pass。其他的平行光则会在Additional
Pass中进行计算。如果没有平行光,那么Base Pass会当成全黑光源处理。
Additional
Pass,我们不需要再次计算自发光(代码中其实没有体现,但如果有还是不能再Additional
Pass中计算)和环境光,并且我们需要开启混合模式(这些理由之前都有讲过了)。只要不计算这个两变量,那么Additional
Pass和Base Pass处理光照的方式差不多。但是在Additional
Pass中我们需要判断光源的类型。本来对于每一个光源类型我们要用对应的方法计算光照衰减。这样往往涉及比较复杂的计算,因此Unity选择了使用一张纹理作为查找表(Lookup
Table,LUT)。我们使用这个查找表就可以得到光源中的衰减。那么我们只要考虑这个光源是否为平行光就可以了。因为平行光必为1,而其他的光源用查找表就好了。我们使用USING_DIRECTIONAL_LIGHT内置宏来判断是否为平行光(关于这个内置宏,我没有在Unity官网中找到描述,但是你可以在UnityShaderVariables.cginc找到它的定义)。所以Shader中才有这段代码:
1 2 3 4 5 6 7 8 #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz); fixed atten = 1.0 ; #else fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz - i.worldPos); float3 lightCoord = mul(unity_WorldToLight,float4(i.worldPos,1 )).xyz; fixed atten = tex2D(_LightTexture0,dot (lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL; #endif
而非平行光的光照方向就要用光源位置减去世界空间下的当前顶点的坐标得到。如果我们要得到光照衰减值,我们要先得到这个顶点在光源空间下的位置然后使用这个位置信息得到查找表中的衰减信息(如果大家想知道为什么公式是这样的可以《Unity
Shader
入门精要》读书笔记_ndc坐标转换到世界坐标为什么要除w_菜猫汤姆的博客-CSDN博客 )。
Unity光照衰减
之前的代码中我们提到Unity使用了一张纹理作为查找表在片元着色器中计算逐像素光照衰减。这样的好处是我们并不需要去做复杂的数学计算,但这样也会有些弊端。如:1.
需要预处理得到采样纹理,而且纹理大小也会影响衰减的精度。2.
不直观,同时也不方便,因此一旦把数据存储到查找表中,我们就无法使用其他数学公式来计算衰减。我们需要注意的是如果光源开启了cookie,那么衰减查找纹理是_LightTextureB0。
Unity的阴影
阴影如何实现
在实时渲染中,我们常用Shadow
Map的技术来实现。它会将摄像机的位置放置到与光源重合的地方,那么场景中该光源的阴影就是那些摄像机看不到的地方。而Unity就有这样的技术。
在前向渲染中,如果场景中最重要的平行光开启了阴影,Unity就会为这个光源计算它的阴影映射纹理。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
计算过程中我们使用一个新的Pass来获取距离光源最近的表面位置。而这个Pass要将LightMode设置为ShadowCaster。我们用这个Pass的目的就是为了阴影映射纹理(或者深度纹理)。Unity会将摄像机放置到光源的位置上,然后调用该Pass,通过顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。如果光源开启了阴影效果后,引擎首先会在当前要渲染物体的Unity
Shader中找到LightMode为ShadowCaster的Pass,如果没有它就会在Fallback指定的Unity
Shader中继续找LightMode为ShadowCaster的Pass。如果最后还是没有找到,物体就无法向其他物体投射阴影(它仍然可以接收到来自其他物体的阴影)。当找到一个LightMode为ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。
Unity使用了屏幕空间的阴影映射技术(Screen Space Shadow
Map)。需要注意的是并不是所有的平台都支持这个技术,如果平台不支持那么就要用传统的阴影技术。我们使用这个技术时,Unity首先会调用LightMode为ShadowCaster的Pass来得到可投射的阴影纹理以及摄像机的深度纹理。然后根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转到阴影映射纹理中的深度值,就是说表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了所有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需在Shader中对阴影图进行采样。由于阴影图是在屏幕空间下的,因此,我们首先需要把表面坐标从模型空间转到屏幕空间,然后使用这个坐标对阴影纹理进行采样。
所以对于一个物体来说接收其他物体的阴影和向其他物体投射阴影是两个过程。
不透明物体的阴影
我们先准备场景两个Plane和一个Cube,光源旋转角度大家可以自行设置。最终摆成下图的样子。
然后让light组件开启Shadow类型soft
shadow并且将其Strength属性设置为0.3(如果是1的话,太黑了)。物体的Mesh
Render也要开启Cast Shadows和Receive Shadows。对于Plane物体,我们要讲Cast
Shadows设置为Tow sided,不然Plane会有一面不会有阴影。
对于让物体投射阴影,书中只是介绍了官方的Shader实现。所以我这里也是直接跳过不做。大家其实可以直接看官网的文章来了解阴影投射:自定义着色器基础
- Unity 手册 。接下来的代码就是让物体接收阴影:
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 Shader "Learn/ShadowAllLightShader" { Properties { _DiffuseColor ("DiffuseColor", Color) = (1 ,1 ,1 ,1 ) _SpecularColor ("SpecularColor",Color) = (1 ,1 ,1 ,1 ) _Gloss ("Gloss",Range(8 ,255 )) = 20 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD0; SHADOW_COORDS(2 ) }; float4 _DiffuseColor; float4 _SpecularColor; fixed _Gloss; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld,v.vertex); fixed3 worldNormal = normalize (mul(v.normal,(float3x3)unity_WorldToObject)); o.worldNormal = worldNormal; TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { fixed shadow = SHADOW_ATTENUATION(i); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz); fixed3 reflectDir = reflect (-worldLight,i.worldNormal); reflectDir = normalize (reflectDir); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot (i.worldNormal,worldLight)); fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow (saturate(dot (viewDir,reflectDir)),_Gloss); fixed3 color = diffuse + specular; color = color * shadow; color = color + ambient; return fixed4(color,1 ); } ENDCG } Pass { Tags { "LightMode"="ForwardAdd" } Blend one one CGPROGRAM #pragma multi_compile_fwdadd_fullshadows #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD0; SHADOW_COORDS(2 ) }; float4 _DiffuseColor; float4 _SpecularColor; fixed _Gloss; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld,v.vertex); fixed3 worldNormal = normalize (mul(v.normal,(float3x3)unity_WorldToObject)); o.worldNormal = worldNormal; TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz); #else fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz - i.worldPos); #endif UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos); fixed3 reflectDir = reflect (-worldLight,i.worldNormal); reflectDir = normalize (reflectDir); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot (i.worldNormal,worldLight)); fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow (saturate(dot (viewDir,reflectDir)),_Gloss); fixed3 color = diffuse + specular; return fixed4(color * atten,1 ); } ENDCG } } FallBack "Specular" } return fixed4(color * atten,1 ); } ENDCG } } FallBack "Specular" }
在Shader中Base
Pass中我们使用了三个内置宏:SHADOW_COORDS、TRANSFER_SHADOW 和
SHADOW_ATTENUATION
宏。SHADOW_COORDS的参数是需要是下一个可用的差值寄存器的值,在上面的例子中就是2,官网中的文章则是1。而我们使用TRANSFER_SHADOW宏的时候需要注意的是,我们必须按照TRANSFER_SHADOW宏规定的变量名去命名才可以得到正确的信息。所以我们要保证a2f结构体中的顶点坐标变量名必须是vertex,顶点着色器输出结构体a2v必须命令为v,且v2f中的顶点位置变量必须命令为pos。不然TRANSFER_SHADOW不会得到正确的结果。
在Additional
Pass中,我们使用UNITY_LIGHT_ATTENUATION函数计算光照衰减和阴影值。我们无需像之前那样去定义一个atten变量,因为UNITY_LIGHT_ATTENUATION会帮我们定义好。它第二个参数是v2f,这个参数会传递给SHADOW_ATTENUATION,用于计算阴影值。通过这个宏,我们直接就得到阴影和衰减值的乘积,这样我们就不用一个个去做单独的处理。因为要在Additional
Pass添加阴影,所以我们要将#pragma multi_compile_fwdadd替换成#pragma
multi_compile_fwdadd_fullshadows。这样Unity才会为这些额外的逐像素光源计算阴影,并传递给Shader。
透明度物体的阴影
对于不透明的物体,我们直接使用之前Unity自带的阴影Shader就可以满足大多数的阴影投射情况。但是透明物体的Shader就比较麻烦。如透明度测试,如果我们使用Unity自带的渲染,一些被我们舍弃掉的片元仍然可以投射阴影。这样的表现就比较奇怪了。但是Unity有另外一个自带Shader,其名为“Transparent/Cutout/VertexLit”可以计算透明度测试的投射阴影。我这里直接给出这个自带Shader投射阴影的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 Pass { Name "Caster" Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile_shadowcaster #pragma multi_compile_instancing // allow instanced shadow pass for most of the shaders #include "UnityCG.cginc" struct v2f { V2F_SHADOW_CASTER; float2 uv : TEXCOORD1; UNITY_VERTEX_OUTPUT_STEREO }; uniform float4 _MainTex_ST; v2f vert( appdata_base v ) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } uniform sampler2D _MainTex; uniform fixed _Cutoff; uniform fixed4 _Color; float4 frag( v2f i ) : SV_Target { fixed4 texcol = tex2D( _MainTex, i.uv ); clip( texcol.a*_Color.a - _Cutoff ); SHADOW_CASTER_FRAGMENT(i) } ENDCG }
Unity编译器中是找不到自带的Shader的源码,你必须去Unity官网下载的网站才能找到。网址如下:Download
Archive 。进入这个网站后,大家根据自己的Unity版本下载对应的Shader。
图中红色框起来的就是下载Shader源码。我下载的是Unity2019.4.40版本的,所以Unity自带透明度测试源码在:builtin_shaders-2019.4.40f1-VertexLit.shader。不同的版本源码位置和名字可能不一样吧,如果不一样大家只能百度一下或者一个个找过去了。
这里有一个问题,因为源码中使用了_Cutoff变量做为裁剪的变量名。那么我们在声明的时候也要声明一个一样的变量作为裁剪的变量名。并且我们裁剪规则要和源码中的规则一样,不然显示出来的阴影也是错误。具体透明度测试的阴影代码如下(这里我们仍然只实现了接受阴影):
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 Shader "Learn/AlphaTestWithShadowShader" { Properties { _Color ("Color",COLOR) = (1 ,1 ,1 ,1 ) _MainTex ("Texture", 2 D) = "white" {} _SpecularColor ("SpecularColor",Color) = (1 ,1 ,1 ,1 ) _Gloss ("Gloss",Range(8 ,255 )) = 20 _Cutoff("Cut Off",Range(0 ,1 )) = 0.5 } SubShader { Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "False" "RenderType" = "TransparentCutout"} Pass { Tags { "LightMode"="ForwardBase"} CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD0; float2 uv : TEXCOORD2; SHADOW_COORDS(3 ) }; float4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float4 _SpecularColor; fixed _Gloss; fixed _Cutoff; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld,v.vertex); fixed3 worldNormal = normalize (mul(v.normal,(float3x3)unity_WorldToObject)); o.worldNormal = worldNormal; o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 albedo = tex2D(_MainTex,i.uv).rgba; clip(albedo.a - _Cutoff); albedo = albedo * _Color; UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 worldLight = normalize (_WorldSpaceLightPos0.xyz); fixed3 reflectDir = reflect (-worldLight,i.worldNormal); reflectDir = normalize (reflectDir); fixed3 viewDir = normalize (_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot (i.worldNormal,worldLight)); fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow (saturate(dot (viewDir,reflectDir)),_Gloss); fixed3 color = (diffuse + specular) * atten; color += ambient; return fixed4(color,1 ); } ENDCG } } FallBack "Transparent/Cutout/VertexLit" }
虽然我们换了一个FallBack,但是我们仍然要加上#pragma
multi_compile_fwdbase。因为现在仍然是前向渲染,而我们也需要使用则个得到正确的阴影值。其他就走正常的接受阴影逻辑了。但是记得FallBack要改成"Transparent/Cutout/VertexLit",这样阴影图才会是正确的。
书中对于透明度混合的阴影解决方法是直接按照不透明来做的,即将FallBack设置为Diffuse来强制得到阴影图和阴影投射然后按照不透明物体去做接受阴影。这样效果的确不好,但是书中也没有说出解决方案。