[学习记录]Unity中的绘制API
使用Unity版本:6000.0.43f1
绘制 API 是引擎与 GPU 沟通的桥梁,用于告诉 GPU 如何渲染三维几何体,它们是底层图形API 的调用,绕过了 Unity Renderer 系统。 Unity 不会为这些调用创建或管理 Renderer 对象,渲染逻辑非常“手动”,不经过 SRP Batcher 的自动合批管线。相当于直接告诉 GPU “绘制多少实例”,Unity 不参与 CPU 端的批处理合并。
一.绘制特性解释
我觉得直接上一些名称近似的API进行对比很容易头大,不利于学习和接受。我觉得可以把一些关键特性名词解释一下,尤其是那些用于高性能渲染的,本质上都是 \"Procedural\"、\"Instanced\" 和 \"Indirect\" 这些特性的不同组合。理解这些特性比死记硬背 API 名称更有用。
1.无
这里指DrawMesh等这些不带Insatcned特性,不使用高性能绘制的绘制API。
1.关键词
1对1绘制
DrawMesh核心思想:一次 Draw Call 对应一个网格的渲染。
2.特点
(1)完全由 CPU 指定绘制参数: 调用时,你需要在 CPU 侧(C# 脚本中)明确提供所有绘制所需的信息:要绘制哪个Mesh。这个网格在世界中的 位置 和 旋转。使用哪个Material等。
(2)Draw Call 数量直接与绘制次数挂钩: 如果想用DrawMesh绘制 100 棵树,你将需要调用绘制函数 100 次。这意味着 CPU 会向 GPU 发送 100 个独立的绘制命令(Draw Call)。
(3)无法利用 GPU 实例化: 这是最大的区别,DrawMesh()无法利用 GPU 的实例化能力。GPU 无法在一次操作中处理多个“实例”的数据,因为它每次只被告知要绘制一个独立的网格。
3.适用场景
(1)绘制数量极少的独立对象: 如渲染一个临时的调试框、一个特殊的 UI 元素等。
(2)一次性的、非重复的几何体: 比如在运行时生成了一个独特形状的网格,并且只绘制一次。
(3)需要精确控制某个特定网格的渲染细节: 当需要为一个网格应用非常特殊的材质属性块,或者在渲染管线的特定阶段精确绘制时。
2. Instanced (实例化)
1.关键词
效率
Instanced 核心思想:同一个东西画很多次,但每次可能位置、大小、颜色有点不一样。
2.工作方式
将一个网格发送到 GPU 一次。然后告诉 GPU 要画多少次这个网格。通过一个额外的数据缓冲区(通常是ComputeBuffer或StructuredBuffer)给 GPU 传递每个实例的独特信息,比如每棵树的位置、旋转、缩放,甚至不同的颜色或材质参数。GPU 在一次绘制调用 (Draw Call) 中完成所有这些实例的渲染,而不是一百次独立的绘制调用。
3.特点
(1)大幅减少 Draw Call 数量: 这是最重要的优势,因为 Draw Call 是 CPU 的一个主要瓶颈。
(2)减少 CPU 开销: CPU 不需要为每个实例准备和发送大量重复数据。
(3)提高 GPU 效率: GPU 可以针对重复的网格数据进行优化,如缓存复用。
4.适用场景
渲染大量重复的物体,如草地、树木、石头、砖块、子弹等。
3. Procedural (程序化)
1.关键词
灵活性,GPU 计算能力。
传统绘制方式是把预先定义好的网格(Mesh)数据(包含顶点坐标、法线、UV等)传给 GPU。而 Procedural 核心思想:不需要Mesh 数据,直接在 GPU 里按算法生成顶点。
2.工作方式
不提供Mesh对象给绘制 API。 Shader(特别是顶点着色器)会接收一个顶点 ID(SV_VertexID
) 或 实例 ID(SV_InstanceID
)。在 Shader 中根据这个 ID 和其他传入的参数,计算出每个顶点的最终位置、法线、UV 等所有属性。这意味着几何体的形状完全是由 Shader 中的数学运算和逻辑决定的。你可以绘制点、线或三角形,完全由 Shader 控制。
3.特点
(1)极致的灵活性: 可以生成任何复杂的几何体,从简单的粒子到复杂的无限地形。
(2)CPU 开销极低: CPU 不需要准备和上传大量的顶点数据,所有几何体生成都在 GPU 上完成。
(3)动态几何体: 非常适合渲染那些形状或数量会实时变化的几何体,如粒子系统、流体模拟、Voxel 渲染等。
4.适用场景
(1)GPU 粒子系统: 大量粒子无需 CPU 参与几何体生成。
(2)体素 (Voxel) 渲染: 如 Minecraft 类型世界的动态生成和修改。
(3)程序化地形/云朵: 实时生成细节无限的复杂几何体。
(4)自定义渲染管线: 当你需要完全掌控几何体生成过程时。
4. Indirect (间接)
1.关键词
GPU 驱动
通常,CPU 会告诉 GPU \"要画多少个顶点\" 或 \"要画多少个实例\"。而
Indirect 核心思想:CPU不直接告诉GPU画多少,这个数量信息在 GPU 的一个缓冲区里,GPU直接去那里读。
2.工作方式
CPU 会创建一个特殊的CompueBuffer,通常称为argsBuffer(arguments buffer) 。这个 argsBuffer里存放了绘制命令的参数,比如要绘制的实例数量、要绘制的顶点/索引数量等。
(可选,ComputeShader不是必须使用的,一个ComputeShader可以在 GPU 上执行计算,并写入这些参数到argsBuffer)。当调用Indirect绘制 API 时,GPU 会直接从argsBuffer中读取这些绘制参数,然后执行绘制。
3.特点
(1)真正的 GPU 驱动: CPU 甚至不需要知道最终要绘制多少个对象或顶点,这些都可以在 GPU 上完成计算和决策。
(2)极大减少 CPU-GPU 通信: 对于需要频繁根据可见性、LOD 或其他条件调整绘制数量的场景,Indirect 绘制避免了 CPU 频繁读取 GPU 数据、计算,再发送回 GPU 的开销。
实现高级剔除和 LOD: 例如,可以实现完全在 GPU 上运行的视锥体剔除和遮挡剔除系统。
4.适用场景
(1)大规模场景剔除: 你的场景中有数百万个潜在物体,但只有一部分可见。
(2)动态 LOD (Levels of Detail): 根据距离或性能需求,动态调整绘制的细节级别。
(3)高度动态的粒子系统或几何体: 粒子的存活数量、几何体的复杂程度完全由 GPU 动态决定。
二.绘制方式介绍
1.DrawMesh( ) [普通绘制]
DrawMesh()是 Unity 最基础的绘制函数。
1.特点:
(1)一对一绘制: 每次调用DrawMesh()都会生成一个独立的绘制命令(Draw Call)。
(2)CPU 驱动: 网格数据、变换信息(位置、旋转、缩放)等所有绘制参数都需要在 CPU 端明确指定并传递给 GPU。
(3)没有实例化概念: 即使绘制相同的网格,每次调用也会被视为一个独立的绘制任务。
2.优点:
简单易用,直接。适用于绘制少量、独立的、形状独特的对象。可以精确控制每个网格的渲染细节。
3.缺点:
(1)性能开销大: 每绘制一个对象就会产生一个 Draw Call,当对象数量多时,CPU 负担迅速增加,容易成为性能瓶颈。
(2)CPU-GPU 通信频繁: 每次 Draw Call 都会涉及 CPU 向 GPU 发送指令的通信开销。
4.使用场景:
绘制场景中数量稀少、不重复的独特物体(例如主角、重要的独立建筑)。调试绘制(如 Gizmos)。一次性的、程序化生成的非重复几何体。
2.DrawProcedural() [程序化绘制]
DrawProcedural() 允许你完全在 GPU 上生成几何体。
1.特点:
(1)无需网格数据: 你不向 Unity 提供预定义的 Mesh
对象。几何体(顶点、三角形、法线等)完全在你的 Shader(通常是顶点着色器)中通过数学计算生成。
(2)CPU 驱动绘制数量: 绘制的基元数量(要绘制多少个点、线或三角形)由 CPU 在调用时直接指定。
(3)高度自定义: 允许你对几何体的生成过程拥有极致的控制。
2.优点:
(1)CPU 开销低: CPU 无需准备和传输大量顶点数据,所有几何体生成都在 GPU 上完成。
(2)灵活性高: 适合渲染形状或数量会实时变化的动态几何体,如粒子系统、流体、体积效果等。
3.缺点:
(1)实现复杂: 需要编写更复杂的 Shader 来手动生成几何体属性。
(2)需要手动剔除/LOD: Unity 不会自动进行视锥体剔除或 LOD,需要你自行在 Shader 或 Compute Shader 中实现。
4.使用场景:
绘制大量简单的粒子。程序化生成的动态云朵、草地(如果草叶是简单四边形)。需要高度自定义渲染管线的场景,或几何体在 GPU 上实时生成的场景。
3.DrawProceduralIndirect()[程序化间接绘制]
DrawProceduralIndirect()是DrawProcedural() 的间接版本,结合了程序化和 GPU 驱动。
1.特点:
(1)无需网格数据: 几何体完全在 Shader 中生成。
(2)GPU 驱动绘制参数: 要绘制的基元数量和实例数量等绘制参数是从 GPU 上的ComputerBuffer(argsBuffer)中读取的,而不是由 CPU 直接提供。
(3)真正的 GPU 驱动: 最能体现“GPU 驱动渲染”的概念,CPU 可以完全不了解最终绘制了多少个对象或几何体。
2.优点:
(1)最高性能潜力: 对于海量、高度动态且需要 GPU 实时决策(如剔除、LOD)的场景,能最大限度地减少 CPU-GPU 通信。
(2)实现复杂 GPU 剔除/LOD:ComputeShader可以将可见对象的数量和信息写入argsBuffer,然后 DrawProceduralIndirect()直接读取并绘制。
3.缺点:
实现复杂度最高: 需要了解 Compute Shader 和高级 Shader 编程,以及手动管理剔除和 LOD。
4.使用场景:
GPU 驱动的大规模粒子系统。完全在 GPU 上执行剔除和 LOD 的海量对象渲染(例如,数百万棵树木)。高度动态、数量不确定且需要 GPU 实时计算的程序化内容。
4.DrawMeshInstansed( ) [实例绘制]
DrawMeshInstanced()是 GPU 实例化的基础。
1.特点:
(1)基于网格实例: 绘制同一个Mesh的多个实例。
(2)CPU 驱动实例数据: 每个实例的独特数据(如模型矩阵、颜色等)通常通过数组或ComputeBuffer从 CPU 传递给 Shader。
(3)CPU 驱动实例数量: 要绘制的实例数量由 CPU 直接指定。
2.优点:
(1)大幅减少 Draw Call: 显著降低 CPU 瓶颈,提高渲染效率。
(2)减少 CPU-GPU 数据传输: 网格数据只需传输一次。
3.缺点:
(1)实例数量仍需 CPU 确定: 不适合实例数量高度动态且由 GPU 决定的情况。
(2)单次调用实例数上限(最大1023):DrawMeshInsatnced一次最多只能绘制 1023个实例,如果你的实例数超过这个限制,Unity 会自动拆分成多批(多次调用)来绘制。
比如实例数是 3000,Unity 会分成 3 批(1023 + 1023 + 954)分别绘制。这就导致批次数(Draw Calls)增加。
4.使用场景:
渲染大量相同或相似的静态或低动态物体,如树木、草地、石头、建筑物、人群。实例数量在 CPU 端是已知且稳定的。
5.DrawMeshInstancedIndirect() [间接实例绘制]
DrawMeshInstancedIndirect())
是DrawMeshInstanced()的间接版本,结合了实例化和 GPU 驱动。
1.特点:
(1)基于网格实例: 绘制同一个 Mesh
的多个实例。
(2)GPU 驱动绘制参数: 要绘制的实例数量以及网格的索引/顶点数据偏移等参数是从 GPU 上的ComputeBuffer(argsBuffer) 中读取的。
(3)通常与 ComputeShader 配合: 最常用于 ComputeShader 在 GPU 上进行剔除、LOD 计算后,将最终可见实例的数量写入argsBuffer。
2.优点:
(1)性能极高: 最大限度地减少 CPU 开销,因为实例数量和绘制指令都由 GPU 决定。
(2)实现 GPU 驱动的实例剔除和 LOD: CPU 无需参与判断哪些实例可见。
3.缺点:
实现复杂度较高: 需要编写 Compute Shader 来管理argsBuffer和实例数据。
4.使用场景:
毛发渲染的优秀选择。大规模开阔世界中海量植被、岩石等静态场景对象的渲染。需要根据距离或遮挡动态增减绘制实例的场景。
三.选择使用场景
选择绘制 API 的核心思想:
(1)对象数量
少量且不重复:DrawMesh()。
大量重复: Instanced API。
(2)几何体来源
(1)有现有Mesh: 考虑DrawMesh系列。
(2)完全在 Shader 中生成几何体: 考虑 Procedural API。
(3)绘制参数(数量、位置等)的来源和动态性
CPU 端已知且稳定:DrawMesh(),DrawProcedural,DrawMeshInstanced。
由 GPU 动态计算和决定: 考虑 Indirect API (DrawProceduralIndirect、DrawMeshInstancedIndirect),可以结合ComputeShader(非必须) 。
四.两个主要的绘制类
关于绘制API的调用主要涉及到CommandBuffer和Graphics两个类。
CommandBuffer和Graphics类中的绘制方法在功能和参数上确实非常相似,甚至很多方法名称都一样(例如DrawMesh,DrawMeshInstanced,DrawProcedural等)。
1.核心区别
何时执行 与 谁来执行
尽管两者绘制功能看似相同,但它们最主要的区别在于:
Graphics类:立即执行
何时执行: 当你调用Graphics.DrawMesh或Graphics.DrawMeshInstancedIndirect等方法时,这些绘制命令会立即被发送到 Unity 的渲染管线中,并在当前帧的相应阶段被执行。
谁来执行: 这些命令通常由CPU在主线程的某个Update()或LateUpdate()循环中触发,直接提交给渲染管线。
用途:立即将绘制指令提交给 GPU。调试绘制、一次性绘制,或者在自定义渲染循环中手动控制绘制顺序。最直接的绘制方式,适用于大多数不需要特殊渲染时机的场景。
CommandBuffer类:延迟执行/ 命令队列
何时执行:CommandBuffer并不是立即执行绘制命令的。它是一个命令列表,你可以往这个列表里添加各种渲染命令(包括绘制、清屏、Blit、DispatchCompute 等)。这些命令会在你指定的时间点(通过CameraEvent)被 Unity 的渲染管线执行。
谁来执行:CommandBuffer本身由 CPU 构建,但它内部的命令最终会由Unity 引擎在特定的渲染阶段(CameraEvent指定)自动提交并由GPU执行。
用途:访问渲染管线中间数据: 可以在特定的CameraEvent访问到当前渲染目标(如 G-Buffer 或深度图),并在此基础上进行绘制或计算。
优化: 通过CommandBuffer,可以将多个操作打包在一起,让 Unity 在最合适的时机执行,从而可能减少一些状态切换的开销。
实现自定义渲染效果: 这是CommandBuffer最常见的用途(RenderFeature底层就是借助了CommandBuffer实现自定义渲染效果的插入)。例如,实现反射、折射、体积光、自定义的后处理效果、动态阴影等。你可以在某个渲染阶段插入你的命令,对渲染目标进行修改或绘制额外的几何体。
四.误区注意
使用DrawMesh或DrawMeshInstanced,DrawMeshInstancedIndirect,DrawProcedural 这些 Graphics API 手动绘制的物体,全都不受 SRP Batcher 作用。
1.绘制API不受 SRP Batcher 作用
1.SRP Batcher 是基于 Renderer 组件的渲染批处理系统
SRP Batcher 的核心机制是针对 Unity 的 Renderer(如 MeshRenderer)组件做的 CPU 优化。
它依赖 Unity 内部管理的渲染队列、材质实例信息和 Renderer 的生命周期。SRP Batcher 会自动批处理同类型 Renderer,减少 CPU 负载。
2. DrawMeshInstanced 等 API 是直接向 GPU 发送绘制命令,不经过 Renderer 管理
这些方法是底层 Graphics API 的调用,绕过了 Unity Renderer 系统。
Unity 不会为这些调用创建或管理 Renderer 对象,渲染逻辑非常“手动”,不经过 SRP Batcher 的自动合批管线。相当于直接告诉 GPU “绘制多少实例”,Unity 不参与 CPU 端的批处理合并。
3. SRP Batcher 的工作机制依赖于 Shader 和材质数据的统一布局
SRP Batcher 通过对比 Renderer 的材质属性布局,快速合批。
但DrawMeshInstanced等调用,是我们自己管理材质属性、Buffer,Unity 不会帮你做材质属性的合批。因此,SRP Batcher 无法自动合并这些命令。
留白-未来有机会继续补充~(*-^)~
本篇完