You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中基于顶点着色器实现弹簧式2D水效果的波传播难题

基于弹簧机制的2D水效果顶点着色器实现方案

嘿,我来帮你搞定这个问题!下面是一步步的实操思路,都是Unity里实现这类效果的常用方法:

一、先给顶点“绑定邻居信息”

顶点着色器本身没法直接遍历Mesh的顶点列表找相邻顶点,所以得先在CPU端预处理好每个顶点的左右邻居数据,把它们塞进顶点属性里。比如用Mesh的额外UV通道来存(毕竟UV通道就是用来放自定义数据的绝佳容器):

在C#代码里可以这么处理你的水面Mesh:

// 假设你的水面是由一排连续顶点组成的网格(比如一条线拉成的面)
List<Vector3> vertices = mesh.vertices.ToList();
List<Vector4> leftNeighborData = new List<Vector4>();
List<Vector4> rightNeighborData = new List<Vector4>();

for (int i = 0; i < vertices.Count; i++)
{
    // 处理左邻居:第一个顶点没有左邻居,就用自身数据代替
    Vector3 leftPos = i > 0 ? vertices[i-1] : vertices[i];
    // Vector4的前两个分量存初始位置,后两个可以存当前位移和速度
    leftNeighborData.Add(new Vector4(leftPos.x, leftPos.y, 0f, 0f));

    // 处理右邻居:最后一个顶点没有右邻居,用自身数据代替
    Vector3 rightPos = i < vertices.Count - 1 ? vertices[i+1] : vertices[i];
    rightNeighborData.Add(new Vector4(rightPos.x, rightPos.y, 0f, 0f));
}

// 把邻居数据存入UV2和UV3通道
mesh.SetUVs(2, leftNeighborData);
mesh.SetUVs(3, rightNeighborData);
mesh.RecalculateBounds();
mesh.RecalculateNormals(); // 可选,水面需要法线的话加上

二、在顶点着色器里读取邻居数据

接下来在Shader里声明对应的顶点属性,就能直接拿到邻居的信息了:

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float4 leftNeighbor : TEXCOORD2; // 左邻居:xy=初始位置,zw=位移/速度
    float4 rightNeighbor : TEXCOORD3; // 右邻居:同上
};

三、计算弹簧作用力与更新位移

弹簧机制的核心是胡克定律,我们结合阻尼来计算顶点的位移更新,避免水面无限振荡。在顶点着色器里可以这么写:

首先定义输出结构体,用来存储更新后的位移和速度(方便下一次迭代):

struct v2f
{
    float4 pos : SV_POSITION;
    float4 updatedState : TEXCOORD1; // 存储新的位移和速度,供下一轮使用
};

// 声明材质参数,在Inspector里可调
float _SpringStiffness; // 弹簧刚度
float _Damping; // 阻尼系数
float _DeltaTime; // 帧时间

然后是顶点着色器的核心逻辑:

v2f vert (appdata v)
{
    v2f o;

    // 提取当前顶点的位移和速度(假设存在vertex的zw分量里)
    float currentDisplacement = v.vertex.z;
    float currentVelocity = v.vertex.w;

    // 计算与左邻居的弹簧作用力
    float restLengthLeft = v.leftNeighbor.x - v.vertex.x; // 静止时的初始距离
    float currentLengthLeft = (v.leftNeighbor.x + v.leftNeighbor.z) - (v.vertex.x + currentDisplacement); // 带位移的当前距离
    float forceLeft = _SpringStiffness * (restLengthLeft - currentLengthLeft);

    // 计算与右邻居的弹簧作用力
    float restLengthRight = v.rightNeighbor.x - v.vertex.x;
    float currentLengthRight = (v.rightNeighbor.x + v.rightNeighbor.z) - (v.vertex.x + currentDisplacement);
    float forceRight = _SpringStiffness * (restLengthRight - currentLengthRight);

    // 总作用力:弹簧力减去阻尼力(阻尼和速度成正比)
    float totalForce = forceLeft + forceRight - _Damping * currentVelocity;

    // 用欧拉积分更新速度和位移
    float newVelocity = currentVelocity + totalForce * _DeltaTime;
    float newDisplacement = currentDisplacement + newVelocity * _DeltaTime;

    // 存储更新后的状态,供下一次迭代使用
    o.updatedState = float4(v.vertex.x, v.vertex.y, newDisplacement, newVelocity);

    // 计算最终的裁剪空间位置
    float3 newWorldPos = float3(v.vertex.x, v.vertex.y + newDisplacement, v.vertex.z);
    o.pos = UnityObjectToClipPos(float4(newWorldPos, 1f));

    return o;
}

四、迭代数据的存储与循环更新

顶点着色器没法直接修改Mesh的原始顶点数据,所以得用RenderTexture或者Compute Shader来存每一轮的位移/速度状态,实现迭代更新:

  • 最简单的方式是用RenderTexture作为“位移场”:每个像素对应一个顶点的位移和速度数据。
  • 写一个Compute Shader,读取当前的位移场Texture,计算每个顶点的作用力并更新状态,然后写入新的RenderTexture。
  • 之后在顶点着色器里采样这个RenderTexture,获取当前顶点的最新位移数据,代入计算。

如果你的水面顶点数量不多,也可以用MaterialPropertyBlock在CPU端批量更新顶点数据,但效率不如GPU端处理高。

五、边界顶点的特殊处理

别忘了第一个和最后一个顶点,它们只有一个邻居,计算作用力的时候要单独判断,比如当i=0时,只计算右邻居的力;当i是最后一个顶点时,只计算左邻居的力,避免出现无效数据。

内容的提问来源于stack exchange,提问作者Jonathan Wright

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:20:29