GameTech/URP 渲染实战

案例 19.1搭场景:一个有深有浅的水池入门

目标:准备一个能“考验”水面的场景,并放上最朴素的水:一块单色半透明平面。

水的效果几乎全部依赖“水下有什么”。一块水面放在空无一物的平地上,深度着色、折射、泡沫、焦散都无从体现。所以我们先搭一个小水池:地形是一张程序生成的高度场网格,中间深约 1.7 米,左后方是缓坡沙滩;水里有几块石头(一块完全淹没、顶部离水面很近,一块露出水面),三根从水底伸出水面的石柱,以及一座横跨在水面上方的木栈道。沙子用棋盘格纹理——规则的格子最能看出折射的扭曲和焦散的形状。

水面本身是一张 60 米见方的细分网格(Shot.Grid),放在 y=0。它被放在内置的 Water 层(第 4 层),后面平面反射的相机会排除这一层。

Shader 的框架如下(顶点、片元函数在后面的案例里逐段讲解):

Assets/Cookbook/Ch19/Water.shader
// 第 19 章:完整的风格化水面。每个特性一个关键字,正文按案例逐个打开。
// 依赖:URP Asset 开启 Depth Texture 与 Opaque Texture;水面放在 Water 层(平面反射相机不渲染它)。
Shader "Cookbook/Ch19/Water"
{
    Properties
    {
        [Header(Depth Color)]
        _ShallowColor ("Shallow Color (A=alpha)", Color) = (0.35,0.85,0.8,0.35)
        _DeepColor ("Deep / Scatter Color (A=alpha)", Color) = (0.03,0.22,0.35,0.95)
        _DepthMax ("Depth For Deep Color (m)", Float) = 2
        _Absorption ("Absorption per meter (RGB)", Vector) = (0.9,0.32,0.22,0)
        _EdgeFade ("Shore Edge Fade (m)", Float) = 0.08

        [Header(Refraction)]
        [Toggle(_REFRACTION)] _Refraction ("Refraction", Float) = 1
        _RefractionStrength ("Refraction Strength", Range(0,0.2)) = 0.05
        [Toggle] _RefractLeakFix ("Fix Foreground Leak", Float) = 1

        [Header(Normals)]
        [Toggle(_NORMALS)] _Normals ("Dual Normals", Float) = 1
        [NoScaleOffset][Normal] _NormalMap ("Normal Map", 2D) = "bump" {}
        _NormalTiling ("Tiling A/B (per meter)", Vector) = (0.18,0.31,0,0)
        _FlowA ("Flow A (xy dir*speed, z strength)", Vector) = (0.03,0.02,0.6,0)
        _FlowB ("Flow B (xy dir*speed, z strength)", Vector) = (-0.025,0.035,0.45,0)

        [Header(Reflection)]
        [Toggle(_REFLECTION)] _Reflection ("Reflection", Float) = 1
        [Toggle(_PLANAR_REFLECTION)] _PlanarReflection ("Use Planar Reflection Texture", Float) = 0
        _ReflectionStrength ("Reflection Strength", Range(0,1)) = 1
        [Toggle] _UseFresnel ("Use Fresnel", Float) = 1
        _Roughness ("Reflection Roughness", Range(0,1)) = 0.05
        _PlanarDistortion ("Planar Reflection Distortion", Range(0,0.2)) = 0.04

        [Header(Specular)]
        [Toggle(_SPECULAR)] _Specular ("Sun Specular", Float) = 1
        _SpecPower ("Spec Power", Range(16,2048)) = 600
        _SpecIntensity ("Spec Intensity", Range(0,4)) = 1

        [Header(Foam)]
        [Toggle(_FOAM)] _Foam ("Shore Foam", Float) = 1
        [NoScaleOffset] _FoamNoise ("Foam Noise", 2D) = "gray" {}
        _FoamColor ("Foam Color", Color) = (0.95,0.98,1,1)
        _FoamDistance ("Foam Distance (m)", Float) = 0.6
        _FoamTiling ("Foam Tiling (per meter)", Float) = 0.35
        _FoamSoftness ("Foam Softness", Range(0.001,0.3)) = 0.03

        [Header(Caustics)]
        [Toggle(_CAUSTICS)] _Caustics ("Caustics", Float) = 1
        _CausticScale ("Caustic Scale (cells per meter)", Float) = 1.6
        _CausticIntensity ("Caustic Intensity", Range(0,4)) = 1.2
        _CausticDepthFade ("Caustic Depth Fade (m)", Float) = 1.5
        _CausticSplit ("Caustic RGB Split", Range(0,0.1)) = 0.025

        [Header(Gerstner Waves)]
        [Toggle(_GERSTNER)] _Gerstner ("Gerstner Waves", Float) = 0
        _WaveA ("Wave A (dir, steepness, wavelength)", Vector) = (1,0.3,0.18,7)
        _WaveB ("Wave B", Vector) = (0.4,1,0.15,4.3)
        _WaveC ("Wave C", Vector) = (-0.7,0.6,0.12,2.6)

        [Header(Time and Debug)]
        [Toggle] _AutoPlay ("Auto Play (_Time)", Float) = 0
        _Phase ("Phase (seconds)", Float) = 0
        [Enum(Off,0,VerticalDepth,1,ViewPath,2,Normal,3,Foam,4,Caustics,5,Fresnel,6)] _DebugView ("Debug View", Float) = 0
    }

    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent-100" "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            Name "Water"
            Tags { "LightMode"="UniversalForward" }
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            Cull Back

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature_local _REFRACTION
            #pragma shader_feature_local _NORMALS
            #pragma shader_feature_local_fragment _REFLECTION
            #pragma shader_feature_local_fragment _PLANAR_REFLECTION
            #pragma shader_feature_local_fragment _SPECULAR
            #pragma shader_feature_local_fragment _FOAM
            #pragma shader_feature_local_fragment _CAUSTICS
            #pragma shader_feature_local_vertex _GERSTNER
            #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
            #pragma multi_compile_fog

            #include "Assets/Cookbook/Ch19/WaterCore.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float4 tangentOS  : TANGENT;
            };

            struct Varyings
            {
                float4 positionCS  : SV_POSITION;
                float3 positionWS  : TEXCOORD0;
                half3  normalWS    : TEXCOORD1;
                half3  tangentWS   : TEXCOORD2;
                half3  bitangentWS : TEXCOORD3;
                half   fogFactor   : TEXCOORD4;
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
                // 平静水面:切线 +X、副切线 +Z、法线 +Y
                float3 tangent = float3(1, 0, 0);
                float3 binormal = float3(0, 0, 1);
            #if defined(_GERSTNER)
                float t = WaterTime();
                float3 p = positionWS;
                positionWS += GerstnerWave(_WaveA, p, t, tangent, binormal);
                positionWS += GerstnerWave(_WaveB, p, t, tangent, binormal);
                positionWS += GerstnerWave(_WaveC, p, t, tangent, binormal);
            #endif
                o.positionWS = positionWS;
                o.positionCS = TransformWorldToHClip(positionWS);
                o.tangentWS = normalize(tangent);
                o.bitangentWS = normalize(binormal);
                o.normalWS = normalize(cross(binormal, tangent));
                o.fogFactor = ComputeFogFactor(o.positionCS.z);
                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                float t = WaterTime();
                float2 screenUV = GetNormalizedScreenSpaceUV(i.positionCS);
                half3 viewWS = GetWorldSpaceNormalizeViewDir(i.positionWS);
                float surfaceEye = LinearEyeDepth(i.positionWS, UNITY_MATRIX_V);
                Light mainLight = GetMainLight(TransformWorldToShadowCoord(i.positionWS));

                // ---- 法线 ----
                half3 normalTS = half3(0, 0, 1);
            #if defined(_NORMALS)
                normalTS = DualNormalTS(i.positionWS.xz, t);
            #endif
                half3x3 tbn = half3x3(i.tangentWS, i.bitangentWS, i.normalWS);
                half3 normalWS = normalize(mul(normalTS, tbn));

                // ---- 深度 ----
                WaterDepth wd = SampleWaterDepth(screenUV, i.positionWS);
                // 水体自身被照亮的颜色(散射色):主光 + 环境光
                half3 waterLight = mainLight.color * saturate(mainLight.direction.y) * mainLight.shadowAttenuation + SampleSH(half3(0, 1, 0));
                half3 color;
                half alpha;

            #if defined(_REFRACTION)
                // ---- 折射 + 吸收(不透明合成:自己把“水下画面”算出来,alpha = 1)----
                float2 uvR = RefractUV(screenUV, normalTS, surfaceEye, wd.vertical);
                WaterDepth wdR = SampleWaterDepth(uvR, i.positionWS);
                half3 sceneColor = SampleSceneColor(uvR);
                #if defined(_CAUSTICS)
                sceneColor += Caustics(wdR.sceneWS, wdR.vertical, SceneUpFacing(wd.sceneWS), mainLight, t);
                #endif
                color = UnderwaterColor(sceneColor, wdR.path, _DeepColor.rgb * waterLight);
                // 岸线处和未扰动的画面过渡,避免水面边缘出现一条硬线
                color = lerp(SampleSceneColor(screenUV), color, saturate(wd.vertical / _EdgeFade));
                alpha = 1;
            #else
                // ---- 最简单的版本:按竖直水深在浅色和深色之间插值,靠 alpha 混合透出水底 ----
                half d01 = saturate(wd.vertical / _DepthMax);
                color = lerp(_ShallowColor.rgb, _DeepColor.rgb, d01) * waterLight;
                alpha = lerp(_ShallowColor.a, _DeepColor.a, d01) * saturate(wd.vertical / _EdgeFade);
            #endif

                // ---- 反射 ----
                half fresnel = _UseFresnel > 0.5 ? WaterFresnel(normalWS, viewWS) : 1.0h;
            #if defined(_REFLECTION)
                #if defined(_PLANAR_REFLECTION)
                    half3 reflection = SAMPLE_TEXTURE2D(_PlanarReflectionTex, sampler_PlanarReflectionTex,
                                                        screenUV + normalTS.xy * _PlanarDistortion).rgb;
                #else
                    half3 reflectDir = reflect(-viewWS, normalWS);
                    // Forward+ 下只有带屏幕 UV 的 5 参数版本(反射探针按屏幕分块查找),4 参数版本被 #if 掉了
                    half3 reflection = GlossyEnvironmentReflection(reflectDir, i.positionWS, _Roughness, 1.0h, screenUV);
                #endif
                half kr = fresnel * _ReflectionStrength;
                color = lerp(color, reflection, kr);
                alpha = lerp(alpha, 1.0h, kr);
            #endif

                // ---- 高光 ----
            #if defined(_SPECULAR)
                color += SunSpecular(mainLight, normalWS, viewWS);
            #endif

                // ---- 泡沫 ----
                half foam = 0;
            #if defined(_FOAM)
                foam = ShoreFoam(i.positionWS.xz, wd.vertical, normalTS, t);
                color = lerp(color, _FoamColor.rgb * waterLight, foam);
                alpha = max(alpha, foam);
            #endif

                // ---- 调试视图:把中间量直接画出来 ----
                if (_DebugView > 0.5)
                {
                    half3 dbg = 0;
                    if (_DebugView < 1.5)      dbg = saturate(wd.vertical / _DepthMax);
                    else if (_DebugView < 2.5) dbg = saturate(wd.path / _DepthMax);
                    else if (_DebugView < 3.5) dbg = half3(normalTS.xy * 1.5h + 0.5h, 0.5h);   // 放大切线空间 xy,看清纹理
                    else if (_DebugView < 4.5) dbg = foam;
                    else if (_DebugView < 5.5) dbg = CausticPattern(wd.sceneWS.xz * _CausticScale, t) * 0.5;
                    else                       dbg = fresnel;
                    return half4(dbg, 1);
                }

                color = MixFog(color, i.fogFactor);
                return half4(color, alpha);
            }
            ENDHLSL
        }
    }
}
// 第 19 章:完整的风格化水面。每个特性一个关键字,正文按案例逐个打开。(GLSL 版,对应 Ch19/Water.shader)
// 依赖:URP Asset 开启 Depth Texture 与 Opaque Texture;水面放在 Water 层(平面反射相机不渲染它)。
Shader "Cookbook/Ch19/Water"
{
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent-100" "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            Name "Water"
            Tags { "LightMode"="UniversalForward" }
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            Cull Back

            GLSLPROGRAM
            #pragma shader_feature_local _REFRACTION
            #pragma shader_feature_local _NORMALS
            #pragma shader_feature_local_fragment _REFLECTION
            #pragma shader_feature_local_fragment _PLANAR_REFLECTION
            #pragma shader_feature_local_fragment _SPECULAR
            #pragma shader_feature_local_fragment _FOAM
            #pragma shader_feature_local_fragment _CAUSTICS
            #pragma shader_feature_local_vertex _GERSTNER
            #pragma multi_compile_fog

            #include "Ch19/WaterCore.glsl"

            VARYING vec3  positionWS;
            VARYING vec3  normalWS;
            VARYING vec3  tangentWS;
            VARYING vec3  bitangentWS;
            VARYING float fogFactor;

            #ifdef VERTEX
            void main()
            {
                vec3 pWS = TransformObjectToWorld(positionOS.xyz);
                // 平静水面:切线 +X、副切线 +Z、法线 +Y
                vec3 tangent = vec3(1, 0, 0);
                vec3 binormal = vec3(0, 0, 1);
            #if defined(_GERSTNER)
                float t = WaterTime();
                vec3 p = pWS;
                pWS += GerstnerWave(_WaveA, p, t, tangent, binormal);
                pWS += GerstnerWave(_WaveB, p, t, tangent, binormal);
                pWS += GerstnerWave(_WaveC, p, t, tangent, binormal);
            #endif
                positionWS = pWS;
                gl_Position = TransformWorldToHClip(pWS);
                tangentWS = normalize(tangent);
                bitangentWS = normalize(binormal);
                normalWS = normalize(cross(binormal, tangent));
                fogFactor = ComputeFogFactor(gl_Position.z);
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                float t = WaterTime();
                vec2 screenUV = GetNormalizedScreenSpaceUV(gl_FragCoord);
                vec3 viewWS = GetWorldSpaceNormalizeViewDir(positionWS);
                float surfaceEye = LinearEyeDepth(positionWS, UNITY_MATRIX_V);
                Light mainLight = GetMainLight(TransformWorldToShadowCoord(positionWS));

                // ---- 法线 ----
                vec3 normalTS = vec3(0, 0, 1);
            #if defined(_NORMALS)
                normalTS = DualNormalTS(positionWS.xz, t);
            #endif
                // HLSL 的 half3x3(T, B, N) 按行构造、mul(normalTS, tbn);GLSL 的 mat3(T, B, N) 按列,写成 M * v 结果相同
                mat3 tbn = mat3(tangentWS, bitangentWS, normalWS);
                vec3 N = normalize(tbn * normalTS);

                // ---- 深度 ----
                WaterDepth wd = SampleWaterDepth(screenUV, positionWS);
                // 水体自身被照亮的颜色(散射色):主光 + 环境光
                vec3 waterLight = mainLight.color * saturate(mainLight.direction.y) * mainLight.shadowAttenuation + SampleSH(vec3(0, 1, 0));
                vec3 color;
                float alpha;

            #if defined(_REFRACTION)
                // ---- 折射 + 吸收(不透明合成:自己把“水下画面”算出来,alpha = 1)----
                vec2 uvR = RefractUV(screenUV, normalTS, surfaceEye, wd.vertical);
                WaterDepth wdR = SampleWaterDepth(uvR, positionWS);
                vec3 sceneColor = SampleSceneColor(uvR);
                #if defined(_CAUSTICS)
                sceneColor += Caustics(wdR.sceneWS, wdR.vertical, SceneUpFacing(wd.sceneWS), mainLight, t);
                #endif
                color = UnderwaterColor(sceneColor, wdR.path, _DeepColor.rgb * waterLight);
                // 岸线处和未扰动的画面过渡,避免水面边缘出现一条硬线
                color = mix(SampleSceneColor(screenUV), color, saturate(wd.vertical / _EdgeFade));
                alpha = 1.0;
            #else
                // ---- 最简单的版本:按竖直水深在浅色和深色之间插值,靠 alpha 混合透出水底 ----
                float d01 = saturate(wd.vertical / _DepthMax);
                color = mix(_ShallowColor.rgb, _DeepColor.rgb, d01) * waterLight;
                alpha = mix(_ShallowColor.a, _DeepColor.a, d01) * saturate(wd.vertical / _EdgeFade);
            #endif

                // ---- 反射 ----
                float fresnel = _UseFresnel > 0.5 ? WaterFresnel(N, viewWS) : 1.0;
            #if defined(_REFLECTION)
                #if defined(_PLANAR_REFLECTION)
                    vec3 reflection = texture(_PlanarReflectionTex, screenUV + normalTS.xy * _PlanarDistortion).rgb;
                #else
                    vec3 reflectDir = reflect(-viewWS, N);
                    // HLSL 版用 Forward+ 的 5 参数版本(带屏幕 UV);网页版没有反射探针分块,用 4 参数版本
                    vec3 reflection = GlossyEnvironmentReflection(reflectDir, positionWS, _Roughness, 1.0);
                #endif
                float kr = fresnel * _ReflectionStrength;
                color = mix(color, reflection, kr);
                alpha = mix(alpha, 1.0, kr);
            #endif

                // ---- 高光 ----
            #if defined(_SPECULAR)
                color += SunSpecular(mainLight, N, viewWS);
            #endif

                // ---- 泡沫 ----
                float foam = 0.0;
            #if defined(_FOAM)
                foam = ShoreFoam(positionWS.xz, wd.vertical, normalTS, t);
                color = mix(color, _FoamColor.rgb * waterLight, foam);
                alpha = max(alpha, foam);
            #endif

                // ---- 调试视图:把中间量直接画出来 ----
                if (_DebugView > 0.5)
                {
                    vec3 dbg = vec3(0.0);
                    if (_DebugView < 1.5)      dbg = vec3(saturate(wd.vertical / _DepthMax));
                    else if (_DebugView < 2.5) dbg = vec3(saturate(wd.path / _DepthMax));
                    else if (_DebugView < 3.5) dbg = vec3(normalTS.xy * 1.5 + 0.5, 0.5);   // 放大切线空间 xy,看清纹理
                    else if (_DebugView < 4.5) dbg = vec3(foam);
                    else if (_DebugView < 5.5) dbg = vec3(CausticPattern(wd.sceneWS.xz * _CausticScale, t) * 0.5);
                    else                       dbg = vec3(fresnel);
                    fragColor = vec4(dbg, 1.0);
                    return;
                }

                color = MixFog(color, fogFactor);
                fragColor = vec4(color, alpha);
            }
            #endif
            ENDGLSL
        }
    }
}

先看渲染状态:"Queue"="Transparent-100",比普通透明物体早一点画,这样水面之后再画的粒子、玻璃会正确地叠在水面上;Blend SrcAlpha OneMinusSrcAlpha 标准 Alpha 混合;ZWrite Off 不写深度。水面不需要 ShadowCaster(它不该在水底投下影子),也不需要 DepthOnly(它不能出现在深度图里,否则后面读深度图时读到的就是水面自己)。

关闭所有关键字时,片元走最简单的分支:颜色和 alpha 都是常数。

图 19-2左:没有水的水池场景;右:最朴素的水——单色、单一透明度。它的问题一眼可见:深处和浅处一样透明,水面像一块蓝色塑料片,和岸边的交线生硬。