1. 项目概述:为什么我们需要DrawMeshInstancedIndirect?
在Unity里做开放世界或者大场景,草地的渲染一直是个老大难问题。你可能会说,用GPU Instancing不就行了吗?确实,对于几千个实例,GPU Instancing是标准答案,性能提升立竿见影。但当你面对的是“10万+”这个量级时,标准答案就开始失效了。我最近在优化一个野外场景,草地的数量轻松突破20万,用传统的GameObject挂MeshRenderer,或者用MaterialPropertyBlock配合Graphics.DrawMeshInstanced,帧率直接掉到个位数,CPU提交Draw Call的压力巨大。
这就是
DrawMeshInstancedIndirect
和
ComputeBuffer
登场的时刻。这套组合拳的核心思想,是把渲染的主动权从CPU完全交给GPU。CPU不再需要为每一个草叶实例准备数据、提交一次Draw Call,而是把所有实例的数据(位置、旋转、缩放、颜色等)打包成一个大的数据块(ComputeBuffer),再告诉GPU一个总的数量和绘制规则,让GPU自己去“批量生产”。这不仅仅是减少了Draw Call,更是将实例数据的处理、剔除(Culling)甚至动画逻辑都转移到了GPU的并行计算管线中,彻底解放了CPU。
简单来说,它解决的是“海量同网格物体高效渲染”的终极性能瓶颈。无论是随风摇曳的十万株青草、战场上密密麻麻的士兵、星空中无尽的繁星,还是建筑群中重复的窗户,只要是网格相同但变换数据不同的物体,这套方案都能带来数量级的性能提升。接下来,我就带你从零开始,拆解如何用这套“核武器”来渲染一片性能丝滑的十万级草地。
2. 核心原理与方案选型:ComputeBuffer与间接绘制
在深入代码之前,我们必须搞清楚背后的原理。这决定了我们如何设计数据结构和渲染流程。
2.1 ComputeBuffer:GPU端的数据仓库
ComputeBuffer
是Unity提供的一个用于在CPU和GPU之间传递大量结构化数据的通道。你可以把它想象成一个在GPU显存中开辟的数组,这个数组的每个元素(即一条数据)的结构由我们自己定义。
对于草地渲染,我们至少需要每个实例的位置信息。一个基础的实例数据结构可能如下:
struct InstanceData {
public Vector3 position;
public Vector4 color; // 使用Vector4存储RGBA颜色,便于GPU对齐
public float scale;
// 可以添加更多属性,如风向影响、生长阶段等
}
在CPU端,我们创建一个
ComputeBuffer
,并指定它能容纳多少个这样的
InstanceData
结构体,然后将我们计算好的所有实例数据(比如20万个
InstanceData
)一次性设置进去。之后,这个Buffer就对GPU可读了。
2.2 DrawMeshInstancedIndirect:GPU的绘制指令
传统的
Graphics.DrawMeshInstanced
需要CPU循环调用,每次调用提交一个批次(比如最多1023个实例)。而
DrawMeshInstancedIndirect
则不同,它需要的参数是一个
ComputeBuffer
,这个Buffer里存放的不是实例数据,而是绘制参数(Arguments)。
绘制参数是一个包含5个
uint
整数的数组:
[instanceCount, startInstance, startVertex, startIndex, baseVertexIndex]
对我们来说,最关键的是
instanceCount
(要绘制多少个实例)和
startInstance
(从哪个实例开始绘制)。神奇之处在于,这个参数Buffer的内容可以在GPU端通过Compute Shader进行修改!
这意味着什么?意味着我们可以先在GPU上执行一个计算着色器(Compute Shader),对所有20万个实例的数据进行视锥体剔除(Frustum Culling)和距离剔除(Distance Culling),计算后真正需要渲染的实例数量可能只有5万个。然后,这个Compute Shader将计算出的最终数量
50000
写回到参数Buffer的
instanceCount
位置。最后,CPU调用一次
DrawMeshInstancedIndirect
,GPU就会读取参数Buffer中的
50000
,并只渲染这5万个可见的实例。
方案选型考量:
- 为什么不只用GPU Instancing? GPU Instancing有单次调用实例数量上限(通常1023),且剔除逻辑仍在CPU,海量实例时CPU遍历计算剔除本身就是性能负担。
- 为什么是ComputeBuffer + Indirect? 它实现了“GPU Driven Rendering”的雏形。数据(InstanceData)和指令(Arguments)都在GPU,CPU仅做一次调用,瓶颈转移到了并行计算能力极强的GPU上,非常适合处理超大规模的同质化物体。
-
兼容性注意:
DrawMeshInstancedIndirect需要图形API支持(如DX11, DX12, OpenGL 4.2+, Metal, Vulkan),在较老的设备或WebGL上可能不可用。在实际项目中需要做备选方案(如回退到分块的GPU Instancing)。
3. 实战步骤:构建十万草地的完整管线
理论讲完,我们开始动手。整个过程可以分为数据准备、计算剔除、间接渲染三个核心阶段。
3.1 阶段一:CPU端数据准备与Buffer创建
首先,我们需要在C#脚本中创建和初始化所有Buffer。
using UnityEngine;
using UnityEngine.Rendering;
public class IndirectGrassRenderer : MonoBehaviour {
public Mesh grassMesh; // 单根草的网格
public Material grassMaterial; // 草的材质球
public int instanceCount = 100000; // 目标实例数量
public Vector2 fieldSize = new Vector2(200, 200); // 草地分布区域
private ComputeBuffer _instanceDataBuffer; // 存储实例数据的Buffer
private ComputeBuffer _argsBuffer; // 存储绘制参数的Buffer
private uint[] _args = new uint[5] { 0, 0, 0, 0, 0 }; // 参数数组
// 定义与Shader中匹配的结构体
struct GrassInstanceData {
public Vector3 worldPos;
public Vector4 color;
public float scale;
public float windStrength;
}
void Start() {
InitializeBuffers();
}
void InitializeBuffers() {
// 1. 创建实例数据Buffer
int stride = System.Runtime.InteropServices.Marshal.SizeOf(typeof(GrassInstanceData));
_instanceDataBuffer = new ComputeBuffer(instanceCount, stride, ComputeBufferType.Structured);
// 2. 生成初始数据(这里简单随机分布)
GrassInstanceData[] instanceDataArray = new GrassInstanceData[instanceCount];
for (int i = 0; i < instanceCount; i++) {
Vector3 pos = new Vector3(
Random.Range(-fieldSize.x / 2, fieldSize.x / 2),
0,
Random.Range(-fieldSize.y / 2, fieldSize.y / 2)
);
// 这里可以加上基于地形高度图(Terrain)的采样,让草长在地上
// pos.y = terrain.SampleHeight(pos);
instanceDataArray[i] = new GrassInstanceData {
worldPos = pos,
color = new Vector4(Random.Range(0.7f, 0.9f), Random.Range(0.8f, 1.0f), Random.Range(0.4f, 0.6f), 1.0f),
scale = Random.Range(0.8f, 1.2f),
windStrength = Random.Range(0.5f, 1.5f)
};
}
_instanceDataBuffer.SetData(instanceDataArray);
// 3. 创建参数Buffer
_argsBuffer = new ComputeBuffer(1, _args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
// 初始化参数:实例数量为0,等GPU剔除后再更新
UpdateArgumentsBuffer();
// 4. 将实例数据Buffer传递给材质球,Shader中可以通过`StructuredBuffer<GrassInstanceData>`来读取
grassMaterial.SetBuffer("_InstanceDataBuffer", _instanceDataBuffer);
}
void UpdateArgumentsBuffer() {
if (grassMesh != null) {
_args[0] = (uint)grassMesh.GetIndexCount(0); // 索引数量
_args[1] = (uint)instanceCount; // 初始设置为总实例数,后续由GPU更新
_args[2] = (uint)grassMesh.GetIndexStart(0);
_args[3] = (uint)grassMesh.GetBaseVertex(0);
_args[4] = 0; // startInstance
_argsBuffer.SetData(_args);
}
}
}
关键点 :
ComputeBufferType.Structured表示这是一个结构化的Buffer,Shader中可以定义对应结构来读取。ComputeBufferType.IndirectArguments专门用于间接绘制参数。
3.2 阶段二:Compute Shader实现GPU剔除与动画
这是性能提升的核心。我们创建一个Compute Shader文件(例如
GrassCulling.compute
)。
// GrassCulling.compute
#pragma kernel CSMain
// 从C#端传入的Buffer
StructuredBuffer<GrassInstanceData> _InstanceDataBuffer;
AppendStructuredBuffer<GrassInstanceData> _VisibleInstanceBuffer; // 用于追加可见实例
RWStructuredBuffer<uint> _ArgsBuffer; // 可读写的参数Buffer
// 相机参数
float4x4 _ViewProjMatrix;
float3 _CameraPosition;
float _MaxDrawDistance;
// 与C#端对应的结构体
struct GrassInstanceData {
float3 worldPos;
float4 color;
float scale;
float windStrength;
};
[numthreads(64, 1, 1)] // 一次调度64个线程并行执行
void CSMain (uint3 id : SV_DispatchThreadID) {
uint index = id.x;
if (index >= _InstanceDataBuffer.Length()) {
return;
}
GrassInstanceData instance = _InstanceDataBuffer[index];
// 1. 距离剔除
float dist = distance(instance.worldPos, _CameraPosition);
if (dist > _MaxDrawDistance) {
return;
}
// 2. 简单的视锥体剔除(这里用球体近似)
// 实际项目可以使用更精确的包围盒(Bounds)判断
float4 clipPos = mul(_ViewProjMatrix, float4(instance.worldPos, 1.0));
clipPos.xyz /= clipPos.w; // 齐次除法
if (any(clipPos.xyz < -1.0) || any(clipPos.xyz > 1.0)) {
return;
}
// 3. 简单的GPU动画(模拟风效)
float windOffset = sin(_Time.y * 0.5 + instance.worldPos.x * 0.1 + instance.worldPos.z * 0.1) * instance.windStrength * 0.1;
instance.worldPos.y += windOffset; // 在Y轴上轻微摆动
// 4. 通过追加Buffer记录可见实例
_VisibleInstanceBuffer.Append(instance);
}
// 另一个Kernel,用于在剔除完成后,更新绘制参数Buffer中的实例数量
#pragma kernel UpdateArgs
RWStructuredBuffer<uint> _ArgsBuffer;
ConsumeStructuredBuffer<GrassInstanceData> _VisibleInstanceBuffer;
[numthreads(1,1,1)]
void UpdateArgs() {
uint visibleCount = _VisibleInstanceBuffer.ConsumeCounter();
_ArgsBuffer[1] = visibleCount; // 更新instanceCount参数
}
在C#脚本中,我们需要每帧调度这个Compute Shader。
// 接上面的C#脚本
public ComputeShader cullingComputeShader;
private ComputeBuffer _visibleInstanceBuffer; // 可见实例Buffer
private int _cullingKernel;
private int _updateArgsKernel;
void Start() {
// ... 初始化其他Buffer ...
InitializeComputeShader();
}
void InitializeComputeShader() {
_cullingKernel = cullingComputeShader.FindKernel("CSMain");
_updateArgsKernel = cullingComputeShader.FindKernel("UpdateArgs");
// 创建可见实例的Append Buffer(最大容量为总实例数)
int stride = System.Runtime.InteropServices.Marshal.SizeOf(typeof(GrassInstanceData));
_visibleInstanceBuffer = new ComputeBuffer(instanceCount, stride, ComputeBufferType.Append);
_visibleInstanceBuffer.SetCounterValue(0); // 初始化计数器为0
// 将Buffer绑定到Compute Shader
cullingComputeShader.SetBuffer(_cullingKernel, "_InstanceDataBuffer", _instanceDataBuffer);
cullingComputeShader.SetBuffer(_cullingKernel, "_VisibleInstanceBuffer", _visibleInstanceBuffer);
cullingComputeShader.SetBuffer(_cullingKernel, "_ArgsBuffer", _argsBuffer);
cullingComputeShader.SetBuffer(_updateArgsKernel, "_ArgsBuffer", _argsBuffer);
cullingComputeShader.SetBuffer(_updateArgsKernel, "_VisibleInstanceBuffer", _visibleInstanceBuffer);
// 将可见实例Buffer也传递给渲染材质球,用于最终绘制
grassMaterial.SetBuffer("_InstanceDataBuffer", _visibleInstanceBuffer); // 注意:这里替换成了可见Buffer
}
void Update() {
PerformGPUCulling();
Render();
}
void PerformGPUCulling() {
// 每帧重置可见实例Buffer的计数器
_visibleInstanceBuffer.SetCounterValue(0);
// 传递本帧相机参数到Compute Shader
Camera cam = Camera.main;
cullingComputeShader.SetMatrix("_ViewProjMatrix", cam.projectionMatrix * cam.worldToCameraMatrix);
cullingComputeShader.SetVector("_CameraPosition", cam.transform.position);
cullingComputeShader.SetFloat("_MaxDrawDistance", 100.0f);
cullingComputeShader.SetFloat("_Time", Time.time);
// 调度剔除计算:线程组数量 = ceil(实例总数 / 64)
int threadGroups = Mathf.CeilToInt(instanceCount / 64.0f);
cullingComputeShader.Dispatch(_cullingKernel, threadGroups, 1, 1);
// 调度参数更新计算(只需要1个线程组)
cullingComputeShader.Dispatch(_updateArgsKernel, 1, 1, 1);
}
3.3 阶段三:Shader与最终渲染
现在,GPU端已经有了经过剔除和动画处理的可见实例数据(在
_visibleInstanceBuffer
中),以及更新了正确实例数量的参数Buffer(
_argsBuffer
)。接下来是渲染。
首先,我们需要一个支持从Buffer读取实例数据的Shader。这是一个简化的Unlit Shader示例:
// GrassInstancedIndirect.shader
Shader "Custom/IndirectGrass" {
Properties {
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5 // 需要Shader Model 4.5以支持StructuredBuffer
#include "UnityCG.cginc"
struct GrassInstanceData {
float3 worldPos;
float4 color;
float scale;
float windStrength;
};
// 从C#传递过来的可见实例Buffer
StructuredBuffer<GrassInstanceData> _InstanceDataBuffer;
struct v2f {
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
v2f vert (uint instanceID : SV_InstanceID, uint vertexID : SV_VertexID) {
// 通过instanceID从Buffer中读取该实例的数据
GrassInstanceData instance = _InstanceDataBuffer[instanceID];
// 获取原始网格的顶点数据(这里假设是简单的四边形草)
// 实际中,你需要一个包含基础草模型顶点数据的Buffer或硬编码
float3 baseVert = FetchBaseVertex(vertexID); // 伪代码,需要实现
// 应用实例的变换:缩放、平移(这里省略旋转)
float3 worldVert = baseVert * instance.scale + instance.worldPos;
// 变换到裁剪空间
v2f o;
o.pos = mul(UNITY_MATRIX_VP, float4(worldVert, 1.0));
o.uv = TRANSFORM_TEX(baseVert.xz, _MainTex); // 假设UV映射
o.color = instance.color * _Color; // 混合实例颜色和材质颜色
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv) * i.color;
return col;
}
ENDCG
}
}
FallBack "Diffuse"
}
最后,在C#的
Update
或
LateUpdate
中,调用一次间接绘制命令:
void Render() {
// 设置渲染所需的材质属性(如矩阵、纹理等)
// grassMaterial.SetMatrix(...);
// grassMaterial.SetTexture(...);
// 执行间接绘制
// 参数依次为:网格、子网格索引(0)、材质、包围盒(这里用null,因为剔除已在GPU完成)、参数Buffer、参数偏移量(0)
Graphics.DrawMeshInstancedIndirect(grassMesh, 0, grassMaterial, new Bounds(Vector3.zero, new Vector3(1000, 1000, 1000)), _argsBuffer);
}
重要提示 :
DrawMeshInstancedIndirect需要提供一个Bounds参数,这个包围盒应该覆盖所有 可能 被绘制的实例范围,用于Unity底层做粗略的裁剪。即使GPU做了精确剔除,这个步骤也必不可少。如果给的范围太小,本应渲染的实例可能会被错误剔除。
4. 性能优化与高级技巧
实现基础功能只是第一步,要让十万级草地真正流畅运行,还需要一系列优化。
4.1 层级细节(LOD)与视距分级
即使做了GPU剔除,远处密密麻麻的草叶也是性能浪费。一个常见的优化是分级渲染:
- 近处(0-20米) :渲染完整的高面数草模型,使用完整的着色计算(法线、高光等)。
- 中距离(20-50米) :渲染简化版的低面数草模型,或使用交叉面片(Cross Quad),着色简化。
- 远处(50米以外) :不渲染单根草,而是渲染一张带有透明通道的草地贴片(Impostor),或者使用地形纹理混合来表现。
实现上,可以在Compute Shader的剔除阶段就加入LOD判断。根据实例与相机的距离,将其数据追加到不同的
AppendStructuredBuffer
中(例如
_VisibleInstanceBufferLOD0
,
_VisibleInstanceBufferLOD1
)。然后创建多个参数Buffer和材质球,分别对应不同的LOD层级,最后分别调用
DrawMeshInstancedIndirect
进行绘制。
4.2 合批与材质变体管理
DrawMeshInstancedIndirect
一次调用只能绘制
同一种材质
和
同一个网格
。如果你的草地有不同种类(颜色、形态差异很大),就需要拆分。
-
策略一:材质属性块(MaterialPropertyBlock)
:如果只是颜色等少量属性不同,可以将这些属性也打包进
InstanceData结构,在Shader中读取,从而避免拆分Draw Call。但注意,DrawMeshInstancedIndirect本身不支持在调用时传递MaterialPropertyBlock,所有变体数据必须通过Buffer传递。 -
策略二:按类别分批次
:将不同种类的草数据放在不同的
ComputeBuffer中,使用不同的Compute Shader Dispatch和DrawMeshInstancedIndirect调用。虽然增加了Draw Call,但每个Call仍然能处理海量实例,总体性能依然远优于传统方式。 - 策略三:纹理数组(Texture2DArray) :将不同草的贴图打包进一个纹理数组,在Shader中通过实例数据里的一个索引来采样对应的图层。这样可以实现单Draw Call绘制多种外观的草。
4.3 避免GPU内存与同步瓶颈
-
Buffer类型选择
:
ComputeBufferType.Default是默认类型。Structured用于结构化数据。Append/Consume用于需要追加/消耗数据的场景(如我们的可见列表)。Raw可以用于存储字节或整数。选择合适的类型有助于驱动优化。 -
避免每帧创建/释放Buffer
:
ComputeBuffer的创建和销毁成本较高。应该在Start或Awake中创建,并在OnDestroy中释放(_instanceDataBuffer.Release()),在整个生命周期内复用。 -
注意CPU-GPU同步
:
ComputeShader.Dispatch和Graphics.DrawMeshInstancedIndirect是异步命令。如果你在Dispatch之后立即从Buffer中读取数据(例如用ComputeBuffer.GetData),会导致CPU等待GPU完成工作,造成卡顿。我们的渲染管线设计应避免这种“回读”操作。
4.4 与地形系统(Terrain)结合
在真实项目中,草需要长在地形上。我们可以在初始化
InstanceData
时,通过
Terrain.SampleHeight
接口获取每个实例生成点的准确高度。对于动态地形(如可破坏地形),则需要将地形高度图也传入Compute Shader,在每帧的剔除/动画计算中实时采样,让草随着地形变化而“生长”或“沉降”。
5. 常见问题与调试技巧实录
在实际踩坑过程中,我总结了以下几个高频问题和解决方法。
5.1 问题一:屏幕上什么都没有(黑屏)
这是最常见的问题。排查步骤:
-
检查参数Buffer
:确保
_argsBuffer中的索引数量(_args[0])设置正确,它应该等于mesh.GetIndexCount(0)。如果为0,GPU就不会绘制任何三角形。 -
检查Bounds
:
DrawMeshInstancedIndirect调用中提供的Bounds是否足够大,包含了所有可能实例的位置?可以暂时将它设得非常大(如new Bounds(Vector3.zero, new Vector3(10000,10000,10000)))来测试。 -
检查Compute Shader剔除
:是不是GPU剔除过于激进,把所有实例都剔除了?在Compute Shader中,可以先注释掉剔除逻辑,让所有实例都进入
_VisibleInstanceBuffer,看看是否能渲染出来。 -
检查Shader编译与属性绑定
:在Unity编辑器Frame Debugger中查看该Draw Call是否被提交。检查材质球是否正常,Shader是否有编译错误。确保在渲染前,材质球已经通过
SetBuffer绑定了正确的_VisibleInstanceBuffer(而不是初始的_instanceDataBuffer)。 -
检查实例数据
:在C#端,可以尝试将
_instanceDataBuffer的数据GetData回来一小部分,打印到控制台,检查位置、颜色等数据是否有效(比如位置y值是否在合理高度,而不是全部为0导致草在地下)。
5.2 问题二:渲染结果错乱或闪烁
-
结构体内存对齐
:C#中的结构体
GrassInstanceData和HLSL中的结构体GrassInstanceData必须 严格一致 ,包括字段顺序和每个字段的字节大小。Vector3在C#中是12字节,在HLSL中float3也是12字节,但HLSL中为了性能通常会对齐到16字节的倍数。最稳妥的办法是在C#结构体中使用[System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)],并显式指定[MarshalAs]属性,或者在HLSL中使用packoffset。一个更简单的方法是全部使用float4(Vector4),虽然浪费一点空间,但能保证对齐。 -
Buffer绑定错乱
:确保在Dispatch Compute Shader和调用Draw Call时,各个Kernel绑定的Buffer是正确的。特别是
_visibleInstanceBuffer在每帧Dispatch前要用SetCounterValue(0)重置。 -
线程组计算溢出
:在Compute Shader中,
[numthreads(64,1,1)]和C#中Dispatch的线程组数量要匹配。确保threadGroups * 64 >= instanceCount,否则部分实例不会被处理。同时,在CSMain开头要检查if (index >= _InstanceDataBuffer.Length()) return;,防止索引越界。
5.3 问题三:性能提升不明显甚至更差
- GPU瓶颈转移 :这套方案将压力从CPU转移到了GPU。如果你的GPU本身已经满载(例如在进行复杂的光照和后处理),那么增加GPU计算和顶点处理可能会成为新的瓶颈。使用Unity Profiler的GPU模块或RenderDoc等工具分析GPU耗时。
- 过多的状态切换 :即使使用间接绘制,如果你在渲染前后频繁切换渲染状态(如切换材质、Shader、全局属性),也会产生开销。尽量将同类型草的渲染集中在一起。
- Compute Shader效率 :你的剔除算法是否过于复杂?视锥体剔除使用精确的包围盒计算会比简单的球体判断更耗时,但剔除效果更好,需要权衡。可以尝试简化剔除逻辑,或使用层级网格(Hierarchical Z-Buffer)等高级剔除技术。
-
带宽压力
:每帧将大量数据(如20万个实例的矩阵)从CPU传到GPU,即使是通过ComputeBuffer,也存在带宽成本。如果实例数据是静态的,确保只在初始化时
SetData一次。如果是动态的(如随风摆动),考虑是否所有数据都需要每帧更新,能否在Shader中用噪声函数模拟,减少Buffer更新量。
5.4 调试工具推荐
-
Frame Debugger
:逐帧查看Draw Call,确认你的
DrawMeshInstancedIndirect调用是否被正确提交,参数是否正确。 - RenderDoc :捕获一帧完整的渲染过程,可以深入查看Compute Shader执行后各个Buffer的具体数据,是调试GPU端问题的神器。
-
Unity Profiler
:关注
RenderThread的耗时和Batches数量的变化。成功应用后,Batches数量会急剧下降,但SRP Batcher可能不适用于这种自定义的间接绘制。 -
在Shader中输出调试颜色
:在Fragment Shader中,可以根据
instanceID或位置输出特定的纯色(如return float4(frac(instanceID/100.0), 0, 0, 1);),来直观判断实例是否被正确区分和渲染。
这套
DrawMeshInstancedIndirect
方案初看起来步骤繁多,但一旦搭建起这个数据流管道,它的扩展性和性能潜力是巨大的。从我自己的项目经验来看,将一片15万实例的草地从传统的每帧数百个Draw Call、CPU耗时10ms+,优化到1个Draw Call、CPU耗时小于1ms,GPU计算耗时2-3ms,这种提升是颠覆性的。它不仅仅是渲染草,更是打开GPU驱动渲染大门的一把钥匙,后续你可以将这套模式应用到任何需要大规模实例渲染的场景中。

286

被折叠的 条评论
为什么被折叠?



