Unity无光照假阴影Shader实现及常见问题总结
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity无光照假阴影Shader实现及常见问题总结相关的知识,希望对你有一定的参考价值。
参考技术A 1.实时光照
实时光照属于真阴影,一般来说效果是最好的,但是开销也是最大的
2.脚底放置阴影面片模拟阴影
一般是无光照小型游戏的常见解决方案,开销较小,表现形式较差,面片是死的,无法根据人物动作变化
3.通过顶点shader变换成面片模拟阴影
如上图Gif所示
优点 : 表现形式上比方案2强,阴影可跟随顶点动画,开销比实时阴影要少
缺点 : 无法在 "非平面" 使用,比如在斜坡上,会穿帮
4.通过 Projector 或者 Decal 来模拟投射阴影
优点 : 表现效果更近一步,也可以在斜面上进行投影了
缺点 : 开销也更近一步
思路
1.我们通过2个Pass来渲染,第二个Pass正常渲染角色,第一个Pass模拟渲染阴影
2.我们需要将模型的所有 Y 值压到地面高度,这样就形成了一个头顶俯视图的阴影效果
3.我们再对 XZ 方向进行偏移,偏移量根据模型原先 Y 值高度为参考做插值
4.阴影的方向我们规定在 XZ 平面上 (X=0,Z=1) 为初始默认方向,以这个向量为基准进行旋转
5.旋转我们可以通过 二维旋转矩阵 来计算
过程中遇到的问题:
1.我在对以Y值高度为参考做XZ方向偏移的插值的时候,由于Y值低于地面取到了负值而这些值有没有被裁剪掉就导致了上图的问题,然后首先想到的是对插值做 max(0,插值结果),然后我们又得到了如下结果
2.上面第一个问题好像是解决了,但是实际上人物到地面以下之后人物还是有一个顶部投影的值,即XZ平面未偏移的结果,那么解决方案自然而然的想到,通过 Y 值和地面的值比较,Y值如果小于地面那么就Clip掉当前片段 Clip( Y值_ws - 地面Y高度_ws) , ws表示世界空间,那么实际上第一个问题也就处理掉了,无需多一步max函数了
3.紧接着,我们需要考虑降低阴影的透明度,但是出现上图问题,导致该问题的原因是,由于将人物顶点变换的同一个Y值后,部分片段是有重叠,就相当于如果直接把人压扁后那么就前胸贴后背,我们需要的是前胸或者后背只渲染依次就可以了,那么自然想到了模板测试Stencil,模板缓存的初始值都为0,我们将渲染阴影通道的Pass中的Ref引用值设为1,当我们的值 [大于] 模板缓存的值就替换,那么假设我们前胸进来一看发现缓冲区里的值为0,自己是1,然后进行替换,但是后背再进来发现已经是1了,不满足[大于]的条件了,所以就不写入了...
4.紧接着,又得到了一些奇怪的问题,在我已经将Blend模式设置为SrcAlpha OneMinusSrcAlpha后发现将影子的颜色Alpha值设置为拖动到0的时候并不会让影子消失,而是颜色更深了,很奇怪,想了很久不知道为什么,但是左边就很正常,左边人物和右边人物的shader唯一差别在于:
左边Shader的两个Pass先渲染人物再渲染阴影
右边出错的Pass先渲染阴影再渲染人物
然后我尝试将左边Shader中的Pass1删除掉,只渲染阴影,我发现它也不正常了...真特娘的奇怪,而且FrameDebug逐Draw渲染的结果也有点奇怪
然后改变摄像机颜色后,发现阴影颜色跟随摄像机颜色改变,于是我发现那可能那不是阴影而是透过去的背面,于是我在地面下方放置了一个小球,然后发现确实是看到地面下方的内容了,再通过FrameDebug调试发现问题所在,将镜头拉近的时候,先渲染的小球,然后紧接着就渲染了右边人物的阴影通道,所以结果是不正确的,因为我们需要阴影渲染在地面上,但是地面还没有渲染,所以我们Blend的时候是和背后的天空盒还有小球混合了导致阴影出错的,但是问题又来了,为什么左边的就没有问题呢,原因是这样的...
地面,左侧人物,右侧人物...由于他们的渲染队列Queue都是Geometry,且值都为2000,所以他们在渲染的时候会随着距离摄像头的远近而可能出现不同的Draw绘制顺序,但是针对多通道的Shader
所以,第一个通道Pass会随着摄像机的远近而变化,但是由于左侧人物的阴影绘制是放在了第2个通道里的,所以绘制的时候,地面是肯定已经绘制完了的,而右侧人物第1个Pass就是阴影,具体是绘制在Plane之前还是之后会随着摄像机远近而不同
该问题解决方案 :
1.将右侧的阴影通道放到第二个Pass中渲染
2.手动修改右侧Shader的渲染排序"Queue" = Gemotry+1" ,让它排在Plane后面渲染
3.将"Queue" = "Transparent",这个实际和2相同,但是不清楚原理的情况下比较容易想到这个方案
Unity Shader ------ 纹理之法线纹理单张纹理及遮罩纹理的实现
笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。
在游戏中,我们除了能看到游戏物体的形体轮廓,还能看到物体的一些具体外观,包括颜色,凹凸等。而实现这一步的就是使用 纹理。与纹理相对应的技术就是 纹理映射技术 ,相当于把一张图贴在物体表面,然后 逐纹素 地控制颜色。
纹理映射坐标:纹理映射坐标定义了一个顶点在纹理中对应的2D坐标。由于常用 U 来表示横向坐标, V 来表示纵向坐标,所以纹理映射坐标也是我们常常见到的 UV坐标。 顶点 UV 坐标通常会被归一化至 【0,1】范围内。当然纹理采样时使用的坐标也不一定在这个范围内。
另外值得注意的是,OpenGL 与 DirectX 的二维坐标系是不一样的,OpenGL中原点位于左下角,DirectX原点位于左上角。当然Unity 会帮我们处理这个差异,同时一般情况下,Unity 采用的纹理空间是符合 OpenGL传统的。
需要注意的是:本文着重讲述纹理采样的原理,由于实现的shader中的光照模型计算如同上文中,并不完整。所以不能直接运用于项目
一. 单张纹理
先看一下我们要实现的效果
shader 的一些书写方式本文便不再赘述,同时本文的计算光照的方式都能够在上一篇文章中找到,如果忘了,可以先复习一下
【Unity Shader】(三) ------ 漫反射和高光反射的实现
1.1. 实现单张纹理
新建一个场景,去掉天空盒子;新建一个 Capsule 与 Material,命名为 SingleTexture;
I. 先定义 Properties 语义块
其中 _MainTex 的纹理用来表示纹理贴图,这里我们用这张纹理贴图来代替物体的漫反射颜色。
II. 为了控制 Properties 中的属性,我们在CG代码片中定义与之相匹配的变量
在Unity中,一般使用 纹理名_ST 来代表某个纹理的属性
_MainTex_ST 代表 _MainTex 这个纹理的属性:S(Scale)缩放,T(Translation)平移。
_MainTex_ST.xy 代表 缩放值;_MainTex_ST.zw 代表 偏移值
III. 定义输入输出结构体
uv 变量存储了纹理坐标,以便在片元着色器中进行采样
IV. 顶点着色器
黄色框中,我们使用了 _MainTex_ST 对顶点纹理坐标进行变换,得到最终的纹理坐标。先使用 _MainTex_ST.xy 对顶点纹理坐标进行缩放,然后使用 _MainTex_ST.zw 进行偏移。而 TRANSFORM_TEX 则是封装了这个计算方式的内置函数,我们可以在 UnityCG.cginc 中找到它的定义
很显然,参数一为顶点纹理坐标,参数二为纹理名
V. 片元着色器
此处光照模型使用的是 Blinn-Phong 模型,所以光照计算方面与之前并没有太大的差异, 如果读者对光照模型不太了解,可以翻看我的前一篇文章。
这个片元着色器主要使用 Cg 函数 tex2D(_MainTex,i.uv) 对纹理进行了采样,然后以采样结果与颜色属性相乘,乘积结果作为反射率。其余的光照计算基本无异。而关于tex2D 的解释如下
完整代码:
保存,进入Unity 查看效果。当然还有附上一张纹理。
二. 凹凸映射
另一种常见的纹理应用就是 凹凸映射 。凹凸映射就是为了使用一张纹理来修改模型表面法线,来为模型提供更多的细节。当然,这并不会真的改变模型的顶点位置,仅仅是使得模型看起来是 “不平滑的” ,更加的真实。
凹凸映射常用的两种方法:
- 高度映射。使用一张高度纹理来模拟表面位移,然后得到修改后的法线值。
- 法线映射。使用一张法线纹理直接存储表面法线。
2.1 高度纹理
高度纹理图存储是强度值,表示表面局部的海拔,颜色越浅表示越向外凸起,颜色越深表示越向内凹进去。这样就可以很形象地看出模型的凹凸,不过这样的计算会更加复杂,在实时计算时并不能直接得到表面法线,而是计算像素的灰度值得到。本文着重讲述的是法线纹理,所以高度映射技术便不再赘述了。
2.2 法线纹理
前文已经提到法线纹理存储的是表面法线方向,法线方向的分量范围在【-1.1】,而像素分量范围在【0,1】。所以为了让两者一致,我们需要做一个简单的映射。相必这个方式大家都有学过
那么可以预知的便是,当我们在shader中对法线纹理采样后,就必须对其进行反映射,得到原先的法线方向。
需要注意的是,这个方向是有着空间之异的。对于模型自带的顶点法线,则是定义在模型空间的,这个纹理称为 模型空间的法线纹理。不过,一般制作法线纹理时,我们一般会采样 切线空间(tangent space)
切线空间:对于每个顶点,它都有一个属于自己的切线空间,切线空间原点就是该顶点本身,Z 轴则是顶点法线方向。X 轴为切线方向。Y 轴可以由法线和切线叉积而得。也称为 副切线 或 副法线。而存储在切线空间的纹理则称为 切线空间的纹理。
上图是一张法线纹理。许多使用过法线纹理但不太了解其原理的朋友或许都有一个疑问:为什么普通的纹理都是红颜六色的,但是法线纹理大都像上图一样是一片蓝色的?
- 模型空间的法线纹理,所有法线的坐标都是在模型空间,每个点存储的法线方向都是各异的,经过映射之后就变成了RGB(x,y,z) ,而x,y,z并不一致,所以对应着不同的颜色。所以模型空间的法线纹理看起来是五颜六色的。
- 切线空间的法线纹理,所有法线的坐标都是在各自的切线空间,新的法线方向就是 Z 轴,即(0,0,1),经过映射就是(0.5,0.5,1)浅蓝色。所以切线空间的法线纹理看上去大部分都是蓝色的,这也说明了顶点的大部分法线是和模型本身法线一样的。
两种法线纹理的优劣·:
模型空间的法线纹理 :
① 直观,简单
② 可以提供平滑的边界部分。
切线空间的法线纹理 :
① 自由度很高:模型空间的法线纹理是 绝对法线信息 ,即只能用于创建它的那个模型,应用于它处就会出错。而切线空间的法线纹理 是 相对法线信息 ,应用于不同的网格都可以得到一个不错的效果
② 可以制作UV动画:可以通过移动UV来实现一个动画,而模型空间下的纹理则会完全错误。
③ 可以压缩。切线空间下的纹理,法线 Z 方向总是正方向,所以只存储XY方向就可以通过推导得到 Z 方向。而模型空间下的纹理则不行
④ 可以重复利用
由于法线方向存储于切线空间,所以在实际计算光照时会有两种计算方式:① 把光照方向,视角方向转换至切线空间,进行光照;② 把采样得到的法线方向转换至世界空间,计算光照;从效率角度,① 优于 ② ,从通用性来看,② 优于 ①。
本文会先给出第一种方法的实践,第二种以后我会补充回来,读者也可以自行实现。
2.3 切线空间下计算光照
新建一个材质和Capsule,命名为NormalTextureTangentSpace
I. 定义 Properties 语义块
其中 _BumpMap 表示法线纹理,_BumpScale 控制凹凸程度
II. 为了控制 Properties 中的属性,我们定义与之相匹配的变量
III. 修改输入输出结构体
因为切线空间是由顶点法线与切线构建的,所以在输入结构体添加一个切线变量,使用 TANGENT 语义。
因为我们是在切线空间下计算光照,所以在输出结构体中添加两个变量来存储转换空间后的光照方向和视角方向
IV. 定义顶点着色器
我们使用了两张纹理,所以 uv 变量修改为 float4 类型,其中,xy分量存储 _MainTex 的纹理坐标,zw分量存储 _BumpMap 的纹理坐标。然后为了对光照方向和视角方向转换至切线空间,我们需要一个变换矩阵 rotaion,而 TRANGENT_SPACE_ROTATION 则是Unity内帮我们实现了计算过程的内置宏,它会返回我们所需 rotation,我们可以在UnityCG.cginc 中找到它的定义。
V. 修改片元着色器
我们在顶点着色器中已经对光照方向和视角方向做了转换空间的工作,所以片元着色器中只需要对法线纹理进行采样,然后计算光照就可以了。tex2D 函数的定义在前文已经给出。然后使用Unity内置函数 UnpackNormal 得到正确的法线方向。然后对得到的法线向量的 xy 分量乘于 _BumpScale 就可以得到 法线的 xy 分量。再计算出 z 分量,就得到了正确的法线方向。
VI. 保存,查看效果
不同 _BumpScale 下的效果:
需要注意的是:
① 使用法线纹理时,注意其类型是否为 Normal map
如果不是,则要在 shader 里面进行以下的更改
把
更改为
如果不进行修改,Unity 也提醒你
因为如果法线纹理类型不是 Normal map 时,我们需要手动对采样结果的 xy 分量进行反映射。而如果是 Normal map 类型,则使用 UnpackNormal 函数。因为,当法线纹理类型设置成 Normal map 时,Unity 会根据平台的不同而对该法线纹理进行压缩,此时 _BumpMap 的 rgb 分量已经不是切线空间下的 xyz 分量了。所以此时再进行以上的手动计算就会得到错误的结果。
而 UnpackNormal 函数则可以在 UnityCG.cginc 中找到其定义
其中 DXT5nm 是一种压缩格式
那么,完整代码如下:
1 Shader "Unity/Custom/01-NormalTexture-Tangent Space" 2 { 3 Properties 4 { 5 _Color("Color Tint",Color) = (1,1,1,1) 6 _MainTex("Main Tex",2D) = "while"{} 7 _BumpMap("Normal Map",2D) = "bump"{} 8 _BumpScale("Bump Scale",Float) = 1.0 9 _Specular("Specular",Color) = (1,1,1,1) 10 _Gloss("Gloss",Range(8.0,256)) = 20 11 12 } 13 SubShader 14 { 15 Pass 16 { 17 Tags { "LightMode"="ForwardBase" } 18 19 20 CGPROGRAM 21 #pragma vertex vert 22 #pragma fragment frag 23 #include "Lighting.cginc" 24 #include "UnityCG.cginc" 25 26 fixed4 _Color; 27 sampler2D _MainTex; 28 float4 _MainTex_ST; 29 sampler2D _BumpMap; 30 float4 _BumpMap_ST; 31 float _BumpScale; 32 fixed4 _Specular; 33 float _Gloss; 34 35 struct a2v{ 36 37 float4 vertex : POSITION; 38 float3 normal : NORMAL; 39 float4 tangent : TANGENT; 40 float4 texcoord : TEXCOORD0; 41 }; 42 43 struct v2f{ 44 45 float4 pos : SV_POSITION; 46 float4 uv : TEXCOORD0; 47 float3 lightDir : TEXCOORD1; 48 float3 viewDir : TEXCOORD2; 49 }; 50 51 v2f vert(a2v v) 52 { 53 v2f o; 54 o.pos = UnityObjectToClipPos(v.vertex); 55 56 o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 57 o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; 58 59 TANGENT_SPACE_ROTATION; 60 61 o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; 62 o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; 63 64 return o; 65 } 66 67 fixed4 frag(v2f i) : SV_Target 68 { 69 //光源方向归一化 70 fixed3 tangentLightDir = normalize(i.lightDir); 71 //视角方向归一化 72 fixed3 tangentViewDir = normalize(i.viewDir); 73 74 //对法线纹理取样 75 fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw); 76 //切线空间下的法线 77 fixed3 tangentNormal; 78 79 //手动反映射 80 //tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale; 81 82 tangentNormal = UnpackNormal(packedNormal); 83 tangentNormal.xy *= _BumpScale; 84 85 86 87 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); 88 89 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; 90 91 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 92 93 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir)); 94 95 //计算得到矢量h 96 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); 97 98 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss); 99 100 return fixed4(ambient + diffuse + specular,1.0); 101 } 102 ENDCG 103 104 } 105 106 } 107 108 FallBack "Specular" 109 }
三. 遮罩纹理
法线纹理是十分常见且重要的纹理,讲完了法线纹理,我们现在讲另外一种非常有用的纹理:遮罩纹理
遮罩可以保护某些区域不受修改,比如我们上一篇光照原理中实现的高光反射则是对于所有像素而言的,现在我希望物体某部分更强烈一些,而另一部分则更弱一些,此时我们就可以用到遮罩纹理了。
遮罩纹理的使用流程:① 采样,得到纹素值 ② 使用其中一个或多个通道的值来与表面属性相乘 ③ 当通道的值为0时,可以保护表面不受该属性影响
现在我们来实现,对高光反射进行遮罩。计算在切线空间,代码与之前相差不多,就不赘述了
完整代码:
1 Shader "Unity/Custom/01-MaskTexture" 2 { 3 Properties 4 { 5 _Color("Color Tint",Color) = (1,1,1,1) 6 _MainTex("Main Tex",2D) = "while"{} 7 _BumpMap("Normal Map",2D) = "bump"{} 8 _BumpScale("Bump Scale",Float) = 1.0 9 _SpecularMask("Specular Mask",2D) = "while"{} 10 _SpecularScale("Specular Scale",Float) = 1.0 11 _Specular("Specular",Color) = (1,1,1,1) 12 _Gloss("Gloss",Range(8.0,256)) = 20 13 14 } 15 SubShader 16 { 17 Pass 18 { 19 Tags { "LightMode"="ForwardBase" } 20 21 22 CGPROGRAM 23 #pragma vertex vert 24 #pragma fragment frag 25 #include "Lighting.cginc" 26 #include "UnityCG.cginc" 27 28 fixed4 _Color; 29 sampler2D _MainTex; 30 float4 _MainTex_ST; 31 sampler2D _BumpMap; 32 float _BumpScale; 33 sampler2D _SpecularMask; 34 float _SpecularScale; 35 fixed4 _Specular; 36 float _Gloss; 37 38 struct a2v{ 39 40 float4 vertex : POSITION; 41 float3 normal : NORMAL; 42 float4 tangent : TANGENT; 43 float4 texcoord : TEXCOORD0; 44 }; 45 46 struct v2f{ 47 48 float4 pos : SV_POSITION; 49 float2 uv : TEXCOORD0; 50 float3 lightDir : TEXCOORD1; 51 float3 viewDir : TEXCOORD2; 52 }; 53 54 v2f vert(a2v v) 55 { 56 v2f o; 57 //o.pos = UnityObjectToClipPos(v.vertex); 58 o.pos = UnityObjectToClipPos(v.vertex); 59 60 o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 61 62 TANGENT_SPACE_ROTATION; 63 64 o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; 65 o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; 66 67 return o; 68 } 69 70 fixed4 frag(v2f i) : SV_Target 71 { 72 //光源方向归一化 73 fixed3 tangentLightDir = normalize(i.lightDir); 74 //视角方向归一化 75 fixed3 tangentViewDir = normalize(i.viewDir); 76 77 //对法线纹理贴图取样 78 fixed4 packedNormal = tex2D(_BumpMap,i.uv); 79 //切线空间下的法线 80 fixed3 tangentNormal; 81 82 tangentNormal = UnpackNormal(packedNormal); 83 tangentNormal.xy *= _BumpScale; 84 //反映射 85 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); 86 87 88 89 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; 90 91 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 92 93 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir)); 94 95 //计算得到矢量h 96 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); 97 98 //高光遮罩 99 fixed3 specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale; 100 101 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss) * specularMask; 102 103 104 return fixed4(ambient + diffuse + specular,1.0); 105 } 106 ENDCG 107 108 } 109 110 } 111 112 FallBack "Specular" 113 }
这里需要注意的是:
① 这里三张纹理共用了 _MainTex_ST ,而不是一张纹理对应一个 _ST 变量。因为随着纹理越来越多,我们会迅速占满顶点着色器中可以使用的插值寄存器。而很多时候,我们并不需要对纹理进行平铺和位移,或者很多纹理使用同一种平铺,那么我们就可以对这些纹理使用同一个纹理坐标。
② 这张遮罩图我们只使用了 r 分量,那么有很多空间都是浪费了,因为一般遮罩纹理的 rgba 存储的是不同的表面属性,善用遮罩纹理,可以创作出高自由度的材质,就可以实现更强的画面效果。
最后给出三种纹理的对比图
总结
纹理是十分重要的一环,它可以决定你看到的事物有多细腻逼真。
另外再强调一次,本文实现的 shader 仅供学习,因为光照计算并不完整,所以不能直接运用于项目之中
如果对光照不太了解的朋友,可以去翻看我的前一篇文章【Unity Shader】(三) ------ 漫反射和高光反射的实现
最后,希望本文能对你有所帮助!!!路漫漫其修远兮 !!!
以上是关于Unity无光照假阴影Shader实现及常见问题总结的主要内容,如果未能解决你的问题,请参考以下文章
Unity - Shader - Projector 高空云层底下透明阴影 - semitransparent shadow
Unity 阴影淡入淡出效果中Shader常量 unity_ShadowFadeCenterAndType和_LightShadowData的问题