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>
149 lines
4.8 KiB
GLSL
149 lines
4.8 KiB
GLSL
#version 430 core
|
||
// Per-cell terrain blending (Phase 3c.4) — ported from WorldBuilder's
|
||
// Landscape.frag, trimmed of editor-specific features (grid, brush,
|
||
// walkable-slope highlighting). Phase G extends this with the shared
|
||
// SceneLighting UBO driving per-vertex sun bake + fragment-stage fog
|
||
// + lightning flash.
|
||
|
||
in vec2 vBaseUV;
|
||
in vec3 vWorldNormal;
|
||
in vec3 vWorldPos;
|
||
in vec3 vLightingRGB;
|
||
in vec4 vOverlay0;
|
||
in vec4 vOverlay1;
|
||
in vec4 vOverlay2;
|
||
in vec4 vRoad0;
|
||
in vec4 vRoad1;
|
||
flat in float vBaseTexIdx;
|
||
|
||
out vec4 fragColor;
|
||
|
||
uniform sampler2DArray uTerrain; // 33+ layers — TerrainAtlas.GlTexture
|
||
uniform sampler2DArray uAlpha; // 8+ layers — TerrainAtlas.GlAlphaTexture
|
||
|
||
// Shared scene-lighting UBO — fog + flash are consumed here; the per-vertex
|
||
// AdjustPlanes bake already incorporated sun + ambient.
|
||
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;
|
||
};
|
||
|
||
// Per-texture tiling repeat count across a cell. WorldBuilder uses
|
||
// uTexTiling[36] uploaded from the dats; we default to 1.0 (one tile per
|
||
// cell, 8 tiles across a landblock).
|
||
const float TILE = 1.0;
|
||
|
||
// Three-layer alpha-weighted composite.
|
||
vec4 maskBlend3(vec4 t0, vec4 t1, vec4 t2, float h0, float h1, float h2) {
|
||
float a0 = h0 == 0.0 ? 1.0 : t0.a;
|
||
float a1 = h1 == 0.0 ? 1.0 : t1.a;
|
||
float a2 = h2 == 0.0 ? 1.0 : t2.a;
|
||
float aR = 1.0 - (a0 * a1 * a2);
|
||
float aRsafe = max(aR, 1e-6);
|
||
a0 = 1.0 - a0;
|
||
a1 = 1.0 - a1;
|
||
a2 = 1.0 - a2;
|
||
vec3 r0 = (a0 * t0.rgb + (1.0 - a0) * a1 * t1.rgb + (1.0 - a1) * a2 * t2.rgb);
|
||
return vec4(r0 / aRsafe, aR);
|
||
}
|
||
|
||
vec4 combineOverlays(vec2 baseUV, vec4 pOverlay0, vec4 pOverlay1, vec4 pOverlay2) {
|
||
float h0 = pOverlay0.z < 0.0 ? 0.0 : 1.0;
|
||
float h1 = pOverlay1.z < 0.0 ? 0.0 : 1.0;
|
||
float h2 = pOverlay2.z < 0.0 ? 0.0 : 1.0;
|
||
vec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0);
|
||
|
||
if (h0 > 0.0) {
|
||
t0 = texture(uTerrain, vec3(baseUV * TILE, pOverlay0.z));
|
||
if (pOverlay0.w >= 0.0) {
|
||
vec4 a = texture(uAlpha, vec3(pOverlay0.xy, pOverlay0.w));
|
||
t0.a = a.a;
|
||
}
|
||
}
|
||
if (h1 > 0.0) {
|
||
t1 = texture(uTerrain, vec3(baseUV * TILE, pOverlay1.z));
|
||
if (pOverlay1.w >= 0.0) {
|
||
vec4 a = texture(uAlpha, vec3(pOverlay1.xy, pOverlay1.w));
|
||
t1.a = a.a;
|
||
}
|
||
}
|
||
if (h2 > 0.0) {
|
||
t2 = texture(uTerrain, vec3(baseUV * TILE, pOverlay2.z));
|
||
if (pOverlay2.w >= 0.0) {
|
||
vec4 a = texture(uAlpha, vec3(pOverlay2.xy, pOverlay2.w));
|
||
t2.a = a.a;
|
||
}
|
||
}
|
||
return maskBlend3(t0, t1, t2, h0, h1, h2);
|
||
}
|
||
|
||
vec4 combineRoad(vec2 baseUV, vec4 pRoad0, vec4 pRoad1) {
|
||
float h0 = pRoad0.z < 0.0 ? 0.0 : 1.0;
|
||
float h1 = pRoad1.z < 0.0 ? 0.0 : 1.0;
|
||
vec4 result = vec4(0.0);
|
||
if (h0 > 0.0) {
|
||
result = texture(uTerrain, vec3(baseUV * TILE, pRoad0.z));
|
||
if (pRoad0.w >= 0.0) {
|
||
vec4 a0 = texture(uAlpha, vec3(pRoad0.xy, pRoad0.w));
|
||
result.a = 1.0 - a0.a;
|
||
if (h1 > 0.0 && pRoad1.w >= 0.0) {
|
||
vec4 a1 = texture(uAlpha, vec3(pRoad1.xy, pRoad1.w));
|
||
result.a = 1.0 - (a0.a * a1.a);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
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 baseColor = vec4(0.0);
|
||
if (vBaseTexIdx >= 0.0) {
|
||
baseColor = texture(uTerrain, vec3(vBaseUV * TILE, vBaseTexIdx));
|
||
}
|
||
|
||
vec4 overlays = vec4(0.0);
|
||
if (vOverlay0.z >= 0.0)
|
||
overlays = combineOverlays(vBaseUV, vOverlay0, vOverlay1, vOverlay2);
|
||
|
||
vec4 roads = vec4(0.0);
|
||
if (vRoad0.z >= 0.0)
|
||
roads = combineRoad(vBaseUV, vRoad0, vRoad1);
|
||
|
||
// Composite: base × (1 - ovlA) × (1 - rdA) + ovl × ovlA × (1 - rdA) + road × rdA
|
||
vec3 baseMasked = baseColor.rgb * ((1.0 - overlays.a) * (1.0 - roads.a));
|
||
vec3 ovlMasked = overlays.rgb * (overlays.a * (1.0 - roads.a));
|
||
vec3 roadMasked = roads.rgb * roads.a;
|
||
vec3 rgb = clamp(baseMasked + ovlMasked + roadMasked, 0.0, 1.0);
|
||
|
||
// Apply the per-vertex baked sun+ambient.
|
||
vec3 lit = rgb * min(vLightingRGB, vec3(1.0));
|
||
|
||
// Lightning flash — additive.
|
||
float flash = uFogParams.z;
|
||
lit += flash * vec3(0.6, 0.6, 0.75);
|
||
|
||
// Atmospheric fog.
|
||
lit = applyFog(lit, vWorldPos);
|
||
|
||
fragColor = vec4(lit, 1.0);
|
||
}
|