feat(render): Phase G.1/G.2 — SceneLighting UBO + sky renderer + shader integration
Wire the existing LightManager + WorldTimeService state into visible
rendering. Every draw call (terrain, static mesh, instanced mesh, sky)
now shares one SceneLighting UBO at binding=1 carrying:
- 8 Light slots (Directional / Point / Spot, retail hard-cutoff)
- Ambient RGB + active light count
- Fog start/end/mode + color + lightning flash scalar
- Camera world position + day fraction
The CPU side (SceneLightingUbo in Core.Lighting) is a POD struct that
gets BufferSubData'd once per frame from GameWindow.OnRender. Shaders
read the block via `layout(std140, binding = 1) uniform SceneLighting`
— no per-program uniform uploads.
Shader changes:
- mesh.frag + mesh_instanced.frag accumulate 8 dynamic lights per
fragment using the retail no-attenuation hard-cutoff model
(r13 §10.2 / §13.1). Sun reads slot 0; spots use hard cos-cone test.
Additive lightning flash + linear fog layered on top. Saturate
clamps per-channel to 1.0.
- terrain.vert bakes AdjustPlanes sun+ambient per vertex using the
retail MIN_FACTOR = 0.08 ambient floor (r13 §7). terrain.frag adds
fog + flash on top of the baked vertex color.
- mesh.vert + mesh_instanced.vert emit vWorldPos so the fragment
stage can do per-pixel lighting against world-space positions.
- New sky.vert / sky.frag pair — unlit, scroll-UV, camera-centered,
with its own 0.1..1e6 far plane. Ports WorldBuilder's skybox.
SkyRenderer (new file in App/Rendering/Sky/) ports WorldBuilder's
SkyboxRenderManager verbatim for the C# idiom: zeroed view translation,
dedicated projection, depth mask off, iterate each visible SkyObject
in the day group, apply arc transform (Z rot for heading + Y rot for
arc sweep), feed TexVelocityX/Y as a scrolling UV offset, apply
per-keyframe SkyObjectReplace overrides (mesh swap + transparency +
luminosity) for overcast / dusk cloud variants.
GameWindow integration:
- OnLoad parses Region (0x13000000) into LoadedSkyDesc and hot-swaps
WorldTime's provider to the dat-accurate keyframes. Seeds to noon
for offline rendering. Creates the SceneLightingUboBinding and the
SkyRenderer.
- OnRender: set clear color from atmosphere fog, tick WeatherSystem,
spawn/stop rain/snow camera-local emitters on kind change, feed
sun to LightManager (zero intensity indoors — r13 §13.7), tick
LightManager against viewer pos, build + upload the UBO, draw
sky before terrain, draw terrain + static + instanced using the
shared UBO.
5 new UBO packing tests (struct sizes, slot population, 8-light cap,
directional slot 0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0df1c5b4a6
commit
9957070cab
15 changed files with 1255 additions and 91 deletions
|
|
@ -1,6 +1,7 @@
|
|||
#version 430 core
|
||||
in vec2 vTex;
|
||||
in vec3 vWorldNormal;
|
||||
in vec3 vWorldPos;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D uDiffuse;
|
||||
|
|
@ -11,35 +12,114 @@ uniform sampler2D uDiffuse;
|
|||
// 2 = AlphaBlend — GL blending handles compositing; do NOT discard
|
||||
// 3 = Additive — GL additive blending; do NOT discard
|
||||
// 4 = InvAlpha — GL inverted-alpha blending; do NOT discard
|
||||
//
|
||||
// Only ClipMap uses the alpha-discard path. AlphaBlend/Additive/InvAlpha
|
||||
// rely entirely on the GL blend stage — discarding low-alpha fragments
|
||||
// would make semi-transparent surfaces (portals, glows) fully invisible.
|
||||
uniform int uTranslucencyKind;
|
||||
|
||||
// Phase 3a: simple directional lighting. A single sun direction + ambient term
|
||||
// gives scenery and building faces enough differentiation to read as 3D instead
|
||||
// of looking like paper cutouts. Hardcoded for now; a later phase can route
|
||||
// light parameters through uniforms driven by the game's time-of-day.
|
||||
// Sun direction tuned after Phase 3a verification: (0.4,0.3,0.8) was too
|
||||
// vertical — roofs and ground both landed near peak brightness and only
|
||||
// walls dropped, so the contrast was hard to read through textures. More
|
||||
// oblique + lower ambient + higher diffuse = contrast ratio ~3.3x.
|
||||
const vec3 SUN_DIR = normalize(vec3(0.5, 0.4, 0.6));
|
||||
const float AMBIENT = 0.25;
|
||||
const float DIFFUSE = 0.75;
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Phase G.1+G.2: shared scene-lighting UBO (binding = 1).
|
||||
//
|
||||
// Layout mirrors SceneLightingUbo in C#:
|
||||
// struct Light {
|
||||
// vec4 posAndKind; xyz = world pos, w = kind (0=dir,1=point,2=spot)
|
||||
// vec4 dirAndRange; xyz = forward, w = range (metres, hard cutoff)
|
||||
// vec4 colorAndIntensity; xyz = RGB linear, w = intensity
|
||||
// vec4 coneAngleEtc; x = cone (rad), yzw = reserved
|
||||
// };
|
||||
// layout(std140, binding = 1) uniform SceneLighting {
|
||||
// Light uLights[8];
|
||||
// vec4 uCellAmbient; xyz = ambient RGB, w = active count
|
||||
// vec4 uFogParams; x = start, y = end, z = flash, w = mode
|
||||
// vec4 uFogColor; xyz = color
|
||||
// vec4 uCameraAndTime; xyz = camera pos, w = day fraction
|
||||
// };
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
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;
|
||||
};
|
||||
|
||||
// Retail hard-cutoff lighting equation (r13 §10.2). No distance
|
||||
// attenuation inside Range; hard edge at Range; spotlights use a
|
||||
// binary cos-cone test. This is deliberate — the retail "bubble of
|
||||
// light" look relies on crisp boundaries.
|
||||
vec3 accumulateLights(vec3 N, vec3 worldPos) {
|
||||
vec3 lit = uCellAmbient.xyz;
|
||||
int active = int(uCellAmbient.w);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (i >= active) break;
|
||||
|
||||
int kind = int(uLights[i].posAndKind.w);
|
||||
vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w;
|
||||
|
||||
if (kind == 0) {
|
||||
// Directional: "forward" is the light's direction vector
|
||||
// pointing INTO the scene. N·(-forward) = light-facing.
|
||||
vec3 Ldir = -uLights[i].dirAndRange.xyz;
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
lit += Lcol * ndl;
|
||||
} else {
|
||||
// Point / spot: falloff is a HARD bubble at Range.
|
||||
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; // retail: no attenuation inside Range
|
||||
if (kind == 2) {
|
||||
// Spotlight: hard-edged cos-cone test.
|
||||
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;
|
||||
}
|
||||
|
||||
// Linear fog (r12 §5.1): mode 1 = LINEAR, 0 = off, others reserved.
|
||||
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);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 sampled = texture(uDiffuse, vTex);
|
||||
|
||||
// Alpha cutout only for clip-map surfaces (doors, windows, vegetation).
|
||||
// Blended surface types (AlphaBlend, Additive, InvAlpha) must NOT
|
||||
// discard here — that would make every semi-transparent pixel invisible
|
||||
// before the blend stage even runs.
|
||||
if (uTranslucencyKind == 1 && sampled.a < 0.5) discard;
|
||||
|
||||
vec3 N = normalize(vWorldNormal);
|
||||
float ndotl = max(dot(N, SUN_DIR), 0.0);
|
||||
float lighting = AMBIENT + DIFFUSE * ndotl;
|
||||
fragColor = vec4(sampled.rgb * lighting, sampled.a);
|
||||
vec3 lit = accumulateLights(N, vWorldPos);
|
||||
|
||||
// Lightning flash (r12 §9) — additive cold-white pulse layered on top
|
||||
// of diffuse lighting.
|
||||
float flash = uFogParams.z;
|
||||
lit += flash * vec3(0.6, 0.6, 0.75);
|
||||
|
||||
// Clamp per-channel to 1.0 — matches retail (r13 §13.1).
|
||||
lit = min(lit, vec3(1.0));
|
||||
|
||||
vec3 rgb = sampled.rgb * lit;
|
||||
|
||||
// Atmospheric fog — applied after lighting.
|
||||
rgb = applyFog(rgb, vWorldPos);
|
||||
|
||||
fragColor = vec4(rgb, sampled.a);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue