案例 18.9冰冻覆盖:世界空间自上而下结冰进阶
目标:一个参数 _Freeze 从 0 到 1,物体从朝上的表面开始结冰,逐渐蔓延到侧面、底面;冰层有厚度、有裂纹、边缘泛蓝白、阳光下有闪点。
原理:法线朝上的程度就是遮罩
霜雪、冰层、苔藓、积灰,这类“覆盖层”都遵循同一个规律:越朝上的面越先覆盖。世界空间法线的 y 分量 normalWS.y 从 −1(朝下)到 1(朝上),本身就是一张遮罩;用阈值去切它,阈值从 1.5 降到 −1.5,覆盖范围就从“只有正顶面”扩展到“全身”。为了让分界线不那么整齐,再加一层噪声扰动。
这里的噪声不能用模型 UV 采样——覆盖是世界空间的现象,几个物体放在一起,冰的纹理应该连续、尺度一致。用世界坐标采样又有一个问题:只用 xz 投影,侧面会被拉成一条条竖线。所以用三平面映射(Triplanar):分别沿 X、Y、Z 三个轴投影采样,再按法线三个分量的权重混合。第 20 章会把三平面映射讲全(包括法线贴图的三平面混合),这里用最精简的版本。
// 极简三平面采样:按法线三个分量的权重,混合 YZ / XZ / XY 三个方向的投影 half TriplanarNoise(TEXTURE2D_PARAM(tex, samp), float3 positionWS, half3 normalWS, float scale) { half3 w = pow(abs(normalWS), 4.0h); w /= (w.x + w.y + w.z); half x = SAMPLE_TEXTURE2D_LOD(tex, samp, positionWS.zy * scale, 0).r; half y = SAMPLE_TEXTURE2D_LOD(tex, samp, positionWS.xz * scale, 0).r; half z = SAMPLE_TEXTURE2D_LOD(tex, samp, positionWS.xy * scale, 0).r; return x * w.x + y * w.y + z * w.z; }
// 极简三平面采样:按法线三个分量的权重,混合 YZ / XZ / XY 三个方向的投影 // (GLSL 没有 TEXTURE2D_PARAM,直接传 sampler2D) float TriplanarNoise(sampler2D tex, vec3 positionWS, vec3 normalWS, float scale) { vec3 w = pow(abs(normalWS), vec3(4.0)); w /= (w.x + w.y + w.z); float x = textureLod(tex, positionWS.zy * scale, 0.0).r; float y = textureLod(tex, positionWS.xz * scale, 0.0).r; float z = textureLod(tex, positionWS.xy * scale, 0.0).r; return x * w.x + y * w.y + z * w.z; }
// 覆盖遮罩:法线越朝上越先结冰,噪声把分界线打散。_Freeze 0→1,阈值从“只有正上方”一路降到“全覆盖” half FrostMask(float3 positionWS, half3 normalWS) { half n = TriplanarNoise(TEXTURE2D_ARGS(_IceNoise, sampler_IceNoise), positionWS, normalWS, _IceNoiseScale); half up = normalWS.y + (n - 0.5h) * 0.8h; // -1.4 ~ 1.4 half threshold = lerp(1.5h, -1.5h, _Freeze); return smoothstep(threshold - 0.1h, threshold + 0.1h, up); }
// 覆盖遮罩:法线越朝上越先结冰,噪声把分界线打散。_Freeze 0→1,阈值从“只有正上方”一路降到“全覆盖” float FrostMask(vec3 positionWS, vec3 normalWS) { float n = TriplanarNoise(_IceNoise, positionWS, normalWS, _IceNoiseScale); float up = normalWS.y + (n - 0.5) * 0.8; // -1.4 ~ 1.4 float threshold = mix(1.5, -1.5, _Freeze); return smoothstep(threshold - 0.1, threshold + 0.1, up); }
冰层的厚度
冰不是贴在表面的一张图,它有厚度。在顶点着色器里把被覆盖的顶点沿法线外推几厘米,轮廓立刻变得“胖”了一圈。顶点着色器里采样纹理必须用 SAMPLE_TEXTURE2D_LOD(顶点阶段没有屏幕导数,无法自动选择 mip),所以三平面函数里统一用了 LOD 版本。
// 冰层有厚度:被覆盖的顶点沿法线外推。阴影和深度 Pass 也要做同样的外推,否则阴影对不上 float3 ThickenIce(float3 positionWS, half3 normalWS) { return positionWS + normalWS * FrostMask(positionWS, normalWS) * _Thickness; }
和溶解一样,顶点变形必须在所有 Pass 里同步:ShadowCaster 和 DepthOnly 也要调用 ThickenIce,否则影子还是原来的瘦轮廓,深度图里的轮廓和屏幕上的也对不上(SSAO、后处理描边都会出错)。
冰的外观
片元里做四件事:① 冰色在浅蓝和深蓝之间按噪声变化,Worley 噪声(公共贴图 noise_cells)的细胞边界很亮,取边界做白色裂纹;② 光滑度提高到 0.9,冰面有锐利的高光;③ 冷色菲涅尔,模拟光在冰层里散射后从边缘透出来;④ 闪点:把世界坐标量化成很小的格子,每个格子一个哈希值,只保留最亮的千分之三,再乘一个视角相关的高光项,转动视角时会一闪一闪。
half4 frag(Varyings i) : SV_Target { half3 normalWS = normalize(i.normalWS); half3 viewWS = GetWorldSpaceNormalizeViewDir(i.positionWS); half mask = FrostMask(i.restWS, normalWS); half3 baseAlbedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb * _BaseColor.rgb; // 冰的颜色:噪声在浅色与深色之间变化,Worley 的细胞边界做成白色裂纹 half n = TriplanarNoise(TEXTURE2D_ARGS(_IceNoise, sampler_IceNoise), i.restWS, normalWS, _IceNoiseScale * 3.0); half cell = TriplanarNoise(TEXTURE2D_ARGS(_CrackMap, sampler_CrackMap), i.restWS, normalWS, _CrackScale); half crack = smoothstep(0.55h, 0.8h, cell) * _CrackStrength; half3 ice = lerp(_IceDeepColor.rgb, _IceColor.rgb, saturate(n * 1.2h + crack)); // 冰层很薄的地方(遮罩边缘)透出一点底色 half3 albedo = lerp(baseAlbedo, ice, mask * (0.75h + 0.25h * mask)); half smooth = lerp(_Smoothness, 0.9h, mask); half3 color = SimpleLighting(albedo, i.positionWS, normalWS, smooth); // 冷色菲涅尔:冰面边缘泛蓝白,模拟光在冰层里散射后从边缘透出来 half rim = FresnelRim(normalWS, viewWS, 3.0h); color += _IceRimColor.rgb * rim * mask; // 闪点:极高频哈希,只保留最亮的极少数点;乘上视角相关的项,转动相机时会“闪” float3 sp = floor(i.restWS * 160.0); half sparkle = step(0.997h, Hash21(sp.xz + sp.y * 17.0)); half3 halfDir = SafeNormalize(GetMainLight().direction + viewWS); sparkle *= pow(saturate(dot(normalWS, halfDir)), 8.0h); color += sparkle * _SparkleStrength * mask * GetMainLight().color; return half4(color, 1); }
void main() { vec3 N = normalize(normalWS); vec3 viewWS = GetWorldSpaceNormalizeViewDir(positionWS); float mask = FrostMask(restWS, N); vec3 baseAlbedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb; // 冰的颜色:噪声在浅色与深色之间变化,Worley 的细胞边界做成白色裂纹 float n = TriplanarNoise(_IceNoise, restWS, N, _IceNoiseScale * 3.0); float cell = TriplanarNoise(_CrackMap, restWS, N, _CrackScale); float crack = smoothstep(0.55, 0.8, cell) * _CrackStrength; vec3 ice = mix(_IceDeepColor.rgb, _IceColor.rgb, saturate(n * 1.2 + crack)); // 冰层很薄的地方(遮罩边缘)透出一点底色 vec3 albedo = mix(baseAlbedo, ice, mask * (0.75 + 0.25 * mask)); float smooth_ = mix(_Smoothness, 0.9, mask); vec3 color = SimpleLighting(albedo, positionWS, N, smooth_); // 冷色菲涅尔:冰面边缘泛蓝白,模拟光在冰层里散射后从边缘透出来 float rim = FresnelRim(N, viewWS, 3.0); color += _IceRimColor.rgb * rim * mask; // 闪点:极高频哈希,只保留最亮的极少数点;乘上视角相关的项,转动相机时会“闪” vec3 sp = floor(restWS * 160.0); float sparkle = step(0.997, Hash21(sp.xz + sp.y * 17.0)); vec3 halfDir = SafeNormalize(GetMainLight().direction + viewWS); sparkle *= pow(saturate(dot(N, halfDir)), 8.0); color += sparkle * _SparkleStrength * mask * GetMainLight().color; fragColor = vec4(color, 1.0); }
完整 Shader 与公共部分:
// 冰冻覆盖的公共部分:材质参数、三平面噪声、覆盖遮罩、顶点加厚。Forward/Shadow/Depth 三个 Pass 共用。 #ifndef COOKBOOK_CH18_FROZEN_CORE_INCLUDED #define COOKBOOK_CH18_FROZEN_CORE_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_IceNoise); SAMPLER(sampler_IceNoise); TEXTURE2D(_CrackMap); SAMPLER(sampler_CrackMap); CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Smoothness; half _Freeze; half4 _IceColor; half4 _IceDeepColor; float _IceNoiseScale; float _CrackScale; half _CrackStrength; half _Thickness; half4 _IceRimColor; half _SparkleStrength; CBUFFER_END // 极简三平面采样:按法线三个分量的权重,混合 YZ / XZ / XY 三个方向的投影 half TriplanarNoise(TEXTURE2D_PARAM(tex, samp), float3 positionWS, half3 normalWS, float scale) { half3 w = pow(abs(normalWS), 4.0h); w /= (w.x + w.y + w.z); half x = SAMPLE_TEXTURE2D_LOD(tex, samp, positionWS.zy * scale, 0).r; half y = SAMPLE_TEXTURE2D_LOD(tex, samp, positionWS.xz * scale, 0).r; half z = SAMPLE_TEXTURE2D_LOD(tex, samp, positionWS.xy * scale, 0).r; return x * w.x + y * w.y + z * w.z; } // 覆盖遮罩:法线越朝上越先结冰,噪声把分界线打散。_Freeze 0→1,阈值从“只有正上方”一路降到“全覆盖” half FrostMask(float3 positionWS, half3 normalWS) { half n = TriplanarNoise(TEXTURE2D_ARGS(_IceNoise, sampler_IceNoise), positionWS, normalWS, _IceNoiseScale); half up = normalWS.y + (n - 0.5h) * 0.8h; // -1.4 ~ 1.4 half threshold = lerp(1.5h, -1.5h, _Freeze); return smoothstep(threshold - 0.1h, threshold + 0.1h, up); } // 冰层有厚度:被覆盖的顶点沿法线外推。阴影和深度 Pass 也要做同样的外推,否则阴影对不上 float3 ThickenIce(float3 positionWS, half3 normalWS) { return positionWS + normalWS * FrostMask(positionWS, normalWS) * _Thickness; } #endif
// 冰冻覆盖的公共部分:材质参数、三平面噪声、覆盖遮罩、顶点加厚。Forward/Shadow/Depth 三个 Pass 共用。 // (GLSL 版,对应 FrozenCore.hlsl) #ifndef COOKBOOK_CH18_FROZEN_CORE_INCLUDED #define COOKBOOK_CH18_FROZEN_CORE_INCLUDED uniform sampler2D _BaseMap; uniform sampler2D _IceNoise; uniform sampler2D _CrackMap; uniform vec4 _BaseMap_ST; uniform vec4 _BaseColor; uniform float _Smoothness; uniform float _Freeze; uniform vec4 _IceColor; uniform vec4 _IceDeepColor; uniform float _IceNoiseScale; uniform float _CrackScale; uniform float _CrackStrength; uniform float _Thickness; uniform vec4 _IceRimColor; uniform float _SparkleStrength; // 极简三平面采样:按法线三个分量的权重,混合 YZ / XZ / XY 三个方向的投影 // (GLSL 没有 TEXTURE2D_PARAM,直接传 sampler2D) float TriplanarNoise(sampler2D tex, vec3 positionWS, vec3 normalWS, float scale) { vec3 w = pow(abs(normalWS), vec3(4.0)); w /= (w.x + w.y + w.z); float x = textureLod(tex, positionWS.zy * scale, 0.0).r; float y = textureLod(tex, positionWS.xz * scale, 0.0).r; float z = textureLod(tex, positionWS.xy * scale, 0.0).r; return x * w.x + y * w.y + z * w.z; } // 覆盖遮罩:法线越朝上越先结冰,噪声把分界线打散。_Freeze 0→1,阈值从“只有正上方”一路降到“全覆盖” float FrostMask(vec3 positionWS, vec3 normalWS) { float n = TriplanarNoise(_IceNoise, positionWS, normalWS, _IceNoiseScale); float up = normalWS.y + (n - 0.5) * 0.8; // -1.4 ~ 1.4 float threshold = mix(1.5, -1.5, _Freeze); return smoothstep(threshold - 0.1, threshold + 0.1, up); } // 冰层有厚度:被覆盖的顶点沿法线外推。阴影和深度 Pass 也要做同样的外推,否则阴影对不上 vec3 ThickenIce(vec3 positionWS, vec3 normalWS) { return positionWS + normalWS * FrostMask(positionWS, normalWS) * _Thickness; } #endif
// 案例 18.9:冰冻 / 结晶覆盖。世界空间“自上而下”覆盖一层冰:加厚、冰裂纹、高光滑度、冷色边缘与闪点。 Shader "Cookbook/Ch18/Frozen" { Properties { [MainTexture] _BaseMap ("Base Map", 2D) = "white" {} [MainColor] _BaseColor ("Base Color", Color) = (1,1,1,1) _Smoothness ("Base Smoothness", Range(0,1)) = 0.3 _Freeze ("Freeze", Range(0,1)) = 0.5 _IceColor ("Ice Color", Color) = (0.78,0.9,1,1) _IceDeepColor ("Ice Deep Color", Color) = (0.25,0.5,0.75,1) [NoScaleOffset] _IceNoise ("Ice Noise", 2D) = "gray" {} _IceNoiseScale ("Ice Noise Scale", Float) = 0.6 [NoScaleOffset] _CrackMap ("Crack (Worley)", 2D) = "black" {} _CrackScale ("Crack Scale", Float) = 1.2 _CrackStrength ("Crack Strength", Range(0,2)) = 0.8 _Thickness ("Thickness (m)", Range(0,0.1)) = 0.025 [HDR] _IceRimColor ("Ice Rim Color", Color) = (0.6,0.9,1.5,1) _SparkleStrength ("Sparkle", Range(0,20)) = 6 } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry" "RenderPipeline"="UniversalPipeline" } Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH #pragma multi_compile _ _FORWARD_PLUS #include "Assets/Cookbook/Ch18/FrozenCore.hlsl" #include "Assets/Cookbook/Ch18/Ch18Common.hlsl" struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; half3 normalWS : TEXCOORD1; float2 uv : TEXCOORD2; float3 restWS : TEXCOORD3; // 加厚之前的位置,用来采样噪声,避免纹理随厚度“游动” }; Varyings vert(Attributes v) { Varyings o; float3 positionWS = TransformObjectToWorld(v.positionOS.xyz); half3 normalWS = TransformObjectToWorldNormal(v.normalOS); o.restWS = positionWS; o.positionWS = ThickenIce(positionWS, normalWS); o.positionCS = TransformWorldToHClip(o.positionWS); o.normalWS = normalWS; o.uv = TRANSFORM_TEX(v.uv, _BaseMap); return o; } half4 frag(Varyings i) : SV_Target { half3 normalWS = normalize(i.normalWS); half3 viewWS = GetWorldSpaceNormalizeViewDir(i.positionWS); half mask = FrostMask(i.restWS, normalWS); half3 baseAlbedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb * _BaseColor.rgb; // 冰的颜色:噪声在浅色与深色之间变化,Worley 的细胞边界做成白色裂纹 half n = TriplanarNoise(TEXTURE2D_ARGS(_IceNoise, sampler_IceNoise), i.restWS, normalWS, _IceNoiseScale * 3.0); half cell = TriplanarNoise(TEXTURE2D_ARGS(_CrackMap, sampler_CrackMap), i.restWS, normalWS, _CrackScale); half crack = smoothstep(0.55h, 0.8h, cell) * _CrackStrength; half3 ice = lerp(_IceDeepColor.rgb, _IceColor.rgb, saturate(n * 1.2h + crack)); // 冰层很薄的地方(遮罩边缘)透出一点底色 half3 albedo = lerp(baseAlbedo, ice, mask * (0.75h + 0.25h * mask)); half smooth = lerp(_Smoothness, 0.9h, mask); half3 color = SimpleLighting(albedo, i.positionWS, normalWS, smooth); // 冷色菲涅尔:冰面边缘泛蓝白,模拟光在冰层里散射后从边缘透出来 half rim = FresnelRim(normalWS, viewWS, 3.0h); color += _IceRimColor.rgb * rim * mask; // 闪点:极高频哈希,只保留最亮的极少数点;乘上视角相关的项,转动相机时会“闪” float3 sp = floor(i.restWS * 160.0); half sparkle = step(0.997h, Hash21(sp.xz + sp.y * 17.0)); half3 halfDir = SafeNormalize(GetMainLight().direction + viewWS); sparkle *= pow(saturate(dot(normalWS, halfDir)), 8.0h); color += sparkle * _SparkleStrength * mask * GetMainLight().color; return half4(color, 1); } ENDHLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma vertex vertShadow #pragma fragment fragShadow #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW #include "Assets/Cookbook/Ch18/FrozenCore.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" float3 _LightDirection; float3 _LightPosition; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; }; float4 vertShadow(Attributes v) : SV_POSITION { float3 positionWS = TransformObjectToWorld(v.positionOS.xyz); half3 normalWS = TransformObjectToWorldNormal(v.normalOS); positionWS = ThickenIce(positionWS, normalWS); #if _CASTING_PUNCTUAL_LIGHT_SHADOW float3 lightDirWS = normalize(_LightPosition - positionWS); #else float3 lightDirWS = _LightDirection; #endif float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS)); #if UNITY_REVERSED_Z positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE); #else positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE); #endif return positionCS; } half4 fragShadow() : SV_Target { return 0; } ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R HLSLPROGRAM #pragma vertex vertDepth #pragma fragment fragDepth #include "Assets/Cookbook/Ch18/FrozenCore.hlsl" struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; }; float4 vertDepth(Attributes v) : SV_POSITION { float3 positionWS = TransformObjectToWorld(v.positionOS.xyz); return TransformWorldToHClip(ThickenIce(positionWS, TransformObjectToWorldNormal(v.normalOS))); } half4 fragDepth(float4 positionCS : SV_POSITION) : SV_Target { return positionCS.z; } ENDHLSL } } }
// 案例 18.9:冰冻 / 结晶覆盖。世界空间“自上而下”覆盖一层冰:加厚、冰裂纹、高光滑度、冷色边缘与闪点。 // (GLSL 版,对应 Ch18/Frozen.shader) Shader "Cookbook/Ch18/Frozen" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry" "RenderPipeline"="UniversalPipeline" } Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } GLSLPROGRAM #include "Ch18/FrozenCore.glsl" #include "Ch18/Ch18Common.glsl" VARYING vec3 positionWS; VARYING vec3 normalWS; VARYING vec2 uv; VARYING vec3 restWS; // 加厚之前的位置,用来采样噪声,避免纹理随厚度“游动” #ifdef VERTEX void main() { vec3 pWS = TransformObjectToWorld(positionOS.xyz); vec3 nWS = TransformObjectToWorldNormal(normalOS); restWS = pWS; positionWS = ThickenIce(pWS, nWS); gl_Position = TransformWorldToHClip(positionWS); normalWS = nWS; uv = TransformTex(texcoord0.xy, _BaseMap_ST); } #endif #ifdef FRAGMENT void main() { vec3 N = normalize(normalWS); vec3 viewWS = GetWorldSpaceNormalizeViewDir(positionWS); float mask = FrostMask(restWS, N); vec3 baseAlbedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb; // 冰的颜色:噪声在浅色与深色之间变化,Worley 的细胞边界做成白色裂纹 float n = TriplanarNoise(_IceNoise, restWS, N, _IceNoiseScale * 3.0); float cell = TriplanarNoise(_CrackMap, restWS, N, _CrackScale); float crack = smoothstep(0.55, 0.8, cell) * _CrackStrength; vec3 ice = mix(_IceDeepColor.rgb, _IceColor.rgb, saturate(n * 1.2 + crack)); // 冰层很薄的地方(遮罩边缘)透出一点底色 vec3 albedo = mix(baseAlbedo, ice, mask * (0.75 + 0.25 * mask)); float smooth_ = mix(_Smoothness, 0.9, mask); vec3 color = SimpleLighting(albedo, positionWS, N, smooth_); // 冷色菲涅尔:冰面边缘泛蓝白,模拟光在冰层里散射后从边缘透出来 float rim = FresnelRim(N, viewWS, 3.0); color += _IceRimColor.rgb * rim * mask; // 闪点:极高频哈希,只保留最亮的极少数点;乘上视角相关的项,转动相机时会“闪” vec3 sp = floor(restWS * 160.0); float sparkle = step(0.997, Hash21(sp.xz + sp.y * 17.0)); vec3 halfDir = SafeNormalize(GetMainLight().direction + viewWS); sparkle *= pow(saturate(dot(N, halfDir)), 8.0); color += sparkle * _SparkleStrength * mask * GetMainLight().color; fragColor = vec4(color, 1.0); } #endif ENDGLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 GLSLPROGRAM #include "Ch18/FrozenCore.glsl" #ifdef VERTEX void main() { vec3 positionWS = TransformObjectToWorld(positionOS.xyz); vec3 normalWS = TransformObjectToWorldNormal(normalOS); positionWS = ThickenIce(positionWS, normalWS); vec3 lightDirWS = _CastingPunctualLightShadow > 0.5 ? normalize(_LightPosition - positionWS) : _LightDirection; vec4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS)); positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE * positionCS.w); gl_Position = positionCS; } #endif #ifdef FRAGMENT void main() { fragColor = vec4(0.0); } #endif ENDGLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R GLSLPROGRAM #include "Ch18/FrozenCore.glsl" #ifdef VERTEX void main() { vec3 positionWS = TransformObjectToWorld(positionOS.xyz); gl_Position = TransformWorldToHClip(ThickenIce(positionWS, TransformObjectToWorldNormal(normalOS))); } #endif #ifdef FRAGMENT void main() { fragColor = vec4(gl_FragCoord.z); } #endif ENDGLSL } } }

常见坑与扩展
- 硬边模型会裂开:立方体这类硬边模型,同一个角上有三个法线不同的顶点,沿各自法线外推后会分开,出现裂缝。解决办法是用平滑法线做外推(第 14 章描边里烘焙平滑法线的工具可以直接复用),或者把厚度设得很小。
- 蒙皮角色:法线要用蒙皮后的世界法线,这在 URP 里是自动的(蒙皮在 GPU 上先于顶点着色器完成);但如果你在 C# 里用烘焙的静态法线做遮罩,角色弯腰时冰会“滑动”。
- 积雪、苔藓、灰尘:把冰的外观换成白色漫反射、绿色苔藓、灰色粉尘,遮罩逻辑完全通用。积雪可以把阈值和噪声换成更柔和的过渡,并配合法线贴图让雪面蓬松。