
把一张涂鸦贴到墙上最常见的结果是要么糊成一团要么被模型UV带到墙角里去要么贴完以后整块像反光的贴纸。抛开这些PBR贴花Decal的难点从来不是“贴上去”而是让它在该发光的地方发光、该粗糙的地方粗糙、该被遮挡的地方透出原始材质。这篇文章想聊的是一套我从实际项目里反复打磨过的流程不依赖复杂的插件用引擎自带的投影组件加一个手写的贴花着色器三分钟搭起框架同时把所有PBR通道、遮罩、逐实例变化都留出定制口子。你会看到完整的贴花投影原理拆解、最简Shader逻辑、通道打包方案还有我在项目里踩过的排序、法线、性能这几个真坑。适合刚接触贴花的新手也适合已经用默认贴花插件但觉得不够灵活的人。1. 为什么“三分钟手搓”和“定制级”并不矛盾1.1 直接叠Alpha贴图为什么总觉得“不太对”我见过太多人把“贴花”理解成给模型加一个半透明材质层叠一张带Alpha的贴图上去。结果不外乎三种贴图跟着模型UV走砖墙的接缝处出现明显拉伸因为模型UV被拆得比较碎涂鸦内容“飞”到隔壁墙面贴上去以后在特定光照角度下整块发灰发亮像一块没撕干净的静电贴。这些问题的根源都在于这种方式做的不是贴花而是在做普通的透明纹理映射。你要贴的位置本质上是模型UV决定的而模型UV当初是为了材质采样服务的不是为了“在这里放一张局部贴纸”服务的。贴花Decal真正的底层逻辑是投影用一个Box或者视锥把纹理像放映机一样投射到表面纹理不绑定模型UV而绑定投影体自身的位置和旋转。所以从原理上看贴花天然就适合快速摆放。你不必重新展UV不必改模型只要把一个投影体放到目标位置它就在目标表面上“印”出内容。这让“三分钟”在流程层面完全成立和“定制级”也不冲突。1.2 “定制级”三个字到底指什么很多自带贴花功能或者第三方贴花工具拿到手以后你会觉得效果是有了但是想改一个细节明显控制不了。比如我想让涂鸦只出现在砖缝之间的位置默认贴花做不到我想让贴花的粗糙度和墙面原始粗糙度差异不要太大默认也做不到我想让同一个贴花资源在不同位置随机变个形还是做不到。这些通通属于“定制能力”。我理解的定制级PBR贴花至少包含两层表现层BaseColor、法线、金属度、粗糙度、环境遮蔽这些PBR通道能按需求局部改写并且自然融入原表面控制层不透明度遮罩、边缘衰减、角度限制、逐实例的颜色和缩放都能独立调节。用个场景类比默认贴花是超市里买回来的贴纸贴在墙上能看定制级贴花是给美术准备了一台“贴纸印刷机”印什么图案、胶水粘多牢、纸面颜色怎么变化都由项目需求决定。而实现这台印刷机的成本其实并不高。1.3 三分钟的边界到底在哪里为了避免误导我得说清楚“三分钟”不是指“从零学会贴花渲染原理只用三分钟”也不是“一套PBR资源都不用准备”。它的真正边界是你已经有了一张可用的贴花贴图比如裂缝、污渍、涂鸦的RGBA贴图已经有一个支持贴花投影的渲染管线比如URP/HDRP这类带Decal Projector组件和对应Shader的环境接下来你只需要新建一个投影体、挂材质、摆位置、调参数大约三分钟就能在场景里看到一条可用的裂缝。如果连贴图都没有时间不能算进这套流程里。贴图制作属于美术资源阶段可以用手绘、照片合成、程序化生成去做那是另一套话题。2. 贴花投影的“砖头”结构对象空间投影与PBR通道拆解2.1 投影本质上是把世界坐标“翻译”成贴图UV贴花投影的原理可以用一句话概括每一个投影体就是一个纹理坐标生成器。具体到实现层面大致是这样先拿到场景中某个表面像素的世界坐标把这个世界坐标乘上投影体世界矩阵的逆矩阵得到投影体局部空间坐标局部空间下x和y就是贴图UVz用来判断该点是否在投影体内超出投影体范围的像素直接剔除投影体内部的像素用这个UV去采样贴花纹理。所以贴花的“UV”不是模型给的而是投影体给的。这就是为什么贴花能无视模型拓扑贴在曲面上也不会明显走样——只要投影体包裹住这块区域就行。在常见引擎里这个流程有两种落地方式一种是在屏幕空间做先生成好深度和法线缓冲再对每个屏幕像素重建世界坐标然后反算贴花盒局部UV这种也叫Deferred Decal是目前主流做法另一种是在几何体阶段做直接在顶点/片元阶段处理投影适合不支持G-Buffer的场景。我自己的经验是能用Deferred就尽量用Deferred因为它把贴花和场景的耦合降到最低而且可以借助现成的深度法线缓冲做更自然的混合。2.2 PBR数据通道一张贴花到底要带哪些信息如果只是印颜色那不叫PBR贴花。真正的PBR贴花要把这些数据写进表面材质通道含义常见来源是否建议打包BaseColor基础颜色彩色贴图独立RGB或打包到RGBANormal表面法线细节法线贴图独立贴图Metallic金属度灰度图打包到G或ARoughness粗糙度灰度图打包到BOpacity/Mask不透明度和遮罩Alpha或独立mask打包到A注意一个容易忽略的点粗糙度对贴花视觉效果的影响往往比颜色更大。同样一道墙面裂缝如果裂缝区域粗糙度和砖面不匹配平行光照过去会有一道很突兀的高光看起来像表面贴了一层塑料膜。这比颜色不准更扎眼。2.3 为什么不建议在场景里堆叠过多动态贴花要定制不代表要乱加。动态贴花的开销一般来自五个方面每个受影响像素要额外采样若干张贴图贴花通常处于半透明混合阶段会造成较大OverDraw需要读取深度缓冲、法线缓冲这个操作在部分平台有额外带宽压力投影体数量多起来之后渲染排序和裁剪的复杂度会上升如果每张贴花都单独切换材质Draw Call会成倍增加。所以正确的策略不是“一个裂缝放一个Box”而是“用一张精心制作的贴图资源通过挂多个投影体实例去覆盖一个区域”。后面我会专门讲怎么用实例参数控制变化尽量避免复制一堆材质球。3. 手搓一套PBR贴花生成流程从写Shader到接入引擎3.1 最简贴花着色器长什么样让我直接给你看一份最简化的着色器核心逻辑。这里用的是HLSL风格的伪代码不依赖某个引擎的具体API目的是让你看懂数据是怎么流动的float4 PBRDecal(float3 worldPos, float3 worldNormal) { // 把世界坐标变换到贴花盒局部空间 float3 localPos mul(_DecalWorldToLocal, float4(worldPos, 1.0)).xyz; clip(0.5 - abs(localPos.xy)); // 局部空间的xy直接变成贴图UV float2 uv localPos.xy 0.5; // 采样主要纹理 float4 albedoMask tex2D(_MainTex, uv); float3 normalTS UnpackNormal(tex2D(_NormalTex, uv)); float4 pbrMask tex2D(_PBRMaskTex, uv); // 通道解包G里放金属度B里放粗糙度A里放遮罩 float metallic pbrMask.g; float roughness pbrMask.b; float alpha albedoMask.a * pbrMask.a; // 返回给后续混合管线使用的数据块 return float4(albedoMask.rgb, metallic, roughness, normalTS); }这段代码没有直接做光照计算因为在PBR管线里贴花要做的不是自己发光而是把局部材质属性注入到表面。真正光照还是在底下的PBR像素着色器里统一算贴花只是在它之前把某些通道替换掉。你看着可能觉得短。我的经验是贴花Shader越短越好因为它的执行频率极高、覆盖面积极大每个节点都会被放大到整个投影区域。堆了二十个节点的贴花Shader在高端电脑上看起来还行一上移动端就原形毕露。3.2 接入投影体的关键设置按照标题里的流程现在实际操作三分钟是这样走的在场景里创建一个空物体给它挂上引擎自带的贴花投影组件Decal Projector之类把上面这个Shader做成一个材质赋值给投影组件设置投影体的Box大小让它包裹住目标表面把贴花贴图、法线贴图、PBR通道贴图拖进材质参数调整Rotation让投影方向对准表面Tiling按需缩放把它另存为一个预制件方便后续大量摆放。这里有个小技巧投影体的Scale不要贪大。Scale一旦超过了目标区域你会发现贴花被拉得又大又糊边缘还容易出现“渗色”。正确做法是让Box刚好比目标区域大一点点给边缘衰减留出余量。投影体本身不需要复杂的网格模型它只是一个用于计算投影的包围区域。所以性能上你不用担心这个Box本身有多复杂。3.3 法线贴图的世界空间陷阱这是贴花流程里最值得单独拿出来讲的坑。法线贴图里存的通常是切线空间偏移量这个偏移量只有在模型表面自己的TBN矩阵下才有意义。但贴花投影体是一个完全独立的空间它和模型表面切线空间往往是两回事。如果你直接在贴花Shader里采样法线贴图然后把它当成世界法线或者模型法线去用结果就是贴花区域的光照方向整体错乱墙面的裂缝看起来像浮在表面换个视角就开始“扭动”。正确做法是在贴花Shader里使用贴花盒自身的方向作为TBN基向量把贴花法线从切线空间转到世界空间再与原表面法线做混合。float3 TransformDecalNormal(float3 normalTS) { return normalize(mul((float3x3)_DecalTBN, normalTS)); }这里的关键是_TBN必须来自投影体而不是来自被贴的模型表面。如果你想把事情做得更简单可以烘焙世界空间法线贴图但那样资产在不同方向下复用性很差我不推荐作为通用方案。4. 定制接口的设计让贴花从“样片”变成“定制款”4.1 单通道遮罩让脏迹“断断续续”默认贴花只有Alpha透明度做出来的效果永远是一整块半透明贴纸整齐地盖下去。但现实中的污渍、裂缝、油迹几乎都是一段一段的墙皮脱落一半油渍蔓延到某处自然消失锈迹在细节处渗透进纹理。这些都需要遮罩来控制。我的做法是单独采样一张Mask灰度图或者直接拿贴花纹理的A通道当遮罩再和投影体的边缘衰减、角度衰减相乘float edgeFade 1.0 - smoothstep(_FadeSize, 1.0, max(abs(localPos.x), abs(localPos.y))); float mask albedoMask.a * _MaskTex.a * edgeFade;这样贴花在Box边缘就会自然淡出不会出现一个笔挺的矩形边界。实战里我一般会把_FadeSize控制在0.7到0.9之间太小会露出硬边太大又会让贴花在边界处提前消失看起来内容不完整。4.2 RGBA通道打包一张贴图喂四路数据很多刚接触贴花的人会犯一个“资源洁癖”上的错误给BaseColor多建一张图给金属度多建一张图给粗糙度多建一张图给Mask再多建一张图。结果一个贴花要采样四张贴图性能开销直接翻倍。更合理的做法是打包到一张RGBA图里通道用途备注R脏度/颜色混合强度控制BaseColor和贴花原色混合比例GMetallic灰度BRoughness高值粗糙低值光滑AOpacity/Mask不透明遮罩在图像处理软件里把这四张灰度图合并到一个PNG或TGA里然后在着色器里一次性读取。少一张采样移动端就少一分带宽压力。注意压缩格式的选择ASTC和ETC2下RGB通道的压缩质量一般比A通道略低一些所以如果你特别依赖某一个通道的细节就要把它放到A通道去或者干脆不压缩那一层。4.3 逐实例定制多个投影体共享材质也能互不干扰同一个贴花资源不可能满足场景里所有需求。比如你要铺几十条裂缝有的粗、有的细有的颜色偏深、有的偏浅。如果你每换一种就要复制一份材质球材质数量会爆炸。正确思路是共享Shader和贴图然后用逐实例参数去微调_Tiling控制贴花密度_RoughnessScale控制粗糙度偏移_Color控制整体颜色_Opacity控制整体强度。实际操作中我会把常用的几个变量暴露成一个参数面板。美术想要一条浅色窄裂缝复制一个投影体实例拖一下Scale和颜色就完事了。这比重新做材质快得多也更容易统一风格。我参与过的一个演示项目里里外外铺了上百处贴花实际上Shader只有一个贴图资源也只有三套剩下的全靠实例参数变化。这个习惯让项目后期改起来非常轻松因为你永远知道去哪里调。5. 实战中容易被贴花坑到的几件事5.1 渲染顺序与半透明排序闪烁从哪来绝大多数贴花走的是透明混合阶段这意味着它必须在深度和法线信息准备好之后再渲染。如果你的场景里还有其他半透明物体玻璃、粒子、烟雾贴花和它们就进入同一个渲染顺序。结果有时贴花被玻璃盖住有时又跑到玻璃前面画面就会闪烁。排查起来也很容易把场景里其它半透明物体临时关掉如果贴花静止了那问题就出在排序上。解决思路有几个把贴花Shader的渲染队列挪到固定Pass不和普通半透明物体混在一起在半透明物体渲染之前、深度预读取之后插入一个独立的贴花Pass在支持Renderer Feature的管线里按需要注册一个自定义贴花渲染特性。我印象很深的一次坑一片贴花放在玻璃门口玻璃挡不住泥点泥点却时不时叠到玻璃表面画面看起来像是玻璃在“闪”。后来把贴花Pass单独提前到半透明物体之前这个问题彻底消失。排版顺序这东西看起来不起眼但直接影响画面结果。5.2 投影体的边缘“发毛、渗血”Box边界控制Box贴花经常在边缘看到贴图内容“出血”或者黑色嵌入就像贴纸边缘没有裁干净。原因是投影体局部坐标在边界处UV贴近0或1纹理的重复模式或者mipmap边界会带进来错误像素。常见的对策有三种在Shader里对UV做边缘平滑衰减贴花纹理自带一圈透明描边不要用有损压缩格式去存边缘信息投影体比实际需要的范围缩小一点让边界对应的贴图区域落在透明区。这三条我会同时做而不是只挑一条。因为边缘问题在掠射角下会被放大单靠一个soft参数不够。5.3 移动端性能账单三分钟流程很快但贴花在移动端很容易被滥用。每个贴花本质上是一个大面积半透明片元OverDraw开销非常高。如果一张贴图里还叠加了法线采样、PBR通道采样、遮罩采样那像素成本是成倍增加的。我给移动端项目的经验限制是这样设备档位同屏贴花数量建议贴花贴图格式低端安卓个位数ASTC 6x6或ETC2中端安卓十几到二十几个ASTC 6x6或8x8高端安卓 / iOS几十个以内ASTC 8x8或RGBA16还要配合几个优化手段投影体一旦离开相机视锥就停用同区域贴花尽量共享材质和贴图Atlas尽量避免逐贴花切换材质。这样做完移动端才能扛得住。6. 渲染质量验证我如何检查贴花“接了地气”6.1 肉眼数值双重验证贴花不是贴上去就算完它必须和原表面融合得像“本来长在那里”。我习惯用两种方式验证第一是肉眼观察。放一盏平行光从掠射角看贴花区域。如果法线有问题贴花边缘会出现不自然的高光带如果粗糙度没衔接好表面会像打了蜡一样反光。这个角度通常最容易暴露问题。第二是数值检查。打开管线的调试视图看GBuffer里金属度、粗糙度是否落在预期区间。开启OverDraw调试看贴花区域是否产生过高填充率。我还会通过固定相机位置和角度观察一段时间的帧率曲线确认贴花导致的开销是稳定的而不是偶发跳变。6.2 我的取舍经过这些使用和排查我最终形成了一套比较固定的策略静态场景里的大面积贴花比如墙体裂缝、地面污渍尽量烘焙成静态光照数据而不是实时动态投影。只有玩家可交互的、需要频繁变化的局部细节才用实时贴花。比如溅射的泥点、弹孔痕迹、玩家踩过之后留下的脚印这些东西用实时贴花才有意义。静态场景烘焙掉以后实时贴花的数量压力就小了很多画面稳定性和性能都能兼顾。这是一个很朴素但有效的思路——技术在合适的地方用才能发挥价值。最后分享一个我个人的习惯我在任何一个需要贴花的项目里都会先花半小时把贴花Shader和一个空投影体预制件准备好参数表上只保留“MainTex、MaskTex、RoughnessScale、Opacity”四个常用字段。以后美术进来导入贴图、实例化预制件、调参数五分钟内就能看到一条可用的裂缝或者一处自然的污渍。这套流程从第一次验证到现在我已经用了很久也从我一个人用变成团队的共同流程。