资讯动态

Quality.TextureStreaming、异步Task、SRPBatch

发布时间:2026/10/27 9:23:13 来源:尧图企业网站定制
1.TextureQuality1.1 TextureQuality 的作用Quality 设置里的 Texture Quality 只对开启了 mipmap 的贴图生效具体表现如下若贴图没有开启 mipmap则无论是否设置该属性贴图都以全分辨率渲染。若贴图开启了 mipmap则贴图才会以该选项设置的分辨率渲染。该选项表示开启 mipmap 之后渲染时允许使用的最大 mip level。level 越大(指的是枚举值Full Res 是0)代表采样的是分辨率更低的 mip 层级因此画面越模糊。需要区分的是Texture Quality 控制的是渲染时使用的 mip 层级上限而下一节的 Texture Streaming 控制的是加载哪些 mip 层级两者分工不同不要混淆。2. Texture Streaming为 mipmap 而生的显存管理机制Texture Streaming 同样是围绕 mipmap 设计的。它的核心目的是通过Memory Budget和Max Level Reduction两个参数来节省 CPU 和 GPU 资源本质上控制的是加载 mipmap 的哪些层级也就是决定一张贴图在运行时究竟把哪几级 mip 真正加载进显存。其中Memory Budget 只是一个阈值。当所有开启了 mipmap 的贴图所占用的内存超过这个阈值之后引擎就会在加载这些贴图时按照 Streaming 的策略丢弃部分高精度的 mip 层级只保留较低分辨率的层级从而把显存占用压回预算之内。对于贴图占用内存很大的游戏来说这个选项非常有用可以在几乎不影响观感的前提下显著降低显存压力。通过该选项还可以直观地查看当前贴图 mipmap 的加载情况判断哪些贴图被降级、哪些仍然保持全分辨率方便定位显存占用异常的问题。关于 Streaming Mipmap 的实际表现也可以参考这篇讨论【厚积薄发】使用 Streaming Mipmap 后纹理内存没有下降的疑问。3. C# Task参考资料使用 Async 和 Await 的任务异步编程 (TAP) 模型 (C#) | Microsoft Docs用async修饰的方法即为异步方法这类方法通常包含一个或多个由await修饰的调用如果方法体内没有任何await它实际上仍是同步执行的。反过来同步方法则不需要async修饰。需要明确的是async与await组合实现的异步编程并不会额外开启一个新线程。下面梳理几个容易混淆的要点关于Task.Run()它适合用来执行计算量较大的工作这部分逻辑会在后台线程中运行。关于await它表示等待在返回结果之前无法访问该结果。Task与await是两件事Task表示任务的启动可以同时启动多个任务这正是异步的意义所在——它不会阻塞线程而不是等一个任务完成后再启动下一个。await等待的是Task的结果你启动的任务可能很早就已经完成此时await只是在你真正需要用到结果时取回它。此外它也可以用来替代协程。4. Stopwatch 类Stopwatch 类 (System.Diagnostics) | Microsoft Docs用来计时用的5. SRPBatch参考资料从DX角度看SRP Batcher通过开启该选项来降低 CPU 的负载原理就是把材质球一些不变的属性缓存到 GPU 上只有在改变的时候CPU 才会重新收集这些发送给 GPU。5.1 为什么需要 SRP Batcher在传统渲染流程里CPU 每绘制一个物体都要把这一帧需要的材质属性、变换矩阵、光照参数等数据重新整理一遍再通过 SetPass Call 提交给 GPU。当场景里的物体数量很多、材质种类又比较杂时这部分 CPU 开销会非常可观往往成为 Draw Call 之外的另一大瓶颈。SRP BatcherScriptable Render Pipeline Batcher是 Unity 在可编程渲染管线URP/HDRP下提供的批处理优化手段。它并不减少 Draw Call 的数量而是减少每次绘制时 CPU 重新收集和上传材质数据的开销从而把 CPU 从重复的数据搬运中解放出来。5.2 核心原理把不变的属性常驻在 GPU 上SRP Batcher 的关键思路是「数据常驻 按需更新」常驻显存把材质Material上那些在一帧内不会变化的属性例如颜色、贴图引用、浮点参数等预先组织成一块结构化的数据缓存在 GPU 的常量缓冲区Constant Buffer中。只在变化时更新只有当材质属性真正发生改变时CPU 才会重新收集这部分数据并发送给 GPU属性没变就直接复用显存里已有的那份数据。批量提交由于同一 Shader 变体下的材质数据在显存中按固定布局连续排列CPU 可以把多个物体的绘制命令合并成一批提交GPU 侧按偏移量直接读取对应数据省去了逐物体绑定材质的过程。换句话说SRP Batcher 把「每帧重复上传」变成了「一次上传、多次复用」CPU 端的 SetPass 开销因此大幅下降。5.2.1 用一个具体例子理解「按偏移量读取」假设场景里有 10 个物体它们用的是同一个 Shader、同一个 Shader 变体关键字组合完全一致只是各自的材质参数不同比如颜色、粗糙度、贴图引用不一样。开启 SRP Batcher 后这 10 份材质数据会被排成一块连续的内存可以理解为下面这样常量缓冲区按 Shader 变体划分的一块连续显存 -------------------------------------------------- | 偏移 0 | 物体 A 的材质数据颜色、粗糙度、贴图引用… | | 偏移 256 | 物体 B 的材质数据 | | 偏移 512 | 物体 C 的材质数据 | | ... | ... | | 偏移 2304 | 物体 J 的材质数据 | --------------------------------------------------每个物体的数据块大小是固定的由 Shader 里CBUFFER的布局决定所以第 N 个物体的数据起始位置就是N × 块大小。CPU 提交这一批绘制时只需要告诉 GPU「这批用同一个 Shader 变体、常量缓冲区基址是这块、每个物体的偏移分别是多少」GPU 在着色时按偏移量去读对应那一段即可不需要每个物体都重新绑定一次材质。这里要特别注意变体不一样就不能合并。如果这 10 个物体里有一个用了「开启阴影」的变体、另一个用了「关闭阴影」的变体它们编译出来的 GPU 程序不同CBUFFER布局也可能不同就无法共用同一块常量缓冲区只能拆成不同的批次。所以「同一个 Shader」还不够必须是「同一个 Shader 变体」。5.2.2 材质数据和常量缓冲区里的数据是一回事吗这是很容易混淆的一点需要拆开看材质数据是「源头」指的是 Material 上设置的那些属性值比如_BaseColor、_Smoothness、贴图引用等它们平时存在 CPU 侧的内存里。常量缓冲区是「搬运后的落脚点」CPU 把这些属性按 Shader 里CBUFFER声明的顺序和类型打包成一块二进制数据上传到 GPU 的常量缓冲区。GPU 读的是这块缓冲区而不是 CPU 侧的 Material 对象。不是所有材质数据都会进常量缓冲区贴图本身Texture2D不会塞进常量缓冲区常量缓冲区里放的只是贴图的引用/句柄以及采样参数真正的大块贴图数据走的是纹理采样通道。所以更准确的说法是材质数据经过打包后被写进了常量缓冲区。常量缓冲区是材质数据在 GPU 侧的一份「快照」布局由 Shader 的CBUFFER决定SRP Batcher 正是靠这份固定布局才能按偏移量寻址。5.2.3 GPU 的常量缓冲区在显存里吗是的常量缓冲区本质上就是显存VRAM里的一块缓冲区只是它的用途和访问方式比较特殊物理位置常量缓冲区由图形 APIDX11/DX12、Vulkan、Metal 等分配最终落在 GPU 可访问的显存中CPU 通过映射map或拷贝的方式把数据写进去。和纹理、顶点缓冲区的区别它们都在显存里但用途不同。纹理走采样单元顶点/索引缓冲区喂给输入装配阶段常量缓冲区则是给 Shader 提供 uniform 参数通常有大小限制比如单个 CB 一般不超过 64KB。「常驻」的含义SRP Batcher 说的常驻是指这块缓冲区在材质属性不变时不会被反复重写而不是说它不在显存里。它一直在显存里待着CPU 只在数据变脏时才更新对应片段。可以这样记显存是「地方」常量缓冲区是「这个地方里的一块专用区域」材质数据是「被搬进这块区域的内容」。三者是位置、容器和内容的关系不是并列关系。5.2.4 不用 SRP Batcher 时CPU 每帧还要收集数据吗要而且这正是传统路径的主要开销来源。Draw Call 每帧都会产生即使材质属性一个都没改CPU 在准备绘制时仍然要做这些事逐物体整理参数把该物体这一帧需要的材质属性、变换矩阵、光照参数等收集起来打包成 GPU 能读的格式。逐物体上传通过SetPass Call或逐物体的常量更新接口把这些数据提交给 GPU切换材质时还要重新绑定。状态切换开销材质种类越多、切换越频繁这部分开销越大往往比 Draw Call 本身还贵。也就是说「属性没变」并不等于「CPU 不用干活」。传统路径下CPU 每帧都要为每个物体重新走一遍收集和上传流程只是数据内容相同而已SRP Batcher 的价值就在于把这部分重复劳动省掉——属性没变就跳过收集和上传直接复用显存里已有的那份数据。反过来说如果属性每帧都在变比如每帧改颜色做动画脏标记会一直被置位SRP Batcher 就退化成了「每帧都要重新上传」优化效果自然大打折扣。这也是为什么实践建议里会提到频繁改属性的逻辑要考虑换一种实现方式。5.3 生效条件SRP Batcher 并不是无条件生效的需要同时满足以下条件使用 SRP 渲染管线URP 或 HDRP内置渲染管线不支持。在管线资源URP Asset / HDRP Asset中勾选SRP Batcher选项。Shader 必须兼容 SRP Batcher即所有材质属性都声明在统一的CBUFFER常量缓冲区中且布局一致。自定义 Shader 若未按规范声明会退化为普通渲染路径。参与批处理的物体需要共享同一个 Shader 变体不同变体之间无法合并。5.4 与 Static Batching、Dynamic Batching、GPU Instancing 的区别方式优化目标适用场景Static Batching合并静态物体的 Draw Call不移动的场景物件Dynamic Batching合并小网格的 Draw Call顶点数很少的动态物体GPU Instancing一次提交绘制多个相同网格大量重复模型如草、树SRP Batcher降低材质数据上传与 SetPass 开销材质种类多、Shader 变体统一的场景需要注意SRP Batcher 与 GPU Instancing 是互补关系Instancing 解决「同一网格大量重复」的问题SRP Batcher 解决「不同材质频繁切换」的问题两者可以同时开启。5.5 实践建议优先在 Frame Debugger 中确认 SRP Batcher 是否真正生效观察SRP Batch与SetPass Calls两个指标的变化。如果发现某个材质没有进入批处理多半是 Shader 的CBUFFER声明不规范或使用了不被支持的属性类型。对于频繁修改材质属性的逻辑例如每帧改颜色做动画可以考虑改用 MaterialPropertyBlock避免破坏批处理。SRP Batcher 优化的是 CPU 侧开销如果瓶颈在 GPU填充率、Overdraw开启它并不会有明显收益。5.6 常见疑问解答下面把几个容易混淆的问题单独拆开说明方便对照理解 SRP Batcher、Shader 变体、MaterialPropertyBlock 与 GPU Instancing 之间的关系。5.6.1 CPU 怎么知道材质属性有没有变化不是靠“每帧打标记”来判断的而是靠属性写入时的脏标记dirty flag。以 Unity 为例材质属性的修改最终都会走到类似Material.SetFloat、SetColor、SetTexture这样的接口这些接口在写入属性值的同时会把该材质标记为“已修改”。渲染线程在准备这一帧的数据时只需要检查这个标记标记为已修改重新把该材质的属性打包进常量缓冲区并上传给 GPU。没有标记直接复用 GPU 上已有的那份数据跳过收集和上传。所以这里的“变化”指的是属性值被真正写入过而不是每帧去逐字段比较。也正因为如此如果每帧都调用SetColor做颜色动画即使颜色值没变也会被判定为“脏”从而破坏 SRP Batcher 的复用效果——这也是实践建议里提到“频繁改属性考虑用 MaterialPropertyBlock”的原因。5.6.2 什么算 Shader 变体是 Shader 里的 if else 分支吗不是运行时执行的if else分支而是编译期由关键字Keyword和宏组合出来的不同编译产物。一个 Shader 源码里通常会有大量#pragma multi_compile或#pragma shader_feature声明例如是否启用阴影、是否启用雾效、是否使用法线贴图等。Unity 在打包或运行时会针对这些关键字的每一种组合分别编译出一份 GPU 程序每一份就是一个 Shader 变体Shader Variant。Shader 变体同一份 Shader 源码因关键字组合不同而编译出的不同 GPU 程序。Shader 实例运行时由材质创建出来的、绑定了一组具体属性值的对象它引用某个 Shader 变体。对 SRP Batcher 来说只有引用同一个 Shader 变体的材质才能被合并到同一批里不同变体意味着 GPU 程序不同自然无法共用同一套常量缓冲区布局。5.6.3 MaterialPropertyBlock 的作用是什么MaterialPropertyBlock简称 MPB是一个不修改材质本身、只覆盖单次绘制属性的容器。它的典型用途是多个物体共享同一个 Material但每个物体需要不同的颜色、缩放或偏移等参数。如果直接改material.colorUnity 会为这个渲染器复制出一份新的材质实例Material Instance既增加内存又会让这些物体不再共享同一份材质数据从而破坏合批。而用 MPB 时属性覆盖只作用于这一次绘制原材质保持不变。// 共享同一个材质但每个物体用不同的颜色 public class ColorVariation : MonoBehaviour { private static readonly int BaseColorId Shader.PropertyToID(_BaseColor); private MaterialPropertyBlock _block; private void Awake() { _block new MaterialPropertyBlock(); } private void Start() { var renderer GetComponentlt;Renderergt;(); renderer.GetPropertyBlock(_block); _block.SetColor(BaseColorId, Random.ColorHSV()); renderer.SetPropertyBlock(_block); } }关于“不同物体的 MPB 属性值不一样算不同变体、不同 Shader还是相同 Shader 所以能合批”可以这样理解不算不同 Shader 变体MPB 只改属性值不改关键字Shader 变体仍然是同一个。不算不同 Shader 实例材质本身没有被复制仍然是共享的那一份。但会打断 SRP Batcher 的批处理SRP Batcher 依赖“材质属性常驻在常量缓冲区”这一前提而 MPB 的属性是逐绘制单独传入的走的是另一条数据通道。因此使用 MPB 的渲染器通常会被排除在 SRP Batcher 之外转而依赖 GPU Instancing 或 Dynamic Batching 来合批。换句话说MPB 解决的是“共享材质 个体差异”的问题代价是放弃 SRP Batcher 的复用如果这些物体本身网格相同、数量很大用 GPU Instancing 配合MaterialPropertyBlock反而是更合适的组合。5.6.3.1 MPB 的原理它到底把数据存在哪里要理解 MPB 为什么“不脏材质”先要看清它的数据归属。MPB 本质上是一个挂在 Renderer 上的属性覆盖表而不是材质的一部分数据存在 Renderer 侧调用renderer.SetPropertyBlock(block)时MPB 里的键值对会被拷贝并绑定到这个 Renderer 上Material 对象本身完全不受影响material的脏标记也不会被置位。绘制时临时合并真正提交绘制时引擎会以 Material 的属性为底再把该 Renderer 的 MPB 覆盖项叠加进去形成这一次绘制专用的属性集合。生命周期跟随 RendererMPB 的覆盖只在 Renderer 存活期间有效Renderer 被销毁或重新SetPropertyBlock后旧覆盖即失效。所以 MPB 并不是“偷偷改材质”而是在材质之外多了一层逐物体的属性覆盖。这也解释了为什么它不会触发材质实例化材质始终只有一份。5.6.3.2 既然不脏材质CPU 还会收集数据吗会但收集的对象和时机与常规材质完全不同。可以分两种情况看常规材质路径属性写入时给 Material 打脏标记渲染线程准备这一帧时检查标记脏了就重新打包整块常量缓冲区并上传 GPU不脏就复用显存里已有的数据。MPB 路径材质不脏所以材质那份常量缓冲区不会重新收集、也不会重新上传但 MPB 的覆盖值仍然需要在每次绘制时被读取并传给 GPU这部分数据是逐绘制单独提交的走的是另一条通道通常是逐实例/逐绘制的常量数据或 uniform 更新而不是材质常量缓冲区。换句话说MPB 省掉的是“材质数据的重复收集与上传”并没有省掉“把个体差异送到 GPU”这一步。差异数据该传还是得传只是它不再污染共享材质也不会让所有使用该材质的物体一起被判定为脏。这里还有一个实践上的细节不要在每帧循环里反复 new 一个 MPB。推荐复用一个成员变量先GetPropertyBlock拿到当前值再修改需要的字段最后SetPropertyBlock写回避免覆盖掉其它系统已经设置过的属性。5.6.3.3 和 SRP Batcher 的关系为什么用了 MPB 反而更慢这正是很多人困惑的地方MPB 明明避免了材质实例化为什么在开启 SRP Batcher 的项目里反而可能更慢原因在于两者优化的不是同一段开销对比项SRP BatcherMaterialPropertyBlock优化目标减少材质常量缓冲区的重复收集与上传避免材质实例化实现共享材质下的个体差异数据存放位置GPU 常量缓冲区按材质常驻Renderer 侧覆盖表逐绘制提交是否打断批处理本身即批处理机制会将该渲染器排除在 SRP Batcher 之外适用场景材质种类多、属性基本不变共享材质但每个物体参数不同所以选择逻辑可以归纳为属性基本不变优先让材质走 SRP Batcher不要用 MPB 去“优化”否则等于主动放弃批处理。属性需要逐物体不同且数量不大用 MPB 比复制材质实例更划算至少省下了材质内存和实例化开销。属性逐物体不同且数量很大、网格相同用 GPU Instancing把差异放进实例缓冲区一次提交完成比逐个 SetPropertyBlock 更高效。属性需要每帧动画先评估是否能用 Shader 里的时间参数或顶点动画实现避免 CPU 侧每帧写属性。一句话总结MPB 不脏材质所以材质那份数据不会被重新收集上传但个体差异数据依然要逐绘制送到 GPU它换来的是“共享材质 个体差异”的灵活性代价是放弃 SRP Batcher 的复用。用不用它取决于你更在意材质内存还是更在意 CPU 的批处理效率。5.6.4 GPU Instancing 的原理是什么优点只是合批吗GPU Instancing 的核心是一次 Draw Call 提交多个相同网格的实例。CPU 只发一条绘制命令同时把每个实例的差异数据变换矩阵、颜色等放进一个实例化缓冲区Instance Buffer一起传给 GPUGPU 在顶点着色器阶段根据SV_InstanceID取出对应实例的数据分别完成每个实例的顶点变换和着色。// 开启 GPU Instancing 的材质配合 Graphics.DrawMeshInstanced 批量绘制 public class InstancedDraw : MonoBehaviour { public Mesh mesh; public Material material; // 需在 Inspector 中勾选 Enable GPU Instancing public int count 1000; private Matrix4x4[] _matrices; private void Start() { _matrices new Matrix4x4[count]; for (int i 0; i lt; count; i) { _matrices[i] Matrix4x4.TRS( new Vector3(i % 50, 0, i / 50), Quaternion.identity, Vector3.one); } Graphics.DrawMeshInstanced(mesh, 0, material, _matrices, count); } }它的优点不只是“合批”具体包括大幅减少 Draw Call上千个相同物体可以压缩到一次提交CPU 侧提交开销显著下降。减少状态切换同一批次内材质和 Shader 不变SetPass Call 次数随之减少。数据上传更紧凑实例差异数据集中放在一块缓冲区里连续上传比逐个物体设置属性更高效。GPU 侧并行度高每个实例的顶点处理相互独立能充分利用 GPU 的并行能力。需要注意的是GPU Instancing 要求网格相同、材质相同同一个 Shader 变体差异只能通过实例数据表达如果每个物体的网格或材质都不同就无法实例化。它与 SRP Batcher 并不冲突Instancing 解决“同网格大量重复”SRP Batcher 解决“不同材质频繁切换”实际项目中常常同时开启由引擎按场景自动选择更合适的路径。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑