TA不一样(十三)

TA不一样(十三)
前言本节实现各种阴影包括平行光阴影聚光灯和点光源阴影软阴影。平行光阴影1.实现原理ShadowMap的实现主要分为两个pass第一个pass:在光源位置创建一个深度摄像机以光源位置作为视点将每个像素点的深度值(z-depth)也就是距离光源最近的对象距离记录在 Z-buffer 中生成深度图第二个pass:从正常摄像机渲染场景将每个fragment 到光源的距离和 Shadow Map 中保存的深度值进行比较如果大于后者则说明被其他物体遮挡处于阴影之中2.编写代码注意点在顶点输出数据结构中添加SHADOW_COORDS(index)(声明阴影纹理坐标),index为可用的空闲寄存器序号如SHADOW_COORDS(4)相当于float4 _ShadowCoord : TEXCOORD4在顶点着色器使用TRANSFER_SHADOW(o)宏。它会根据当前的光照模式和平台特性计算出正确的阴影采样坐标并赋值在片元着色器这使用SHADOW_ATTENUATION(i)或UNITY_LIGHT_ATTENUATION(atten, i, worldPos)宏。这些宏会使用之前计算的_ShadowCoord对阴影贴图进行采样返回一个阴影衰减因子0 表示完全在阴影中1 表示完全受光为了让SHADOW_COORDS和相关宏正常工作你通常需要引入头文件‌(#include AutoLight.cginc)、LightMode通常需要设置为ForwardBase或ForwardAdd、Pass 中添加#pragma multi_compile_fwdbase对于主平行光或#pragma multi_compile_fwdadd以确保 Unity 生成处理阴影所需的变体、添加fallback specular(建议添加)。3.代码实现步骤1生成深度图Shader MyCustom/CastShadow { Properties { } SubShader { Tags { RenderTypeOpaque } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; }; struct v2f { float2 depth : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.depth o.vertex.zw; return o; } fixed4 frag (v2f i) : SV_Target { // 执行了透视除法‌。得到的 depth 值是‌归一化设备坐标 (NDC)‌ 中的 Z 值 float depth i.depth.x / i.depth.y; // 平台兼容性处理统一映射到 [0, 1] #if defined (SHADER_TARGET_GLSL) depth depth * 0.5 0.5; // (-1, 1) - (0, 1) #elif defined (UNITY_REVERSED_Z) depth 1 - depth; // (1, 0) - (0, 1) #endif // 编码为 RGBA 颜色 float4 col EncodeFloatRGBA(depth); return col; } ENDCG } } }步骤2生成阴影Shader MyCustom/ShadowMap { SubShader { Pass { NAME DirectionalShadow Blend DstColor Zero CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma shader_feature _PCFSOFTSHADOW #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 shadowCoord : TEXCOORD0; float4 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float4 pos : SV_POSITION; }; // 步骤1生成的图 sampler2D _MyCustomShadowMap; float4 _MyCustomShadowMap_TexelSize; // 灯光视图投影矩阵,将世界空间转换到灯光的视界空间 // 外部C#计算GL.GetGPUProjectionMatrix(cam.projectionMatrix, false) * cam. worldToCameralatrix // cam为平行光上的摄像机 float4x4 _MyCustomLightSpaceMatrix; float _MyCustomShadowStrength; float _pcfSoftShadow(float depth, float2 uv, float bias) { float shadow 0; // 3x3 网格采样 for (int x -1; x 1; x) { for (int y -1; y 1; y) { float2 _uv uv float2(x, y) * _MyCustomShadowMap_TexelSize.xy; float4 col tex2D(_MyCustomShadowMap, _uv); float sampleDepth DecodeFloatRGBA(col); // 累加每个采样点的阴影结果 shadow sampleDepth bias depth ? _MyCustomShadowStrength : 1; } } // 取平均值产生柔和过渡 return shadow / 9; } v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); // 1. 转换到世界空间 float4 worldPos mul(unity_ObjectToWorld, v.vertex); // 2. 使用预计算的灯光视图投影矩阵将世界坐标转换到灯光裁剪空间 o.shadowCoord mul(_MyCustomLightSpaceMatrix, worldPos); o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos worldPos; return o; } fixed4 frag (v2f i) : SV_Target { // 透视除法得到 NDC 坐标 (-1 到 1) float2 uv i.shadowCoord.xy / i.shadowCoord.w; uv uv * 0.5 0.5; // (-1, 1) - (0, 1) // 从世界空间拿到的深度值 float depth i.shadowCoord.z / i.shadowCoord.w; #if defined (SHADER_TARGET_GLSL) depth depth * 0.5 0.5; #elif defined (UNITY_REVERSED_Z) depth 1 - depth; #endif // 计算 Bias偏移量 // 由于阴影贴图分辨率有限表面自遮挡会导致错误的阴影条纹 // 根据法线与光线的夹角动态调整偏移。当表面与光线接近平行时ndotl 接近 0需要更大的 Bias 来避免误判 float3 lightDir normalize(UnityWorldSpaceLightDir(i.worldPos)); float ndotl dot(normalize(i.worldNormal), lightDir); float bias max(0.05 * (1.0 - ndotl), 0.005); #ifdef _PCFSOFTSHADOW // 模式一软阴影PCF Soft Shadow float shadow _pcfSoftShadow(depth, uv, bias); #else // 模式二硬阴影Hard Shadow float4 col tex2D(_MyCustomShadowMap, uv); // 解码阴影贴图中存储的深度值 float sampleDepth DecodeFloatRGBA(col); // 如果 贴图深度 Bias 当前像素深度说明当前像素在遮挡物后面处于阴影中 float shadow sampleDepth bias depth ? _MyCustomShadowStrength : 1; #endif // 避免采样到错误的边缘数据 if (uv.x 1 || uv.y 1 || uv.x 0 || uv.y 0) shadow 1; return shadow; } ENDCG } } }Bias优化1.问题成因在实时渲染中阴影会出现条纹。阴影条纹问题通常被称为‌阴影痤疮Shadow Acne‌或‌自阴影错误‌。这是基于阴影贴图Shadow Mapping技术的一个经典缺陷。阴影条纹表现为模型表面出现不规则的黑白相间条纹或斑点仿佛物体自己在给自己投射阴影。其根本原因在于‌深度比较时的精度误差‌具体由以下三个因素共同导致离散采样与分辨率限制阴影贴图是一张有限分辨率的 2D 纹理如 1024x1024 或 2048x2048。当场景中的几何体非常复杂或距离光源较远时阴影贴图中的单个像素Texel可能覆盖较大的世界空间区域。这意味着阴影贴图记录的深度值是离散的、近似化的而非连续精确的。浮点数精度误差GPU 在进行深度计算时使用浮点数。当光线以极低的角度 grazing angle 照射表面时表面在灯光视角下的深度变化非常剧烈。由于浮点数的精度限制渲染当前像素时计算出的深度值Receiver Depth与阴影贴图中存储的深度值Occluder Depth即同一表面之前被记录的值之间会产生微小的数值偏差。深度比较逻辑缺陷理论上对于一个平面currentDepth应该等于shadowMapDepth。但由于上述的插值误差和精度问题有时会出现currentDepth略大于shadowMapDepth的情况。这导致本应受光的表面像素被误判为“位于遮挡物之后”从而错误地渲染为阴影。这种误判在表面上随机分布形成了条纹状的视觉瑕疵。2.引入 Bias偏移量的解决方案为了解决 Shadow Acne最通用且有效的方法是引入 ‌Depth Bias深度偏移‌。其核心思想是‌在生成阴影贴图或进行深度比较时人为地将遮挡物的深度值“推远”一点或者将接收者的深度判定阈值放宽确保表面不会误判自己为遮挡物。这样当再次比较同一表面的深度时currentDepth就不会轻易超过shadowMapDepth bias从而消除自阴影。3.普通Bias的弊端及引入斜率缩放偏移(Normal Bias)对所有像素应用一个固定的深度偏移值无法适应不同角度的表面。角度过大或过小依然会造成问题。Normal Bias根据表面法线与光线方向的夹角动态调整偏移量能更精确地消除条纹。GD FG * tanθFG (视锥体大小/shadow map大小)cosθ [ dot(n,l) / (|n| *|l|) ] dot(n,l) 当nl为单位向量tanθ sqrt(1 - cosθ * cosθ)补充当θ过大时计算出的GD过大使用法线方向的GH代替(GH平行nH为CD上的垂足), 则GH GD * sinθ (基于法线方向的bias)聚光灯阴影在shader实现上没有什么区别在灯光空间矩阵计算略有不同。(注cam为光源绑定的摄像机Shader为传入的阴影shader)平行光C#灯光空间转换// 将物体从世界坐标转到当前光源坐标Matrix4x4 projectionMatrix GL.GetGPUProjectionMatrix(cam.projectionMatrix, false);Shader.SetGlobalMatrix(_MyCustomLightSpaceMatrix, projectionMatrix * cam.worldToCameraMatrix);聚光灯C#灯光空间转换// 矫正shadowmap上下颠倒nMat new Matrix4x4();nMat.SetRow(0, new Vector4(1f, 0f, 0f, 0f));nMat.SetRow(1, new Vector4(0f, -1f, 0f, 0f));nMat.SetRow(2, new Vector4(0f, 0f, 1f, 0f));nMat.SetRow(3, new Vector4(0f, 0f, 0f, 1f));// 将物体从世界坐标转到当前光源坐标Matrix4x4 projectionMatrix GL.GetGPUProjectionMatrix(cam.projectionMatrix, false);Shader.SetGlobalMatrix(_MyCustomLightSpaceMatrix, projectionMatrix * nMat * cam.worldToCameraMatrix);// 传入摄像机空间在这个空间中计算normal biasShader.SetGlobalMatrix(_MyCustomWorldToCameraMatrix, cam.worldToCameraMatrix);// 将视锥体大小和shadow map大小传入计算normal biasShader.SetGlobalFloat(_MyCustomFrustumSize, projectionMatrix.m11);Shader.SetGlobalFloat(_MyCustomShadowMapSize, shadowMapSize);点光源阴影平行光、聚光灯都只用‌单张2D纹理‌存储阴影而点光源是全向360°覆盖的光源无法用单一2D贴图完整记录所有方向的深度必须使用‌立方体贴图Cube Map。立方体贴图由6张独立的2D面组成分别对应点光源的±X、±Y、±Z六个方向每个面都有自己的视锥体共同包裹住整个点光源的球形照射范围。深度值为物体在灯光空间位置到最远截面的距离// 记录当前摄像机 Quaternion currentQuat cam.transform.rotation; float currentFOV cam.fieldOfView; // 1: 在当前点光空间生成shadow cube // 绘制场景 cam.transform.rotation Quaternion.identity; // x cam.transform.eulerAngles new Vector3(0, 90, 180); shadowCube.SetPixels(RT2Colors(shadowMap), CubemapFace.PositiveX); // -z cam.transform.eulerAngles new Vector3(0, 0, 180); shadowCube.SetPixels(RT2Colors(shadowMap), CubemapFace.NegativeZ); // -x cam.transform.eulerAngles new Vector3(0, 270, 180); shadowCube.SetPixels(RT2Colors(shadowMap), CubemapFace.NegativeX); // z cam.transform.eulerAngles new Vector3(0, 180, 180); shadowCube.SetPixels(RT2Colors(shadowMap), CubemapFace.PositiveZ); // y cam.transform.eulerAngles new Vector3(-90, 0, 0); shadowCube.SetPixels(RT2Colors(shadowMap), CubemapFace.PositiveY); // -y cam.transform.eulerAngles new Vector3(90, 0, 0); shadowCube.SetPixels(RT2Colors(shadowMap), CubemapFace.NegativeY); // 保存当前的cubemap shadowCube.Apply(); // 2: 将shadow cube计算需要的数据传递给custom shader Shader.SetGlobalTexture(_MyCustomShadowCube, shadowCube); // 绘制完毕之后camera赋回初值 cam.transform.rotation currentQuat; cam.fieldOfView currentFOV; // 使用摄像机最远截面计算深度值 Shader.SetGlobalFloat(_MyCustomFarPlane, cam.farClipPlane);锥体光源的Normal BiasPCSS软阴影1.原理及流程介绍之前的PCF软阴影通过采样周围像素的阴影平均计算实现模糊软阴影而PCSS软阴影是模拟真实世界中“接触阴影硬、远处阴影软”的特性。W_Light: 光源面积尺寸D_Blocker:遮挡物的深度D_Receiver:被投影物的深度Blocker Search:通过多重采样计算出平均遮挡深度 D_BlockerPenumbra Size:计算圆盘半径大小 W_PenumbraFiltering:通过多重采样计算出平均 Visibility(实际上就是调用PCF算法)2.代码实现// --------步骤一辅助函数-------- // 1.寻找阻挡物 void _findBlocker(float depth, _ShadowCoord coord, float bias,float2 rotation, out float avgBlockerDepth, out float numBlockers) { float blockerSum 0; // (depth - _MyCustomNearPlane) / depth这是一个归一化的深度因子。随着 depth当前像素深度增加这个比值趋近于 1当 depth 接近 _MyCustomNearPlane 时比值趋近于 0。这意味着‌距离光源越远搜索范围越大‌。 float sampleSize (depth - _MyCustomNearPlane) / depth * _ShadowSoftness / _MyCustomShadowMapSize; // 使用预定义的 64 个泊松分布偏移量。相比规则网格泊松分布能更好地避免采样别名效应使阴影边缘更自然。 for (int i 0; i 64; i) { #ifdef _MYCUSTOM_POINTLIGHT // 点光源的坐标可能是 3D 方向向量或立方体贴图 UV因此只旋转 XY 分量保持 Z/W 分量不变 _ShadowCoord _coord coord float3(_rotate(PoissonOffsets[i] * sampleSize, rotation), 0); #else _ShadowCoord _coord coord _rotate(PoissonOffsets[i] * sampleSize, rotation); #endif float sampleDepth 0; if (_sampleDepth(depth, _coord, bias, sampleDepth)) { // 累加所有遮挡物的深度值并返回 blockerSum sampleDepth; // 统计遮挡物的数量并返回 numBlockers 1; } } avgBlockerDepth blockerSum / numBlockers; } // 返回是否是阻挡物并返回其深度值 bool _sampleDepth(float depth, _ShadowCoord coord, float bias, out float sampleDepth) { // 点光采集ShadowCube,其他光采集ShadowMap #ifdef _MYCUSTOM_POINTLIGHT float4 col texCUBE(_MyCustomShadowCube, normalize(coord)); sampleDepth DecodeFloatRGBA(col); sampleDepth pow(sampleDepth, 2.2); #else float4 col tex2D(_MyCustomShadowMap, coord); sampleDepth DecodeFloatRGBA(col); #endif return sampleDepth bias depth; } // ---------------- // // --------步骤三辅助函数-------- float _pcfFilter(float depth, _ShadowCoord coord, float bias, float penumbra, float2 rotation) { float shadow 0; for (int i 0; i 64; i) { #ifdef _MYCUSTOM_POINTLIGHT _ShadowCoord _coord coord float3(_rotate(PoissonOffsets[i] * penumbra, rotation), 0); #else _ShadowCoord _coord coord _rotate(PoissonOffsets[i] * penumbra, rotation); #endif shadow _shadowValue(depth, _coord, bias); } return shadow / 64; } // 如果是阻挡物返回阴影强度 float _shadowValue(float depth, _ShadowCoord coord, float bias) { float sampleDepth 0; float shadow _sampleDepth(depth, coord, bias, sampleDepth) ? _MyCustomShadowStrength : 0; return shadow; } // ---------------- float _pcssSoftShadow(float depth, _ShadowCoord coord, float bias, float2 texcoord) { // PCSS 需要进行大量采样这里用了64次。如果采样图案是固定的网格会在阴影边缘产生明显的周期性条纹Banding Artifacts。 // 使用 ‌蓝噪声纹理 (Blue Noise)‌。蓝噪声具有高频分布均匀且低频能量少的特性比纯随机噪声更平滑比规则网格更自然。 // 为每个像素生成一个随机的旋转角度 rotation在后续采样时旋转采样图案将固定的条纹误差打散成高频噪声视觉上更柔和。 float random tex2D(_ShadowBlueNoise, texcoord * _ShadowBlueNoiseRepeat * _ScreenParams.xy).r; random random * 2 - 1; // (0, 1) - (-1, 1) float rotationAngle random * 3.1415926; float2 rotation float2(cos(rotationAngle), sin(rotationAngle)); // Step 1. 求得遮挡物的平均深度 float avgBlockerDepth 0; float numBlockers 0; _findBlocker(depth, coord, bias, rotation, avgBlockerDepth, numBlockers); if (numBlockers 1) { // 该区域不在阴影内 return 0; } // Step 2. 求得半影大小 float penumbra (depth - avgBlockerDepth) / avgBlockerDepth * _ShadowSoftness / _MyCustomShadowMapSize; // Step 3. pcf filter计算阴影 float shadow _pcfFilter(depth, coord, bias, penumbra, rotation); return shadow; }