acdream/src/AcDream.Core/Lighting/SceneLightingUbo.cs
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

150 lines
5.6 KiB
C#
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.

using System;
using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.Core.World;
namespace AcDream.Core.Lighting;
/// <summary>
/// GPU-facing scene-lighting UBO layout. Matches the GLSL block in
/// <c>mesh.frag</c> / <c>mesh_instanced.vert</c> / <c>terrain.vert</c>
/// bound at binding=1. std140-compliant — each <c>vec4</c> member
/// lives on a 16-byte boundary, arrays of <c>vec4</c> pack contiguously,
/// and no pad elements are required because the struct's fields are
/// already 16-byte-aligned.
///
/// <para>
/// Layout (r13 §12.3 extended with R12 §13.2 sun+fog):
/// <code>
/// 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 scalar
/// vec4 coneAngleEtc; // x = cone (rad), y=unused, z=unused, w=unused
/// };
///
/// layout(std140, binding = 1) uniform SceneLighting {
/// Light uLights[8]; // 8 * 64 bytes = 512 bytes
/// vec4 uCellAmbient; // xyz = ambient RGB, w = active light count
/// vec4 uFogParams; // x = fogStart, y = fogEnd, z = lightningFlash, w = fogMode
/// vec4 uFogColor; // xyz = fog RGB, w = unused
/// vec4 uCameraAndTime; // xyz = camera world pos, w = day fraction (debug / sky shader)
/// };
/// </code>
/// </para>
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct UboLight
{
public Vector4 PosAndKind;
public Vector4 DirAndRange;
public Vector4 ColorAndIntensity;
public Vector4 ConeAngleEtc;
/// <summary>Pack a <see cref="LightSource"/> into UBO-ready bytes.</summary>
public static UboLight FromSource(LightSource ls)
{
return new UboLight
{
PosAndKind = new Vector4(ls.WorldPosition, (float)(int)ls.Kind),
DirAndRange = new Vector4(ls.WorldForward, ls.Range),
ColorAndIntensity = new Vector4(ls.ColorLinear, ls.Intensity),
ConeAngleEtc = new Vector4(ls.ConeAngle, 0f, 0f, 0f),
};
}
/// <summary>Packed "zero" light — stored in unused UBO slots so shaders
/// don't read garbage. <c>dirAndRange.w = 0</c> disables the light
/// even if the active-count sentinel is wrong.</summary>
public static UboLight Empty => new()
{
PosAndKind = Vector4.Zero,
DirAndRange = Vector4.Zero,
ColorAndIntensity = Vector4.Zero,
ConeAngleEtc = Vector4.Zero,
};
}
/// <summary>
/// Full CPU-side scene-lighting UBO buffer. One per frame; lives on the
/// render thread. The GL-side wrapper (<c>SceneLightingUboBinding</c>
/// in AcDream.App) uploads this to binding=1 once per frame.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SceneLightingUbo
{
// 8 lights × 64 bytes = 512 bytes
public UboLight Light0;
public UboLight Light1;
public UboLight Light2;
public UboLight Light3;
public UboLight Light4;
public UboLight Light5;
public UboLight Light6;
public UboLight Light7;
public Vector4 CellAmbient; // xyz = ambient RGB, w = active count
public Vector4 FogParams; // x = fogStart, y = fogEnd, z = flash, w = fogMode
public Vector4 FogColor; // xyz = color, w = reserved
public Vector4 CameraAndTime; // xyz = camera pos, w = day fraction
public const int SizeInBytes = 8 * 64 + 4 * 16; // 576
public const int BindingPoint = 1;
/// <summary>
/// Build the full per-frame UBO payload from:
/// <list type="bullet">
/// <item><description>An already-ticked <see cref="LightManager"/>.</description></item>
/// <item><description>The current <see cref="AtmosphereSnapshot"/> (sky + weather).</description></item>
/// <item><description>The current camera world position (sky shader needs it, fog shader needs it).</description></item>
/// <item><description>The current day fraction (sky shader needs it for scrolling clouds).</description></item>
/// </list>
/// </summary>
public static SceneLightingUbo Build(
LightManager lights,
in AtmosphereSnapshot atmo,
Vector3 cameraWorldPos,
float dayFraction)
{
ArgumentNullException.ThrowIfNull(lights);
var ubo = new SceneLightingUbo();
// Pack up to 8 lights. Empty slots stay zero.
var active = lights.Active;
int count = active.Length;
if (count > 8) count = 8;
for (int i = 0; i < 8; i++)
{
var packed = (i < count && active[i] is not null)
? UboLight.FromSource(active[i]!)
: UboLight.Empty;
SetLightAt(ref ubo, i, packed);
}
ubo.CellAmbient = new Vector4(lights.CurrentAmbient.AmbientColor, count);
ubo.FogParams = new Vector4(
atmo.FogStart,
atmo.FogEnd,
atmo.LightningFlash,
(float)(int)atmo.FogMode);
ubo.FogColor = new Vector4(atmo.FogColor, 0f);
ubo.CameraAndTime = new Vector4(cameraWorldPos, dayFraction);
return ubo;
}
private static void SetLightAt(ref SceneLightingUbo ubo, int i, in UboLight v)
{
switch (i)
{
case 0: ubo.Light0 = v; break;
case 1: ubo.Light1 = v; break;
case 2: ubo.Light2 = v; break;
case 3: ubo.Light3 = v; break;
case 4: ubo.Light4 = v; break;
case 5: ubo.Light5 = v; break;
case 6: ubo.Light6 = v; break;
case 7: ubo.Light7 = v; break;
}
}
}