phase(N.5) Task 5: mesh_modern.vert + .frag — bindless + SSBO + indirect

New entity shaders for the WB modern rendering path. Modeled on WB's
StaticObjectModern.* but adapted to acdream's lighting model:
- Drops uActiveCells (we cull cells on CPU in WbDrawDispatcher)
- Drops uDrawIDOffset (full passes, no pagination)
- Drops uHighlightColor (deferred to Phase B.4 follow-up; field reserved
  in InstanceData struct comment)
- Preserves mesh_instanced's SceneLighting UBO at binding=1 with 8 lights,
  fog params, lightning flash, per-channel clamp — full visual identity

vert reads InstanceData[] @ binding=0 indexed by gl_BaseInstanceARB +
gl_InstanceID for the per-entity model matrix; reads BatchData[] @
binding=1 indexed by gl_DrawIDARB for the per-group bindless texture
handle + layer.

frag samples sampler2DArray reconstructed from a uvec2 bindless handle
+ uint layer. uRenderPass uniform picks two-pass alpha-test thresholds:
0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95
and alpha<0.05).

Not yet wired to the dispatcher — Task 6 sets up shader load + capability
detection in GameWindow; Task 7-10 rewrite the dispatcher to use SSBO +
glMultiDrawElementsIndirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 20:05:35 +02:00
parent 6f90997a43
commit aad2aa67da
2 changed files with 149 additions and 0 deletions

View file

@ -0,0 +1,96 @@
#version 430 core
#extension GL_ARB_bindless_texture : require
in vec3 vNormal;
in vec2 vTexCoord;
in vec3 vWorldPos;
in flat uvec2 vTextureHandle;
in flat uint vTextureLayer;
// 0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95)
uniform int uRenderPass;
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
struct Light {
vec4 posAndKind;
vec4 dirAndRange;
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std140, binding = 1) uniform SceneLighting {
Light uLights[8];
vec4 uCellAmbient;
vec4 uFogParams;
vec4 uFogColor;
vec4 uCameraAndTime;
};
vec3 accumulateLights(vec3 N, vec3 worldPos) {
vec3 lit = uCellAmbient.xyz;
int activeLights = int(uCellAmbient.w);
for (int i = 0; i < 8; ++i) {
if (i >= activeLights) break;
int kind = int(uLights[i].posAndKind.w);
vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w;
if (kind == 0) {
vec3 Ldir = -uLights[i].dirAndRange.xyz;
float ndl = max(0.0, dot(N, Ldir));
lit += Lcol * ndl;
} else {
vec3 toL = uLights[i].posAndKind.xyz - worldPos;
float d = length(toL);
float range = uLights[i].dirAndRange.w;
if (d < range && range > 1e-3) {
vec3 Ldir = toL / max(d, 1e-4);
float ndl = max(0.0, dot(N, Ldir));
float atten = 1.0;
if (kind == 2) {
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);
atten *= (cos_l > cos_edge) ? 1.0 : 0.0;
}
lit += Lcol * ndl * atten;
}
}
}
return lit;
}
vec3 applyFog(vec3 lit, vec3 worldPos) {
int mode = int(uFogParams.w);
if (mode == 0) return lit;
float d = length(worldPos - uCameraAndTime.xyz);
float fogStart = uFogParams.x;
float fogEnd = uFogParams.y;
float span = max(1e-3, fogEnd - fogStart);
float fog = clamp((d - fogStart) / span, 0.0, 1.0);
return mix(lit, uFogColor.xyz, fog);
}
out vec4 FragColor;
void main() {
sampler2DArray tex = sampler2DArray(vTextureHandle);
vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer)));
// Two-pass alpha-test (N.5 Decision 2).
if (uRenderPass == 0) {
if (color.a < 0.95) discard; // opaque pass
} else {
if (color.a >= 0.95) discard; // transparent pass
if (color.a < 0.05) discard; // skip totally-empty
}
vec3 N = normalize(vNormal);
vec3 lit = accumulateLights(N, vWorldPos);
// Lightning flash — additive scene bump (matches mesh_instanced.frag).
lit += uFogParams.z * vec3(0.6, 0.6, 0.75);
// Retail clamp per-channel to 1.0 (r13 §13.1).
lit = min(lit, vec3(1.0));
vec3 rgb = color.rgb * lit;
rgb = applyFog(rgb, vWorldPos);
FragColor = vec4(rgb, color.a);
}

View file

@ -0,0 +1,53 @@
#version 430 core
#extension GL_ARB_bindless_texture : require
#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
};
layout(std430, binding = 0) readonly buffer InstanceBuffer {
InstanceData Instances[];
};
layout(std430, binding = 1) readonly buffer BatchBuffer {
BatchData Batches[];
};
uniform mat4 uViewProjection;
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[gl_DrawIDARB];
vTextureHandle = b.textureHandle;
vTextureLayer = b.textureLayer;
}