unity 减少drawcall_unity scroll

unity 减少drawcall_unity scroll文章目录 啥叫 DrawCall 啥叫 SetPassCall 啥叫 Batch Dynamic Batch 动态合批 Static Batch 静态合批 GPU Instance GPU 实例绘制 SRP Batcher Unity SRP Scriptable Render Pipeline 的合批 GPU Driven Pipeline 动态合批伪代码 静态合批伪代码

文章目录

啥叫:DrawCall

啥叫:SetPassCall

啥叫:Batch

Dynamic Batch – 动态合批

Static Batch – 静态合批

GPU Instance – GPU 实例绘制

SRP Batcher – Unity SRP(Scriptable Render Pipeline) 的合批

GPU Driven Pipeline

动态合批伪代码

静态合批伪代码

选择合批的优先级

References

----

一般我们 在实时渲染中,DC也就是 DrawCall 都会尽可能的降低,因为这会比较直接的降低 CPU 与 GPU 的绘制沟通

----

啥叫:DrawCall

以 OpenGL 为例,就是调用带有绘制功能的 API 的次数

如:DrawCall : 10 次,那就意味着调用了 glDrawXXXX 的 API 10 次

----

啥叫:SetPassCall

Unity 中,就无缘无故多了个叫:SetPassCall 的家伙

其实早在以前的游戏引擎里,没有 pass 这么一个说法,或是 techni 的说法

因为这些都是封装的功能

可以查看我之前学习 OpenGL 时,写的一篇,添加 类似 Unity Pass 功能的文章:
LearnGL – 17 – Geometry Shader – 几何着色器 – 直接网页锚点定位到对应的 Pass 实现

一般 OpenGL 中,绘制一个对象,就是提供,VBO,IBO(IBO还不一定需要提供,可以使用 DrawArray 直接通过VBO来绘制,如果通过DrawArrayIndex 之类的就需要 IBO 了,前面的 VBO,IBO 也可以通过 VAO 统一绑定后设置),然后指定 shader(VS,FS,其他的按需提供),就可以调用 DC(DrawCall) API 来绘制就可以了

而 Unity 的 ShaderLab 中可以看到有 Pass 块的代码

其实每个 Pass 块的代码都是一个可以用于完整的 SetDrawState, DrawCall 的过程

因为 ShaderLab 中指定了一部分 DrawCall 前的绘制状态的设置配置,如:ZTest,ZWrite,Cull,Blend,ColorMask,Stencil 等,而 Pass 中的 #param vert XXX, #param frag XXX 就是我们的 VS,FS

最简单的理解 SetPassCall :在绘制此 Pass 前,需要设置的所有状态配置、或是BUFFER设置,都算是 SetPassCall 的内容,或是叫:SetGPUDataBeforeDraw 会更适合理解(在绘制前设置GPU数据,这些数据包括渲染系统,如:DX 或是 OpenGL 的状态值,或是 Buffer 数据)

所以 Unity 多了个:SetPassCall

SetPassCall = SetStateBeforeDraw

----

啥叫:Batch

Batch 直译:批量,的意思

----

Dynamic Batch – 动态合批

在 实时渲染 中,以动态合批为例(Dynamic Batch)一般理解为:为了减少 DrawCall,或是减少 SetPassCall 而将绘制时材质一样(或是说 shader + shader 参数 + 绘制前状态,都一样)的 VBO,IBO,等数据打包到一个大的 VBO、 IBO 中,然后在调用一次 DrawCall,从而提升性能:SetPass 的 State 时,或是多次 Draw API 调用产生过多的 CPU 消耗的性能的问题

但是现在在渲染 API 设置中,调用绘制的 API 的消耗远没有设置渲染状态的 API 的消耗大,比如:OpenGL 中的 glDrawElement 之类 API

这些渲染状态相关的 API,在 unity 叫:SetPassCall

所以下面的静态合批是为了减少 SetPassCall 的

详细可以参考 Unity Dynamic Batch 文档:Dynamic batching

----

Static Batch – 静态合批

静态合批 是将在运行前 或是 发布前,将场景中的 相同材质,并且勾上了 Static Batching 的 MeshRenderer 的 VBO, IBO 都直接放到一个巨大的川村中,并将这个缓存存到文件,具体什么文件格式这个 unity 自己定

这个缓存会记录着每一个 渲染对象的 IBO 的范围,然后在遍历每个渲染对象前,先设置他们同一个渲染状态(也就是材质信息要一直的原因),然后再逐个遍历渲染对象的 IBO,再调用类似 glDrawElement 的 API 来绘制即可,绘制前,要判断这个 渲染对象时是否在视锥体内,如果不在,就不绘制。所以静态合批不是减少 DC,而是减少 DrawState 的设置,在 unity 就是减少 SetPassCall 的设置

Unity 还提供了 Runtime 阶段的实时合并API:StaticBatchingUtility

----

GPU Instance – GPU 实例绘制

(另外还有:Instanced 批量(GPU Instancing Batch)绘制,都算是 Batch 的方式)

所以 Batch 的目的是:将原本需要 多次 SetDrawState + 多次 DrawCall,优化为:1次 SetDrawState + 1次 DrawCall

详细可以参考 Unity GPU Instancing 文档:GPU instancing

----

SRP Batcher – Unity SRP(Scriptable Render Pipeline) 的合批

2021/11/8 – 偶然看到自己这篇文章,而且刚刚好之前在研究 URP,了解到 SRP Batcher,所以再添加一些 “Batch” 的说明,这 SRP Batcher 并不是 DC 上的 Batch,而是类似上面 SetPassCall 的 SetRenderState 的 Batch

详细可以参考:Scriptable Render Pipeline Batcher – Unity 官方 SRP Batcher 介绍

另外,可以查看,某乎上钱总的RenderDoc 抓帧分析:从DX角度看SRPBatcher

----

GPU Driven Pipeline

2021/12/3 – 理论还有另一种方式,GPU Driven Pipeline 中的 One DrawCall per Frame(1帧1Draw),我还没去详细了解过,但是啊,想想也是有可能实现的,具体思路:对 object(vertex array, index array, matrix array, etc.) array, material(shader array, buffers array, etc.) array 都创建一个巨大的数组,一次上传到 GPU,然后 GPU 用每个渲染对象对应的 IDX取到对应的 object, material 信息来渲染,所以1帧1Drawcall理论上是可行的

----

下面的伪代码中,具体对应 OpenGL 中的代码,可以我之前写的参考:LearnGL – 02 – DrawTriangle – VBO/Shader – 了解一个三角形如何在 OpenGL 中调用绘制

动态合批伪代码

//(暂时未实现伪代码)

----

静态合批伪代码

// jave.lin 伪代码
// =======================================
// jave.lin : 下面模拟运行签 或是 发布前的数据提取,所以这就是为何 包体变大,和内存变大
// =======================================
// jave.lin : 静态合批的单个绘制对象的存储信息
[Serializable]
class StaticObjInfo
{

public uint startIdx;
public uint endIdx;
public Bounds bounds; // jave.lin : 用于绘制时识别是否在 视锥体 内
}
[Serializable]
class StaticBatchInfo
{

public uint materialGUID;
public List objInfos;
public byte[] vbo;
public byte[] ibo;
}
List renderers;
StaticBatchInfo batchInfo;
var vertexBufferStream = new MemoryStream();
var indexBufferStream = new MemoryStream();
uint startIdx = 0;
foreach (var r in renderers) {

// jave.lin : 先将所有顶点位置都转换到 世界坐标,shader 中就不要使用 mul(o2w, v)
var vertexBuffer = new VertexBuffer(r.sharedMesh.vertexBuffer);
for (int i = 0; i < r.sharedMesh.vertexBuffer.Count; i ++) {

vertexBuffer.pos = mul(r.objToWorldMatrix, r.sharedMesh.vertexBuffer.pos);
}
// jave.lin : 写入 vertex data
vertexBufferStream.WriteVec4(vertexBuffer.pos); // POSITION
vertexBufferStream.WriteVec2(vertexBuffer.ui); // TEXCOORD0
vertexBufferStream.WriteVec4(vertexBuffer.color0); // COLOR0
vertexBufferStream.WriteVec3(vertexBuffer.normal); // NORMAL
vertexBufferStream.WriteVec3(vertexBuffer.tangent); // TANGENT
...
// jave.lin : 记录并写入 index data
startIdx = indexBufferStream.position;
indexBufferStream.WriteBytes(r.sharedMesh.indexBuffer.GetRawBytes());
batchInfo.objInfos.Add(new StaticObjInfo
{

startIdx = startIdx,
endIdx = indexBufferStream.position,
bounds = r.bounds,
});
}
// 设置 vbo, ibo
batchInfo.vbo = vertexBufferStream.GetRawBytes();
batchInfo.ibo = indexBufferStream.GetRawBytes();
// 设置 材质 GUID
batchInfo.materialGUID = renderers[0].sharedMaterial.GetGUID();
// jave.lin : 导出文件,这里也就是为何 unity static batching 多了包体会增大的原因
File.WriteBytes("Jave.Lin:这里填写你的导出路径", batchInfo.GetSerializedBytes());
// =======================================
// jave.lin : 下面模拟运行时的读取与绘制,所以这就是为何 包体变大,和内存变大
// =======================================
batchInfo = StaticBatchInfo.Deserialized(File.ReadBytes("Jave.Lin:这里填写你的读取路径"));
// 遍历前,先设置材质
var mat = AssetMgr.GetMatFromGUID(batchInfo.materialGUID);
glDrawState(mat); // jave.lin : 假设封装了这么个 API 直接 unity 材质中的 shaderlab 对象的 draw state 的配置。一个 set pass call,当然这里还可以优化:如果一个 staticObj 都不在 camera frustum 内,就不用设置 set pass call 的渲染状态
// jave.lin : 遍历前,生成 buffer 信息
uint vbo, ibo;
// jave.lin : 生成 buffer,这里一般生成一次就够了,但是这里伪代码,就随意写在这就好
glGenBuffer(GL_ARRAY_BUFFER, batchInfo.vbo, sizeof(batchInfo.vbo), &vbo, ...);
glGenBuffer(GL_ELEMENT_BUFFER, batchInfo.ibo, sizeof(batchInfo.ibo), &ibo, GL_ELEMENT_UINT, ...);
// jave.lin 绑定 buffer
glBindBuffer(vbo);
glBindBuffer(ibo);
// 然后遍历绘制
var cam = currentCamerma;
foreach (var info in batchInfo.objInfos) {

if (!cam.Visible(info.bounds)) continue; // jave.lin : 不在视锥体内,就跳过绘制
glElement(batchInfo.startIdx, batchInfo.endIdx); // jave.lin : 一个dc
}

----

选择合批的优先级

可以参考:Optimizing draw calls

优先级从上往下(从高到低)

SRP Batcher 和 Static Batch

CPU Instancing

Dynamic Batching

----

References

Draw call batching – Unity 官方 Dynamic/Static Batch

GPU instancing – Unity 官方 GPU Instancing

Scriptable Render Pipeline Batcher – Unity 官方 SRP Batcher 介绍

Batch, Draw Call, Setpass Call – 讲的还不错,但还不够完美

Draw Calls vs Batches optimization? [Unity 5] – 该帖子中的 colin299 用户回答得唯一不对的就是 Batch 的理解

Unity渲染优化的4种批处理:静态批处理,动态批处理,SRP Batcher 与 GPU Instancing – 在 2022/07/15 发现一篇写的不错的文章

URP 系列教程 | 能讲讲如何在 URP 中使用 SRP Batcher 吗?安排上

SRPBatcherProfiler.cs – SRP 在 GameView 下的 statices 数据显示有问题,可以使用这个脚本来替代显示

编程小号
上一篇 2025-03-04 20:06
下一篇 2025-07-03 15:30

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/hz/140101.html