案例 22.1用 Decal Projector 贴花入门
目标:在场景里放几个 Decal Projector:台阶边缘的警示条纹、地面的喷漆箭头、箱子上的弹孔和地上的血迹。
在编辑器里的步骤是:
- 选中 URP Asset 使用的 Renderer 资源(Universal Renderer Data),,Technique 选 Automatic 或指定一种。
- 创建材质,Shader 选 (URP 包自带的
Shaders/Decal.shadergraph),设置 Base Map,需要的话设置 Normal Map 和 Normal Blend。也可以用 自己做一个。 - ,把材质拖进去,调整 Width/Height/Projection Depth 和朝向。
本书的配图在代码里搭场景,需要用代码完成这三步。第一步有个障碍:DecalRendererFeature 和它的设置类在 URP 14 里都是 internal 的,不能直接 new 或 AddComponent。可以用反射拿到类型、ScriptableObject.CreateInstance 创建,再通过 SerializedObject 修改它的序列化字段——这与在 Inspector 里修改的是同一份数据。这只是为了自动化拍摄,项目里在 Renderer 资源上手动添加即可。
Assets/Cookbook/Ch22/Editor/Ch22Shots.cs(节选:addDecalFeature)
// DecalRendererFeature 及其设置类在 URP 14 里是 internal 的:编辑器里在 Renderer 资源上点 Add Renderer Feature › Decal 即可; // 代码里只能用反射创建,再通过 SerializedObject 改它的序列化字段(与 Inspector 改的是同一份数据)。 // technique: 0 Automatic / 1 DBuffer / 2 ScreenSpace;surfaceData: 0 Albedo / 1 AlbedoNormal / 2 AlbedoNormalMAOS static ScriptableRendererFeature AddDecalFeature(Shot s, int technique, int surfaceData = 2, int normalBlend = 1) { var type = typeof(ScriptableRendererFeature).Assembly.GetType("UnityEngine.Rendering.Universal.DecalRendererFeature"); var feature = (ScriptableRendererFeature)ScriptableObject.CreateInstance(type); feature.name = "Decal"; var so = new SerializedObject(feature); so.FindProperty("m_Settings.technique").enumValueIndex = technique; so.FindProperty("m_Settings.dBufferSettings.surfaceData").enumValueIndex = surfaceData; so.FindProperty("m_Settings.screenSpaceSettings.normalBlend").enumValueIndex = normalBlend; so.ApplyModifiedPropertiesWithoutUndo(); s.Temp.Add(feature); s.Renderer.rendererFeatures.Add(feature); s.Renderer.SetDirty(); return feature; }
Assets/Cookbook/Ch22/Editor/Ch22Shots.cs(节选:urpDecal)
static Material UrpDecalMat(Shot s, Texture2D baseMap, Texture2D normal = null, bool angleFadeGraph = false) { // URP 包自带的 Decal Shader Graph(Packages/.../Shaders/Decal.shadergraph),属性:Base_Map / Normal_Map / Normal_Blend var m = s.Mat(angleFadeGraph ? "Cookbook/Ch22/DecalAngleFade" : "Shader Graphs/Decal"); m.SetTexture("Base_Map", baseMap); if (normal != null) { m.SetTexture("Normal_Map", normal); m.SetFloat("Normal_Blend", 1f); } else m.SetFloat("Normal_Blend", 0f); m.enableInstancing = true; // Decal 系统按材质分批用 GPU Instancing 绘制 return m; } static DecalProjector UrpDecal(Material m, Vector3 pos, Vector3 euler, Vector3 size, float opacity = 1f) { var go = new GameObject("Decal Projector"); go.transform.SetPositionAndRotation(pos, Quaternion.Euler(euler)); var d = go.AddComponent<DecalProjector>(); d.material = m; d.size = size; // x/y = 贴花宽高,z = 投影深度 d.pivot = new Vector3(0, 0, size.z * 0.5f); // 默认:盒子从投影器位置向前延伸 d.fadeFactor = opacity; return d; }
Decal Projector 的主要参数:
| 参数(代码属性) | 含义 |
|---|---|
| size | x/y 是贴花的宽高,z 是投影深度。深度只要能覆盖到目标表面即可,太深会贴到不该贴的远处物体上 |
| pivot | 盒子相对于物体原点的偏移;默认 (0, 0, z/2),即盒子从投影器位置向前延伸 |
| scaleMode | 是否让 Transform 的缩放参与盒子尺寸(ScaleInvariant / InheritFromHierarchy) |
| uvScale / uvBias | 贴图的平铺与偏移,用来从图集里选一格 |
| fadeFactor | 整体不透明度,做“随时间淡出”的弹孔就改它 |
| drawDistance / fadeScale | 超过绘制距离不画;fadeScale 是开始淡出的比例 |
| startAngleFade / endAngleFade | 按表面法线与投影方向的夹角淡出,需要 Shader Graph 开启 Angle Fade(案例 22.3) |
| renderingLayerMask | 配合 Feature 的 Use Rendering Layers,只贴到指定渲染层的物体上(例如不贴到角色身上) |

注意台阶踢面上的拉伸:投影方向与踢面平行,贴图在这个方向上没有任何“分辨率”,只剩被无限拉长的一行像素。这正是图 22-1 里说的问题,案例 22.3 处理它。
坑:Decal 材质要开 GPU InstancingDecal 系统按材质把投影器分组,用实例化批量绘制。用代码
new Material 创建的材质默认没有开启实例化(enableInstancing = false),要手动打开;在编辑器里创建的 Decal Shader Graph 材质默认已勾选。