feat(render): Phase G.1 — billboard particle renderer for weather + spells

Add ParticleRenderer that draws every live particle from the shared
ParticleSystem as a billboarded quad. Unit quad VBO + per-instance
(pos, size, color) VBO with glVertexAttribDivisor for one draw call
per emitter. Billboards using the camera's basis vectors so quads
always face the viewer.

Fragment shader does a procedural radial falloff (no texture pipeline
needed — raindrops / snowflakes read as soft dots). AttachLocal
emitters get re-centred on the camera each frame so the rain volume
follows the player per r12 §7.

Two-pass render splits additive from alpha-blend emitters so blend
state flips once per kind rather than per-emitter.

Wired into GameWindow.OnRender after static-mesh draw with depth
write off (particles occluded by walls but don't self-occlude).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-19 10:42:05 +02:00
parent 9957070cab
commit 9618c66813
4 changed files with 283 additions and 0 deletions

View file

@ -140,6 +140,7 @@ public sealed class GameWindow : IDisposable
private readonly AcDream.Core.Vfx.EmitterDescRegistry _emitterRegistry = new();
private AcDream.Core.Vfx.ParticleSystem? _particleSystem;
private AcDream.Core.Vfx.ParticleHookSink? _particleSink;
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
// Remote-entity motion inference: tracks when each remote entity last
// moved meaningfully. Used in TickAnimations to swap to Ready when
@ -737,6 +738,12 @@ public sealed class GameWindow : IDisposable
_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(
_gl, _dats, skyShader, _textureCache);
// Phase G.1 particle renderer — renders rain / snow / spell auras
// spawned into the shared ParticleSystem as billboard quads.
// Weather uses AttachLocal emitters so the rain volume follows
// the player.
_particleRenderer = new ParticleRenderer(_gl, shadersDir);
// Phase A.1: replace the one-shot 3×3 preload with a streaming controller.
// Parse runtime radius from environment (default 2 → 5×5 window).
// Values outside [0, 8] fall back to the field default of 2.
@ -3035,6 +3042,13 @@ public sealed class GameWindow : IDisposable
neverCullLandblockId: playerLb,
visibleCellIds: visibility?.VisibleCellIds);
// Phase G.1 / E.3: draw all live particles after opaque
// scene geometry so alpha blending composites correctly.
// Runs with depth test on (particles occluded by walls)
// but depth write off (no self-occlusion sorting needed).
if (_particleSystem is not null && _particleRenderer is not null)
_particleRenderer.Draw(_particleSystem, camera, camPos);
// Debug: draw collision shapes as wireframe cylinders around the
// player so we can visually verify alignment with scenery meshes.
if (_debugCollisionVisible && _debugLines is not null)
@ -3690,6 +3704,7 @@ public sealed class GameWindow : IDisposable
_shader?.Dispose();
_sceneLightingUbo?.Dispose();
_skyRenderer?.Dispose();
_particleRenderer?.Dispose();
_debugLines?.Dispose();
_textRenderer?.Dispose();
_debugFont?.Dispose();