Why a Dedicated Water Model?为何需要专用水体模型?

Water is optically unique: it simultaneously reflects (air–water interface with Fresnel), refracts (objects underwater are displaced by IOR ≈ 1.33), and absorbs (exponential extinction per wavelength — red dies fastest, blue persists longest). None of the standard shading models handle all three together. The Single Layer Water shading model was purpose-built to solve this, treating the water surface as a thin interface between air and a participating medium.

水在光学上是独特的:它同时反射(具有 Fresnel 的空气–水界面)、 折射(水下物体因 IOR ≈ 1.33 而发生位移),以及吸收 (每个波长的指数衰减——红色最先消失,蓝色持续最久)。 没有任何标准着色模型能同时处理这三者。 单层水体着色模型专为解决此问题而构建, 将水面视为空气与参与介质之间的薄界面。

Water = 3 phenomena at the surface + 1 beneath: ┌──── AIR ────────────────────────────────────────────────┐ │ Reflected ray: Fresnel(IOR=1.33) × sky/reflection │ │ Refracted ray: bent by IOR, enters water volume │ ├──── WATER SURFACE ──────────────────────────────────────┤ │ Scatter color: Mie/Rayleigh — the "teal/blue" of water│ │ Absorption: exp(-extinction × depth) per R,G,B │ │ → deep water kills red first │ ├──── WATER BED / OBJECTS ────────────────────────────────┤ │ Scene depth buffer used to determine water depth │ │ Depth = WaterSurface.z - SceneDepth.z │ └─────────────────────────────────────────────────────────┘

Single Layer Water BRDF — The Math单层水体 BRDF — 数学原理

The water shading model evaluates four components and combines them into the final pixel color:

水体着色模型计算四个分量并将它们合并为最终像素颜色:

// SingleLayerWaterShading.ush — conceptual structure
float4 EvaluateSingleLayerWater(FSingleLayerWaterData W, FGBufferData G, ...)
{
    // 1. SURFACE SPECULAR — GGX reflection, same as Default Lit
    float3 SurfaceSpecular = SpecularGGX(G.Roughness, 0.02 /*F0 for water*/, Context);

    // 2. FRESNEL REFLECTANCE — how much light reflects vs refracts
    // Schlick approximation with IOR=1.33
    float F0_water = ((1.33f - 1.0f) / (1.33f + 1.0f));
    F0_water *= F0_water; // ≈ 0.02
    float FresnelR = F0_water + (1 - F0_water) * pow5(1 - saturate(dot(N, V)));
    float Transmission = 1.0f - FresnelR;   // complement: refracted fraction

    // 3. WATER ABSORPTION — Beer-Lambert law per channel
    // WaterDepth = distance from surface to bed (in cm)
    float WaterDepth = max(0, W.SurfaceWorldZ - SceneBedWorldZ);
    float3 Transmittance = exp(-W.AbsorptionCoeff * WaterDepth / 100.0f);

    // 4. SCATTER COLOR — in-water scattering (Mie: forward scatter, blue-ish)
    // Henyey-Greenstein phase function for directional scatter
    float ScatterPhase = HenyeyGreenstein(W.PhaseG, dot(L, V));
    float3 ScatterColor = W.ScatterColor * ScatterPhase * WaterDepth;

    // 5. UNDERWATER COLOR — what's below, attenuated by absorption
    float3 RefractedUV  = ComputeRefractedUV(PixelPos, G.Normal, IOR);
    float3 Underwater   = SampleSceneColor(RefractedUV) * Transmittance;

    // 6. FINAL COMPOSITE
    return FresnelR * (SurfaceSpecular + ReflectionColor) +   // reflected
           Transmission * (Underwater + ScatterColor);          // refracted
}

Key Physical Parameters Explained关键物理参数说明

Parameter参数 Physical Meaning物理含义 Typical Values典型值
WaterScatterColorThe color of light scattered within the water body — gives tropical water its teal/cyan, or murky water its greenish-brown tint水体内散射光的颜色——使热带水呈现蓝绿/青色,或浑浊水呈现绿棕色Tropical: (0.1, 0.6, 0.6)
Ocean: (0.05, 0.3, 0.5)
热带:(0.1, 0.6, 0.6)
WaterAbsorptionColorPer-channel extinction coefficient (cm⁻¹). Higher = faster absorption. Red absorbs first in real water.逐通道消光系数(cm⁻¹)。越高吸收越快。真实水中红色最先被吸收。Clear: (0.3, 0.05, 0.01)
Murky: (0.7, 0.4, 0.2)
清澈:(0.3, 0.05, 0.01)
ColorAbsorptionDepthReference depth (cm) for absorption curve normalization吸收曲线归一化的参考深度(cm)200–500
WaterPhaseGHenyey-Greenstein phase anisotropy: 0=isotropic, +1=full forward scatter, -1=full back scatterHenyey-Greenstein 相位各向异性:0=各向同性,+1=完全前向散射,-1=完全后向散射0.1–0.5

Water Rendering Pipeline — Per-Frame Passes水体渲染管线 — 逐帧通道

Per-frame water rendering (SingleLayerWaterRendering.cpp): 1. Depth Pre-Pass (normal): → Water mesh writes to depth buffer normally → Allows correct depth-based occlusion 2. Scene Opaque Rendering: → All opaque objects rendered to GBuffer + SceneColor 3. SingleLayerWater Pass ← THE KEY PASS → Water surface rendered with SingleLayerWater material → At this point: SceneColor has the underwater scene → Shader reads SceneColor (with refraction offset) → Applies absorption: attenuate by exp(-absorption * depth) → Adds scatter color → Adds Fresnel reflection (from Reflection Capture / SSR / Lumen) → Writes final composited color to SceneColor 4. Translucency Pass (after water): → Foam, splashes, particles drawn on top

The critical insight is that water renders after all opaque geometry. At render time, the SceneColor texture already contains the correctly-lit underwater floor. The water shader simply reads it (with a refraction offset for distortion), applies the absorption/scatter math, and adds the surface reflection — all in a single pass. No separate "underwater render" is needed.

关键洞察是水体在所有不透明几何体之后渲染。在渲染时, SceneColor 纹理已经包含正确光照的水下地面。 水体 Shader 只需读取它(使用折射偏移进行扭曲),应用吸收/散射数学, 然后添加水面反射——全在单一通道中完成。不需要单独的"水下渲染"。

Refraction & Underwater Distortion折射与水下扭曲

Refraction in UE5 water works by offsetting the screen-space UV when sampling the underwater scene color. The offset direction and magnitude is derived from the surface normal: a flat calm water surface has zero offset (you see straight down); a normal-mapped choppy surface distorts the underwater view.

UE5 水体中的折射通过在采样水下场景颜色时偏移屏幕空间 UV 来实现。 偏移方向和大小从表面法线推导: 平静的水面偏移为零(你直视水下);法线映射的波涛汹涌的水面扭曲水下视图。

// Refraction UV offset (SingleLayerWaterShading.ush concept)
float2 ComputeRefractionUVOffset(float3 WorldNormal, float3 V, float RefractStrength)
{
    // Project normal deviation onto screen plane
    float2 NormalXY = WorldNormal.xy;

    // Scale by depth (closer to camera = larger apparent distortion)
    float DepthScale = 1.0f / max(0.01f, SceneDepth);
    return NormalXY * RefractStrength * DepthScale;
}

// Sample underwater with refraction:
float2 RefractUV = PixelUV + ComputeRefractionUVOffset(N, V, RefractStrength);

// Clamp to avoid sampling outside the screen
RefractUV = clamp(RefractUV, float2(0.001f), float2(0.999f));

// Depth test: if refracted UV hits geometry above water, fall back to no-refract
float RefractedDepth = SceneDepthTexture.SampleLevel(PointSampler, RefractUV, 0).r;
if (RefractedDepth < SurfaceDepth)
    RefractUV = PixelUV;  // fallback: no distortion for above-water pixels

float3 UnderwaterColor = SceneColorTexture.SampleLevel(BilinearSampler, RefractUV, 0).rgb;

Water Info Texture — Per-Pixel Water Properties水体信息纹理 — 逐像素水体属性

The Water Info Texture (WaterInfoTextureRendering.cpp) is a screen-space render target that stores per-pixel water properties: surface height, normal, and flow velocity. It's generated before the main water pass by rendering the water surface to a dedicated 2D texture. Downstream effects (foam, depth fade, caustics) sample this texture to know whether a given screen pixel is underwater and at what depth.

水体信息纹理WaterInfoTextureRendering.cpp)是一个 存储逐像素水体属性的屏幕空间渲染目标:水面高度、法线和流速。 它在主水体通道之前通过将水面渲染到专用 2D 纹理来生成。 下游效果(泡沫、深度消退、焦散)采样此纹理以了解给定屏幕像素是否在水下以及深度如何。

Reflections — SSR, Lumen & Planar反射 — SSR、Lumen 与平面反射

Water integrates with all three reflection systems in UE5:

水体与 UE5 中的所有三种反射系统集成:

Material Setup Guide — Creating a Water Material材质配置指南 — 创建水体材质

Step 1: Material Settings步骤 1:材质设置

Step 2: Required Material Inputs步骤 2:必要材质输入

// After selecting SingleLayerWater shading model, these special pins appear:

Normal:
  → Use two Panner nodes on a Normal Map texture (different speed/scale)
  → Cross-fade between two tiling scales for convincing motion
  → Adjust amplitude with HeightLerp or Lerp(Flat, Normal, WaveHeight)

Water Scatter Color:  float3
  → The "in-water" color. Tropical: (0.05, 0.5, 0.5). Deep ocean: (0.01, 0.2, 0.4)

Water Absorption Color:  float3
  → How fast each RGB channel dies with depth
  → For realistic ocean: (0.3, 0.06, 0.01) — red dies fastest
  → Connected to: Multiply(BaseColor, AbsorptionDepthMask)

Water Phase G:  scalar [−1, 1]
  → 0 = isotropic scatter, 0.3 = forward bias (sunlit underwater glow)

Specular:  scalar [0, 1]
  → Leave at 0.5 (maps to F0=0.04 for water)

Roughness:  scalar
  → 0 = dead calm (mirror), 0.05–0.2 = ocean chop, 0.5+ = storm

Refraction (implicit):  controlled by Normal map strength
  → Stronger normals = more underwater distortion

Step 3: Foam & Shoreline步骤 3:泡沫与海岸线

// Shore fade using Water Body depth:

// In the material, use the built-in WaterBodyDepth output
// (available on Water Body component, or via WaterInfoTexture)

float ShoreFade = saturate(WaterBodyDepth / ShoreTransitionDist);
float3 FoamMask = 1 - ShoreFade;  // white at shore, zero in deep

// Blend foam texture using FoamMask
float3 FoamColor = FoamTexture.Sample(Sampler, UV * 8) * FoamMask;

// Add foam to Emissive or blend into BaseColor
// Note: foam uses Emissive not BaseColor to avoid shadowing
Emissive = FoamColor * SunLightColor;

Performance Tuning性能调优

CVarEffect效果
r.Water.SingleLayer.RefractionEnable/disable screen-space refraction (0 = no underwater distortion, saves ~0.5ms)启用/禁用屏幕空间折射(0 = 无水下扭曲,节省约 0.5ms)
r.Water.SingleLayer.ReflectionEnable/disable surface reflections (0 = no reflections, significant savings)启用/禁用水面反射(0 = 无反射,显著节省)
r.Water.SingleLayer.ShadowReceivingEnable/disable shadow receiving on water (0 = no shadows cast on water)启用/禁用水体接收阴影(0 = 水体上无阴影)
r.SSR.QualitySSR quality on water (0=off, 1–4, 4=best). Reduce to 2 on mobile.水体上的 SSR 质量(0=关,1–4,4=最佳)。移动端减为 2。
r.Water.VisualizeWaterInfo1 = show the WaterInfo texture (depth + normal + velocity) for debugging1 = 显示 WaterInfo 纹理(深度 + 法线 + 速度)用于调试
Water + Translucency sorting: Objects that overlap with the water surface (boats, buoys, shore foam) must be carefully ordered. Objects inside the water should use the default opaque pipeline so they appear in the underwater scene color. Objects on the water (foam, splashes) should be translucent. Misclassifying these causes objects to ignore the underwater absorption or appear doubled. 水体 + 半透明排序:与水面重叠的物体(船只、浮标、海岸泡沫)必须仔细排序。水的物体应使用默认不透明管线,以便出现在水下场景颜色中。水的物体(泡沫、水花)应该是半透明的。对这些物体的错误分类会导致物体忽略水下吸收或出现重影。

Source Navigation源码导航

Unreal Engine 5.6 · Engine/Source/Runtime/Renderer/Private/ · Engine/Shaders/Private/
Source analysis · For learning purposes