docs(N.5): plan amendment — Task 5 shader matches mesh_instanced lighting
Original Task 5 draft used hardcoded vec3 ambient/sun uniforms in mesh_modern.frag. Reading actual mesh_instanced.frag revealed it uses a SceneLighting UBO at binding=1 with 8 lights, fog params (start/end/ lightning/mode), fog color, camera/time, and per-channel clamp. Revised: mesh_modern.frag preserves the full SceneLighting UBO + accumulateLights + applyFog + lightning flash + per-channel clamp. mesh_modern.vert adds vWorldPos output (consumed by accumulateLights and applyFog). Visual identity to N.4's lighting model preserved. Two-pass alpha-test (N.5 Decision 2) sits inside the same shader, gated by uRenderPass instead of uTranslucencyKind. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0bfe536858
commit
6f90997a43
1 changed files with 141 additions and 19 deletions
|
|
@ -600,9 +600,13 @@ void main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5.3: Write mesh_modern.frag**
|
- [ ] **Step 5.3: Write mesh_modern.frag — preserve existing lighting model**
|
||||||
|
|
||||||
Create `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`:
|
**AMENDED 2026-05-08:** original plan draft used hardcoded `uAmbient/uSunDir/uSunColor` uniforms. Reading the actual `src/AcDream.App/Rendering/Shaders/mesh_instanced.frag` revealed it uses a `SceneLighting` UBO at `binding=1` with 8 lights, fog params, and lightning flash. The N.5 shader must preserve this lighting machinery to maintain visual identity to N.4.
|
||||||
|
|
||||||
|
The vert outputs need to ADD `vWorldPos` (used by `accumulateLights` and `applyFog`). Update the vert from Step 5.2 to also emit `out vec3 vWorldPos;` and `vWorldPos = worldPos.xyz;` in main.
|
||||||
|
|
||||||
|
Create `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` with the same lighting UBO + functions as `mesh_instanced.frag`, plus the bindless texture + alpha-test discard logic:
|
||||||
|
|
||||||
```glsl
|
```glsl
|
||||||
#version 430 core
|
#version 430 core
|
||||||
|
|
@ -610,13 +614,69 @@ Create `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`:
|
||||||
|
|
||||||
in vec3 vNormal;
|
in vec3 vNormal;
|
||||||
in vec2 vTexCoord;
|
in vec2 vTexCoord;
|
||||||
|
in vec3 vWorldPos;
|
||||||
in flat uvec2 vTextureHandle;
|
in flat uvec2 vTextureHandle;
|
||||||
in flat uint vTextureLayer;
|
in flat uint vTextureLayer;
|
||||||
|
|
||||||
uniform int uRenderPass; // 0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95)
|
// 0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95)
|
||||||
uniform vec3 uAmbient;
|
uniform int uRenderPass;
|
||||||
uniform vec3 uSunDir;
|
|
||||||
uniform vec3 uSunColor;
|
// 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;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
|
@ -624,30 +684,92 @@ void main() {
|
||||||
sampler2DArray tex = sampler2DArray(vTextureHandle);
|
sampler2DArray tex = sampler2DArray(vTextureHandle);
|
||||||
vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer)));
|
vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer)));
|
||||||
|
|
||||||
|
// Two-pass alpha-test (N.5 Decision 2 — replaces mesh_instanced's
|
||||||
|
// uTranslucencyKind=1 ClipMap-only discard with a more aggressive
|
||||||
|
// pattern that also handles AlphaBlend correctly via two passes).
|
||||||
if (uRenderPass == 0) {
|
if (uRenderPass == 0) {
|
||||||
// Opaque pass: discard soft pixels — they belong to the transparent pass.
|
if (color.a < 0.95) discard; // opaque pass
|
||||||
if (color.a < 0.95) discard;
|
|
||||||
} else {
|
} else {
|
||||||
// Transparent pass: discard hard pixels (already drawn opaque).
|
if (color.a >= 0.95) discard; // transparent pass
|
||||||
if (color.a >= 0.95) discard;
|
if (color.a < 0.05) discard; // skip totally-empty
|
||||||
if (color.a < 0.05) discard; // skip totally-empty fragments
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 N = normalize(vNormal);
|
vec3 N = normalize(vNormal);
|
||||||
vec3 L = normalize(uSunDir);
|
vec3 lit = accumulateLights(N, vWorldPos);
|
||||||
float diff = max(dot(N, L), 0.0);
|
|
||||||
vec3 lit = uAmbient + uSunColor * diff;
|
|
||||||
color.rgb *= clamp(lit, 0.0, 1.0);
|
|
||||||
|
|
||||||
FragColor = color;
|
// 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);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: this initial version uses `uniform vec3` for the lighting params instead of a UBO. This matches the existing `mesh_instanced.frag` pattern (verify by reading it). If `mesh_instanced.frag` actually uses a UBO, change to match.
|
- [ ] **Step 5.4: Update mesh_modern.vert to emit vWorldPos**
|
||||||
|
|
||||||
- [ ] **Step 5.4: Read existing mesh_instanced.frag to verify lighting layout**
|
Add `vWorldPos` output to the vert from Step 5.2. The full vert becomes:
|
||||||
|
|
||||||
Read `src/AcDream.App/Rendering/Shaders/mesh_instanced.frag`. Compare its lighting uniform shape to the version above. Adjust `mesh_modern.frag` to match (UBO if existing uses UBO, vec3 uniforms if existing uses uniforms).
|
```glsl
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(The vert from Step 5.2 should be REPLACED with this. The two are the same except for `vWorldPos` and a small comment cleanup.)
|
||||||
|
|
||||||
- [ ] **Step 5.5: Build to verify shaders are copied to output**
|
- [ ] **Step 5.5: Build to verify shaders are copied to output**
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue