鱼的游动+Compute Shader
鱼的vertex游动
--鱼的each vertex都在自己的xz平面移动
自己在属性里先规定掉,然后是对vertex.xyz的旋转矩阵
// 围绕指定“上”轴做平面旋转(yaw)float3 rotateAroundUp(float3 p, float angle, int upAxis){float s, c; sincos(angle, s, c);//给一个角度,创造旋转矩阵if (upAxis == 0){// X 作为上轴 -> 旋转 yzfloat2 yz = mul(float2x2(c, -s, s, c), p.yz);p.y = yz.x; p.z = yz.y;}else if (upAxis == 2){// Z 作为上轴 -> 旋转 xyfloat2 xy = mul(float2x2(c, -s, s, c), p.xy);p.x = xy.x; p.y = xy.y;}else{// Y 作为上轴 -> 旋转 xzfloat2 xz = mul(float2x2(c, -s, s, c), p.xz);p.x = xz.x; p.z = xz.y;}return p;}
Simple
首先是简单的顶点旋转:
vert里对each vertex.xyz 绕着object space的中心点进行旋转,是比较简单的
(鱼的mesh网格的默认model space就是鱼脖子,所以这里是绕着鱼的脖子进行旋转)
很明显的是,obj space的-z方向的each vertex都不参与旋转
Lerp
但如果在进入vert时拿一个新的p0记录初始vertex.xyz,一个p记录被进行的各种变化
到最后的时候v.vertex.xyz = lerp(p0, p, factor);
利用factor进行lerp,
float FinalLerpFactor(appdata_full v){float a = GetSingleXYZFromVertex(v.vertex.xyz, _BaseSwingAxis);float d = max(0, a - _HeadPivot);//distancereturn saturate(d / _TransformIntensity);}
距离越靠近鱼脖子,factor越小(但始终是有0到1的界限)
----运行时调整 _TransformIntensity
这里的鱼尾巴看上去少移动了,实际上是lerp已经是1了,不能再增加
所以目前
加上Sin
但其实还是有些单调了,因为看上去只是绕着 鱼脖子原点 来回旋转。所以这时候要在Lerp的影响包围下,再进行一些处理了
要制造各个vertex的差异化旋转angle,对each vertex接受的旋转angle进行“个人特色”处理
float a = GetSingleXYZFromVertex(v.vertex.xyz, _BaseSwingAxis);float phase = a * _Frequency - _Time.y * _Speed;float angle = _Amplitude * sin(phase);//提供变化的角度,乘上幅度
这种特色是 距离鱼脖子的距离 导致的
原本的是共用时间影响-->共用angle,上面的则添加了各自的距离影响,如果距离远的就在sin函数里进度靠前一些(重点是每个vertex都在sin函数上以一种连续的方式加上了影响)
可以想象成,原本是所有vertex在同一个sin函数中,在同一竖x向右移动,而修改之后,是
x0-xn的n竖x在同时向右移动。这样的移动方式带上了sin的感觉
float phase = _Time.y * _Speed; float angle = _Amplitude * sin(phase);//提供变化的角度,乘上幅度
值得考虑的应该就是暴露参数的问题了吧,
compute shader
放到.compute里面用于计算鱼的移动数据,而放到.shader里,才是真的要把这些数据都用上
(因为数据在同一个compute buffer,.shader里直接取算好的)-渲染n次fish 用在一个材质实例
cs中:
public struct GPUBoid_Draw
{public Vector3 position; // Boid 的位置---默认从脚本本地作为坐标系public Vector3 direction; // Boid 的朝向---默认从脚本本地作为坐标系public float noise_offset; // (compute shader中控制speed随机)public Vector3 padding; // 内存对齐的,凑到28+12==40
}
对于这个padding,听说是可以优化帧率?但是又不是16的整数倍,,去掉也可以
public ComputeShader _ComputeFlock;
private ComputeBuffer BoidBuffer;
ComputeBuffer _drawArgsBuffer;
初始化数据start:
创建数据实例& 绑定.compute文件的kernel入口函数
// 初始化 Boid 数据this.boidsData = new GPUBoid_Draw[this.BoidsCount];this.kernelHandle = _ComputeFlock.FindKernel("CSMain");for (int i = 0; i < this.BoidsCount; i++){this.boidsData[i] = this.CreateBoidData();this.boidsData[i].noise_offset = Random.value * 1000.0f;}
数据buffer的绑定
//12 + 12 + 4 + 12 = 40字节---pos + dir + noise_offset + paddingBoidBuffer = new ComputeBuffer(BoidsCount, 40);BoidBuffer.SetData(this.boidsData);
.shader 渲染模式定义的buffer设定
_drawArgsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);//这个缓冲区会被 Graphics.DrawMeshInstancedIndirect 读取, 之后 一次性批量渲染所有Boid实例_drawArgsBuffer.SetData(new uint[5] { BoidMesh.GetIndexCount(0), (uint)BoidsCount, 0, 0, 0 });// 后面三个高级用法设为0
update中:
void Update(){// 向计算着色器传递参数_ComputeFlock.SetFloat("DeltaTime", Time.deltaTime);_ComputeFlock.SetFloat("RotationSpeed", RotationSpeed);_ComputeFlock.SetFloat("BoidSpeed", BoidSpeed);_ComputeFlock.SetFloat("BoidSpeedVariation", BoidSpeedVariation);_ComputeFlock.SetVector("FlockPosition", Target.transform.position);_ComputeFlock.SetFloat("NeighbourDistance", NeighbourDistance);_ComputeFlock.SetInt("BoidsCount", BoidsCount);_ComputeFlock.SetBuffer(this.kernelHandle, "boidBuffer", BoidBuffer);// 调度计算着色器,更新 Boid 状态_ComputeFlock.Dispatch(this.kernelHandle, Mathf.CeilToInt(this.BoidsCount / (float)GROUP_SIZE), 1, 1);// _props作为最后一个参数,但改成null也没有出现bug?、、_drawArgsBuffer是确认数量和渲染方式,而BoidMaterial.SetBuffer则是对单体data的集合的绑定BoidMaterial.SetBuffer("boidBuffer", BoidBuffer);Graphics.DrawMeshInstancedIndirect(BoidMesh, 0, BoidMaterial,new Bounds(Vector3.zero, Vector3.one * 1000),_drawArgsBuffer, 0, null);}
优化修改
但目前的效果很明显就是,所有的鱼都是用同一个运动动画,没有差异,如果要添加差异的话,也比较简单,
就是在.shader中angle计算中再加入each fish mesh 从compute buffer里拿出的数据的单独影响
unity_InstanceID,这样确保fish内的vertices 整体运动计算不变,只是初始angle状态不同
不影响fish内的vertices 的运动变化计算
noise_offset
让每只 boid 的噪声相位不同,其实也可以使用,但这里独立出来一些
void vert(inout appdata_full v, out Input o){UNITY_INITIALIZE_OUTPUT(Input, o);// 相位:沿“鱼长轴”推进;速度由 _Speed 控float a = GetSingleXYZFromVertex(v.vertex.xyz, _BaseSwingAxis);float phase = a * _Frequency - _Time.y * _Speed; // _Time.y 为秒级时间#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLEDphase += unity_InstanceID;#endiffloat angle = _Amplitude * sin(phase);//提供变化的角度,乘上幅度// 以 HeadPivot 为枢轴做 yaw,再按尾权重插值(头稳、尾摆)float3 p0 = v.vertex.xyz;float3 p = p0;// 把点移到枢轴坐标系(只在“Swing轴”上挪动),同样的套路,先回归0旋转,然后再加到原来的位置(为了顶点可以在自己的位置为基准进行移动)if (_BaseSwingAxis == 0) p.x -= _HeadPivot;else if (_BaseSwingAxis == 1) p.y -= _HeadPivot;else p.z -= _HeadPivot;p = rotateAroundUp(p, angle, _YawAxis);//Swing平面的upif (_BaseSwingAxis == 0) p.x += _HeadPivot;else if (_BaseSwingAxis == 1) p.y += _HeadPivot;else p.z += _HeadPivot;v.vertex.xyz = lerp(p0, p, FinalLerpFactor(v));#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLEDv.vertex = mul(_LookAtMatrix, v.vertex); // 变向矩阵(每个vertex都会look_at_matrix计算出自己的矩阵),appdata_full v,是自己的object space内的旋转v.vertex.xyz += _BoidPosition; // 绕obj(0,0,0)旋转后加上_BoidPosition,重新把鱼放到cs给的data里的Position里//不然只是鱼转了,加的不是compute buffer里的数据,也就没有连续性了#endif// 之后 Unity 会把对象空间顶点自动变换到裁剪空间(Surface Shader流水线)。:contentReference[oaicite:4]{index=4}}
关于噪声
对于.compute里的速度噪声
[numthreads(GROUP_SIZE, 1, 1)] // 定义线程组的维度
void CSMain(uint3 id : SV_DispatchThreadID)
{uint instanceId = id.x; // 获取当前线程的IDBoid boid = boidBuffer[instanceId]; // 获取当前Boid的数据// 整体鱼群的时间作为系数的采样noise1 (加上了噪声位移,所以每条鱼又错开了float noise = clamp(noise1(_Time / 100.0 + boid.noise_offset), -1, 1) * 2.0 - 1.0;float velocity = BoidSpeed * (1.0 + noise * BoidSpeedVariation); // 计算Boid的速度
注释里就解释了很好了
// 哈希函数,用于生成伪随机数
float hash(float n)
{return frac(sin(n) * 43758.5453); // 通过正弦函数生成伪随机数}
// 噪声函数,返回范围为 -1.0f 到 1.0f 的值
float noise1(float3 x)
{//return (hash(x.x), hash(x.y), hash(x.z)); // 简化的噪声函数,效果很差,鱼就是凑成一个球的样子//把空间划分成整数网格单元,p 是格子 ID,f 是格子内的局部坐标float3 p = floor(x);//x:输入的 3D 坐标。float3 f = frac(x);//f:小数部分f = f * f * (3.0 - 2.0 * f);//经典的 Hermite smoothstep 平滑曲线公式,让插值在 0 和 1 处一阶导数为 0(平滑过渡)--在做插值时不会有明显的硬边。float n = p.x + p.y * 57.0 + 113.0 * p.z;//用 p.x, p.y, p.z 按不同的系数组合成一个唯一的格子索引值。57 和 113 是选的质数,避免模式重复(减少哈希冲突)。/*hash(n + 0.0) // (0,0,0)hash(n + 1.0) // (1,0,0)hash(n + 57.0) // (0,1,0)hash(n + 58.0) // (1,1,0)hash(n + 113.0) // (0,0,1)hash(n + 114.0) // (1,0,1)hash(n + 170.0) // (0,1,1)hash(n + 171.0) // (1,1,1)一个立方体有 8 个角点(每个角点一个随机值)。hash() 返回该点的随机值(0~1),不依赖输入顺序。上面的加法是为了区分不同角点的索引。下面第一层 lerp:沿 X 轴在两个顶点间插值(左 vs 右)。第二层 lerp:沿 Y 轴在 X 插值结果之间插值(前 vs 后)。第三层 lerp:沿 Z 轴在 Y 插值结果之间插值(下 vs 上)。这就是三维的线性插值,用平滑过的 f 做权重,让结果连续光滑。*/returnlerp(lerp(lerp(hash(n + 0.0), hash(n + 1.0), f.x), lerp(hash(n + 57.0), hash(n + 58.0), f.x),f.y),lerp(lerp(hash(n + 113.0), hash(n + 114.0), f.x), lerp(hash(n + 170.0), hash(n + 171.0), f.x),f.y),f.z);
}
至于为什么.compute里使用这个噪声函数去扰乱速度
没稳定之前都是类似的
稳定后
三重Lerp 的noise1的效果:
简单的随机噪点图的效果
其实差别也不算很大??把noise直接去掉,鱼始终恒定速度不变化,最后运动的轨迹也是差不多的绕圈子,就是因为速度没有变化了,所以看上去可能更加机械了,所有鱼的速度都统一,少了动态感,但 差的真的不是很多,如下
第一张是“平滑值噪声(跟你贴的
noise1
一样的逻辑:8 个角点哈希→三线性插值→平滑曲线)”,块状平滑、连续。第二张是“白噪声(点对点独立的随机)”,颗粒感强、完全不连续。
这样你能直观看到:为什么要 floor/frac
划分格子、为什么要对 8 个角点做 三重 lerp、以及 f*f*(3-2*f)
(Hermite smoothstep)带来的平滑效果。
GPT:
要点再补两句(便于你对照代码):
p=floor(x)
、f=frac(x)
:把空间切成整数格子,p
是格子编号,f
是格子内坐标。
n = p.x + p.y*57 + 113*p.z
:把三维格子坐标压成一个索引,避免冲突用质数权重。
hash(n + offsets)
:8 个角点各取一次伪随机值。三层
lerp(..., f.x / f.y / f.z)
:分别沿 X、Y、Z 做线性插值。
f = f*f*(3-2*f)
:把线性插值的权重“抛光”,在 0 和 1 处一阶导数为 0,避免硬边。想更进一步的话,我还能给你:
同一块区域不同 z 切片的动画(看 3D 噪声沿 z 的连贯性);
去掉 smoothstep 的对比图,让你看到边缘为什么“打结”;
把 octaves(多频叠加) 做成 fractal noise(fbm),看看云雾/水面那种层级细节怎么出来的。
要哪个我就继续画 👌