decal是运行时将纹理投影到任意几何体表面的渲染层,不依赖模型uv或实体mesh,需实时计算局部坐标系、正交对齐顶点,并通过深度偏置与模板测试避免z-fighting和穿模。

Decal 是什么,为什么不能直接用贴图或模型
Decal 不是普通贴图,也不是带碰撞的实体模型,而是一组「投影到已有几何体表面」的渲染层。游戏里子弹孔和血迹必须贴合墙面曲率、能跨多个三角面、随视角变化保持正交感——用 Quad 面片硬摆会穿模、缩放失真、Z-fighting 严重;用独立 StaticMesh 则无法动态生成、内存爆炸、剔除逻辑复杂。
核心约束在于:它得是「运行时实时投射 + 局部覆盖 + 深度测试绕过」的组合。主流引擎(Unreal / Unity)底层都靠 GPU 的 stencil buffer 或 depth bias 投影实现,C++ 自研需手动处理这三层:
- 射线检测后获取世界坐标 + 表面法线(
FHitResult或自定义Raycast结果) - 构造面向法线的局部坐标系,生成四边形顶点(不是简单平移,要正交对齐)
- 在渲染管线中启用
DepthWrite=Off、DepthTest=LessEqual、StencilOp防止被遮挡物覆盖
怎么用 C++ 构造 Decal 四边形顶点(不依赖引擎内置类)
关键不是画个矩形,而是让四边形始终「正对视线方向」又「贴合表面倾斜」。常见错误是只用表面法线推平面,结果斜墙上的弹孔拉伸成细条。
正确做法是构建一个以撞击点为原点、法线为 Z 轴的局部空间,再用摄像机右/上向量在该空间内做正交展开:
// 假设 hitPos, hitNormal 已通过射线检测获得 FVector right = FVector::CrossProduct(hitNormal, cameraForward).GetSafeNormal(); FVector up = FVector::CrossProduct(right, hitNormal).GetSafeNormal(); <p>const float size = 0.05f; // 弹孔直径约 5cm FVector v0 = hitPos - right <em> size - up </em> size; FVector v1 = hitPos + right <em> size - up </em> size; FVector v2 = hitPos + right <em> size + up </em> size; FVector v3 = hitPos - right <em> size + up </em> size;</p>
注意:cameraForward 必须是当前帧摄像机前向(非世界 Z),否则转头时 Decal 会“滑动”;如果不用摄像机方向,可改用 FVector::CrossProduct(hitNormal, FVector(0,0,1)) 保底,但天花板/地板会翻转。
Decal 渲染时最容易崩的三个深度问题
90% 的 Decal 显示异常(闪烁、被遮挡、半透明错乱)都源于深度配置错误,不是材质写错了。
-
DepthWrite=Off必须开启:Decal 本身不修改深度缓冲,否则后续物体被误裁剪 -
DepthTest=LessEqual(不是Less):避免因浮点精度导致刚好卡在表面深度值上被剔除 - 必须加
DepthBias(如DepthBias=1.0f):防止与原始表面 Z 值完全重合引发 Z-fighting,数值太小无效,太大则悬空
Unity URP 中对应 ShaderLab 参数是 ZWrite Off + ZTest LEqual + Offset 1, 1;Unreal HLSL 中需在 VertexShader 输出前手动偏移 clipPos.z += 0.001 * clipPos.w。
怎么批量管理 Decal 生命周期(别让子弹孔堆满内存)
每发子弹都 new 一个 Decal?很快 OOM。真实项目必须用对象池 + 时间/距离双淘汰策略。
- 预分配固定大小数组(如 256 个 slot),用
std::vector<decalinstance></decalinstance>管理活跃实例 - 每个 Decal 记录生成时间(
float LifeTime)和距玩家距离(float DistanceToPlayer) - 每帧遍历:距离 > 30m 且存活 > 10s 的自动标记为
Free;复用时直接覆盖其数据,不 delete/new - 材质纹理尽量共用:子弹孔和血迹可用同一张 atlas,靠 UV offset 区分类型,减少 DrawCall
特别注意:Decal 的 WorldTransform 更新必须在每帧渲染前完成,不能等到逻辑帧末尾——否则运动物体(如旋转门)上的血迹会滞后一帧跳变。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











