当前位置: 首页 > news >正文

鱼的游动+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),看看云雾/水面那种层级细节怎么出来的。
    要哪个我就继续画 👌

http://www.dtcms.com/a/323987.html

相关文章:

  • 特征模理论中的特征值、模式重要性、特征角和模式权重系数的物理含义分别是什么?彼此间存在什么数学关系?如何用CST 仿真软件查看这些参数?
  • 一篇文章解决 Win10 同时部署多个版本的Tomcat
  • WiFi原理与WiFi安全
  • 【秋招笔试】2025.08.09美团秋招算法岗机考真题-第一题
  • C语言——深入理解指针(二)
  • JavaSE:入门
  • LeetCode 组合总数
  • 五种Excel表格导出方案
  • 40.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--初始化网关
  • Pytorch深度学习框架实战教程-番外篇05-Pytorch全连接层概念定义、工作原理和作用
  • afsim2.9_使用QtCreator和VSCode编译
  • ShadowKV 机制深度解析:高吞吐长上下文 LLM 推理的 KV 缓存“影子”方案
  • GPT OSS深度解析:OpenAI时隔6年的开源模型,AI民主化的新里程碑?
  • FFmpeg实现音视频转码
  • Java 日常开发笔记(小程序页面交互传参-id)
  • ATF(TF-A)安全通告 TFV-13(CVE-2024-7881)
  • 《从零构建大语言模型》学习笔记4,注意力机制1
  • ubuntu22.04安装autoware.universe
  • 【YOLOv8改进 - C2f融合】C2f融合Outlook Attention:将细粒度特征编码到 token 中, 提高在图像分类和语义分割等任务中的性能
  • Docker Compose 部署高可用 MongoDB 副本集集群(含 Keepalived + HAProxy 负载均衡)
  • 106-基于Flask的重庆充电桩投建数据可视化分析系统
  • Spring Boot WebSocket实时在线人数统计
  • 从onnx模型到om模型的全自动化转化
  • Spring Boot集成WebSocket
  • Vue 3 的编译时优化如何改写 DOM 操作规则
  • ubuntu超简单自动化Vim配置
  • 【嵌入式硬件实例】-555定时器PWM调光电路
  • vue: Module “vue“ has no exported member xxx
  • Dify 从入门到精通(第 26/100 篇):Dify 的知识图谱集成
  • [激光原理与应用-224]:机械 - 机械设计与加工 - 常见的术语以及含义