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

基于Compute shader的草渲染

(注:本文基于 Unity URP 22 实现)

GPU Instancing

是一种绘制调用优化技术,传递一个对象的Mesh,指定其绘制次数和材质,Unity就会为我们在GPU的统一/常量缓冲区开辟好必要的缓冲区,然后以我们指定的材质对Mesh进行我们指定次数的渲染,这样就可以达成一次Drawcall绘制海量对象的目的,可以在单个Draw Call中渲染多个相同的网格但可以有不同的变换和材质属性。

 与常规渲染对比

特性传统渲染GPU Instancing
DrawCall数量每个实例1个DrawCall所有实例1个DrawCall
CPU开销极低
GPU内存带宽
实例数据上限无限制硬件/API限制
动态更新支持灵活需特殊处理
适用场景少量独特对象大量相似对象

使用compute shader绘制草

Compute Shader(计算着色器)是运行在显卡(GPU)上的一种特殊程序。它与传统的用于处理顶点或像素的着色器不同,​​compute shader利用GPU强大的并行计算能力来进行通用的、与图形渲染无关的计算​​。

简单理解CPU和GPU

​CPU​​:像是一位聪明的教授,能快速、顺序地处理复杂多变的指令,告诉GPU该做什么。

​GPU(包含Compute Shader)​​:像是一支由成千上万名小学生组成的军队,每个小学生都不那么聪明,但他们可以​​同时​​处理大量简单、重复的任务(比如每人计算一个数字的平方),从而在总速度上碾压教授。

​​简单来说,Compute Shader就是一把让开发者可以直接调用GPU庞大计算能力进行通用计算的“钥匙”,非常适合处理像粒子模拟、物理计算、图像处理以海量植被生成等高度并行的任务。​

 想要深入理解,可以看看这篇文章:

 如何理解和使用Compute Shader - 知乎https://zhuanlan.zhihu.com/p/595726279

好了,现在编写来compute shader。

先定义组成草的单个三角形所需的基本数据:3个顶点的世界空间位置和法线

struct VertexData
{half3 normalWS; // 法向half3 positionWS; // 位置
};struct TriangleData
{VertexData vertices[3]; // 三个顶点
};

 生成的草数据(三角形)存入AppendStructuredBuffer<TriangleData> _Triangles;在所有线程执行完毕后,这个缓冲区里就按顺序存放了所有需要绘制的草的三角形数据。

GPU 通过 _IndirectArgsBuffer知道要去 _Triangles缓冲区里取多少个顶点数据来进行绘制。间接绘制参数存入RWStructuredBuffer<IndirectArgs> _IndirectArgsBuffer;在所有线程执行完毕后,这个缓冲区中的 numVerticesPerInstance值就是所有草地顶点数量的总和。CPU 在渲染时不需要知道具体生成了多少草,只需要发出一个命令:“​​请按照 _IndirectArgsBuffer里记录的参数去画吧!​​”(即间接绘制)。

// 间接绘制参数
struct IndirectArgs
{uint numVerticesPerInstance;uint numInstances;uint startVertexIndex;uint startInstanceIndex;uint startLocation;
};AppendStructuredBuffer<TriangleData> _Triangles;
RWStructuredBuffer<IndirectArgs> _IndirectArgsBuffer;

在C#脚本中创建_trianglesBuffer作为Compute Shader的输出目的地,用于存储所有生成的三角形数据。_indirectArgsBuffer用于间接绘制,它存储绘制调用所需的参数(顶点数、实例数等)。

_trianglesBuffer = new ComputeBuffer(maxTriangles, SIZE_TRIANGLE_DATA, ComputeBufferType.Append);
_indirectArgsBuffer = new ComputeBuffer(1, SIZE_INDIRECT_ARGS, ComputeBufferType.IndirectArguments);

根据草的数量计算需要多少个线程组(每个线程组512个线程)。然后调度Compute Shader执行。

int threadGroups = Mathf.CeilToInt(grassCount / 512f);
grassComputeShader.Dispatch(_grassGenKernel, threadGroups, 1, 1);

使用Graphics.DrawProceduralIndirect间接绘制命令,绘制类型为三角形。绘制所需的参数(顶点数、实例数等)从_indirectArgsBuffer中读取。

  Graphics.DrawProceduralIndirect(grassMaterial,new Bounds(transform.position, Vector3.one * areaSize),MeshTopology.Triangles,_indirectArgsBuffer,0,null,null,UnityEngine.Rendering.ShadowCastingMode.On,true,gameObject.layer
);

在compute shader中,定义了每个线程组有512个线程。每个线程通过id.x来索引,对应一株草。

[numthreads(256, 1, 1)]
void GrassGen(uint3 id : SV_DispatchThreadID)

在脚本中

int threadGroups = Mathf.CeilToInt(grassCount / 256f);
grassComputeShader.Dispatch(_grassGenKernel, threadGroups, 1, 1);

Dispatch启动Compute Shader的执行。计算需要多少个线程组:grassCount / 256f,因为我们在Shader中定义了 [numthreads(256,1,1)],即每个组256个线程。我们总共需要 grassCount个线程,所以需要 grassCount/256个组。

GPU开始并行计算,生成所有草的三角形并填入 _trianglesBuffer,同时将最终的顶点数量计数到 _indirectArgsBuffer

绘制草

采用分段构建,方便后续使用LOD优化技术

 for (int blade = 0; blade < _NUM_BLADES; blade++){float3 bladePos = positionWS + float3(bladeOffset.x, 0, bladeOffset.y);// 生成单株草使用连续的四边形for (int seg = 0; seg < _NUM_SEGS; seg++){VertexData vert;TriangleData tri;// 计算当前段和下一段的高度float currentYPos = seg * segmentHeight;float nextYPos = (seg + 1) * segmentHeight;// 计算当前段和下一段的宽度(随高度减少)float currentWidth = randomWidth * (1.0 - float(seg) / _NUM_SEGS);float nextWidth = randomWidth * (1.0 - float(seg + 1) / _NUM_SEGS);// 计算四个顶点位置float3 bottomLeft = bladePos + float3(-currentWidth * 0.5, currentYPos, 0);float3 bottomRight = bladePos + float3(currentWidth * 0.5, currentYPos, 0);float3 topLeft = bladePos + float3(-nextWidth * 0.5, nextYPos, 0);float3 topRight = bladePos + float3(nextWidth * 0.5, nextYPos, 0);// 计算法线 (垂直于草面)float3 edge1 = bottomRight - bottomLeft;float3 edge2 = topLeft - bottomLeft;float3 normal = normalize(cross(edge1, edge2));// 第一个三角形 (左下, 右下, 左上)vert.normalWS = normal;vert.positionWS = bottomLeft;tri.vertices[0] = vert;vert.normalWS = normal;vert.positionWS = bottomRight;tri.vertices[1] = vert;vert.normalWS = normal;vert.positionWS = topLeft;tri.vertices[2] = vert;_Triangles.Append(tri);// 第二个三角形 (右下, 右上, 左上)vert.normalWS = normal;vert.positionWS = bottomRight;tri.vertices[0] = vert;vert.normalWS = normal;vert.positionWS = topRight;tri.vertices[1] = vert;vert.normalWS = normal;vert.positionWS = topLeft;tri.vertices[2] = vert;_Triangles.Append(tri);}// 添加草尖if (_NUM_SEGS > 0){VertexData vert;TriangleData tri;float topYPos = randomHeight;float topWidth = randomWidth * 0.1; // 草尖很细float3 bottomLeft = bladePos + float3(-topWidth * 0.5, topYPos - segmentHeight, 0);float3 bottomRight = bladePos + float3(topWidth * 0.5, topYPos - segmentHeight, 0);float3 top = bladePos + float3(0, topYPos, 0);// 计算法线float3 edge1 = bottomRight - bottomLeft;float3 edge2 = top - bottomLeft;float3 normal = normalize(cross(edge1, edge2));// 草尖三角形vert.normalWS = normal;vert.positionWS = bottomLeft;tri.vertices[0] = vert;vert.normalWS = normal;vert.positionWS = bottomRight;tri.vertices[1] = vert;vert.normalWS = normal;vert.positionWS = top;tri.vertices[2] = vert;//将生成的三角形追加到缓冲区_Triangles.Append(tri);}}

避免多株草完全重叠在一起,在基础位置(positionWS)周围进行小范围随机偏移(bladeOffset),使草分布更自然。


float2 bladeOffset = float2((rand(float2(id.x + blade, id.x + blade * 2)) - 0.5) * 0.3,(rand(float2(id.x + blade * 3, id.x + blade * 4)) - 0.5) * 0.3
);

更新间接绘制参数

 // 累加顶点数量 (每段2个三角形,每个三角形3个顶点)
uint trianglesAdded = _NUM_BLADES * (_NUM_SEGS * 2 + (_NUM_SEGS > 0 ? 1 : 0));
InterlockedAdd(_IndirectArgsBuffer[0].numVerticesPerInstance, trianglesAdded * 3);

 在顶点着色器可以直接从这个缓冲区中读取数据来绘制图形。

// 三角形数据结构
struct TriangleData
{VertexData vertices[3];
};StructuredBuffer<TriangleData> _Triangles;// 计算三角形索引和顶点在三角形内的索引uint triangleIndex = vertexID / 3;uint vertexInTriangleIndex = vertexID % 3;// 从缓冲区获取顶点数据VertexData v = _Triangles[triangleIndex].vertices[vertexInTriangleIndex];

优化

LOD

近处生成高质量草(多株,高细节),远处生成低质量草(可能0株,低细节)。

int _lodLevel(float3 positionWS) {float3 delta = positionWS - _WorldSpaceCameraPos;//_WorldSpaceCameraPos通过脚本传递过来float distSq = dot(delta, delta); float minDistSq = LOD_MIN_DISTANCE * LOD_MIN_DISTANCE;float maxDistSq = LOD_MAX_DISTANCE * LOD_MAX_DISTANCE;float f = 1.0 - clamp((distSq - minDistSq) / (maxDistSq - minDistSq), 0.01, 1.00);f = (f * f) * (f * f);int lod = int(LOD_LEVELS * f);return lod;
}
int lod = _lodLevel(positionWS);
int _NUM_BLADES = 0; // 这个位置要生成的草数量
int _NUM_SEGS = max(lod, 1); // 每株草的段数(细节)......float3 positionWS = float3(randomPos.x, 0, randomPos.y);// 计算LOD级别int lod = _lodLevel(positionWS);// 根据LOD决定生成多少株草int _NUM_BLADES = 0;if (lod >= LOD_MAX){_NUM_BLADES = 3; }else if (lod <= LOD_MIN){// 远处: 50%概率不生成草,50%概率生成1株float randomValue = rand(float2(id.x, id.x * 2.0));_NUM_BLADES = (randomValue > 0.5) ? 1 : 0;}else{//_NUM_BLADES = max(lod - 1, 1); // 中等距离_NUM_BLADES = 2; }if (_NUM_BLADES == 0)return;// 根据LOD决定草的段数int _NUM_SEGS = max(lod, 1); // 至少1段
视锥体剔除

对于完全不在摄像机视野范围内的草,不生成任何几何数据,不进行绘制。

// 使用NDC空间的视锥体剔除函数
bool FrustumCull(float3 positionWS, float grassHeight, float grassWidth)
{float radius = max(grassHeight, grassWidth) * 0.5;float3 centerWS = positionWS + float3(0, grassHeight * 0.5, 0);float4 clipPos = mul(_ViewProj, float4(centerWS, 1.0));// 透视除法得到NDC坐标float3 ndc = clipPos.xyz / clipPos.w;float ndcRadius = radius / max(clipPos.w, 0.0001);const float TOLLERANCE = 0.1 + ndcRadius;// 检查是否在NDC立方体内bool isInsideFrustum =ndc.x >= (-1.0 - TOLLERANCE) && ndc.x <= (1.0 + TOLLERANCE) &&ndc.y >= (-1.0 - TOLLERANCE) && ndc.y <= (1.0 + TOLLERANCE) &&ndc.z >= (0.0 - TOLLERANCE) && ndc.z <= (1.0 + TOLLERANCE);return !isInsideFrustum;
}

草的shader

之前讲过Shader通过​StructuredBuffer<TriangleData> _Triangles读取由Compute Shader生成的所有草的三角形数据。

在顶点着色器中,通过 vertexID和 instanceID计算出当前顶点对应缓冲区中的哪个数据,并提取出来

uint triangleIndex = vertexID / 3;
uint vertexInTriangleIndex = vertexID % 3;
VertexData v = _Triangles[triangleIndex].vertices[vertexInTriangleIndex];
float3 positionWS = v.positionWS; // 获取世界空间位置
float3 normalWS = v.normalWS; // 获取世界空间法线

实现风效

使用​​正弦波​​ (sin) 来模拟风的周期性。

heightFactor: 高度比例(0在草根,1在草尖)。风的影响从草根(草根不动)到草尖逐渐增强,使草叶呈现出自然的弯曲,而不是整体平移。

float2 CalculateWindOffset(float3 worldPos, float heightFactor) {float windTime = _Time.y;float windWave = sin(windTime + dot(worldPos.xz, _WaveFrequency));float2 windOffset = _WindDirection * windWave * _WindStrength * heightFactor * _WaveScale;return windOffset;
}
麦浪效果 (Rippling Wheat)

主要通过采样一张噪声纹理实现

// 采样噪声纹理
float2 sampleUV = float2(positionWS.x / _WaveSize + _Time.x * _WaveSpeed, positionWS.z / _WaveSize);
float3 waveRipplingSample = tex2Dlod(_NoiseRipplingMap, float4(sampleUV, 0, 0)).xyz;// 应用偏移
positionWS.xz += sin(waveRipplingSample.r * 0.5) * _WaveSpeed * heightFactor * o.pulseIntensity * 0.3;

颜色与光照

草的基础颜色

根据从顶点着色器传递来的 i.height(该像素在草上的高度)来决定是使用根部的颜色(_Color)还是顶部的颜色(_TopColor),实现渐变的效果。

half gradientFactor = pow(i.height, _GradientPower);
gradientFactor = saturate((gradientFactor - _GradientThreshold) / max(0.001, 1 - _GradientThreshold));
fixed4 grassColor = lerp(_Color, _TopColor, gradientFactor);
光照
half ndotl = saturate(dot(normal, lightDir)); // 兰伯特
// 高光 (Blinn-Phong)
half3 halfVector = normalize(lightDir + viewDir);
half specular = pow(saturate(dot(normal, halfVector)), _SpecularPower);
grassColor.rgb += specular * _SpecularIntensity * _LightColor0.rgb;// 边缘光 
half rim = 1.0 - saturate(dot(viewDir, normal));
half3 rimLight = pow(rim, _StylizedRimPower) * _StylizedRimIntensity * _LightColor0.rgb;
grassColor.rgb += rimLight;

阴影用unity内置的方法,不过多赘述了。

交互

基于球体检测的顶点偏移,在角色的脚部或碰撞体周围预先定义不可见的“影响球”这些球体代表了角色对草地施加影响的范围。

在脚本中设置影响球

[System.Serializable]
public class InteractionSphere
{public Vector3 position;public float radius;public int id;public InteractionSphere(Vector3 pos, float rad, int identifier){position = pos;radius = rad;id = identifier;}
}

在脚本中将球体数据传递给Shader,可以实现多角色交互。在Shader中,可以通过计算草的位置与这些球体的距离,来实现弯曲、压扁等效果。

    void UpdateInteractionSpheres(){for (int i = 0; i < _shaderInteractionSpheres.Length; i++){_shaderInteractionSpheres[i] = Vector4.zero;}// 更新有效的交互球体_shaderSphereCount = Mathf.Min(interactionSpheres.Count, 10);for (int i = 0; i < _shaderSphereCount; i++){InteractionSphere sphere = interactionSpheres[i];_shaderInteractionSpheres[i] = new Vector4(sphere.position.x,sphere.position.y,sphere.position.z,sphere.radius);}// 传递到ShaderShader.SetGlobalInt("_InteractionSphereCount", _shaderSphereCount);Shader.SetGlobalVectorArray("_InteractionSpheres", _shaderInteractionSpheres);}

在顶点着色器中遍历所有交互球体,检查当前顶点是否在某个球体影响范围内。如果是,则计算一个使顶点​​远离球心水平方向​​的偏移量。

 // 遍历所有交互球体
for (int i = 0; i < _InteractionSphereCount; i++)
{float4 sphere = _InteractionSpheres[i];float3 sphereCenter = sphere.xyz;float sphereRadius = sphere.w;// 计算顶点到球心的距离float3 toCenter = positionWS - sphereCenter;float sqrDistance = dot(toCenter, toCenter);float sqrRadius = sphereRadius * sphereRadius;if (sqrDistance < sqrRadius){float distanceToSphere = sqrt(sqrDistance);float influence = 1.0 - (distanceToSphere / sphereRadius);influence = pow(influence, 2); float3 bendDirection = normalize(toCenter);bendDirection.y = 0;bendDirection = normalize(bendDirection);float heightFactorBend = saturate(positionWS.y / _GrassHeight);bendOffset += bendDirection * influence * _BendStrength * heightFactorBend;}
}// 应用弯曲偏移
positionWS.xz += bendOffset.xz; 

还有很多可以实现,比如:基于 depth buffer 剔除,交互效果可以使用RT,火烧草等

参考文章:基于GPU Instance的草地渲染学习笔记 - 知乎https://zhuanlan.zhihu.com/p/397620652

实时大规模草地渲染、优化与交互(附源码) - 知乎https://zhuanlan.zhihu.com/p/717674449

大规模草渲染-- 知乎https://www.zhihu.com/collection/987682198

 URP渲染管线 - GPUInstance绘制草地 - 知乎https://zhuanlan.zhihu.com/p/354633512

 Compute Shader学习笔记(三)之 草地渲染 - 知乎https://zhuanlan.zhihu.com/p/701633578

 植被的动态交互---草 - 知乎https://zhuanlan.zhihu.com/p/151342889


文章转载自:

http://iJ9j7hpt.xdpjs.cn
http://rZ91tSx1.xdpjs.cn
http://eSBlDzcn.xdpjs.cn
http://rTEcWUQP.xdpjs.cn
http://xd46vCv2.xdpjs.cn
http://LA2voqeK.xdpjs.cn
http://CiQPgWpp.xdpjs.cn
http://8GQGN1vG.xdpjs.cn
http://zlAwd29z.xdpjs.cn
http://1o7GsZ0L.xdpjs.cn
http://hgLCMIHG.xdpjs.cn
http://NFGI2iPZ.xdpjs.cn
http://Ue4pSemt.xdpjs.cn
http://OvjWju08.xdpjs.cn
http://DHqJYPMt.xdpjs.cn
http://SeF0mBFk.xdpjs.cn
http://WDNgIXF9.xdpjs.cn
http://AVEoroSB.xdpjs.cn
http://w1ogvOw4.xdpjs.cn
http://iHf1L5Cs.xdpjs.cn
http://TqaSL2gw.xdpjs.cn
http://pr3tKoB0.xdpjs.cn
http://6XdccInq.xdpjs.cn
http://TUWKtaso.xdpjs.cn
http://Hep9Barh.xdpjs.cn
http://kdPYX1Bf.xdpjs.cn
http://pc3LvVO2.xdpjs.cn
http://ZFpnsobP.xdpjs.cn
http://pgv4G9mO.xdpjs.cn
http://gIdjs43B.xdpjs.cn
http://www.dtcms.com/a/368241.html

相关文章:

  • go webrtc - 1 go基本概念
  • OSI七层模型与tcp/ip四层模型
  • WebRTC进阶--WebRTC错误Failed to unprotect SRTP packet, err=9
  • 自由学习记录(95)
  • 商业融雪系统解决方案:智能技术驱动下的冬季安全与效率革命
  • 用 epoll 实现的 Reactor 模式详解(含代码逐块讲解)
  • Linux ARM64 内核/用户虚拟空间地址映射
  • linux inotify 功能详解
  • C++中虚函数与构造/析构函数的深度解析
  • 工业客户最关心的,天硕工业级SSD固态硬盘能解答哪些疑问?
  • 在宝塔面板中修改MongoDB配置以允许远程连接
  • 84 数组地址的几种计算方式
  • GCC编译器深度解剖:从源码到可执行文件的全面探索
  • OpenSCA开源社区每日安全漏洞及投毒情报资讯| 4th Sep. , 2025
  • Java 操作 Excel 全方位指南:从入门到避坑,基于 Apache POI
  • 多云战略的悖论:为何全局数据“看得见”却“算不起”?
  • 深入剖析Spring动态代理:揭秘JDK动态代理如何精确路由接口方法调用
  • More Effective C++ 条款29:引用计数
  • 人形机器人控制系统核心芯片从SoC到ASIC的进化路径
  • Docker学习笔记(三):镜像与容器管理进阶操作
  • excel里面店铺这一列的数据结构是2C【uniteasone17】这种,我想只保留前面的2C部分,后面的【uniteasone17】不要
  • Qt图片资源导入
  • 苍穹外卖Day10 | 订单状态定时处理、来单提醒、客户催单、SpringTask、WebSocket、cron表达式
  • 01-Hadoop简介与生态系统
  • 如何利用静态代理IP优化爬虫策略?从基础到实战的完整指南
  • 信息安全工程师考点-网络信息安全概述
  • 功能强大的多线程端口扫描工具,支持批量 IP 扫描、多种端口格式输入、扫描结果美化导出,适用于网络安全检测与端口监控场景
  • 自定义格式化数据(BYOFD)(81)
  • 人工智能时代职能科室降本增效KPI设定全流程与思路考察
  • 使用 chromedp 高效爬取 Bing 搜索结果