多 Pass Shader:为什么“每个 Pass 都是一个独立 Batch“ 开场一个合批全失效的诡异 Bug小王给场景做了合批优化DrawCall 从 500 降到 100很开心。结果给一批物体换了个描边 Shader带描边效果DrawCall 突然又飙回 400“我明明用了同一个材质、同一个网格按理说该合批成 1 个 DrawCall 才对怎么反而 DrawCall 翻了好几倍Frame Debugger 里看每个物体拆成了好几个 DrawCall这到底是怎么回事”老鸟看了一眼“你这 Shader 是多 Pass的吧一个描边 Shader 通常两个 Pass——一个画描边、一个画本体。每个 Pass 就是一次独立的 DrawCall合批只能合’同一个 Pass’不能跨 Pass 合今天讲清楚为什么每个 Pass 必然是一个 Batch” 第一幕先理解什么是 Pass一个 Shader 可以有多个 PassShader里的结构 Shader MyShader { SubShader { Pass { ... } ← Pass 1 Pass { ... } ← Pass 2 Pass { ... } ← Pass 3 } } ↓ 一个Shader可以包含多个Pass!Pass 是什么Pass 一次完整的渲染流程 → 一次顶点着色 片元着色 → 把物体画一遍 ↓ 多Pass 把同一个物体画多遍, 每遍用不同的设置/效果!生动理解 PassPass 像给一幅画上色的一道工序 画一个描边角色: 工序1(Pass1): 先画一圈黑边(描边) 工序2(Pass2): 再画角色本体盖在上面 ↓ 两道工序 两个Pass → 同一个角色画了两遍!常见的多 Pass 用途多Pass的典型场景 - 描边效果(先画放大的黑色轮廓,再画本体) - 卡通渲染(本体Pass 高光Pass) - 某些透明/特效叠加 - 旧版前向渲染的多光源 (每个光源一个Pass!) ↓ 都是同一物体渲染多遍! 第二幕核心原因——一次 DrawCall 只能执行一个 Pass⭐DrawCall 的本质一次DrawCall CPU告诉GPU: 用这套渲染状态(Shader的一个Pass), 把这批顶点画出来 ↓ 关键: 一次DrawCall只绑定 一套着色器程序(一个Pass)!为什么一次只能一个 Pass一个Pass有自己的 - 顶点着色器程序 - 片元着色器程序 - 渲染状态(混合模式、深度测试、 剔除方向等) ↓ 不同Pass 不同的着色器程序 不同的渲染状态! ↓ GPU一次DrawCall只能用一套! → 换Pass 换程序换状态 必须新开一次DrawCall!所以多 Pass 必然多 DrawCall一个物体有2个Pass Pass1: 用着色器程序A 状态A → DrawCall 1 Pass2: 用着色器程序B 状态B → DrawCall 2 ↓ 同一个物体 2次DrawCall! (画了两遍)生动理解Pass像用不同的笔画同一幅画 Pass1用黑色粗笔画轮廓 Pass2用彩色细笔画细节 ↓ 换笔(换Pass) 一个新动作 → 不可能一笔同时用两种笔! ↓ 两支笔 两个动作(两次DrawCall)! 第三幕为什么合批只能同 Pass 合⭐合批的原理回顾合批(Batching)在做什么 把渲染状态相同的多个物体 → 合成一次DrawCall一起画 ↓ 前提: 它们用同一套渲染状态 (同一个Pass)!跨 Pass 无法合批⚠️ 合批的铁律: 只能合用同一个Pass的物体! 因为不同Pass渲染状态不同, → 无法在一次DrawCall里同时执行! ↓ Pass1的合批 归Pass1 Pass2的合批 归Pass2 → 各合各的,不能跨Pass合!多 Pass 让合批效果减半假设10个物体,每个1个Pass → 理想合批: 1个DrawCall 改成每个2个Pass → Pass1: 10个物体合成1个DrawCall → Pass2: 10个物体合成1个DrawCall → 总共2个DrawCall! ↓ DrawCall直接翻倍(× Pass数量)! → 这就是小王DrawCall暴涨的原因!生动理解合批像把同工序的活儿一起干 10个角色都要画轮廓(Pass1): → 一起画轮廓(合成1次)✅ 10个角色都要画本体(Pass2): → 一起画本体(合成1次)✅ ↓ 但画轮廓和画本体是不同工序, → 不能混在一起干! → 所以至少2次(2个Pass)! 第四幕为什么会是每个物体 × Pass 数量最坏情况连同 Pass 都合不了如果物体之间还不满足合批条件 (材质不同、网格不同等): ↓ 那连同Pass都合不了! ↓ 结果 物体数 × Pass数 全是独立DrawCall! ↓ 这就是小王翻好几倍的极端情况!分情况看 DrawCall 数量10个物体,每个2个Pass 情况A(能同Pass合批): → 2个DrawCall(每Pass合1个) 情况B(不能合批): → 10×2 20个DrawCall! ↓ 多Pass放大了不能合批的代价!生动理解就像每道工序还得一个个物体单独做 不能合批时: 角色1画轮廓、角色1画本体 角色2画轮廓、角色2画本体... ↓ 10个角色 × 2道工序 20个动作! → Pass越多,放大越狠! 第五幕多 Pass 的常见来源容易踩坑的地方来源①显式写的多 PassShader里明确写了多个Pass块 Pass { 描边 } Pass { 本体 } ↓ 这是最直接的多Pass! → 你自己或买的Shader写的来源②前向渲染的多光源⭐⚠️ 大坑: 前向渲染(Forward)中, 多个逐像素光源 多个Pass! Base Pass: 主光 环境光(1个Pass) Add Pass: 每个额外逐像素光源 → 各来1个Pass! ↓ 3个逐像素光照亮一个物体: → 1(Base)2(Add) 3个Pass 3次DrawCall! ↓ 灯光越多,DrawCall越爆!来源③ShadowCaster 等隐藏 Pass阴影投射也是独立的Pass → ShadowCaster Pass → 渲染阴影贴图时执行 ↓ 虽然通常不算在主渲染里, 但也是额外的渲染开销!生动理解光源多 Pass前向渲染多光源像每盏灯重画一遍 一个物体被3盏灯照: → 在灯1下画一遍 → 在灯2下再画一遍(叠加) → 在灯3下再画一遍(叠加) ↓ 灯越多,画越多遍(Pass越多)! → 这是前向渲染的经典性能坑!️ 第六幕怎么应对多 Pass 的开销应对①减少不必要的 Pass✅ 检查Shader是否真需要多Pass ↓ 比如描边: - 多Pass描边(贵) - 后处理描边(全屏一次,可能更划算) - 是否所有物体都需要描边? ↓ 能单Pass实现的效果,别用多Pass!应对②控制逐像素光源数量⭐✅ 前向渲染: 严格控制逐像素灯数量! ↓ - 把次要灯设为Vertex或Not Important (不产生额外Pass) - 只保留必要的逐像素灯 - 或改用延迟渲染(Deferred) → 光源不再产生每物体的额外Pass! ↓ 这是前向渲染最重要的优化点!应对③延迟渲染绕开多光源 Pass延迟渲染(Deferred)的优势 → 光照在屏幕空间统一算 → 不会每个物体每盏灯一个Pass ↓ 多光源场景: 延迟渲染更友好! (但延迟有自己的取舍,如透明物体等)应对④合理利用合批✅ 即使多Pass,也尽量让同Pass能合批: - 统一材质 - 用GPU Instancing - 图集合并 ↓ 至少让每个Pass内部合批, → 把物体数×Pass数压到Pass数×几个批!生动理解优化优化多Pass像减少重复劳动: ① 真的需要画两遍吗?(减Pass) ② 别每盏灯都重画一遍(控灯/延迟) ③ 同工序的活儿尽量一起干(合批) ↓ 核心: 减少重复渲染同一物体的次数!✅ 多 Pass Shader 理解检查清单基础概念 □ 明白Pass是一次完整渲染流程 □ 明白多Pass是同物体画多遍 核心原因 □ 明白一次DrawCall只能执行一个Pass⭐ □ 明白不同Pass 不同着色器不同状态⭐ □ 明白换Pass必须新开DrawCall 合批关系 □ 明白合批只能同Pass合⭐ □ 明白多Pass让DrawCall翻倍(×Pass数) □ 明白不能合批时是物体数×Pass数 常见来源 □ 知道显式多Pass Shader □ 知道前向渲染多光源多Pass⭐ □ 知道ShadowCaster等隐藏Pass 优化 □ 知道减少不必要Pass □ 知道控制逐像素灯数量⭐ □ 知道延迟渲染可绕开多光源Pass 一句话总结多 Pass Shader 为什么每个 Pass 一个 Batch核心原因——一次 DrawCall 本质是CPU 告诉 GPU 用这一套着色器程序和渲染状态把这批顶点画出来而一次 DrawCall 只能绑定一套着色器程序一个 Pass。不同 Pass 有各自不同的顶点/片元着色器程序、不同的混合模式/深度测试/剔除等渲染状态GPU 一次画不可能同时用两套——所以每换一个 Pass就必须重新开一次 DrawCall多 Pass 必然对应多次 DrawCall同一物体被画多遍。这也决定了合批的铁律合批只能合用同一个 Pass的物体无法跨 Pass 合并。所以多 Pass 会让合批效果按 Pass 数量翻倍——10 个物体每个 2 个 Pass最好情况也是 2 个 DrawCall每个 Pass 合一批如果连材质网格都不满足合批条件那就是物体数 × Pass 数全是独立 DrawCall小王 DrawCall 暴涨的极端情况。多 Pass 的常见来源① 显式写的多 Pass Shader如描边描边 Pass本体 Pass② 最容易踩坑的——前向渲染中每个额外的逐像素光源都会给物体添加一个 Add Pass3 盏逐像素灯 Base 2 个 Add 3 个 Pass 画 3 遍③ ShadowCaster 等隐藏 Pass。应对减少不必要的 Pass、严格控制逐像素灯数量次要灯设为 Vertex/Not Important 不产生额外 Pass、多光源场景考虑延迟渲染光照统一在屏幕空间算不再每物体每灯一个 Pass、以及尽量让每个 Pass 内部能合批统一材质、GPU Instancing核心口诀一次DrawCall只能绑一套着色器一个Pass不同Pass不同程序不同状态换Pass必开新DrawCall所以多Pass画多遍多DrawCall合批只能同Pass合不能跨Pass合所以DrawCall按Pass数翻倍常见来源显式多Pass和前向渲染多光源每灯一个AddPass优化减Pass控灯数用延迟渲染让每Pass内部合批 多 Pass 与 DrawCall 关系速查表情况Pass 数能否合批DrawCall 数10物体单Pass同材质1能1合批✅10物体2Pass同材质2能各Pass合2⭐10物体2Pass不能合批2否2010×2⚠️1物体被3逐像素灯照(前向)3(Base2Add)—3画3遍⭐ 一句话记住核心一次 DrawCall 只能执行一个 Pass一套着色器程序一套渲染状态换 Pass 必须新开 DrawCall所以多 Pass 同物体画多遍 多 DrawCall合批也只能同 Pass 合导致 DrawCall 按 Pass 数量翻倍。最常见的隐藏多 Pass 来源是前向渲染的多逐像素光源每盏灯一个 Add Pass。优化核心减少不必要 Pass、控制逐像素灯数量、多光源用延迟渲染