Commit graph

13 commits

Author SHA1 Message Date
3a87d02696 packages: ludic.ui's render3d backend threads Render3dState; render3d's settable vars (r3d_dem_path, post_*, grass_*, wt_wade_*, ...) are Render3dState's fields again, not lets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:11:15 +03:00
19fcf60599 wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run
ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
  1804 vars into 126 states, 64 into lets; 23498 edits in 460 files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
5ffe50ed02 wip(0.S2): migrate - a var nothing writes becomes a let; a state is keyed by its module, directory or program, so every program's plan agrees; paths normalized; program states named SceneDemoState / scene_demo_st; the LSP reads state, mut and entry/handler parameters
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
07505e7ef2 wip(0.S3): the packages migrated by ludic migrate state packages - every package test green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:02:21 +03:00
cc89fc37a4 feat(lang): strict numbers in float files; render3d on float
A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.

render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:13:25 +03:00
18d23ca20d feat(render3d): a bole that does not quake, leaves out of the lens, a light with a reach
layer_flutter's mask was the vertex's height alone, so on a ten metre aspen every part of
the trunk above 1.2 m trembled with the leaves and the bole read as cloth. What separates
a leaf from a bole is distance from the model's centre line, not height; the mask is
radial now, and small plants keep the old one because a flower is all leaf.

NEAR_FADE dithers tree foliage out inside arm's length of the camera, in the depth
prepass, so the lit pass never sees those pixels and the equal-depth optimisation is
untouched. gl_Position.w is the view depth, so it costs one varying and no uniform.
Blades, cards and flowers are excluded - they live at the player's feet - and the shadow
pass keeps every leaf.

daylight_hand takes a reach in metres. The falloff was an inverse square windowed between
26 and 6 with both numbers hard-coded: two per cent of its own near field at ten metres,
so a torch lit your boots. It is a gentle power out to the reach now.

Measured in Maroon Lake, twice per side: the crown mask moves 1.7% of an aspen frame and
nothing at all in the meadow under it; the near fade moves 7.1% of a first-person frame
at a conifer and 0% where there is no foliage in the lens; the torch's reach lifts the lit
ground 20 to 45% and leaves the sky bit-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:06:50 +03:00
b5d13180a6 fix(render3d): the ground keeps more of its own colour, and the rock keeps all of it
55% of every ground pixel was the orthophotograph, at every distance and on every
material. A survey image records WHERE the forest and the scree are well and what a
steep rock face is coloured badly - it is shot through kilometres of air at an angle no
player stands at. The photograph keeps the meadow and the forest and gives the rock back
to the rock, by material and by elevation and never by distance: a distance fade used to
live here and was removed because the photograph has the day's own shadows baked in, so
grass grew dark patches as you backed away and they slid as you walked.

The far tier's rock fallback is the maroon mean, not a neutral grey - TFAST_3 skips the
rock sample and leaves that constant standing in for a whole mountain.

Ortho-classified snow needs altitude now. Bright and unsaturated is what snow looks like
from a satellite and also what a sunlit rock face looks like. Gully snow is gated on the
snow line rather than two absolute heights.

Distance desaturation eased 1.9 -> 1.15: tuned on a valley whose rock was grey anyway.

NOT FIXED, and I want it recorded rather than implied: the Bells themselves are still
not maroon. The near and middle rock is warmer and measurably so, but the distant peak
does not respond to ANY of this - not the rock tint, not the ortho weight, not the snow
line, not the gully gate. Painting sA bright red left it unchanged, so whatever surface
that is, it is not this shader's snow and not this shader's material blend. Somebody
should find out what draws it before tuning any of these numbers further.

400 frames: GL 7.0 s, VK 7.2 s. Backends agree to 0.62/255.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:29:17 +03:00
b12228c662 feat(render3d): light you can see - sun shafts and valley mist
Everything before this made the air a COLOUR APPLIED TO A SURFACE. Nothing put light
in the space between surfaces, so the basin had no shafts, no pooled mist and no rays
off a ridge at any hour, whatever was done to the fog.

A half-resolution march from the camera to the depth buffer, asking the same shadow
the rest of the frame asks - the cascades, the baked height-field shadow and the cloud
mask - so a shaft is cast by the actual trees and the actual ridge and a passing cloud
dims its own rays. Henyey-Greenstein scattering, because real air throws light forward.
Density and a separate ground-hugging mist layer ride the sun's elevation, so mist
forms in the cold at either end of the day and burns off by mid-morning.

Composited with the bloom pyramid's own tent upsample under ONE/ONE - what was wanted
and already there - and before bloom, so a shaft blooms. Into post_hdr, not post_scene:
post_scene is what the water refracts and shafts added there would sit under the lake.

The tuning that mattered was the sky term, which is added at every step: at 0.06 it
accumulated into a flat grey wash lifting lit and shadowed air equally, which is the
contrast a shaft is made of, and the valley came out one pale sheet. At 0.012 the sun
dominates and there is light rather than fog. I cut the density and mist three times
before the frame looked like air instead of paint.

R3D_NOVOL=1 for an A/B; Off in Settings skips the pass whole.

400 frames at 07:00: GL 7.1 -> 7.3 s, VK 7.2 -> 7.4 s. Backends agree to 0.08/255.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:06:16 +03:00
d34fb5bc63 feat(render3d): the air and the light are the hour's, not one constant apiece
Aerial perspective is a curve now. A low sun shines through far more air than a
high one and shines ALONG the ground rather than down onto it, so density, height
falloff and forward scatter all ride the sun's elevation; overcast thickens the air
and flattens the scatter, because a grey sky has no disc to scatter from. `lowsun`
falls away BELOW the horizon as well as above it, or the middle of the night gets a
dawn's haze with no dawn to justify it.

The term that was missing entirely is distance DESATURATION. Blending a saturated
green ridge toward a saturated blue noon sky leaves a saturated ridge - which is why
the same valley read as a photograph at dusk, where the fog colour happened to be a
warm grey, and as a toy at one o'clock. A surface is now pulled toward its own
luminance faster than the fog itself arrives. Measured far/near saturation at the
camp: 07:00 1.11 -> 0.89, 09:00 1.04 -> 0.93, 13:00 0.98 -> 0.89.

The grade is the hour's too - nine literals bound at the draw, written by
daylight_set now. Noon is the case worth naming: direct sun is warm-white and the
only thing filling a midday shadow is a blue sky, so noon gets a cool balance over a
blue-lifted shadow with hard contrast, and dawn and dusk the reverse. Ground R-B,
lit vs shadowed: 07:00 +42.8/+14.2 -> +48.9/+15.1, 13:00 +32.2/+14.8 -> +25.2/+2.9.
Gain is left alone deliberately: the grade is `c * gain + lift * (1 - c)`, so warming
it warms the whole frame, and warming it at noon made one o'clock yellower than seven
in the morning - the opposite of the point.

The visible sky is relit. Turning a photograph on its axis does not change what
colour it was taken at, so every sunset had a mid-morning blue overhead. An analytic
sky supplies the chroma and the photograph keeps the luminance: the cloud stays where
it is and goes orange at dusk, the zenith goes deep blue at noon, and no second sky
is shipped. It fades out under the horizon and eases off under cloud.

The ground bounce follows the ground, crossing meadow to rock at the map's treeline
instead of being one green constant everywhere including above the scree.

R3D_NOAIR=1 restores all of it, so a before-and-after comes from one binary at one
hour; it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI.

Verified on macOS OpenGL, macOS Vulkan (MoltenVK) and Windows Vulkan (RTX 3070 Ti).
Backends agree: mean difference 0.15-0.88/255 within a machine. Across machines the
ORIGINAL renderer already differed by 5.02/255 at 19:12 and this build differs by
2.80, so cross-platform variance is pre-existing and did not grow. 400 frames: GL
7.4 s before and after, VK 7.0 s before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:42:30 +03:00
f2b976c976 feat(render3d): load steps for a loading screen, four quality switches, and the ring's uniform usage
- r3d_open opens the window with nothing baked; r3d_load_count / r3d_load_step run the rest (sky
  and light, terrain, shadow and screen targets, cover and actors, grass) so a game can present a
  loading frame between them. r3d_init is the same calls in a row.
- sky_set_quality(width): the prefiltered sky light at 256 / 512 / 1024, baked again.
- post_set_msaa(samples): multisampling on OpenGL, remade in place (post_msaa_live is false on
  Vulkan); post_free frees the multisampled framebuffer too.
- STREAM_BUDGET_US is a variable; r3d_fog_scale multiplies the fog the day sets.
- Vulkan buffers carry UNIFORM_BUFFER usage: the frame's ring is bound as uniform buffers and was
  created without it (VUID-VkWriteDescriptorSet-descriptorType-00330, found on MoltenVK).

OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 14:46:55 +03:00
54eeef5f8d feat(render3d): gpu.ludic - the seam for a second graphics API, and what the GPU can do
Render state (depth, blending, culling, colour writes, alpha-to-coverage, depth bias,
scissor) and uniforms go through gpu_* / u_* and nowhere else; OpenGL state is cached and
only changes reach the driver. Frames are bit-identical to before at the fixed viewpoints.
R3D_GFX=gl|vk or gpu_request chooses a backend, falling back to OpenGL with a reason.

gpu_caps_probe() asks Vulkan, on Windows, for the 1.3 floor, ray tracing, mesh shaders, the
NVIDIA RTX generation, Reflex and HDR colour spaces, for a game's settings to grey out what
a machine cannot use; R3D_CAPS pretends to be a given card for tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:52:12 +03:00
6043a67631 feat(render3d): give the moon a phase
The moon was fixed opposite the sun and worth a constant trickle of light. It
now carries a phase, 0 new .. 0.5 full .. 1 new again, and that one number
decides three things at once: the disc's terminator, the fraction of its light
that reaches the ground, and where in the sky it rides.

The moon is where the sun was `lag` of a day ago, so a full moon rises as the
sun sets and a new moon travels with the sun and is never seen. A new moon is
now a properly dark night, which is what makes a carried light worth having.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:15:19 +03:00
f25289db20 feat(gl): OpenGL 4.1 and the ludic.render3d renderer
Some checks failed
ci / build-and-test (push) Waiting to run
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Has been cancelled
docs / build-and-deploy (push) Successful in 34s
`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the
platform gl3.h with every GL_* constant, generated by `ludic-dev glgen`
with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the
existing window at Retina resolution; headless builds render into an
offscreen CGL context, so a program that uses Gl.* renders and
screenshots identically under the test harness. It links gl.ll, the
thunks and OpenGL.framework only when used; every other build stays
byte-identical.

packages/ludic.render3d is a physically based renderer written on that
surface: HDRI image-based lighting, GPU-generated terrain with scanned
PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced
vegetation with impostors, procedural grass, water, SSAO, and an HDR
pipeline with bloom, auto-exposure and ACES.

It also carries this session's work on it: the terrain at half its cost
(10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer
you played, a resize that emptied the world, and the packaging that lets
a game use the renderer from its own repository — `ludic assets`, the
material manifest shipping with the package, and shader lookup falling
back to the install root. See changes/ for each, with its numbers.

The camping game that drove all of it has moved out to its own
repository, Maroon Lake; examples/rendering/smooth.ludic stays as the
renderer's example here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 03:31:12 +03:00