Fensalir/docs/hlsl-renderer.md
2026-05-09 12:05:18 +01:00

15 KiB

HLSL Renderer

Aquarium now has a first shader-owned image path.

Current Path

  • D3D11Renderer compiles Render/Shaders/Aquarium.hlsl at startup with Vortice.D3DCompiler.
  • The renderer draws one fullscreen triangle.
  • The pixel shader raymarches the Grid heightfield, Self sun, and orbiting planets.
  • C# feeds only explicit frame constants: resolution, time, Grid radius, camera position, presentation controls, temporal state, and debug mode.
  • CultMath is referenced as the CPU-side math grammar. Vortice math stays at the graphics API membrane.

Current Limits

This is still a first image path, not the final renderer. The shader keeps planet broad-phase cheap by avoiding terrain samples inside every planet anchor. Terrain-hugging orbit cups should come back through a real object table or precomputed frame constants, not repeated procedural work inside every SDF sample. That was the tiny furnace. It has been told to sit down.

Ported Renderer Lessons

The first native renderer pass carries the pieces that fit a single HLSL raymarch path:

  • body-driven powerPulse Grid wells
  • world-space Grid weather with domain warping and filament layers
  • stable planet-local displaced SDFs
  • material-specific normals so terrain and body shading do not re-evaluate the entire scene six times per visible pixel

Volumetric lighting, brick maps, HDR bloom, and debug controls are not one-pass pixel-shader jobs. They need engine systems around the shader rather than more stuffing in AquariumPS.

The SDF field renderer plan lives in docs/sdf-field-renderer-plan.md. Its central constraint is that Aquarium/Aetheria-style clouds are local SDF media: above, below, around, and sometimes enclosing the camera. They are not a fancy skybox, so sky-dome/paraboloid-only cloud optimizations are not valid as the core cloud architecture.

Deferred Grid Pass

The renderer now has a deferred Grid height target.

Current frame order:

  1. C# bins Self and planet bounds into a small Grid-space froxel table and uploads fixed primitive id slots as a structured buffer.
  2. GridHeightPS renders body/gravity height into a 128x128 Grid-space R32G32B32A32_Float render target.
  3. AquariumScenePS raymarches solids and the Grid against that height texture, writing linear HDR color plus ray travel.
  4. Body SDF checks read only the primitive ids binned into the current froxel. Empty froxels skip body SDF work entirely.
  5. Displaced body SDFs report a noisy hit distance separately from a conservative envelope step distance, so high-frequency surface detail can shape the silhouette without letting rays skip over it. When a ray is inside the conservative envelope but outside the noisy surface, it takes a shell escape step instead of starving on the minimum stride.
  6. Terrain marching uses heightfield crossing and slope-aware steps instead of pretending z - height(xy) is a true Euclidean SDF. Body objects still use SDF distance steps.
  7. Terrain normals sample adjacent Grid texels in world space instead of re-evaluating analytic height per normal tap.
  8. The Grid target is centered on the camera target; body anchors remain in world space.
  9. Direct2D/DirectWrite draws overlay text onto the swapchain backbuffer after the shader image is complete and before Present. Overlay text is therefore final-pixel UI, not HDR scene material, and should stay out of bloom, tonemapping, and raymarching unless a specific diegetic surface asks for the trouble.

The shader far distance is frame-derived, not a fixed constant. C# sends the distance from the camera to the Grid origin plus the Grid radius, which matches the circular Grid fade boundary and avoids wasting raymarch work past the visible field. Changes to the AquariumFrame shader constant buffer are host contract changes; dev-watch.ps1 fingerprints that block and restarts the host instead of treating it as ordinary shader hot reload.

The target currently stores height in .r only. The next useful expansion is to store packed gradients in .g/.b during the Grid pass so the raymarcher can use one texture fetch for height and a cheaper normal path when fidelity allows it.

Grid linework is derivative-aware in the final raymarch pass. Cartesian Gridlines use screen-space fwidth against their world-domain coordinate so minor and major lines stay the same pixel width across zoom distances. Terrain height isolines and gradient-angle field lines follow the older Aetheria UI shader pattern: height contours are derived from the Grid height field, while angle lines quantize the terrain gradient direction and fade out on flat surfaces.

SDF Cloud Scaffold

Aquarium.hlsl still contains an analytic SDF cloud-medium scaffold. It defines four bounded local cloud fields in world/Grid space, including clouds below and around the Grid, but AquariumPS no longer composites those clouds into the scene. The scaffold is retained only as evidence and throwaway machinery while the waterfall field renderer is built.

color = cloud_scattering + cloud_transmittance * surface_color

That composition shape belongs to the future explicit field/volume renderer, not to the current fullscreen shader.

Current prototype traits:

  • ellipsoid SDF cloud containers
  • stable world/Grid-space placement
  • analytic ray/ellipsoid interval stepping, so empty space jumps to real cloud entries instead of spending samples on SDF shells
  • procedural erosion/noise inside each field
  • feathered interior density, without positive-distance absorbing shells around the ellipsoid boundary
  • Self-lit scattering and simple forward/back phase shaping
  • no skybox/paraboloid assumption
  • no sparse brick storage yet
  • no debug view yet

Transparent Grid Overlay

The Grid now renders as a transparent schematic overlay. AquariumScenePS traces bodies separately from the Grid surface, shades solid bodies first, then intersects the Grid height field. The Grid only draws when its surface is closer than the nearest solid body, so it behaves like diegetic scene UI rather than a final-image HUD layer over planets.

Grid coverage follows Aetheria's dithered particle transparency pattern: screen-space stochastic alpha test instead of conventional alpha blending. This uses Aetheria's LDR_LLL1_0 blue-noise texture bound as a wrapping R8_UNorm shader resource, following the _DitheringTex + _FrameNumber * 1.61803398875 ditherClip path from Dither Functions.cginc.

This keeps the current scene legible while leaving future volumetrics to provide the real spatial mass.

Temporal Resolve

The first clean-room TSR-inspired temporal pass is documented in docs/tsr-inspired-taa-spec.md.

Aquarium now renders the scene into an HDR/travel target first:

rgb = linear scene color
a   = ray travel in world units

AquariumResolvePS then resolves that target against a ping-pong HDR history texture. The resolver reconstructs the current world hit from the jittered camera ray and current travel, projects that point into the previous camera basis, samples history, clamps it to the current 3x3 neighborhood, rejects by travel delta, and only then tonemaps to the backbuffer.

This started as Gate 1 of the TAA plan: camera reprojection, jitter, travel rejection, neighborhood clamping, and history ping-pong. Gate 2A now adds a scene metadata target and matching history metadata, storing field id and surface normal so history is rejected across field/normal discontinuities. The Grid also records its own travel and normal when it is the nearest diegetic surface, even if the current stochastic coverage sample does not draw color. Gate 2B adds stable field ids for Self/Grid/each planet and maps orbiting planet hits back to their previous center before camera reprojection. Gate 3A adds a current-frame temporal-control target. It carries reactive strength, stochastic/composition coverage, and a reserved medium-opacity slot; the resolve now reduces history for low-coverage reactive Grid samples before future volumetrics arrive. Gate 3B ping-pongs that control target as history too, so the resolver can compare previous/current coverage and reserved medium opacity at the reprojected UV. Gate 3C stores accepted history age in the control-history w channel and ramps history authority as that age grows. Gate 3D fixes travel validation to compare previous history travel against the reprojected point's expected distance from the previous camera, not the current frame's camera-to-hit travel. Gate 3E gives stochastic Grid history its own validation path: it bypasses opaque-surface neighborhood color clamp/color-delta rejection while still using field, travel, normal, coverage, reactive, and age checks. Gate 3F gives that stochastic Grid path a higher-retention history blend and no coverage/reactive penalty, because low coverage is the signal that needs temporal accumulation rather than the signal that should lose history. Gate 3G point-loads identity/control history and nearest-loads Grid history color, preventing bright foreground silhouettes from bilinearly bleeding into background Grid accumulation. Grid retention is also pulled back from 0.975 to 0.94 to keep lines less anesthetized. Gate 3H makes Grid temporal coverage line support rather than broad overlay alpha, so history age/weight cannot survive as smoke across low-alpha field tint. Grid retention is pulled back again to 0.90. Gate 3I reconstructs analytic Grid overlay color in the resolve pass for Grid pixels. Raw scene debug mode can still show stochastic hit/miss noise; final resolve uses the analytic current estimate plus validated history instead of averaging binary coverage like a tiny broken casino. Gate 3J restores debug mode 1 to true raw scene color and stops blending previous Grid color history into final Grid pixels. Grid history remains a diagnostic/output history surface, but final Grid color comes from the analytic current estimate. Gate 3L scales projection jitter to zero for the live frame because Self and planet surfaces were visibly juddering. Jitter should remain off until the resolve hides it on both opaque surfaces and the Grid. The abandoned unjittered Grid retrace has been removed; with jitter disabled, Grid analytic color uses the existing current hit. Gate 3M cuts temporal history out of final presentation. The resolver still writes history, metadata, control, age, and weight for debug and future medium work, but mode 0 presents current-frame color instead of blending stale surface history into opaque bodies.

Transparent stochastic surfaces need a separate composition and temporal contract from opaque solids. The Grid and future particles belong to the shared event-stream design in docs/stochastic-transparent-surface-pipeline.md, not to the scene first-hit surface path. A ray can encounter multiple transparent events; stochastic coverage may choose different events across frames; temporal validation must compare distribution summaries instead of one canonical travel.

The presentation path now applies an explicit exposure before display transformation and adds a low-gain pre-tonemap bloom/veil pyramid. The bloom pass renders exposed scene-linear HDR color into half-, quarter-, and eighth-resolution targets, uses firefly-safe downsampling, blurs each level with separable horizontal/vertical passes, then contributes gently before the ACES fit. This is not a thresholded glow pass; bright energy spreads without turning the frame into a 2000s bloom accident.

Temporal debug modes are available in the running window: F1 cycles modes and number keys 0 through 9 select the first ten modes directly. Mode 0 is final resolve, 1 is raw current scene, 2 is the reprojected history sample, 3 is history age, 4 is history weight, 5 is current temporal control, 6 is current field identity, 7 is bloom contribution, and 8 is exposed luminance. The startup mode can still be set with --render-debug or AQUARIUM_RENDER_DEBUG_MODE.

Fog work has started behind diagnostics rather than final-frame haze. The host packs stable field instances into a structured buffer shared with HLSL. A packed froxel medium pass writes 32 frustum slices into diagnostic and transport textures before the scene pass. At 1280x720 the first volume is 160x90x32, packed as an 8x4 D3D11 atlas. Each froxel reconstructs a world position and samples registered medium density there; resolve integrates those cells front-to-back for composition and diagnostics. Mode 9 previews direct registered-medium density at the selected ray step, mode 10 previews direct ray transmittance to that step, and mode 11 shows integrated froxel density. Direct ray debug remains the correctness reference while the froxel pass earns final-frame authority. Final composition uses surface * transmittance + in-scattering, gated by the persisted Medium Composite control. The default is 0, so the pass can be inspected without changing the presented frame until the medium is tuned.

The running window also has a Direct2D debug panel toggled with F2. It follows the CultLib code-first composition style rather than an immediate-mode toolkit: controls are retained rows bound to Func<T>/Action<T> pairs. The first panel drives render debug mode, exposure, bloom intensity, bloom veil, and medium composite strength. Those presentation controls feed the shader constant buffer directly and round-trip through the global GraphicsSettings CultCache document; they are not cosmetic labels painted over hardcoded renderer state.

It deliberately does not pretend to solve general non-rigid field velocity, volumetric history, or history resurrection yet.

Overlay Text

Readable overlay text is handled by DirectWriteOverlay, using DirectWrite for font layout/rasterization and Direct2D for the final draw. The D3D swapchain uses B8G8R8A8_UNorm so the same backbuffer can be shared with Direct2D without format drama. This path is for debug UI, terminal text, inspectors, and future CultUI overlay surfaces. Diegetic labels that live in the scene still belong in the renderer as MSDF/SDF billboards.

The debug controls live in Render/Ui/DebugUi.cs. The intended grammar is a native version of CultLib's generator API: panels, sections, sliders, toggles, options, and buttons are declared in code and bound to engine state with explicit read and write delegates. The render debug selector uses named options instead of a numeric slider so the growing debug surface remains legible. The visual language is flat and compact: dark panel, slab rows, thin dividers, orange accent, and pale values. It is debug UI, but it does not have permission to be ugly by default.

The overlay owns a private DirectWrite font collection built from bundled Google Fonts files in Assets/Fonts: Montserrat for thin small-cap display text and Ubuntu Sans for body/debug copy. The runtime does not depend on those fonts being installed in Windows.

Startup status text is drawn by the Win32 splash path before D3D exists. That path uses GDI with the same bundled font files registered privately for the process, then reports coarse initialization stages while the renderer creates the device, swapchain targets, shaders, and buffers.