Unity深度纹理实现水面渲染:从原理到实战的完整Shader开发指南
2026/8/4 13:32:42 网站建设 项目流程

1. 项目概述:从深度纹理到水面涟漪

做实时渲染,特别是涉及环境交互的效果时,水面永远是那个既让人着迷又让人头疼的课题。一个逼真的水面,远不止是贴上一张带法线扰动的纹理那么简单。它需要反射周围的环境,需要有基于深度的透明与颜色过渡,需要有岸边细腻的泡沫,更重要的是,它需要与场景中的物体(比如角色、船只、石块)产生真实的互动,形成涟漪或波浪。如果仅仅用平面加Shader,水面就会像一块僵硬的塑料布,物体穿行而过时毫无痕迹, immersion(沉浸感)瞬间被打破。

要实现这种动态交互,核心在于让Shader“知道”场景的几何信息。而Camera Depth Texture(摄像机深度纹理),正是Unity提供给我们的一把窥视场景几何结构的钥匙。它不是什么高深莫测的黑科技,本质上就是摄像机在渲染完不透明物体后,将每个像素到摄像机的深度值(通常是线性或眼空间深度)存储在一张纹理中。这张纹理,后续的渲染管线(比如透明物体、后处理)都可以读取。对于水面Shader来说,这意味着我们可以获取到水面下方(即被水面覆盖的)场景点的深度,与水面的深度进行比较,从而精确计算出水体的厚度、实现基于深度的颜色衰减、控制岸边泡沫的生成,甚至驱动顶点位移来模拟物体划过水面激起的涟漪。

这次要实现的,就是一个深度感知的互动水面。它不是一个简单的展示,而是一个可以直接投入项目使用的解决方案。我会从深度纹理的获取与解码讲起,一步步构建出包含深度色、岸边泡沫、动态涟漪和简单反射的水面Shader,并提供完整的、注释详尽的代码。无论你是想在自己的开放世界游戏中加入一片湖泊,还是在独立游戏中营造一个小水洼,这套方法都能给你一个扎实的起点。

2. 核心原理:深度纹理的获取、解码与应用逻辑

在动手写代码之前,我们必须把几个核心概念彻底理清。很多水面效果看起来假,问题往往出在对深度数据的理解和使用上。

2.1 Camera Depth Texture 是什么?Unity如何生成它?

Unity中的深度纹理并非自动开启。我们需要在摄像机或需要深度纹理的Shader中显式声明。最常见的方式是在摄像机组件上勾选“Allow Depth Texture”,或者在脚本中设置camera.depthTextureMode = DepthTextureMode.Depth;

当这个开关被打开,Unity会在渲染完所有标记为“Opaque”(不透明)的物体后,将它们的深度信息写入一张全局纹理,即_CameraDepthTexture。这里有一个关键点:半透明(Transparent)物体不会写入深度纹理。这是由深度缓冲(Z-Buffer)的工作原理决定的。因此,如果你的水面下方有半透明的物体,深度纹理中存储的将是更后方的不透明物体(或天空盒)的深度,这在进行深度比较时需要考虑。

深度纹理的格式通常是RenderTextureFormat.Depth,其每个像素存储的是一个非线性的深度值。这个值在透视投影下,并不是世界空间或观察空间中线性的距离,而是经过投影矩阵变换和齐次除法后的深度(z/w)。这个值在近裁剪面接近0,远裁剪面接近1,并且分布是不均匀的(近处精度高,远处精度低)。直接使用这个值来计算距离是不准确的。

2.2 从深度纹理到线性深度:关键的解码过程

为了得到有物理意义的距离(比如从摄像机到某点的实际单位距离),我们必须进行解码。Unity在UnityCG.cginc中提供了非常便利的函数来帮我们完成这个工作。

在Shader中,我们通常这样采样和转换深度:

// 在Fragment Shader中 float2 screenPos = i.screenPos.xy / i.screenPos.w; // 将裁剪空间坐标归一化到[0,1] float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenPos); float sceneLinearDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams);
  • SAMPLE_DEPTH_TEXTURE: 这是一个宏,负责以正确的方式采样深度纹理。在URP中,它可能对应SampleSceneDepth
  • LinearEyeDepth: 这个函数是魔法发生的地方。它接受非线性的深度纹理值sceneRawDepth和包含投影参数的_ZBufferParams,输出从摄像机(眼睛)到该像素所在世界位置的实际距离(线性深度),单位与你的世界空间单位(通常是米)一致。

得到水面本身的线性深度相对简单。在片元着色器中,水面上的点已经经过了顶点变换,我们可以直接使用其裁剪空间坐标的w分量,或者通过i.posWorld和摄像机世界坐标计算得出。更通用的方法是使用内置的LinearEyeDepth函数处理水面自身的深度值,确保比较基准一致。

2.3 水面效果的核心算法:深度差的应用

当我们同时拥有了水面下方场景的线性深度sceneDepth和水面自身的线性深度waterDepth后,最关键的变量就出现了:深度差depthDifference = sceneDepth - waterDepth

这个depthDifference的物理意义是水体的厚度。它是驱动几乎所有水面视觉特性的核心数据:

  1. 深度色(Depth Color):水越深,颜色越暗(如深蓝),吸收光线越多。我们可以用saturate(1.0 - exp(-_DepthAttenuation * depthDifference))这样的指数衰减函数来控制基础颜色与深度色的混合权重。_DepthAttenuation系数控制衰减速度。
  2. 透明度(Transparency):同样基于深度差。水越浅越透明,能清晰看到水底;水越深越不透明。通常与深度色共用一套衰减控制,或者单独使用一个透明度衰减系数。
  3. 岸边泡沫(Shoreline Foam):这是让水面脱离“塑料感”的关键。泡沫应该出现在水体极浅的地方,即depthDifference非常小的区域。我们可以定义一个_FoamThreshold,当深度差小于此阈值时,通过一个平滑阶梯函数(如smoothstep)来生成泡沫纹理的混合权重。为了更自然,通常还会结合水面法线或世界坐标采样的噪音纹理来扰动泡沫的边缘,使其看起来参差不齐。
  4. 折射(Refraction):虽然本项目不涉及复杂的屏幕空间折射(SSR),但简单的偏移折射可以通过用屏幕坐标加上由法线扰动后的偏移量,去采样场景颜色(通过_CameraOpaqueTexture,即不透明物体渲染后的抓屏纹理)来模拟。深度信息在这里可以用于微调偏移量,例如在浅水区减小折射强度。

理解了这套以深度差为核心的数据流,就等于掌握了编写深度感知水面Shader的“内功心法”。接下来,我们就进入实战环节,一步步搭建起这个Shader。

3. Shader架构设计与关键模块实现

一个结构清晰的Shader是易于维护和调试的基础。我们将采用Unity URP的Lit Shader Graph或手写HLSL代码两种方式来阐述,但原理相通。这里以手写HLSL代码为例,因为它能更清晰地展示每一个计算步骤。

3.1 属性(Properties)定义:暴露给美术调节的参数

首先,在Shader的Properties块中,定义所有需要调节的参数。良好的参数命名和合理的默认值是友好性的体现。

Properties { // 基础颜色 _BaseColor("Base Color", Color) = (0.1, 0.3, 0.5, 1.0) _DepthColor("Depth Color", Color) = (0.02, 0.05, 0.1, 1.0) _DepthAttenuation("Depth Attenuation", Range(0.1, 5.0)) = 1.0 // 泡沫 _FoamColor("Foam Color", Color) = (1,1,1,1) _FoamTexture("Foam Texture", 2D) = "white" {} _FoamThreshold("Foam Threshold", Range(0, 0.5)) = 0.1 _FoamSoftness("Foam Softness", Range(0, 0.2)) = 0.05 _FoamTiling("Foam Tiling", Float) = 5.0 _FoamSpeed("Foam Speed", Vector) = (0.05, 0.05, 0, 0) // 法线扰动(模拟波浪) _NormalMap("Normal Map", 2D) = "bump" {} _NormalStrength("Normal Strength", Range(0, 2)) = 0.5 _NormalTiling("Normal Tiling", Float) = 1.0 _WaveSpeed("Wave Speed", Vector) = (0.1, 0.1, 0, 0) // 反射 _ReflectionStrength("Reflection Strength", Range(0, 1)) = 0.5 _ReflectionDistortion("Reflection Distortion", Range(0, 0.1)) = 0.01 // 顶点涟漪 _RippleStrength("Ripple Strength", Range(0, 0.5)) = 0.05 _RippleFrequency("Ripple Frequency", Float) = 10.0 _RippleDecay("Ripple Decay", Float) = 2.0 }

注意_FoamSpeed_WaveSpeed使用Vector类型,方便独立控制U和V方向的速度。_RippleDecay用于控制涟漪从中心向外扩散时的衰减速度。

3.2 顶点着色器:准备数据与涟漪生成

顶点着色器的主要任务有三个:完成标准的顶点变换,为片元着色器准备必要的纹理坐标和向量数据,以及计算动态的顶点涟漪。

struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 normalWS : TEXCOORD1; float4 tangentWS : TEXCOORD2; float2 uv : TEXCOORD3; float4 screenPos : TEXCOORD4; // 用于深度纹理采样 float2 foamUV : TEXCOORD5; // 流动的泡沫UV }; // 假设有一个全局变量存储了涟漪中心点和强度(可由脚本传入) float3 _RippleCenter; float _RippleCurrentStrength; Varyings vert(Attributes input) { Varyings output; // 1. 基础顶点变换 VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz); output.positionCS = positionInputs.positionCS; output.positionWS = positionInputs.positionWS; // 2. 计算涟漪位移(可选,动态效果) float3 toVertex = output.positionWS - _RippleCenter; float distanceToCenter = length(toVertex); float rippleFactor = exp(-_RippleDecay * distanceToCenter) * sin(_RippleFrequency * distanceToCenter - _Time.y * 10); // 只影响Y轴(高度) output.positionWS.y += rippleFactor * _RippleCurrentStrength; // 需要重新计算裁剪空间位置 output.positionCS = TransformWorldToHClip(output.positionWS); // 3. 准备其他数据 VertexNormalInputs normalInputs = GetVertexNormalInputs(input.normalOS, input.tangentOS); output.normalWS = normalInputs.normalWS; output.tangentWS = float4(normalInputs.tangentWS, input.tangentOS.w); // w存储副切线方向 output.uv = TRANSFORM_TEX(input.texcoord, _NormalMap); // 假设法线纹理为主纹理 // 4. 计算流动的泡沫UV output.foamUV = output.uv * _FoamTiling + _Time.y * _FoamSpeed.xy; // 5. 计算屏幕空间位置,用于后续深度采样 output.screenPos = ComputeScreenPos(output.positionCS); // 注意:这里不对screenPos做透视除法,留到片元着色器进行,以保证正确的线性插值 return output; }

实操心得:计算screenPos时使用ComputeScreenPos函数是标准做法。透视除法 (/w) 必须在片元着色器中进行,因为它在屏幕空间不是线性变化的,在顶点着色器计算会导致插值错误。涟漪计算放在顶点着色器是性能与效果的折中。对于精细、高频率的涟漪,可能需要使用贴图在片元着色器中进行法线扰动,或者使用曲面细分。

3.3 片元着色器:核心效果合成

这里是所有魔法发生的地方。我们将按步骤计算深度、颜色、泡沫、法线扰动和反射,最后合成最终颜色。

// 在片元着色器开头声明需要的纹理采样器 TEXTURE2D(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture); TEXTURE2D(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture); TEXTURE2D(_FoamTexture); SAMPLER(sampler_FoamTexture); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); half4 frag(Varyings input) : SV_Target { // --- 1. 深度计算 --- float2 screenUV = input.screenPos.xy / input.screenPos.w; float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV); float sceneLinearDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams); // 计算水面该点的线性深度(从摄像机到水面点的距离) // 方法:使用水面点的观察空间深度。可以通过重建,这里用更直接的方法: // 水面点的深度就是其裁剪空间坐标的w分量经过LinearEyeDepth转换。 // 但更准确的是直接计算水面点在世界空间的高度差(如果水面是平面)。 // 这里采用通用方法:假设我们通过某种方式得到了水面的深度,例如从平面方程计算。 // 为了简化演示,我们假设水面是一个平面,其世界空间Y坐标为0。 // 在实际项目中,你可能需要传递水面高度或使用更复杂的计算。 float waterPlaneHeight = 0.0; // 假设水面在世界空间Y=0 float3 cameraPosWS = GetCameraPositionWS(); float3 viewDirWS = normalize(input.positionWS - cameraPosWS); // 计算从摄像机到水面平面的距离(沿视线方向) // 这是一个简化版射线-平面求交。对于非平面水面,此方法不适用,需要更精确的深度。 float waterLinearDepth = (waterPlaneHeight - cameraPosWS.y) / viewDirWS.y; // 注意:viewDirWS.y可能为负或零,需要处理。这里仅为示意。 // 更稳健的做法是在顶点着色器计算水面点的观察空间Z值并传递给片元。 // 为了示例的简洁和可运行,我们采用一个近似:用水面点的观察空间深度。 // 这需要我们在顶点着色器计算出观察空间位置并传递。 // 假设我们在Varyings中增加了`float viewDepth : TEXCOORD6`,并在顶点着色器中赋值: // output.viewDepth = -TransformWorldToView(positionInputs.positionWS).z; // 那么在片元中: // float waterLinearDepth = input.viewDepth; // 这已经是线性深度 // 此处我们假设`waterLinearDepth`已通过正确方式获得。 float depthDifference = sceneLinearDepth - waterLinearDepth; // 确保深度差非负 depthDifference = max(0, depthDifference); // --- 2. 基础颜色与深度色混合 --- float depthFactor = saturate(1.0 - exp(-_DepthAttenuation * depthDifference)); half3 waterColor = lerp(_BaseColor.rgb, _DepthColor.rgb, depthFactor); // --- 3. 泡沫计算 --- half4 foamTex = SAMPLE_TEXTURE2D(_FoamTexture, sampler_FoamTexture, input.foamUV); // 泡沫强度:深度差越小,泡沫越强 float foamDepthFactor = 1.0 - saturate(depthDifference / _FoamThreshold); // 使用smoothstep让泡沫边缘过渡柔和 float foamMask = smoothstep(0.5 - _FoamSoftness, 0.5 + _FoamSoftness, foamDepthFactor); half3 foamColor = foamTex.rgb * _FoamColor.rgb * foamMask; // --- 4. 法线扰动 --- float2 normalUV = input.uv * _NormalTiling + _Time.y * _WaveSpeed.xy; half3 normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, normalUV)); normalTS.xy *= _NormalStrength; normalTS.z = sqrt(1.0 - saturate(dot(normalTS.xy, normalTS.xy))); // 确保归一化 // 将法线从切线空间转换到世界空间 float3 bitangentWS = cross(input.normalWS, input.tangentWS.xyz) * input.tangentWS.w; float3x3 TBN = float3x3(input.tangentWS.xyz, bitangentWS, input.normalWS); float3 normalWS = normalize(mul(normalTS, TBN)); // --- 5. 简单反射(基于法线扰动的屏幕空间偏移)--- // 采样场景不透明纹理作为反射源 float2 reflectionUV = screenUV + normalWS.xz * _ReflectionDistortion; half3 sceneColor = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, reflectionUV).rgb; half3 reflection = sceneColor * _ReflectionStrength; // --- 6. 最终颜色合成 --- half3 finalColor = waterColor; finalColor += foamColor; // 叠加泡沫 finalColor = lerp(finalColor, reflection, _ReflectionStrength); // 混合反射 // 应用光照(此处简化,使用简单的NdotL) Light mainLight = GetMainLight(); float NdotL = saturate(dot(normalWS, mainLight.direction)); finalColor *= (mainLight.color * NdotL + unity_AmbientSky); // 透明度计算(基于深度差) float transparency = 1.0 - saturate(depthDifference / _DepthAttenuation); // 使用另一个衰减系数更佳 half alpha = lerp(_BaseColor.a, 0.5, transparency); // 示例混合 return half4(finalColor, alpha); }

注意事项:深度计算是水面Shader中最容易出错的部分。确保sceneLinearDepthwaterLinearDepth是在同一空间(线性观察空间)同一度量标准下进行比较的。LinearEyeDepth函数是确保这一点的关键。对于复杂的水面网格(如波浪起伏的水面),获取精确的waterLinearDepth可能需要将水面网格的观察空间Z值渲染到一张单独的深度纹理中,这属于更高级的实现。

4. 完整Shader代码整合与关键参数解析

将上述模块组合起来,并补充必要的Pass和Tags,我们就得到了一个完整的、功能丰富的深度感知水面Shader。以下是基于Unity URP管线的简化版完整Shader代码框架。

Shader "Custom/AdvancedWater" { Properties { // ... (同上文Properties块,此处省略以节省篇幅) ... } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" "RenderPipeline"="UniversalPipeline" } // 需要深度纹理和不透明纹理 HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl" ENDHLSL Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off // 透明物体通常关闭深度写入,但需要深度测试 Cull Back HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE // 声明Properties中定义的变量 // ... (变量声明,如 half4 _BaseColor; 等) ... // 纹理定义 TEXTURE2D(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture); TEXTURE2D(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture); TEXTURE2D(_FoamTexture); SAMPLER(sampler_FoamTexture); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); struct Attributes { ... }; // 同上文 struct Varyings { ... }; // 同上文,需包含viewDepth Varyings vert(Attributes input) { ... } // 同上文,需计算viewDepth half4 frag(Varyings input) : SV_Target { // --- 深度采样与计算 --- float2 screenUV = input.screenPos.xy / input.screenPos.w; float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV); float sceneLinearDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams); float waterLinearDepth = input.viewDepth; // 从顶点着色器传递而来 float depthDifference = sceneLinearDepth - waterLinearDepth; depthDifference = max(0, depthDifference); // --- 颜色混合 --- float depthFactor = saturate(1.0 - exp(-_DepthAttenuation * depthDifference)); half3 waterColor = lerp(_BaseColor.rgb, _DepthColor.rgb, depthFactor); // --- 泡沫 --- half4 foamTex = SAMPLE_TEXTURE2D(_FoamTexture, sampler_FoamTexture, input.foamUV); float foamDepthFactor = 1.0 - saturate(depthDifference / _FoamThreshold); float foamMask = smoothstep(0.5 - _FoamSoftness, 0.5 + _FoamSoftness, foamDepthFactor); half3 foamColor = foamTex.rgb * _FoamColor.rgb * foamMask; // --- 法线与光照 --- float2 normalUV = input.uv * _NormalTiling + _Time.y * _WaveSpeed.xy; half3 normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, normalUV)); normalTS.xy *= _NormalStrength; normalTS.z = sqrt(1.0 - saturate(dot(normalTS.xy, normalTS.xy))); float3 bitangentWS = cross(input.normalWS, input.tangentWS.xyz) * input.tangentWS.w; float3x3 TBN = float3x3(input.tangentWS.xyz, bitangentWS, input.normalWS); float3 normalWS = normalize(mul(normalTS, TBN)); // --- 简单反射 --- float2 reflectionUV = screenUV + normalWS.xz * _ReflectionDistortion; half3 sceneColor = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, reflectionUV).rgb; half3 reflection = sceneColor * _ReflectionStrength; // --- 最终合成与光照 --- half3 finalColor = waterColor + foamColor; finalColor = lerp(finalColor, reflection, _ReflectionStrength * (1.0 - depthFactor)); // 深水区反射减弱 // URP光照计算 Light mainLight = GetMainLight(TransformWorldToShadowCoord(input.positionWS)); float NdotL = saturate(dot(normalWS, mainLight.direction)); half3 lighting = mainLight.color * NdotL + SampleSH(normalWS); finalColor *= lighting; // 透明度 float transparency = exp(-_DepthAttenuation * depthDifference * 2.0); // 使用更独立的透明度衰减 half alpha = lerp(_BaseColor.a, 0.1, transparency); return half4(finalColor, alpha); } ENDHLSL } } FallBack "Universal Render Pipeline/Unlit" }

关键参数解析与调节建议:

  • _DepthAttenuation:这是控制水体颜色随深度变化速度的核心。值越大,颜色从浅水色过渡到深水色的速度越快。对于清澈的泳池,可以设小一些(如0.5);对于浑浊的河水或深海,可以设大一些(如2.0)。
  • _FoamThreshold_FoamSoftness:这两个参数需要配合调节。_FoamThreshold决定了泡沫出现的最大深度。_FoamSoftness控制泡沫边缘的模糊程度。一个典型的设置是Threshold=0.15,Softness=0.03,这样泡沫会在深度小于15厘米的区域出现,并有3厘米的柔和过渡。
  • _NormalStrength_WaveSpeed:控制水面波浪的视觉强度和平移速度。_NormalStrength过大会导致水面看起来像沸腾的开水,通常0.3-0.8之间比较自然。_WaveSpeed的x和y分量可以设置不同的值,让波浪方向看起来更随机。
  • _ReflectionDistortion:用做法线扰动反射的偏移量。这个值通常非常小(0.005-0.02),过大会导致反射图像过于扭曲失真。

5. 实战调试与常见问题排查

即使有了完整的代码,把它应用到实际项目中时,依然会遇到各种“坑”。下面是我在多个项目中总结出的常见问题及其解决方案。

5.1 深度纹理采样结果为1(全白)或0(全黑)

这是最常遇到的问题,意味着深度纹理没有正确生成或采样。

  • 检查1:摄像机设置。确保你的主摄像机(或渲染水面的摄像机)上勾选了“Opaque Texture”“Depth Texture”(在URP摄像机的Renderer Features设置中,或通过脚本设置camera.depthTextureMode |= DepthTextureMode.Depth;)。
  • 检查2:Shader中的纹理声明。确保在Shader中正确声明了_CameraDepthTexture和对应的采样器。在URP中,有时需要使用SAMPLER(sampler_CameraDepthTexture)SAMPLE_DEPTH_TEXTURE宏。
  • 检查3:屏幕UV计算。确保screenPos的计算和透视除法(/w)是正确的。一个快速的调试方法是将screenUV直接作为颜色输出,看看是否是一个从(0,0)到(1,1)的渐变。
  • 检查4:渲染队列。确保水面的渲染队列(“Queue”)在“Transparent”。不透明物体会在透明物体之前渲染,并写入深度纹理。如果水面被设置为不透明,它自身会写入深度,导致后续采样自己的深度,结果混乱。

5.2 水面与物体交界处出现闪烁或硬边(Z-Fighting)

这是由于水面和底部物体(如水底石头)的深度值过于接近,深度缓冲精度不足以区分导致的。

  • 解决方案1:增加深度偏移。在Shader的Pass中,使用Offset指令。例如:Offset 0, -1。这个指令会让水面在深度测试时“退让”一点,避免与非常接近的物体竞争。但偏移量不宜过大,否则会导致水面悬浮。
  • 解决方案2:微调水面网格位置。在建模时,让水面网格略微高于水底几何体(例如,高出一个很小的值,如0.001个单位)。这是最直接有效的方法。
  • 解决方案3:使用clip()函数。在片元着色器中,如果计算出的depthDifference小于一个极小值(如0.001),直接丢弃该像素(clip(depthDifference - 0.001);)。但这可能会导致边缘出现锯齿,需要配合抗锯齿。

5.3 泡沫出现在不该出现的地方(如深水区)

这通常是因为深度差计算有误,或者泡沫阈值参数设置不合理。

  • 排查1:可视化深度差。将depthDifference乘以一个系数后直接输出为颜色(如return float4(depthDifference, depthDifference, depthDifference, 1);)。观察深水区和浅水区的灰度值是否符合预期(浅水区接近0,深水区较大)。
  • 排查2:检查waterLinearDepth的准确性。这是最容易出错的地方。确保你用于计算水面深度的数据是准确的。对于平面水面,使用射线-平面求交;对于网格水面,传递观察空间Z值是最可靠的方法。
  • 排查3:调整_FoamThreshold。如果深度差本身计算正确,但泡沫范围不对,就调整这个阈值。记住,泡沫出现在depthDifference < _FoamThreshold的区域。

5.4 反射扭曲失真严重或方向错误

  • 检查法线方向:将normalWS可视化(return float4(normalWS * 0.5 + 0.5, 1);),确保法线方向正确。错误的切线空间计算会导致法线混乱,进而使反射扭曲方向异常。
  • 调整_ReflectionDistortion:这个值非常敏感。从0.005开始尝试,逐步增加,直到获得自然的波纹扭曲效果,而不是剧烈的图像撕裂。
  • 检查反射UV:确保reflectionUV是在正确的屏幕空间坐标上添加的偏移。偏移量normalWS.xz是合理的,因为它使用世界空间法线的X和Z分量(假设水面主要在XZ平面)来扰动UV。

5.5 性能优化建议

一个全功能的水面Shader对移动端可能负担较重。以下是一些优化方向:

  1. 简化计算:在移动平台上,可以考虑移除动态顶点涟漪,或者使用更简单的正弦波叠加。将反射强度_ReflectionStrength调低甚至关闭。
  2. 降低纹理采样:使用更小尺寸的法线贴图和泡沫贴图(如256x256)。如果泡沫效果不是核心需求,可以考虑关闭。
  3. 使用Shader LOD:为Shader设置不同的LOD级别。在远处使用简化版本(只保留深度色和基础法线扰动),在近处使用完整版本。
  4. 分块渲染:对于大型水域,可以考虑将水面网格分成多个区块,根据摄像机距离决定使用高精度还是低精度网格(LOD Group)。
  5. 后处理替代:一些屏幕空间效果(如全局的折射、焦散)可以尝试用后处理来实现,比在每个水面像素上计算可能更高效,但灵活性较低。

调试Shader是一个需要耐心和技巧的过程。最有效的工具就是“可视化调试”——将中间变量(深度、法线、混合权重等)输出为颜色,亲眼看看数据到底长什么样。Unity的Frame Debugger也是分析渲染流程和纹理的利器。当你把这些问题都解决掉,看到一片随着深度变化颜色、岸边泛着细腻泡沫、微风拂过泛起涟漪的水面时,那种成就感绝对是实实在在的。这套基于CameraDepthTexture的方案,为你构建更复杂的水体交互(比如动态波浪传播、船只尾迹、局部雨滴涟漪)打下了坚实的基础。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询