#version 450 // UE 编辑器选中描边:对 SelectionMask 做 16 方向圆周边缘检测, // alpha 叠加在 Tonemap 之后的 PresentTarget 上(LDR 恒定 UI 色, // 不进 bloom/曝光/TAA——与 UE 编辑器选中轮廓一致)。 // // 规则: // 外描边 自身 mask=0 且邻域有 1 → 描边(物体外圈一圈) // 贴边线 自身 mask=1 且邻域采样越出视口(视为 0)→ 描边——物体 // 延伸出视口时 mask 在边缘截断,描边自动贴着渲染视口边缘 // 完全出视野 mask 全 0 → 无描边 // 描边不做深度遮挡判定(透墙可见)——UE 编辑器同行为,便于找到 // 被挡住的选中物体。 layout(location = 0) in vec2 fragUV; layout(location = 0) out vec4 outColor; layout(set = 0, binding = 0) uniform sampler2D maskImage; // 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)。 // 注意:必须用 4×float 平铺——"float + vec3" 会被 GLSL 对齐成 28B // (vec3 落 16 偏移),超出 FullscreenPush 的 [0,16] 声明范围 // (VUID-10065)。语义与 FullscreenPush 对齐:exposure 槽位=描边宽度, // param0-2=描边色。 layout(push_constant) uniform PushConstants { float widthPx; // 描边宽度(物理像素) float colorR; // 描边色 R float colorG; // 描边色 G float colorB; // 描边色 B } pc; void main() { const float m = texture(maskImage, fragUV).r; const vec2 texel = vec2(pc.widthPx) / vec2(textureSize(maskImage, 0)); float ringMax = 0.0; bool anyOutside = false; const int N = 16; for (int i = 0; i < N; ++i) { const float a = float(i) * (6.2831853 / float(N)); const vec2 suv = fragUV + vec2(cos(a), sin(a)) * texel; // 越界 = 视口截断(视为无 mask)→ 贴边像素产生描边 const bool outside = suv.x < 0.0 || suv.x > 1.0 || suv.y < 0.0 || suv.y > 1.0; anyOutside = anyOutside || outside; if (!outside) { ringMax = max(ringMax, texture(maskImage, suv).r); } } // 外描边:自身 mask=0 且邻域有 1(真轮廓;mask 无洞,不会描出 // 前方遮挡物的轮廓) // 贴边内描边:自身 mask=1 且邻域越出视口(仅视口截断,物体延伸 // 出视野时描边贴着渲染视口边缘) const bool outer = m < 0.5 && ringMax > 0.5; const bool edgeCut = m > 0.5 && anyOutside; const float edge = (outer || edgeCut) ? 1.0 : 0.0; // LDR 恒定色直出,alpha 混合叠加在 Tonemap 输出上 outColor = vec4(pc.colorR, pc.colorG, pc.colorB, edge); }