Unity Shader Graph自定义贴花与贴纸投影实现

发布时间:2026/10/17 14:27:57
Unity Shader Graph自定义贴花与贴纸投影实现
贴花Decal是游戏渲染里非常常见的一种效果弹孔、脚印、路面标志、墙面涂鸦、角色身上的贴纸几乎都能用贴花技术实现。很多同学一遇到“贴花”就会想到 Unity 的 Decal Projector 组件但在实际项目中组件的使用场景、渲染顺序、材质配置往往有很多限制遇到不满足需求时还是要回到 Shader 层面自己控制。这篇文章我会从图形学原理开始带你在 Unity 的 Shader Graph 里一步步实现一套自定义贴花与贴纸方案让纹理可以投影到任意曲面和物体上。整个方案不绑定任何内置 Decal 组件理解之后你可以自由扩展成弹孔、喷漆、脚印、贴纸等效果。文章会覆盖几个部分先讲清楚贴花的底层原理和常用分类再梳理 Unity 中 Shader Graph 的环境准备接着拆解投影、深度、法线这些核心概念然后给出一套完整的实战流程包含 Shader Graph 节点连接和配套 C# 脚本最后补充边缘柔化、曲面贴合、排错思路和工程建议。适合有一定 Unity 基础、想深入渲染细节的开发者也适合刚接触 Shader Graph 的新手照着做一遍。1. 背景与核心概念1.1 什么是贴花贴花英文叫 Decal本质上是一张贴图但它的呈现方式和普通材质不同。普通材质是“贴在模型表面上跟着模型 UV 走”而贴花更像是“从某个虚拟投影器投射到模型表面的一层图案”。一个典型的例子是墙上的弹孔弹孔的纹理并不是墙体材质的一部分它需要叠在墙面上并且随着观看角度变化保持正确的透视关系。如果用普通材质给墙面贴一张弹孔图图案会随着墙面 UV 拉伸而变形无法做到“任意位置出现一个圆形弹孔”的效果。更准确的描述是贴花是一个处于三维空间中的投影体它把一张纹理沿某个方向投射到场景物体表面然后再把投射结果叠加到原物体颜色上。这个“投影体”可以是一个长方体、球体、圆柱体也可以只是一个方向向量加上一个距离范围。Shader 要做的事情就是判断当前像素点是否落在投影体内、计算出该像素点在贴花纹理上的 UV 坐标、再根据深度和法线信息决定是否显示以及显示成什么样。1.2 贴花的常见分类从实现方式上贴花大体可以分为三类。网格贴花Mesh Decal把一个带有贴花材质的平面或薄片网格紧贴在目标物体表面。这种方案实现简单性能开销低但需要把网格贴合到曲面上复杂曲面会出现穿插、拉伸问题。适合静态场景里少量固定位置的标记。投影器贴花Projector Decal传统 Unity Projector 组件的方式通过一个投影矩阵把贴图沿某个方向投射到物体上。实现直观但是早期 Projector 在部分渲染管线下存在穿透、排序、阴影交互问题需要做大量额外处理。屏幕空间贴花Screen Space Decal这是现代引擎里使用最多的方案。相机渲染完场景后通过深度纹理重建世界空间位置再判断像素是否在贴花投影体内最后将纹理叠加上去。它的优点是不依赖目标模型的 UV 和网格结构可以“无差别”贴在任意物体表面非常适合弹孔、血迹、喷漆等效果。缺点是依赖深度纹理对透明物体和半透明物体支持有限需要额外处理。本文要实现的 Shader Graph 自定义贴花核心思路接近于屏幕空间贴花与投影器贴花的结合我们借用深度信息来获取物体表面位置用自己的投影矩阵来决定贴花的位置和方向最终在 Shader Graph 里完成 UV 计算和纹理混合。1.3 为什么要自己实现直接用 Unity URP 自带的 Decal Projector 确实方便但很多项目会遇到下面这些情况需要贴花和贴纸在 UI 层、特效层、角色身上同时出现而 URP 的 Decal 功能一般用于不透明几何体表面透明物体、Skinned Mesh 的场景需要额外适配。需要完全控制贴花混合逻辑、边缘过渡、扭曲变形、法线混合方式。需要兼容 Built-in 管线或者需要在一套自定义渲染框架里控制贴花渲染顺序。Built-in Render Pipeline 没有官方 Decal Projector很多老项目只能自己写。自己用 Shader Graph 做的好处是逻辑透明、依赖少。理解了核心原理后不管未来 Unity 怎么更新你都能在 Shader 层面快速实现需要的变化。2. 环境准备与版本说明2.1 Unity 与渲染管线选择本文示例基于 Unity 2021.3 LTS 或 Unity 2022.3 LTS渲染管线建议使用 URPUniversal Render Pipeline。因为 URP 提供深度纹理、法线纹理的获取方式并且 Shader Graph 在 URP 下的表现和性能都更可控。需要说明的是Shader Graph 的节点在不同 Unity 版本里会有细微差别。比如节点名称可能在 17.0 之后从 “Screen Position” 到 “Position” 的映射不同但核心逻辑一致。如果你使用的是 Unity 6 或更高版本节点位置和名称可能略有变化请以你编辑器里实际搜索到的节点为准。如果你的项目是 Built-in 管线思路同样适用只是获取深度、法线的方式需要改成对应的内置节点或采样深度纹理本文以 URP 为主展开。2.2 依赖包版本在 Package Manager 中需要确认安装了以下包Universal RP用于提供 URP 渲染环境。Shader Graph创建和编辑 Shader Graph 资源。如果没有 URP 的 Decal 需求不需要额外启用 Decal 功能我们会自己实现投影逻辑因此不必依赖 URP Decal 的“DBuffer”渲染流程。在 Project Settings 中建议做两件事把项目的渲染管线设置为 URP Asset。在 URP Asset 的 Depth Texture 选项上勾选 Depth Texture确保场景物体能被记录到深度纹理中。勾选深度纹理的原因是屏幕空间贴花需要读取场景深度来重建像素在世界空间的位置。如果深度纹理没有开启Shader 采到的深度数据会是空值或默认值贴花就无法正确显示。2.3 示例项目结构为了下面实战环节不混乱建议按这样的目录结构组织资源Assets/ ├── Scenes/ │ └── DecalDemo.unity ├── Shaders/ │ ├── DecalShader.shadergraph │ └── DecalHlsl.hlsl ├── Textures/ │ ├── DecalAlbedo.tga │ ├── DecalNormal.tga │ └── DecalMask.tga ├── Scripts/ │ └── DecalProjectorHelper.cs └── Materials/ └── DecalMaterial.mat这里把 HLSL 文件提前放好是为了给 Shader Graph 的 Custom Function 节点提供自定义函数。环境版本不需要追求最新稳定的 LTS 版本更适合学习。3. 贴花投影的核心原理要理解 Shader Graph 里那些节点在干什么建议先掌握三个核心概念三维到二维的投影坐标计算、深度重建位置、法线参与筛选。3.1 三维到二维的投影一张贴花纹理是二维的它有 U 和 V 两个坐标。要让纹理出现在三维物体的表面我们必须把物体表面的三维坐标转换成二维坐标。最直观的方式是“平面投影”选定贴花投影的方向比如从贴花投影器的 z 轴方向往外投射。对于任意一个三维点 P它在贴花局部空间下的坐标是 (Px, Py, Pz)其中 Px、Py 可以作为纹理的 U、V 坐标Pz 则用来判断该点是否在投影器的有效范围内。这句话非常关键贴花纹理的 UV 并不是模型的 UV而是“贴花空间下的相对位置 UV”。这也是贴花能做到不拉伸、不依赖模型 UV 的原因。在 Shader Graph 中我们可以把世界空间位置变换到贴花局部空间然后用局部空间的 X、Y 去采样纹理用局部空间的 Z 做范围裁剪。如果你想让贴花像一个垂直于墙面投射的圆形弹孔那么贴花的局部空间 Z 轴应该指向墙面方向墙面上的点投影到贴花空间里XY 正好构成圆的坐标。3.2 深度与法线的作用有了投影矩阵我们还需要知道“应该往哪个表面投影”。屏幕空间贴花常用的方法是在当前像素处通过深度纹理还原出该像素对应的世界空间位置。这个位置代表相机看到的最外层物体表面而不是场景中任意的透明物体或后侧面。然后再把这个表面位置放到贴花局部空间里判断。所以流程变成从深度纹理取出当前屏幕上该像素的深度值。由深度值、屏幕坐标、相机参数反推出世界空间坐标。将世界空间坐标变换到贴花空间。如果该点在贴花范围内就采样贴花纹理并叠加到原颜色上。法线信息的作用则是“朝向筛选”。一片贴花投影器通常只能影响面向贴花方向的表面。如果一个物体表面背对着贴花投影方向就不应该被贴上图案。比如从正面投射一个“禁止停车”标志物体侧面就不应该出现图案。通过对比表面法线和贴花投影方向我们可以用点积结果控制贴花的透明度实现这种“只贴正面”的效果。3.3 Shader Graph 里的数据流如果你习惯写 HLSL上面这套逻辑其实就是几行代码的事。但在 Shader Graph 里它变成了若干节点之间的连接。整个数据流可以整理成下面这样屏幕坐标 → 场景深度节点 → 重建世界位置 世界位置 → 变换到贴花空间 → 位置分量拆分 贴花空间 XY → 贴花 UV → 采样纹理 贴花空间 Z → 范围裁剪 世界法线 → 点积投影方向 → 朝向裁剪 最终纹理颜色 × 裁剪结果 → 叠加到 BaseColor理解了这个数据流再去看节点连接图就不会觉得 Shader Graph 是一堆线乱飞。你只需要把“世界位置”、“深度”、“贴花空间变换”、“法线点积”这几个关键点串起来就能实现 90% 的贴花效果。为了让这个思路更直接我们可以在 Shader 里用一个 Custom Function 节点来封装核心变换逻辑C# 侧只负责提供贴花的投影矩阵。这样既保留了 Shader Graph 可视化编辑的便利性又减少了节点连线的数量排查问题也更快。4. 用 Shader Graph 实现自定义贴花现在进入实战环节。我们会从零创建一个 Shader Graph然后编写辅助的投影 C# 脚本最后把贴花材质挂到一个空物体上观察效果。4.1 准备贴花纹理在开始之前准备一张带透明通道的贴花纹理。比如一张“作战标记”或“喷漆涂鸦”。纹理的 Alpha 通道用来控制贴花的透明区域RGB 通道是图案颜色。如果你暂时没有合适的纹理可以在 Unity 工程里右键菜单创建一张纯色贴图然后在 PS 或在线工具里画上图案。更简单的方式使用 Unity 自带的一张粒子贴图或者任何带 Alpha 的 PNG。纹理导入设置需要注意Alpha Is Transparency 按需勾选。Wrap Mode 建议设为 Clamp防止 UV 越界时出现重复平铺。sRGB 对于颜色纹理勾选对于数据纹理不勾选。Wrap Mode 设为 Clamp 很重要因为贴花 UV 越界时我们通常希望它透明消失而不是平铺出很多重复图案。如果投影范围裁剪写得好Wrap Mode 的影响不大但 Clamp 会更保险。4.2 创建基础 Shader在 Project 窗口中右键选择 Create - Shader Graph - URP - Unlit Shader Graph命名为 DecalShader然后双击打开。在 Graph Inspector 的 Graph Settings 里把 Surface Type 改为 Transparent因为贴花要叠加到物体表面不能是不透明 Shader。Render Face 一般保持 Front但在贴花场景中更建议设置为 Both这样贴花从背面看也能看到对应效果视项目需求而定。同时把 Blend Mode 调整为你需要的混合方式。贴花最常用的是 Alpha 混合或者 Premultiply。Alpha 混合适合大多数弹孔、喷漆效果Premultiply 适合带预乘 Alpha 的纹理可以避免边缘黑边。我们这里选 Alpha。接下来在 Workspace 里添加核心节点。首先要加一个 Scene Depth 节点。它可以采样当前屏幕像素的深度值。然后把这个深度值转换成世界空间位置。更精细的做法是在 Shader Graph 中添加一个 Custom Function输入屏幕坐标和深度输出世界坐标。但为了减少自定义函数数量我们可以用一个比较实用的简化方案直接用 Camera 相关节点计算。实际上 Shader Graph 中有一个节点的组合——Depth 节点加 View Direction 等节点——可以重构位置但这些节点在不同版本中差异较大因此我这里更推荐把“深度重建”写在 HLSL 里。4.3 编写 HLSL 自定义函数创建一个 HLSL 文件路径为 Assets/Shaders/DecalHlsl.hlsl。该文件包含两个函数一个用于从深度重建世界位置一个用于计算贴花 UV 和裁剪信息。下面是一段可运行参考的 HLSL 代码。它基于 URP 的相机参数把当前像素深度转换为世界位置。#ifndef DECAL_HLSL_INCLUDED #define DECAL_HLSL_INCLUDED float4x4 unity_CameraToWorld; // Shader Graph 中已内置声明 float3 ReconstructWorldPosition(float2 screenUV, float depth) { #if UNITY_REVERSED_Z float deviceDepth depth; #else float deviceDepth depth; #endif float2 ndcxy screenUV * 2.0 - 1.0; ndcxy.y * -1.0; float4 ndcPos float4(ndcxy, deviceDepth, 1.0); #if UNITY_UV_STARTS_AT_TOP // 不同图形 API 的 UV 原点可能不同这里做兼容 // 实际项目中可以根据平台宏调整 #endif float4 viewPos mul(UNITY_MATRIX_I_P, ndcPos); viewPos viewPos / viewPos.w; float4 worldPos mul(unity_CameraToWorld, viewPos); return worldPos.xyz; } float3 DecalProject(float3 worldPos, float4x4 decalMatrix, float3 decalRight, float3 decalUp, float3 decalForward) { float3 localPos mul(decalMatrix, float4(worldPos, 1.0)).xyz; float2 uv localPos.xy * 0.5 0.5; uv.y 1.0 - uv.y; float range 1.0 - saturate(length(max(abs(localPos) - 0.5, 0.0)) * 2.0); return float3(uv, range); } #endif这段代码的核心思路是先用屏幕 UV 和深度构造 NDC 坐标再通过逆投影矩阵转换到观察空间最后用相机到世界的矩阵转换到世界空间。其中 unity_CameraToWorld、UNITY_MATRIX_I_P 在 Shader Graph 的 HLSL 环境中是可以使用的具体宏是否可用取决于你的平台和着色器变体。如果你在某个版本遇到编译报错可以改用 Shader Graph 内置的 Scene Depth 节点加 Position 节点组合来实现原理完全一样。DecalProject 函数用来把世界坐标变换到贴花局部坐标返回一个三元组UV 和有效范围。localPos.xy 经过 0.5 偏移后变成 UVlocalPos.z 用于判断是否超过投影器前后范围。我这里做了一个简化只要 X、Y、Z 任意一维超出 [-0.5, 0.5]通过 saturate 和 max 运算就会让 range 变为 0这样超出范围的像素就不会显示。4.4 在 Shader Graph 中连接节点回到 Shader Graph我们创建以下节点首先添加 Screen Position 节点输出屏幕坐标。同时添加 Scene Depth 节点采样深度。这两个节点分别对应屏幕坐标和深度值。然后添加一个 Custom Function 节点。在 Type 里选择 File把 DecalHlsl.hlsl 拖进去并选择函数 ReconstructWorldPosition。输入连接 Screen Position 的 UV 输出和 Scene Depth 的 Depth 输出输出得到世界位置。下一步需要知道贴花投影器的位置和方向。我们可以在材质里暴露四个属性Decal Matrix4x4 矩阵。Decal Right贴花右方向向量。Decal Up贴花上方向向量。Decal Forward贴花前方向向量。用 C# 脚本每帧更新这些材质参数。在 Shader Graph 中添加 Vector 4 属性把 Right、Up、Forward 分别拆成 Vector 3。矩阵在 Shader Graph 里可以用 Matrix 4x4 类型属性或者用四个 Vector4 组合。为了方便我这里用矩阵属性。接着把 ReconstructWorldPosition 得到的世界位置输入到 DecalProject 函数中。Custom Function 选择 File 方式函数名填 DecalProject输入是世界位置、贴花矩阵、三个方向向量输出是 UV 和 range。使用 Split 节点把 Custom Function 的输出拆开UV 连接到 Sample Texture 2D 节点的 UV。Range 则作为透明度的门控。这样贴花纹理就能正确投影到场景物体表面了。最后把采样结果连接到 Fragment 的 BaseColor 和 Emission 或者 Alpha 上。一个最小可用的贴花 Shader 就完成了。如果你只想体验流程可以先不做法线方向裁剪等基础效果出来了再加上。4.5 编写 C# 辅助脚本Shader 里的投影矩阵不会自己更新我们需要一个 C# 脚本来驱动。下面是一个简单的 DecalProjectorHelper 脚本可以挂在贴花空物体上。它会在每一帧把自身的位置、旋转、缩放信息以及局部空间到世界空间的矩阵传递到材质中。using UnityEngine; [ExecuteInEditMode] public class DecalProjectorHelper : MonoBehaviour { public Material decalMaterial; public Texture decalTexture; private static readonly int DecalMatrix Shader.PropertyToID(_DecalMatrix); private static readonly int DecalRight Shader.PropertyToID(_DecalRight); private static readonly int DecalUp Shader.PropertyToID(_DecalUp); private static readonly int DecalForward Shader.PropertyToID(_DecalForward); private static readonly int DecalTex Shader.PropertyToID(_DecalTex); private void Update() { if (decalMaterial null) return; Matrix4x4 worldToDecal transform.worldToLocalMatrix; decalMaterial.SetMatrix(DecalMatrix, worldToDecal); decalMaterial.SetVector(DecalRight, transform.right); decalMaterial.SetVector(DecalUp, transform.up); decalMaterial.SetVector(DecalForward, transform.forward); if (decalTexture ! null) decalMaterial.SetTexture(DecalTex, decalTexture); } }这个脚本通过 transform.worldToLocalMatrix 把世界坐标转换到贴花局部空间。贴花的局部空间原点就是 GameObject 位置X、Y 轴对应纹理的 U、V 方向Z 轴对应投影方向。你调整 GameObject 的位置、旋转和缩放就相当于调整贴花的投影范围。在 Shader Graph 里对应的属性需要在 Graph Inspector 的 Properties 面板中创建并且命名与 C# 脚本里 Shader.PropertyToID 的参数名一致。矩阵属性类型选择 Vector 4 也是可行的但为了直观建议使用 Matrix 4x4 类型某些版本可能需要在 Shader Graph 中手动进行矩阵转置你可以先不改如果出现坐标反了再考虑转置。4.6 在场景中验证效果现在创建一个场景添加一个平面或立方体作为目标物体再创建一个空物体挂着 DecalProjectorHelper。给 DecalProjectorHelper 赋值材质并把贴花纹理指定给它。调整空物体的位置让它朝向目标物体表面比如放在墙面前方旋转让 Z 轴指向墙面。运行后你会在目标物体表面看到一张投影出来的贴花图案。移动贴花空物体贴花会跟着移动并投射到不同物体表面。如果看不到效果优先检查这几项URP Asset 是否开启了 Depth Texture。材质是否使用了 Transparent 表面类型。贴花 GameObject 离目标表面是否太远超出投影范围。贴花纹理的 Alpha 是否有效。这个环节得到的还是最基础的正投影效果已经能覆盖很多贴纸需求。接下来做进阶增强。5. 进阶让贴花“贴”得更自然5.1 边缘柔化和透明度直接采样纹理的 Alpha 作为透明度边缘会出现明显的“硬边”。尤其是喷漆、弹孔这类效果硬边会显得非常突兀。解决思路是在 Shader 中增加边缘距离计算让越靠近投影边界的地方透明度越低。在 DecalProject 函数返回的 range 基础上我们可以用 Smoothstep 把 range 从 [0,1] 映射到 [0,1] 的中间区间例如float softRange smoothstep(0.0, 0.5, range);这样只有 range 大于 0.5 的区域才会逐渐显示边缘过渡更柔和。也可以在 Shader Graph 里直接加 Smoothstep 节点把 Custom Function 输出的 range 接进去。另外贴花纹理自身的 Alpha 通常带有图案形态。把 smoothstep 后的边缘 Mask 与纹理 Alpha 相乘就能生成“内部有细节、外部柔和过渡”的合成 Alphafloat finalAlpha texAlpha * softRange;这种方式非常适合做喷漆、水渍、涂鸦。调节 smoothstep 的两个阈值可以控制边缘渐变的宽度。5.2 曲面贴合与法线修正屏幕空间贴花本身已经可以贴在曲面上了因为每像素都会从深度纹理重建真实表面位置理论上不受网格 UV 影响。但如果你直接在物体表面叠加一张贴花纹理曲率较大的区域比如球体边缘会出现拉伸。要改善拉伸可以在贴花空间中使用“三平面映射Triplanar Mapping”思路。根据表面法线选择最合适的投影平面而不是只使用单一方向。对于弹孔这种局部效果单一方向投影其实问题不大但对于大面积的墙面标识或角色身上的贴纸三平面映射会更稳定。在 Shader Graph 里可以采样世界法线计算 abs(normal.z) 最大分量决定用 XY、ZY、XZ 中哪一个平面作为采样方向。这部分会增加很多节点如果不想在 Shader Graph 里铺太多节点可以用 Custom Function 封装一个三平面映射函数。核心逻辑是float3 TriplanarUV(float3 worldPos, float3 worldNormal, float scale) { float3 blend abs(worldNormal); blend saturate(pow(blend, 4) * 2); blend / dot(blend, 1.0); float2 uvXY worldPos.xy * scale; float2 uvZY worldPos.zy * scale; float2 uvXZ worldPos.xz * scale; return float3(uvXY * blend.z uvXZ * blend.y uvZY * blend.x); }因为贴花只是局部效果使用 Triplanar 时还需要和投影范围裁剪配合避免大面积平铺。实际项目中简单贴花不建议无条件使用三平面映射性能开销和不稳定性都会上升。5.3 法线方向裁剪提升贴花真实感的一个重要步骤是加入朝向判断。我们需要一个“只贴正面”的效果。在 Shader Graph 中采样场景法线或使用世界法线节点与贴花前方向做点积。如果点积小于某个阈值就把 Alpha 乘到接近 0。例如float facing dot(worldNormal, decalForward); float facingMask smoothstep(-0.2, 0.2, facing); finalAlpha * facingMask;这个逻辑可以防止贴花穿透到物体背面。比如一个立方体贴花从正面投射侧面和背面的表面法线与投影方向夹角太大时贴花就会消失。这正是我们想要的。如果要更精确地得到目标物体表面法线可以在 URP 中开启 Depth Normal 纹理然后在 Shader 里采样并解码法线。法线解码会引入额外带宽但它能让贴花与曲面细节结合得更好比如凹凸墙面上的贴纸。5.4 贴纸的细节增强贴纸类效果还有几个常用增强点Tiling通过 Tiling 节点控制纹理重复次数适合做重复喷漆或规则纹理。Offset给 UV 加上随时间变化的偏移可以做血迹蔓延或特效贴纸动画。Distortion采样一张噪声图对 UV 做轻微扰动让贴花边缘更像手绘或油漆喷溅。Mask使用一张遮罩纹理控制贴花不同通道的强度比如只让红色通道出现在特定区域。这些都是在 Sample Texture 2D 节点前后增加运算即可完成不需要改动核心投影逻辑。如果你嫌节点太多可以把扰动和遮罩运算都写进 HLSL 的 Custom Function让 Shader Graph 保持简洁。6. 常见问题与排查思路在实际运行贴花 Shader 时最常遇到的问题我都列在下面。这张表格基本覆盖了从“看不见贴花”到“贴花显示错误”的绝大多数情况。问题现象常见原因解决思路贴花完全不显示深度纹理未开启在 URP Asset 中勾选 Depth Texture贴花完全不显示GameObject 位置离目标表面太远调整贴花物体位置确保目标点在投影范围内贴花完全不显示材质 Surface Type 不是 Transparent修改为 Transparent 并确认混合模式贴花显示为黑色或透明块纹理 Alpha 无效检查纹理导入设置和 Alpha 通道贴花图案拉伸严重投影方向与表面不垂直调整贴花物体旋转让 Z 轴朝向表面贴花出现在物体背面缺少法线朝向裁剪加入法线与投影方向点积场景中部分物体无贴花深度纹理只记录不透明物体透明物体需要额外处理Shader Graph 报错矩阵相关函数找不到HLSL 版本兼容问题改用 Position 节点和 Scene Depth 节点组合贴花闪烁或边缘锯齿深度精度不足、过度平滑调整平滑参数或使用更高精度深度纹理6.1 贴花完全不显示时先查什么如果一个项目刚搭建好贴花跑不出来我一般会先检查三件事。第一URP Asset 的 Depth Texture 是否打开。这个选项在 URP 管线资源里默认状态可能是不开启的。不开启时Scene Depth 节点采样不到有效数据后面的重建逻辑全部失效。第二贴花 GameObject 的位置和旋转是否正确。贴花投影范围默认是以物体自身局部坐标原点为中心范围是 [-0.5, 0.5] 之间的盒子。如果物体离目标表面太远或者缩放过小贴花自然看不到。第三材质是否使用了正确的渲染队列。Shader Graph 里的 Transparent 材质会被归入透明队列透明队列和不透明物体之间的深度写入行为需要注意。如果不希望贴花被其他透明物体遮挡可以关闭材质的 Depth Write或者通过脚本调整 material.renderQueue。6.2 贴花穿透到场景不可见有时候你会看到贴花出现在墙后面、地板下面这是因为屏幕空间贴花采用的是深度纹理但场景中透明物体会被排除在深度纹理外。如果一扇透明玻璃窗立在贴花前面玻璃不写入深度贴花会直接投射到玻璃后面的物体上。解决思路有两种一是把透明物体也写入深度或者使用 Custom Depth二是根据实际需求把贴花限制在特定 Layer 上或者在采集深度时做 Mask。另一种特殊穿透情况是贴花投影体本身与目标表面重叠但深度排序异常。这时可以在贴花 Shader 中关闭 Depth Write并把 ZTest 改为 LEqual 或 Less Equal让贴花始终在场景物体表面之上显示。6.3 贴花边缘出现浓度叠加多次使用贴花后同一个位置可能被多个贴花覆盖Alpha 混合会导致边缘出现“叠加黑边”。这通常是因为贴花纹理边缘带有黑色或灰色半透明像素Alpha 混合时黑色残留被叠进场景。解决办法是把纹理在导入设置中处理为 Premultiplied Alpha然后把 Shader 的混合模式改为 Premultiply或者先在 Shader 中对纹理颜色做一次去黑处理。另一种办法是在 Shader 里对 RGB 乘以一个可控的强度值并在采样前把 Alpha 低于阈值的像素剔除掉避免脏边缘混入最终颜色。7. 最佳实践与工程建议7.1 矩阵与属性命名规范贴花方案里最容易被坑的是矩阵方向。C# 传进去的矩阵是 worldToLocalMatrixShader 里如果直接拿世界坐标去乘得到的是贴花局部坐标。但不同图形 API 下 UV 原点不同你需要统一约定贴花局部坐标的 Y 轴对应贴图 V 轴并且在上传矩阵前或 Shader 内完成 Y 翻转。建议所有贴花相关参数都以_Decal为前缀例如_DecalMatrix、_DecalForward、_DecalTex。这样属性在 Shader Graph 和 C# 脚本里都能快速对应也方便后期做批量修改。不要混用_WorldPos、_Tex这种模糊命名排查问题时会很痛苦。7.2 性能优化贴花 Shader 涉及深度纹理采样和矩阵乘法性能损耗不是线性的。项目里大量贴花时需要关注几个性能关键点。一是采样数量。每像素多一次贴花采样就多一次纹理读取和混合。屏幕空间贴花本身是逐像素操作贴花数量和贴花 Shader 的复杂度都会影响填充率。墙面上的大面积涂鸦可以使用大块贴花合并而不是几十个小型贴花叠加。二是深度纹理重建。如果项目中每个贴花 Shader 都重复做深度重建GPU 会做大量重复运算。实际系统里通常先计算出贴花范围内的像素再做深度重建或者把深度重建提前到公共的 G-Buffer 阶段。我们演示的 Custom Function 是教学版本直接用在项目里会带来额外开销。三是分支控制。Shader 中尽量避免大范围分支。如果裁剪逻辑可以用 saturate 和乘法实现就不用 if 语句这样 GPU 可以保持并行执行。三是纹理格式。贴花纹理尽量使用 ASTC/BC7 等压缩格式不要使用未压缩 RGBA否则缓存命中率会很低尤其在移动端影响明显。7.3 渲染顺序与合批贴花通常不会被 SRP Batcher 或静态合批自动合并因为每个贴花依赖自己的矩阵参数。要保证性能可以尝试以下方案将贴花 Shader 的材质变体数量降到最低。使用 MaterialPropertyBlock 而不是复制材质实例。但不推荐每帧设置很多全局材质参数这会打断合批。大量的动态贴花例如射击游戏弹孔建议使用 Decal Render Feature 或自己维护一个 Mesh Instancing 方案。在 URP 项目中如果是少量静态贴花可以接受单材质渲染开销如果是大量动态弹孔必须考虑批量渲染或纹理烘焙。7.4 安全与数据校验从工程可靠性角度讲贴花辅助脚本接收外部数据时要有校验。例如矩阵可能因为缩放为 0 导致退化贴花纹理可能缺失深度纹理在编辑器预览模式中尚未生成。在这些情况下脚本和 Shader 应当给出明确提示而不是静默失败。在 C# 脚本中建议加入这样的保护逻辑检查 decalMaterial 和 transform 是否有效。检查 Shader 中对应属性是否存在。在 OnEnable/OnValidate 中更新一次材质参数避免进入运行模式后没触发 Update 导致贴花消失。贴花物体在场景中应当放在独立 Layer方便筛选和批处理。7.5 编辑器工作流使用 ExecuteInEditMode 的 C# 脚本可以让贴花在编辑器里实时预览。但要注意编辑器预览模式下深度纹理可能未正常生成导致贴花时有时无。建议在画面视图下开启 Frame Debugger 检查材质球是否被正确渲染。另外做一个贴花预览框很有帮助。可以在贴花 GameObject 上绘制一个线框 Box Gizmo这样调整投影范围时能看到贴花实际影响的体积范围而不是只能凭感觉旋转缩放。如果使用 Shader Graph 的 Custom Function建议把 HLSL 文件里的函数注释写清楚标注输入输出的坐标空间。两三个月后你自己回来看能快速回忆起这个矩阵是从世界到贴花局部空间而不是世界到观察空间。8. 总结与学习路线这篇文章从图形学原理出发完成了三件事。第一解释了贴花的本质是把二维纹理通过三维投影体投射到物体表面第二用 Shader Graph 和 HLSL 实现了一套不依赖内置 Decal Projector 的自定义贴花方案支持深度重建、投影矩阵变换、UV 采样和范围裁剪第三补充了曲面贴合、法线裁剪、边缘柔化和排错思路让贴花效果可以在实际项目里用上。如果你想把这条渲染学习路线走得更深建议按下面顺序继续深入先掌握 URP 的基础管线流程理解深度纹理、法线纹理、G-Buffer 和渲染顺序的关系。学习 HLSL 基本语法尤其是坐标空间转换的矩阵运算。尝试把贴花效果从 Shader Graph 改写为手写 HLSL Shader熟悉 ShaderLab 的 Pass 和混合状态配置。探索 URP 内置 Decal Feature 的原理对照我们自定义方案中的差异加深对渲染管线的理解。实践一个完整的射击游戏弹孔系统把贴花、物理射线检测、对象池、材质参数刷新整合在一起。最后提醒一句贴花效果在静态演示里好看不算本事能稳定地跑在不同平台、不同画质配置、不同渲染管线里才算合格。建议你在开发初期就规划好贴花 Shader 的通用性比如把深度重建和贴花绘制拆成两个功能模块以后不管怎么改渲染方案核心代码都能复用。如果本文对你有帮助可以收藏备用后续实践过程中遇到问题再回来对照一遍节点连接和排查思路。