GameTech/URP 渲染实战

案例 18.9冰冻覆盖:世界空间自上而下结冰进阶

目标:一个参数 _Freeze 从 0 到 1,物体从朝上的表面开始结冰,逐渐蔓延到侧面、底面;冰层有厚度、有裂纹、边缘泛蓝白、阳光下有闪点。

原理:法线朝上的程度就是遮罩

霜雪、冰层、苔藓、积灰,这类“覆盖层”都遵循同一个规律:越朝上的面越先覆盖。世界空间法线的 y 分量 normalWS.y 从 −1(朝下)到 1(朝上),本身就是一张遮罩;用阈值去切它,阈值从 1.5 降到 −1.5,覆盖范围就从“只有正顶面”扩展到“全身”。为了让分界线不那么整齐,再加一层噪声扰动。

这里的噪声不能用模型 UV 采样——覆盖是世界空间的现象,几个物体放在一起,冰的纹理应该连续、尺度一致。用世界坐标采样又有一个问题:只用 xz 投影,侧面会被拉成一条条竖线。所以用三平面映射(Triplanar):分别沿 X、Y、Z 三个轴投影采样,再按法线三个分量的权重混合。第 20 章会把三平面映射讲全(包括法线贴图的三平面混合),这里用最精简的版本。

Assets/Cookbook/Ch18/FrozenCore.hlsl(节选:triplanar)
// 极简三平面采样:按法线三个分量的权重,混合 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;
}
Assets/Cookbook/Ch18/FrozenCore.hlsl(节选:mask)
// 覆盖遮罩:法线越朝上越先结冰,噪声把分界线打散。_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 版本。

Assets/Cookbook/Ch18/FrozenCore.hlsl(节选:thicken)
// 冰层有厚度:被覆盖的顶点沿法线外推。阴影和深度 Pass 也要做同样的外推,否则阴影对不上
float3 ThickenIce(float3 positionWS, half3 normalWS)
{
    return positionWS + normalWS * FrostMask(positionWS, normalWS) * _Thickness;
}
// 冰层有厚度:被覆盖的顶点沿法线外推。阴影和深度 Pass 也要做同样的外推,否则阴影对不上
vec3 ThickenIce(vec3 positionWS, vec3 normalWS)
{
    return positionWS + normalWS * FrostMask(positionWS, normalWS) * _Thickness;
}

和溶解一样,顶点变形必须在所有 Pass 里同步:ShadowCaster 和 DepthOnly 也要调用 ThickenIce,否则影子还是原来的瘦轮廓,深度图里的轮廓和屏幕上的也对不上(SSAO、后处理描边都会出错)。

冰的外观

片元里做四件事:① 冰色在浅蓝和深蓝之间按噪声变化,Worley 噪声(公共贴图 noise_cells)的细胞边界很亮,取边界做白色裂纹;② 光滑度提高到 0.9,冰面有锐利的高光;③ 冷色菲涅尔,模拟光在冰层里散射后从边缘透出来;④ 闪点:把世界坐标量化成很小的格子,每个格子一个哈希值,只保留最亮的千分之三,再乘一个视角相关的高光项,转动视角时会一闪一闪。

Assets/Cookbook/Ch18/Frozen.shader(节选:frag)
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 与公共部分:

Assets/Cookbook/Ch18/FrozenCore.hlsl
// 冰冻覆盖的公共部分:材质参数、三平面噪声、覆盖遮罩、顶点加厚。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
Assets/Cookbook/Ch18/Frozen.shader
// 案例 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
        }
    }
}
图 18-18冰冻覆盖,Freeze 从左到右为 0、0.35、0.62、0.9。三个物体使用不同的底色贴图,但冰的纹理在世界空间里连续,顶面最先结冰,最后蔓延到底部。

常见坑与扩展