Three root causes regressed the Holtburg lifestone since the WB rendering migration (Phase N.5 retirement amendment, commitdcae2b6, 2026-05-08). All confirmed via temporary [LIFESTONE-DIAG] instrumentation and visually verified by the user through the +Acdream test character. 1. **Alpha-test discard** in mesh_modern.frag transparent pass killed high-α pixels of dat-flagged transparent surfaces. Native AC transparent surfaces routinely include effectively-opaque pixels — e.g. the lifestone crystal core (surface 0x080011DE) — that compose correctly under (SrcAlpha, 1-SrcAlpha) blending. The original N.5 §2 rationale ("high-α belongs in opaque pass") doesn't hold for surfaces flagged transparent at the dat level: those pixels can't reach the opaque pass at all. Fix: remove `α >= 0.95 discard` from the transparent pass, keep `α < 0.05 discard` as a fragment-cost optimization (skip totally-empty pixels). 2. **Cull state** for the transparent pass was unset by WbDrawDispatcher after the N.5 retirement amendment deleted StaticMeshRenderer.cs (which had the Phase 9.2 setup at commit6f1971a, 2026-04-11). Closed-shell translucents — lifestone crystal, glow gems — need GL_CULL_FACE + GL_BACK + GL_CCW in the transparent pass; otherwise back faces composite over front faces in iteration order under DepthMask(false). Fix: re-establish Phase 9.2's exact GL state setup at the top of Phase 8. 3. **uDrawIDOffset uniform** was missing from mesh_modern.vert. gl_DrawIDARB resets to 0 at the start of each glMultiDrawElementsIndirect call, so the transparent pass — which begins later in the indirect buffer — was fetching Batches[0..transparentCount) instead of its actual section at Batches[opaqueCount..end). The lifestone crystal ended up reading the FIRST OPAQUE batch's TextureHandle every frame; as the camera moved and the front-to-back opaque sort reordered which group landed at BatchData[0], the crystal's apparent texture flickered to whatever sat first — typically the player character's body parts. Fix: add `uniform int uDrawIDOffset` to the vertex shader, change Batches[gl_DrawIDARB] → Batches[uDrawIDOffset + gl_DrawIDARB], and set the uniform per-pass in WbDrawDispatcher (0 for opaque, _opaqueDrawCount for transparent). Mirrors WorldBuilder's BaseObjectRenderManager.cs line 845. Tests: 1688/1696 passing (8 pre-existing physics/input failures unchanged). N.5b conformance sentinel 94/94 clean. Visual: Holtburg lifestone now renders with the spinning blue crystal correctly composed over the pedestal. Other transparent content (glass, particle effects, NPC clothing) is unaffected — the same uniform fix applies globally and is correct for all transparent draws. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
3.1 KiB
GLSL
77 lines
3.1 KiB
GLSL
#version 430 core
|
|
#extension GL_ARB_shader_draw_parameters : require
|
|
|
|
layout(location = 0) in vec3 aPosition;
|
|
layout(location = 1) in vec3 aNormal;
|
|
layout(location = 2) in vec2 aTexCoord;
|
|
|
|
struct InstanceData {
|
|
mat4 transform;
|
|
// Reserved for Phase B.4 follow-up (selection-blink retail-faithful
|
|
// highlight): vec4 highlightColor; — extend stride here, increase the
|
|
// _instanceSsbo upload size in WbDrawDispatcher, add a flat varying out,
|
|
// and consume in mesh_modern.frag.
|
|
};
|
|
|
|
struct BatchData {
|
|
uvec2 textureHandle; // bindless handle for sampler2DArray
|
|
uint textureLayer; // layer index (always 0 for per-instance composites)
|
|
uint flags; // reserved — N.5 dispatcher owns all blend state
|
|
// (glBlendFunc per pass). If a future phase wants
|
|
// shader-side per-batch additive flag (Decision 2
|
|
// fallback), encode it here as bit 0.
|
|
};
|
|
|
|
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
|
InstanceData Instances[];
|
|
};
|
|
|
|
// binding=1 here is the SSBO namespace — distinct from the UBO namespace.
|
|
// SceneLighting UBO also uses binding=1 in the fragment shader; GL keeps
|
|
// GL_SHADER_STORAGE_BUFFER and GL_UNIFORM_BUFFER binding tables separate.
|
|
// Task 10 dispatcher binds:
|
|
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSsbo)
|
|
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, batchSsbo)
|
|
// Existing SceneLightingUboBinding handles the UBO side.
|
|
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
|
BatchData Batches[];
|
|
};
|
|
|
|
uniform mat4 uViewProjection;
|
|
|
|
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
|
// gl_DrawIDARB resets to 0 at the start of each glMultiDrawElementsIndirect
|
|
// call, so the transparent pass — which begins later in the indirect buffer
|
|
// — was fetching Batches[0..transparentCount) instead of its actual section
|
|
// at Batches[opaqueCount..end). The lifestone crystal (a transparent draw)
|
|
// ended up reading the FIRST OPAQUE batch's TextureHandle every frame. As
|
|
// the camera moved and the opaque front-to-back sort reordered which group
|
|
// landed at BatchData[0], the lifestone's apparent texture flickered to
|
|
// whatever was first — frequently the player character's body parts.
|
|
//
|
|
// WbDrawDispatcher.Draw sets this to 0 before the opaque MDI call and to
|
|
// _opaqueDrawCount before the transparent MDI call, matching WorldBuilder's
|
|
// uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845.
|
|
uniform int uDrawIDOffset;
|
|
|
|
out vec3 vNormal;
|
|
out vec2 vTexCoord;
|
|
out vec3 vWorldPos;
|
|
out flat uvec2 vTextureHandle;
|
|
out flat uint vTextureLayer;
|
|
|
|
void main() {
|
|
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
|
mat4 model = Instances[instanceIndex].transform;
|
|
|
|
vec4 worldPos = model * vec4(aPosition, 1.0);
|
|
gl_Position = uViewProjection * worldPos;
|
|
|
|
vWorldPos = worldPos.xyz;
|
|
vNormal = normalize(mat3(model) * aNormal);
|
|
vTexCoord = aTexCoord;
|
|
|
|
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
|
vTextureHandle = b.textureHandle;
|
|
vTextureLayer = b.textureLayer;
|
|
}
|