Unity Shadergraph与C#波浪高度计算结果不一致问题排查
我正在开发一个浮力脚本,该脚本会采样点的位置,判断其是否低于某个“水位”,并根据深度在该位置施加力。同时,我用Shadergraph制作了视觉效果良好的水面Shader,通过Simple Noise节点结合顶点位移实现波浪效果。为将位移后的波浪值作为“水位”用于浮力计算,我把Shadergraph的节点逻辑完整重写为C#代码,以此采样指定位置的水位。
最终位移后的水面网格与C#计算出的水位位置存在差异,导致浮力脚本判断的水位与实际水面高低不符。差异不大,我推测问题出在C#对节点图或Simple Noise的转换实现中。请问我的推测是否正确?若正确,问题出在哪?我的理解误区是什么?若不正确,还有哪些可能的原因?
节点图逻辑
- 将世界坐标作为UV,进行偏移和平铺处理
- 将UV输入Simple Noise节点,将噪声值乘以强度
- 对输出结果进行钳制
- 重复步骤1-3并将结果相加以增加细节
- 将顶点位置的Y值替换为合并后的波浪值
C#脚本
通过获取材质参数,实现GetWaveHeightAtPosition方法计算指定位置的波浪高度,代码如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class WaveHeightCalculator : MonoBehaviour { // Step 1 [SerializeField] Material _waterMaterial; [Header("Waves")] [SerializeField] float _waveTiling; [SerializeField] float _waveOffset; [SerializeField] float _waveMin; [SerializeField] float _waveMax; [Header("Small Waves")] [SerializeField] float _wavesSmallScale; [SerializeField] float _wavesSmallStrength; [SerializeField] Vector2 _wavesSmallVelocity; [Header("Large Waves")] [SerializeField] float _wavesLargeScale; [SerializeField] float _wavesLargeStrength; [SerializeField] Vector2 _wavesLargeVelocity; // Step 2 private void OnValidate() { _waterMaterial = GetComponent<Renderer>().sharedMaterial; SetVariables(); } void SetVariables() { _waveTiling = _waterMaterial.GetFloat("_Wave_Tiling"); _waveOffset = _waterMaterial.GetFloat("_Wave_Offset"); _waveMin = _waterMaterial.GetFloat("_Wave_Min"); _waveMax = _waterMaterial.GetFloat("_Wave_Max"); _wavesSmallScale = _waterMaterial.GetFloat("_Waves_Small_Scale"); _wavesSmallStrength = _waterMaterial.GetFloat("_Waves_Small_Strength"); _wavesSmallVelocity = _waterMaterial.GetVector("_Waves_Small_Velocity"); _wavesLargeScale = _waterMaterial.GetFloat("_Waves_Large_Scale"); _wavesLargeStrength = _waterMaterial.GetFloat("_Waves_Large_Strength"); _wavesLargeVelocity = _waterMaterial.GetVector("_Waves_Large_Velocity"); } // Step 3 public float GetWaveHeightAtPosition(Vector3 position) { Vector2 noiseMapUV; noiseMapUV = new Vector2(position.x, position.z) * _waveTiling; // Calculate Small Waves Vector2 wavesSmallUVOffset = (Time.time / 20) * _wavesSmallVelocity; float noiseValueAtUVPlusOffset = UnitySimpleNoiseAtUV(noiseMapUV + wavesSmallUVOffset, _wavesSmallScale); float wavesSmall = noiseValueAtUVPlusOffset * _wavesSmallStrength; // Calculate Large Waves Vector2 wavesLargeUVOffset = (Time.time / 20) * _wavesLargeVelocity; noiseValueAtUVPlusOffset = UnitySimpleNoiseAtUV(noiseMapUV + wavesLargeUVOffset, _wavesLargeScale); float wavesLarge = noiseValueAtUVPlusOffset * _wavesLargeStrength; // Combine float waveHeight = wavesSmall + wavesLarge; // Clamp waveHeight = Mathf.Clamp(waveHeight, _waveMin, _waveMax); // Offset waveHeight += _waveOffset; return waveHeight; } }
转换的Simple Noise代码
将Shadergraph生成的Simple Noise代码转换为C#实现,代码如下:
float float_frac(float x) { return x - Mathf.Floor(x);} Vector2 frac(Vector2 x) { return x - new Vector2(Mathf.Floor(x.x), Mathf.Floor(x.y));} float sin(float x) { return Mathf.Sin(x);} float dot(Vector2 a, Vector2 b) { return a.x * b.x + a.y * b.y;} float float_floor(float x) { return Mathf.Floor(x);} Vector2 floor(Vector2 x) { return new Vector2(Mathf.Floor(x.x), Mathf.Floor(x.y));} float float_abs(float x) { return Mathf.Abs(x);} Vector2 abs(Vector2 x) { return new Vector2(Mathf.Abs(x.x), Mathf.Abs(x.y));} float pow (float x, float y) { return Mathf.Pow(x, y);} float Unity_SimpleNoise_RandomValue_float (Vector2 uv) { float angle = dot(uv, new Vector2(12.9898f, 78.233f)); return float_frac(sin(angle) * 43758.5453f); } float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t) { return (1.0f - t) * a + (t * b); } float Unity_SimpleNoise_ValueNoise_float (Vector2 uv) { Vector2 i = floor(uv); Vector2 f = frac(uv); f = (f * f) * (new Vector2 (3.0f, 3.0f) - new Vector2(2.0f, 2.0f) * f); uv = abs(frac(uv) - new Vector2 (0.5f, 0.5f)); Vector2 c0 = i + new Vector2(0.0f, 0.0f); Vector2 c1 = i + new Vector2(1.0f, 0.0f); Vector2 c2 = i + new Vector2(0.0f, 1.0f); Vector2 c3 = i + new Vector2(1.0f, 1.0f); float r0 = Unity_SimpleNoise_RandomValue_float(c0); float r1 = Unity_SimpleNoise_RandomValue_float(c1); float r2 = Unity_SimpleNoise_RandomValue_float(c2); float r3 = Unity_SimpleNoise_RandomValue_float(c3); float bottomOfGrid = Unity_SimpleNnoise_Interpolate_float(r0, r1, f.x); float topOfGrid = Unity_SimpleNnoise_Interpolate_float(r2, r3, f.x); float t = Unity_SimpleNnoise_Interpolate_float(bottomOfGrid, topOfGrid, f.y); return t; } float UnitySimpleNoiseAtUV(Vector2 UV, float Scale) { float t = 0.0f; float freq = pow(2.0f, 0); float amp = pow(0.5f, 3-0); t += Unity_SimpleNoise_ValueNoise_float(new Vector2(UV.x*Scale/freq, UV.y*Scale/freq))*amp; freq = pow(2.0f, 1); amp = pow(0.5f, 3-1); t += Unity_SimpleNoise_ValueNoise_float(new Vector2(UV.x * Scale / freq, UV.y * Scale / freq)) * amp; freq = pow(2.0f, 2); amp = pow(0.5f, 3-2); t += Unity_SimpleNoise_ValueNoise_float(new Vector2(UV.x * Scale / freq, UV.y * Scale / freq)) * amp; return t; }
你的推测是正确的,问题主要出在Simple Noise的C#转换实现和逻辑细节上:
1. Simple Noise实现的核心错误
在Unity_SimpleNoise_ValueNoise_float函数中,你额外添加了一行uv = abs(frac(uv) - new Vector2 (0.5f, 0.5f));,这行代码在ShaderGraph的原生Simple Noise节点中不存在。它会修改后续计算网格坐标c0-c3的基准,直接导致噪声输出结果和Shader中的逻辑完全偏离,这是差异产生的主要原因。删除这行多余代码后,噪声计算逻辑就能和ShaderGraph对齐。
2. 逻辑顺序的潜在偏差
确认ShaderGraph中的步骤顺序:是否是合并波浪值 → 钳制 → 加偏移?你的C#代码逻辑是符合这个顺序的,但如果Shader中是先加偏移再钳制,就会出现数值差异。不过从你的描述来看,这部分不是主要问题。
3. 其他次要可能原因
- 浮点精度差异:GPU和CPU的浮点计算精度存在细微差别,多次噪声叠加后可能积累极小误差。可以用
Mathf.Approximately判断近似相等,或调整精度容忍度。 - UV坐标空间不一致:确认Shader中使用的是世界坐标的XZ轴作为UV,和C#中
new Vector2(position.x, position.z)的处理一致。如果Shader用的是模型空间坐标,会直接导致偏差。 - 时间同步问题:C#中的
Time.time和Shader中的_Time.y是否同步?若项目使用了Time.timeScale,要确保两者使用的是同一时间源(比如都用缩放时间或都用 unscaled 时间)。
内容的提问来源于stack exchange,提问作者feenix

