acdream/src/AcDream.App/Rendering/Shaders/terrain.frag
Erik 9957070cab 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>
2026-04-19 10:39:48 +02:00

149 lines
4.8 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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);
}