本章是 catlikeCoding 渲染系列的第七章对应
无
好像都没什么前置知识,要不以后取消了好了
首先设置好我们的场景
场景中有两个光,一个是比较强的白色光,还有一个从左侧射入的黄色的稍微弱一点的方向光。
大概这样
注意!如果是看原文的朋友,这里要注意一个深渊巨坑
这里我们首先介绍的思路是 ShadowMapping。原文的编排结构里,虽然也上来也介绍了 ShadowMapping 的思路,介绍了很多关于级联阴影,Bias 之类的内容。
但是在接下来实际的代码实践中,包括 Unity5 的阴影实现方案,都用的 ScreenSpaceShadow 的方案。。。但是恕我眼拙,好像并没有说明这一点
不得不说这是相当令人迷惑的行为。。。
虽然如果理论归类来说,我们可以认为 ScreenSpaceShadow 的思路和我们接下来的 ShadowMapping 的思路差不多,但是我认为关键在于,具体转换的空间是不同的
ScreenSpaceShadow 的思路是将阴影贴图转换到屏幕空间,然后直接将顶点坐标转为屏幕空间坐标,然后再对屏幕空间下的 ShadowMap 采样
而我们先介绍的方案是:ShadowMap 是在光源空间下的深度贴图,通过将顶点坐标转到光源空间下,然后采样对比深度值
我认为这两者的思路是截然不同的。但是原文其实解释得并不清楚,这导致了一些理解上的偏差,以至于一开始我完全无法理解原文开始计算部分的数学原理
关于这一点在冯乐乐老师的《UnityShader 入门精要》也有所提及
我是看了这个才明白过来,这两者看似是同样的思路,但是事实上的实现计算,需要注意的问题,优化的方案是截然不同的
当然这只是我粗浅的理解,也许后面很快我就打脸自己了
不论如何,我们首先介绍传统的阴影映射方案,先把屏幕空间的阴影映射放到一边
我们首先介绍一下 ShadowMapping 的操作原理
简单来说,阴影的原理就是以光源的方向和位置渲染一张深度图,然后通过矩阵变换,将其他 Pass 的像素点变换到对应到这张贴图上,计算这个像素点在光源空间下的深度值,通过与深度贴图比较深度值,从而确定这一点是否被遮盖
我们会随着逐步制作的深入尽量讲解这个思路
首先我们需要投射阴影
我们通过定义 LightMode 来定义一个阴影投射器。
1 Tags {"LightMode" = "ShadowCaster"}
事实上这个 shader 几乎什么都不用写(暂时),我们甚至都不需要做任何的坐标变换。因为我们要的只是光源方向的深度贴图
1Pass
2{
3 Name "ShadowCaster"
4
5 Tags {"LightMode" = "ShadowCaster"}
6
7 HLSLPROGRAM
8
9 #pragma vertex vert
10 #pragma fragment frag
11
12 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
13 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
14
15 struct appdata
16 {
17 float4 vertex : POSITION;
18 };
19
20 struct v2f
21 {
22 float4 positionCS : SV_POSITION;
23 };
24
25 v2f vert(appdata v)
26 {
27 v2f o;
28
29 return o;
30 }
31
32 half4 frag(v2f i) : SV_Target
33 {
34 return 0.0;
35 }
36 ENDHLSL
37}
我们看一下 FrameDebugger
立马有一个绘制 ShadowCaster 的 Pass,绘制的就是从光源方向观察的的深度贴图
看这个贴图的形状和位置也看的出来是光源空间下的,而非屏幕空间下的
投射阴影是非常简单的,当然,要让我们的阴影显示出来,我们需要着重写的是接收阴影的部分。
首先需要定义两个变体,一个用来支持 MainLight 阴影,一个用来支持软阴影
1#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
2#pragma multi_compile _ _SHADOWS_SOFT
紧接着,我们只需要将我们第一个 Pass 获取 MainLight 的地方改成这样
1// 获取主光源信息
2Light mainLight = GetMainLight(TransformWorldToShadowCoord(i.worldPos));
回到场景,将主光源的阴影调成软阴影,你会发现,阴影已经有了!
是的,就是这么简单!
这里针对 urp 推荐这个参考,官方的文档。虽然没有特别清楚,但是常用的函数都列出来了。
目前对我们来说,主要的黑盒存在于 TransformWorldToShadowCoord
和 GetMainLight
这两个函数之中
我们尝试用我们的理解重新构建这个过程,来达到理解的目的
思考一下,我们需要的东西是什么?
首先我们需要一张阴影贴图,这个倒是没什么难度,Unity 已经为我们处理好了。通过查阅资料我们得知,他就是这个()
1//Shadow.hlsl
2TEXTURE2D_SHADOW(_MainLightShadowmapTexture);
我们还缺少两个东西。一个是这个像素点在光照空间下坐标(其中包含了这一点在光照空间下的深度值),另一个是这个像素点对应的阴影贴图中采样出来的深度值(实际从光照空间观察时的深度值)
然后我们通过比大小,来确定这个像素点是否在光照空间下被遮盖,如果被遮盖,那么就是阴影部分
我们首先将这个像素点转换到光照空间下,我们所用的函数是 TransformWorldToShadowCoord
。根据我们前面的参考,官方推荐使用的函数是 GetShadowCoord
看一下这个函数的实际定义
1float4 GetShadowCoord(VertexPositionInputs vertexInput)
2{
3#if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT)
4 return ComputeScreenPos(vertexInput.positionCS);
5#else
6 return TransformWorldToShadowCoord(vertexInput.positionWS);
7#endif
8}
再次声明,我们首先介绍的是传统的阴影映射方案,而非屏幕空间的阴影映射(ScreenSpaceShadowMapping)
因此我们只需要 TransformWorldToShadowCoord
这个函数,看下它的定义,只需要看黄色部分:
1float4 TransformWorldToShadowCoord(float3 positionWS)
2{
3#if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT)
4 float4 shadowCoord = float4(ComputeNormalizedDeviceCoordinatesWithZ(positionWS, GetWorldToHClipMatrix()), 1.0);
5#else
6 #ifdef _MAIN_LIGHT_SHADOWS_CASCADE
7 half cascadeIndex = ComputeCascadeIndex(positionWS);
8 #else
9 half cascadeIndex = half(0.0);
10 #endif
11
12 float4 shadowCoord = float4(mul(_MainLightWorldToShadow[cascadeIndex], float4(positionWS, 1.0)).xyz, 0.0);
13#endif
14 return shadowCoord;
15}
事实上就是一个矩阵乘法,_MainLightWorldToShadow[cascadeIndex],应该是世界空间到光源空间的变换矩阵
为了佐证这一猜想,我们可以在 Urp 的代码里找到对应的部分:在 MainLightShadowCasterPass.cs
这个文件中
我们可以直接看到部分实现
1private static class MainLightShadowConstantBuffer
2{
3 public static readonly int _WorldToShadow = Shader.PropertyToID("_MainLightWorldToShadow");
4 public static readonly int _ShadowParams = Shader.PropertyToID("_MainLightShadowParams");
5 public static readonly int _CascadeShadowSplitSpheres0 = Shader.PropertyToID("_CascadeShadowSplitSpheres0");
6 public static readonly int _CascadeShadowSplitSpheres1 = Shader.PropertyToID("_CascadeShadowSplitSpheres1");
7 public static readonly int _CascadeShadowSplitSpheres2 = Shader.PropertyToID("_CascadeShadowSplitSpheres2");
8 public static readonly int _CascadeShadowSplitSpheres3 = Shader.PropertyToID("_CascadeShadowSplitSpheres3");
9 public static readonly int _CascadeShadowSplitSphereRadii = Shader.PropertyToID("_CascadeShadowSplitSphereRadii");
10 public static readonly int _ShadowOffset0 = Shader.PropertyToID("_MainLightShadowOffset0");
11 public static readonly int _ShadowOffset1 = Shader.PropertyToID("_MainLightShadowOffset1");
12 public static readonly int _ShadowmapSize = Shader.PropertyToID("_MainLightShadowmapSize");
13 public static readonly int _MainLightShadowmapID = Shader.PropertyToID(k_MainLightShadowMapTextureName);
14}
追着看,我们找到 DirectionalLight 的矩阵计算
1/// <summary>
2/// Extracts the directional light matrix.
3/// </summary>
4/// <param name="cullResults">The results of a culling operation.</param>
5/// <param name="shadowData">Data containing shadow settings.</param>
6/// <param name="shadowLightIndex">The visible light index.</param>
7/// <param name="cascadeIndex">The cascade index.</param>
8/// <param name="shadowmapWidth">The shadow map width.</param>
9/// <param name="shadowmapHeight">The shadow map height.</param>
10/// <param name="shadowResolution">The shadow map resolution.</param>
11/// <param name="shadowNearPlane">Near plane value to use for shadow frustums.</param>
12/// <param name="cascadeSplitDistance">The culling sphere for the cascade.</param>
13/// <param name="shadowSliceData">The struct container for shadow slice data.</param>
14/// <returns>True if the matrix was successfully extracted.</returns>
15public static bool ExtractDirectionalLightMatrix(ref CullingResults cullResults, UniversalShadowData shadowData, int shadowLightIndex, int cascadeIndex, int shadowmapWidth, int shadowmapHeight, int shadowResolution, float shadowNearPlane, out Vector4 cascadeSplitDistance, out ShadowSliceData shadowSliceData)
16{
17 bool success = cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(shadowLightIndex,
18 cascadeIndex, shadowData.mainLightShadowCascadesCount, shadowData.mainLightShadowCascadesSplit, shadowResolution, shadowNearPlane, out shadowSliceData.viewMatrix, out shadowSliceData.projectionMatrix,
19 out shadowSliceData.splitData);
20
21 cascadeSplitDistance = shadowSliceData.splitData.cullingSphere;
22 shadowSliceData.offsetX = (cascadeIndex % 2) * shadowResolution;
23 shadowSliceData.offsetY = (cascadeIndex / 2) * shadowResolution;
24 shadowSliceData.resolution = shadowResolution;
25 shadowSliceData.shadowTransform = GetShadowTransform(shadowSliceData.projectionMatrix, shadowSliceData.viewMatrix);
26
27 // It is the culling sphere radius multiplier for shadow cascade blending
28 // If this is less than 1.0, then it will begin to cull castors across cascades
29 shadowSliceData.splitData.shadowCascadeBlendCullingFactor = 1.0f;
30
31 // If we have shadow cascades baked into the atlas we bake cascade transform
32 // in each shadow matrix to save shader ALU and L/S
33 if (shadowData.mainLightShadowCascadesCount > 1)
34 ApplySliceTransform(ref shadowSliceData, shadowmapWidth, shadowmapHeight);
35
36 return success;
37}
我还没搞清楚这个 shadowSliceData** **的来源,姑且认为是光源的一些变换矩阵吧
里面的计算
1static Matrix4x4 GetShadowTransform(Matrix4x4 proj, Matrix4x4 view)
2{
3 // Currently CullResults ComputeDirectionalShadowMatricesAndCullingPrimitives doesn't
4 // apply z reversal to projection matrix. We need to do it manually here.
5 if (SystemInfo.usesReversedZBuffer)
6 {
7 proj.m20 = -proj.m20;
8 proj.m21 = -proj.m21;
9 proj.m22 = -proj.m22;
10 proj.m23 = -proj.m23;
11 }
12
13 Matrix4x4 worldToShadow = proj * view;
14
15 var textureScaleAndBias = Matrix4x4.identity;
16 textureScaleAndBias.m00 = 0.5f;
17 textureScaleAndBias.m11 = 0.5f;
18 textureScaleAndBias.m22 = 0.5f;
19 textureScaleAndBias.m03 = 0.5f;
20 textureScaleAndBias.m23 = 0.5f;
21 textureScaleAndBias.m13 = 0.5f;
22 // textureScaleAndBias maps texture space coordinates from [-1,1] to [0,1]
23
24 // Apply texture scale and offset to save a MAD in shader.
25 return textureScaleAndBias * worldToShadow;
26}
事实上也很简单,除去 ReverseZBuffer 的计算之外,就是将 PV 矩阵相乘,然后再乘上一个缩放 + 平移的 textureScaleAndBias 矩阵来让坐标从-1,1 变换到 0,1(整体*0.5+0.5)
之后,我们进行一个采样
1mainLight.shadowAttenuation = SAMPLE_TEXTURE2D_SHADOW(_MainLightShadowmapTexture, sampler_LinearClampCompare , TransformWorldToShadowCoord(i.worldPos).xyz);
这是 hlsl 为我们提供的一个方法,其实是融合了阴影贴图的采样,它的宏这样定义
1#define SAMPLE_TEXTURE2D_SHADOW(textureName, samplerName, coord3) textureName.SampleCmpLevelZero(samplerName, (coord3).xy, (coord3).z)
看这个函数定义 textureName.SampleCmpLevelZero(samplerName, (coord3).xy, (coord3).z)
,其实就是用 xy 坐标采样了阴影贴图之后得到深度,与顶点自己的深度 z 来做对比
(深度可能不准确,但是大概这么个意思)
如果采样出来的点比转换过去的顶点更靠近光源,那么说明这一点就被遮盖在阴影之中了,返回 0.反之则返回 1.
之后再将这个给光源强度,用来实现阴影。
到此,整个流程完成
未完待续
未完待续
不一定写,看情况
不一定写,看情况
总觉得这个翻译非常搞笑
一些神奇优化
感觉应该单独开一章节
一个大佬的访谈,主要讲 VirtualShadowMap
https://www.bilibili.com/video/BV1Q24y1g7mq/?spm_id_from=333.1391.0.0&vd_source=fac8a31b7aac66f52e0568883e6013c7
好多年前的原神的主机端优化分享,阴影部分讲了一些东西