Three things a game could not say, each of which had been worked around. Audio.play_at(id, gain:, pitch:, pan:) fires a one-shot with its own gain, pitch and stereo position. Audio.volume and Audio.pitch are global - they are the options screen - so a game placing a sound in the world was fighting them, and distance attenuation was simply not expressible. The backend already took volume and rate per call; this adds setPan: alongside them and stops routing through the master state. ludic.render3d gains outline_model(model, mat, width, r, g, b): a rim around something that is not an Actor. The outline pass walked the actor list and stopped, so instanced scatter - a forest - could not be highlighted at all. It is a queue flushed by the same pass, which is what gets the depth test right when the caller does not control pass order. And terrain_coast(cx, cz, margin, fall), the other way to make an island: the sea around the survey's own edge rather than cut out of the middle of it. terrain_island measures a radius from a centre, which drowns two thirds of a real survey to make an island of the rest; this measures inward from the boundary, so everything the data covers stays land and the coast is where the data runs out. Both modes gained a strand - the last few metres of height either side of the water line compressed, which stretches a cliff plunge out into beach and shallows. 132 regression tests and the self-host fixpoints pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
14 KiB
Markdown
120 lines
14 KiB
Markdown
# ludic.render3d — what is implemented, where, and how it compares
|
||
|
||
Every item below names the file (and function or shader) that implements it, so
|
||
the claim can be checked in the tree. Paths are relative to `packages/ludic.render3d`
|
||
unless stated; `Gl.*` lives in `runtime/native/`.
|
||
|
||
## The OpenGL surface (`Gl.*`)
|
||
|
||
- **Every OpenGL 4.1 core entry point** — 478 of 478 `GLAPI` declarations in the
|
||
platform `gl3.h`, generated by `ludic-dev glgen` (`tools/ludic-cli/glgen.ludic`) into
|
||
`runtime/native/gl_api.ludic` (`grep -c '^extern function gl_'` = 478) with one ABI
|
||
thunk each in `runtime/native/gl_thunks.ll`; **901 `GL_*` constants**. Float and
|
||
double parameters take `fixed`; 64-bit sizes take `long`; `GLboolean` returns are
|
||
widened. macOS exposes 4.1 core as its ceiling, so this is the whole API the platform has.
|
||
- Context on the window at Retina resolution (`runtime/native/cocoa.ll`,
|
||
`win_gl_attach`), offscreen CGL context for headless runs (`runtime/native/gl.ll`),
|
||
screenshots, shader/program helpers, IEEE-float helpers over Q16.16 (`runtime/native/gl.ludic`).
|
||
|
||
## Rendering features (numbered so they can be counted)
|
||
|
||
Lighting and materials
|
||
1. Cook-Torrance GGX specular, Smith height-correlated visibility, Schlick Fresnel — `shaders/lighting.glsl` `shade()`
|
||
2. Diffuse image-based lighting from a convolved irradiance map — `sky.ludic` `sky_precompute`, `shaders/ibl_irradiance.frag`
|
||
3. GGX-prefiltered specular environment, 6 roughness levels (importance sampled, mip-filtered) — `shaders/ibl_prefilter.frag`
|
||
4. Split-sum BRDF lookup table — `shaders/ibl_brdf.frag`
|
||
5. HDRI sky with the sun extracted from the map and its irradiance integrated from the clipped texels — `texture.ludic` `hdr_decode`, `sky.ludic`
|
||
6. Sky rotation with all convolutions rebuilt (time-of-azimuth control) — `sky.ludic` `sky_set_yaw`
|
||
7. Normal mapping with derivative-reconstructed tangent frames (models) and a heightfield tangent basis (terrain) — `shaders/model.frag` `cotangentFrame`, `shaders/terrain.frag`
|
||
8. Distant-grass carpet: the clump cards baked straight down into a tiling tile (alpha = coverage) and draped on the ground beyond the blade rings with stochastic anti-tiling, so the far meadow carries the near cover's clumps and gaps — `scatter.ludic` `carpet_bake`, `shaders/terrain.frag` `sampleCarpet` (replaced the parallax pass, whose sampler slot it took)
|
||
9. Stochastic (triangle-grid) anti-tiling texture sampling with rotated taps and `textureGrad` — `shaders/terrain.frag` `sampleMat`
|
||
10. Triplanar mapping for steep rock — `shaders/terrain.frag` `sampleTri`
|
||
11. Foliage translucency and wrapped diffuse (thin-leaf subsurface approximation) — `shaders/model.frag` `FOLIAGE`
|
||
12. Two-sided lighting for thin cards — `shaders/model.frag` `CARD`
|
||
13. Ground-bounce fill light (one bounce from the sunlit meadow onto undersides) — `shaders/lighting.glsl` `bounce`
|
||
14. Screen-space global illumination: one indirect diffuse bounce gathered from the previous anti-aliased frame, plus ambient occlusion, half resolution, depth-aware blur — `shaders/ssgi.frag`, `shaders/ssao_blur.frag`, `post.ludic` `post_ssao_pass`
|
||
15. Specular occlusion from ambient occlusion — `shaders/lighting.glsl`
|
||
16. Cloud shadows projected along the sun from an animated cloud layer — `shaders/lighting.glsl` `cloudShadow`
|
||
17. Regional vegetation colouring (aspen groves, dry ridges) and slope-aspect colouring — `shaders/lighting.glsl` `regionTint`, `shaders/terrain.frag`
|
||
|
||
Shadows
|
||
18. Cascaded shadow maps, 5 cascades (16 / 60 / 250 / 1100 / 6000 m) fitted to frustum-slice bounding spheres, texel-snapped, PCF radius widened on the far cascades — `shadow.ludic` `shadow_fit`
|
||
19. PCF with rotated Poisson taps and interleaved-gradient noise, normal-offset and world-unit slope bias, cascade edge blending — `shaders/lighting.glsl` `sunShadow`
|
||
20. Per-layer caster culling (grass in near cascades, coarse terrain meshes for far cascades) — `scatter.ludic` `scatter_draw_casters`, `terrain.ludic`
|
||
21. Sun-facing impostor cards in the shadow pass with mip-biased coverage — `shaders/impostor.vert`, `shaders/impostor.frag`
|
||
|
||
Geometry, terrain and world
|
||
22. GPU-generated terrain height field (ridged multifractal, fbm) — `shaders/heightgen.frag`
|
||
23. Real-world terrain from the Copernicus GLO-30 elevation model with slope-scaled fractal detail; a lake bed carved under the surface the model records — the survey window in the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake), `shaders/heightgen.frag` `DEM`, `terrain.ludic` `terrain_use_dem`, `terrain_lake`
|
||
24. Satellite orthophoto drape (Sentinel-2 true colour) sampled bicubically, blended in past the middle ground, keeping the scanned materials' luminance grain plus three-scale procedural grain — the survey window in the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake), `shaders/terrain.frag` `u_ortho`
|
||
25. Satellite-classified materials and placement: bare ground (cobble shore near, talus far), snow, meadow, dense conifer stands (tree density and forest-floor duff follow the photograph) — `shaders/terrain.frag`, `terrain.ludic` `terrain_ortho_green/scree/forest`
|
||
26. Terrain material splatting by slope, altitude and a track mask; height-map CPU read-back for placement queries — `shaders/terrain.frag`, `terrain.ludic` `terrain_height`
|
||
27. Snow by altitude and slope, gully snow, forest-floor material under stands — `shaders/terrain.frag`
|
||
28. glTF 2.0 loading of photogrammetry models by byte-range reads from the `.bin` — `gltf.ludic`
|
||
29. GPU instancing with divisor attributes and per-instance transforms — `scatter.ludic`, `shaders/model.vert`
|
||
30. Baked impostors (16-view atlas with coverage and normals, hull-normal blending, LOD switch) — `scatter.ludic` `impostor_bake`, `shaders/impostor.*`
|
||
31. Baked branch-card trees: wedge slices of a scanned crown (sectors × height bands) on radial cards over the real trunk mesh — `scatter.ludic` `impostor_bake_wedges`, `model_branch_cards`
|
||
32. Card atlases baked from scanned grass clumps and a dense procedural lupine, instanced as crossed cards — `scatter.ludic` `layer_cards`, `model_lupine_dense`
|
||
33. Procedural grass blades with wind, dryness and base occlusion; distant blades widen, lie flat to the ground normal and sink into the carpet at the ring's edge — `scatter.ludic` `model_blade`, `shaders/model.vert`, `shaders/model.frag` `BLADE`
|
||
34. Wind animation (height-weighted sway, per-instance phase) — `shaders/model.vert` `WIND`
|
||
35. Chunk-streamed ground cover with distance bands (blades in four rings, densest underfoot), deterministic per-chunk generation, nearest-first amortised over frames with an instance budget — `stream.ludic`
|
||
36. Multi-scale noise clustering for natural placement, forest density by elevation/slope/aspect, avalanche chutes, shoreline scrub — the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake)
|
||
37. Level-of-detail by distance: full scan mesh → branch cards → single impostor — `scatter.ludic` `layer_update`
|
||
38. Texture edge dilation for cut-out atlases; 16-bit PNG decoding to RGB16/R16; RGBE HDR decoding — `texture.ludic`
|
||
39. Anisotropic filtering and mipmapping on all scanned maps — `texture.ludic` `tex_upload`
|
||
40. Still water: planar reflection pass through a reflection matrix with clip-below discard, depth-tinted body, soft shores, ripple normals, foam streaks, sun glitter; the surface writes depth so occlusion and temporal passes see it — `water.ludic`, `shaders/water.frag`
|
||
|
||
Anti-aliasing, camera and post-processing
|
||
41. Temporal anti-aliasing: Halton-jittered projection, depth reprojection, YCoCg neighbourhood clamping, history ping-pong — `camera.ludic` `cam_set_jitter`, `shaders/taa.frag`
|
||
42. Optional MSAA with alpha-to-coverage and per-mip alpha sharpening — `post.ludic`, `shaders/impostor.frag`
|
||
43. HDR pipeline (RGBA16F) with NaN/inf sanitising at every stage — `post.ludic`, `shaders/lighting.glsl` `sane`
|
||
44. Bloom: 13-tap downsample pyramid with soft threshold, tent upsample — `shaders/bloom_down.frag`, `shaders/bloom_up.frag`
|
||
45. Auto-exposure by mean luminance with temporal adaptation — `post.ludic` `post_measure`
|
||
46. ACES filmic tonemapping with log-space contrast — `shaders/tonemap.frag`
|
||
47. Colour grading: white balance, lift/gain, saturation — `shaders/tonemap.frag`
|
||
48. Vignette, dithering, film grain — `shaders/tonemap.frag`, `shaders/sharpen.frag`
|
||
49. Luma unsharp sharpening — `shaders/sharpen.frag`
|
||
50. Height-based exponential fog / aerial perspective from the prefiltered sky with sun inscatter — `shaders/lighting.glsl` `applyFog`
|
||
51. Specular anti-aliasing (highlight cap) and firefly prevention — `shaders/lighting.glsl`
|
||
52. Fly camera with mouse look — the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake)
|
||
53. Profiling switches per stage and a frame benchmark — `render.ludic` `r3d_env_flags`
|
||
|
||
Characters and moving objects
|
||
54. glTF skins: node hierarchy, joints and inverse bind matrices loaded per model, four-influence GPU skinning, poses set per node in the model's frame — `skin.ludic`, `shaders/skin.vert`, `quat.ludic`
|
||
55. Actors: models placed and moved per frame (skinned or rigid), lit and cast into every cascade — `actor.ludic`
|
||
56. The drawn terrain height on the CPU (cubic B-spline of the texels) for things that stand on it — `terrain.ludic` `terrain_height_smooth`
|
||
57. A third-person character controller with a distance-driven procedural gait (walk / run / idle), slope and water limits, and a settling orbit camera — the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake)
|
||
58. A 2D overlay over the finished frame: batched rectangles, images and proportional text from a baked font atlas (`tools/blender/font_build.py`) — `overlay.ludic`, `shaders/overlay.*`
|
||
59. Time of day over the HDRI: a solar arc drives the sun's direction and colour, the sky's light scales toward a deep-blue starry night, a moon lights the night, the visible sky turns with the sun and its convolutions rebake when far off, the height-field shadow rebakes as the sun moves, auto-exposure is capped at night — `daylight.ludic`, `shaders/sky.frag`, `shaders/lighting.glsl`
|
||
60. One point light (the campfire) in the shading — `shaders/lighting.glsl` `fireLight`
|
||
61. Static collision: ground circles in a cell grid with push-out and a segment test — `collide.ludic`
|
||
62. Cut-out and emissive actors (a flame's cards) — `actor.ludic`, `shaders/model.frag` `u_emissive`
|
||
63. Per-instance skin clones (a herd posed individually), per-part tints and hidden parts on actors, uniform-location caching with frustum and distance culling — `skin.ludic` `skin_clone`, `actor.ludic`
|
||
64. A second point / cone light (a torch or flashlight in the hand) — `daylight.ludic` `daylight_hand`, `shaders/lighting.glsl` `handLight`
|
||
65. Auto-exposure adapted on the GPU: a 1x1 pass eases last frame's value toward key / mean and the tonemapper samples it, so nothing is read back (a pixel-buffer read still synchronised on Apple's GL) — `post.ludic` `post_measure`, `shaders/adapt.frag`
|
||
66. A PPM reader with box-filtered downscale (photo thumbnails) and word-wrapped overlay text — `texture.ludic` `tex_load_ppm`, `overlay.ludic` `ov_text_wrap`
|
||
67. The overlay batches a whole frame into one upload and draws per texture range with optional scissor clips (per-change uploads stalled the driver: 40 ms -> 25 ms), plus nine-slice, rotated, line, disc and arc primitives — `overlay.ludic` `ov_flush`, `ov_clip`, `ov_nine`, `ov_sub_rot`, `ov_line`, `ov_disc`, `ov_arc`
|
||
68. Weather on the daylight: an overcast factor greys and dims the sun and the sky's light, a fog multiplier thickens the air, a lightning flash — `daylight.ludic` `daylight_weather`
|
||
69. An outline pass: an actor carries a rim width and colour, drawn as an inverted hull depth-tested against the scene, and anything that is not an actor can queue one at a transform (a scatter instance, a tile) — `actor.ludic` `actor_draw_outlines`, `outline_model`, `shaders/outline.frag`
|
||
70. Two ways to make an island out of a survey: a radius from a centre, or the sea put around the survey's own edge with a noise-wobbled coast, both scaling the existing relief into the water and compressing the last few metres of height either side of the line into beach and shallows — `terrain.ludic` `terrain_island`, `terrain_coast`, `shaders/heightgen.frag`
|
||
|
||
## Against Unreal Engine 5 and RAGE (RDR2), honestly
|
||
|
||
| Technique | UE5 / RAGE | Here |
|
||
|---|---|---|
|
||
| Physically based shading, IBL, split-sum | both | implemented (1–4) |
|
||
| Cascaded shadow maps with PCF | both (UE5 also virtual shadow maps) | implemented (18–21); no virtual shadow maps |
|
||
| Temporal AA / upsampling | TSR / TAA | TAA implemented (41), no upscaling |
|
||
| Screen-space AO / GI | SSAO, SSGI, Lumen | SSGI + AO implemented (14); no Lumen-class GI, no ray tracing (not available on OpenGL 4.1) |
|
||
| Volumetric clouds and fog | both | HDRI sky with cloud shadows and analytic fog; no ray-marched clouds |
|
||
| Foliage: impostors, wind, translucency | both | implemented (30–34) |
|
||
| Nanite / virtualised geometry | UE5 | not applicable; LOD chain (37) instead |
|
||
| Terrain: height fields, layered materials, real-world data | both | implemented (22–27) |
|
||
| Water with planar reflection | RAGE planar, UE SSR | planar reflection implemented (40); no screen-space reflection |
|
||
| Post: bloom, exposure, tonemap, grading, DoF, motion blur | both | bloom/exposure/tonemap/grading implemented (44–49); no depth of field, no motion blur |
|
||
| Streaming world | both | chunk streaming of cover (35); terrain is one 8 km tile |
|
||
| Compute shaders, indirect draw, bindless | both | not available in OpenGL 4.1 core on macOS |
|
||
|
||
Where the remaining visual gap sits: photogrammetry cards for several meadow
|
||
species, ray-traced or probe-based global illumination, virtual shadow maps, and
|
||
volumetric clouds. Those are the techniques that separate this from the reference.
|