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
150
src/AcDream.Core/Lighting/SceneLightingUbo.cs
Normal file
150
src/AcDream.Core/Lighting/SceneLightingUbo.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue