Compare commits

..
Author SHA1 Message Date
Fr0zkaandClaude Opus 5.5 3b71d35eba experiment: LivingPlane repeatable + standalone UE runtime module (L164)
Readback barrier fix (GL_TEXTURE_UPDATE_BARRIER_BIT before glGetTexImage); check_repeatability.py compares full-field
checksums across query/capture settings - Intel GPU PASS (3b9d45905c7aafff), llvmpipe PASS, mismatch control fails.
New runtime module VoxelForgeLivingPlane (separate from generation): RDG global shader (Shaders/LivingPlane.usf),
ping-pong RGBA32F render targets, actor with WPO plane and object inputs, no CPU readback. Compiles in Linux UE 5.7.
Windows build: Source/VoxelForgeLivingPlane/*, Shaders/LivingPlane.usf, VoxelForge.uplugin. Setup: UE-SETUP.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-02 11:10:43 +01:00
Fr0zkaandClaude Opus 5.5 e1d87eed26 experiment: LivingPlane readable + alive mode (L163)
RGBA32F displacement/velocity/imprint field; rolling base relief, grazing light, elevation colours, 7.5 cm contours,
follow camera; scenes: roll, rest and wobble, lift-off, second sphere; alive mode = travelling swells + viscoelastic
imprint. Intel ADL GT2: ~0.17 ms/step at 512^2. Open: run-to-run difference with queries on/off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-02 10:55:32 +01:00
Fr0zkaandClaude Opus 5.5 5b15595e4a experiment: LivingPlane - GPU wave-equation height field reacting to a moving sphere (L162)
Standalone EGL/OpenGL 4.3 compute runner (runtime GLSL, no shader compiler needed), ping-pong height/velocity field,
damped wave + foundation spring + Gaussian sphere push + optional breathing, vertex-displaced render to PNG.
Independent of generation. Intel ADL GT2: ~0.08 ms/step at 512^2, ~0.29 ms/step at 1024^2 (wall-time difference).
.gitignore now allows Experiments/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-02 10:35:37 +01:00
Fr0zkaandClaude Opus 5.5 eb47d72258 research: R47 liminal/dreamcore style and mapping to Core (text only; images kept local)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-01 04:11:52 +01:00
Fr0zkaandClaude Opus 5.5 95bf654fbc docs: AGENTS.md for Codex (standing rules + working procedure)
Codex only auto-loads AGENTS.md; CodexGuidance.md was never read. Carries the per-brief standing rules (limits,
no commit/push, quiet-machine timing, same-world bar, units) and a short procedure distilled from systematic-
debugging/verification-before-completion (obra/superpowers) and ub-mode: count first, suspect the measurement,
root cause before fix, one change at a time, attack the premise / stop after three failed fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-01 01:37:58 +01:00
19 changed files with 2600 additions and 518 deletions

No files matched your search

+8
View File
@@ -9,12 +9,20 @@
!/Source/ !/Source/
!/Source/** !/Source/**
# Plugin runtime shader source.
!/Shaders/
!/Shaders/**
# Portable CPU generator and D3D11 viewer. # Portable CPU generator and D3D11 viewer.
!/Standalone/ !/Standalone/
!/Standalone/** !/Standalone/**
/Standalone/Build/ /Standalone/Build/
/Standalone/bin/ /Standalone/bin/
# Research and prototype experiments.
!/Experiments/
!/Experiments/**
# The guarded single-entry test harness is source, not generated output. # The guarded single-entry test harness is source, not generated output.
!/Tools/ !/Tools/
!/Tools/** !/Tools/**
+57
View File
@@ -0,0 +1,57 @@
# AGENTS.md — VoxelForge (read automatically by Codex)
You are the implementation agent. The orchestrator (Claude) writes each round's brief, reviews your diff against the
code, and commits. The owner decides direction. Be warm and direct; credit what worked; say plainly what didn't.
Read first, as the task needs: `CURRENT-STATE.md` (what ships, reference numbers, **closed experiments: don't reopen
them without new evidence**), then `CODEMAP.md` and `ARCHITECTURE.md`. Round reports live in
`Standalone/Research/WorldAssemblerM3/Linux*/REPORT.md`.
## Standing rules (every round, unless the brief says otherwise)
- Run as user `jahni`. Write only inside this plugin, plus `/home/jahni/VFHost` and `/home/jahni/UnrealEngine` when
the brief asks for a UE build. No installs, no system changes.
- **No `git commit`, `push` or `stash`.** Leave changes in the working tree; the orchestrator reviews and commits.
- Headless processes and builds: `taskset -c 0-11 prlimit --as=6442450944` (12 P-core threads, 6 GiB). Never use all
cores.
- Unreal (UBT, the editor, commandlets): `taskset -c 0-11` only, **never `prlimit`** (CoreCLR fails). UE builds use
`-NoUBA -MaxParallelActions=4`, never while timing.
- Stop any single run that passes 30 minutes. Logs go in `Saved/Linux/l<round>/`.
- **Timing:** quiet machine only (check `uptime` first; load < ~1.5, nothing else building). L80 protocol: one warm-up
per binary, ≥5 interleaved runs, W8 and W12, all four owner layers. Every number states the window that produced it.
- **Same world for everyone:** a seed must give the same world on every machine and worker count (Clang and GCC,
W1/W8/W12). Byte-identity with older versions is NOT required. If the output changes: bump
`WorldAssemblyEvaluatorVersion`, add baselines, and show captures.
- Density sign: negative = solid, positive = air. Core voxels are 25 cm. Never mix metres and voxels without an
explicit conversion (v225 shipped a reach bug from exactly that).
- The fast tier (`Standalone/Tools/verify/verify.py fast`) must stay green. Don't loosen an audit to make a change pass.
## How to work
1. **Count before you build.** Measure where the time or the failure actually is (stack sampler, census, counters).
State which parts are measured and which are inferred.
2. **Suspect the measurement first.** An alarming or contradictory number is the instrument until a control rules it
out (A/A runs, an ablation that verifiably removes the work).
3. **No fix without a root cause.** Reproduce it, trace it to its source, fix the cause. A guard that hides the symptom
is not a fix.
4. **One change at a time,** each with its own check.
5. **Subtract before you add.** Prefer removing work to reorganising it. The smallest edit that solves the problem.
6. **Attack the premise.** When two fixes sharing one premise fail the same gate, write that premise in one sentence
and question it before trying a third. **After three failed fixes, stop:** it is probably the design, not the
hypothesis. Report it instead of trying fix #4.
7. **Tests must be able to fail.** Name what would make each check fail. Add a negative control when you add an audit.
8. **Refactors:** pin the behaviour (hashes), change the structure, prove nothing changed.
9. **Verify before you claim.** Don't report a check as passed unless it ran on the final source. Say which checks
didn't run and why.
10. Changes that speed things up go in the shared layer, so every generator benefits.
## Report
Write the round's `REPORT.md`, short and plain:
- the result first;
- what changed and why;
- the measurements, with their windows;
- the checks run, and those not run;
- remaining risks.
List any changed Unreal `Source/` files, because the owner compiles them on Windows.
File diff suppressed because it is too large. Load diff
+201
View File
@@ -0,0 +1,201 @@
# L162 — Living Plane
The EGL/OpenGL 4.3 runner builds and passes its llvmpipe correctness and frame checks. The sandbox has no accessible DRM render node, so it did not run or report Intel GPU performance. The runner refuses `--device=gpu` here instead of falling back to software.
## Implementation
`LivingPlane.cpp` runs a ping-pong `RG32F` height/velocity field through a runtime-compiled GLSL compute shader. The damped wave update includes a weak foundation spring, a Gaussian sphere push, and an optional slow breathing force. The spring is needed because a persistent local push otherwise accelerates the zero-frequency height mode and sinks the whole plane. The sphere moves across the plane, stops, and bobs. A vertex-displaced mesh and lit sphere render to an EGL pbuffer-backed framebuffer and save as PNGs.
The timestep is selected from the largest 2D grid eigenfrequency:
`dt = 1.30 / sqrt(8*c*c/(dx*dx) + foundationStiffness)`
The resulting spectral step ratio is 1.30, below the explicit update's 2.0 stability limit. The runner warms the compute kernel for eight unmeasured dispatches, then records one `GL_TIME_ELAPSED` query per requested simulation step in `timing.json`.
The first-step equation audit also caught a GL state error during bring-up: frame rendering left the graphics program active before the next compute dispatch. `Dispatch` now binds the compute program before every step, so the audit fails if a frame silently stops simulation.
Device selection enumerates EGL devices and uses `EGL_EXT_platform_device` for an accessible Intel render node first. On this host, the known integrated device is Intel on `renderD128`. Automatic mode falls back to a surfaceless Mesa context with `LIBGL_ALWAYS_SOFTWARE=1`; explicit GPU mode fails when no accessible hardware node is present. EGL does not expose Vulkan's integrated/discrete enum, so Intel vendor/render-node identity is the selection heuristic.
The Unreal handoff is in [UNREAL-MAPPING.md](UNREAL-MAPPING.md). It keeps the prototype render-only and describes the density mapping if the surface later needs to affect voxel queries. No Unreal `Source/` files changed.
## Measurements and checks
- Built with `Experiments/LivingPlane/build.sh` using the installed EGL, desktop GL, and libpng development files; no shader compiler or package install was needed.
- Final llvmpipe run: 128² field, 360 steps, `dt=9.56814 ms`, spectral ratio 1.30. The first-step equation audit and finite-field/dynamics audit passed. Final field range was −0.139533 m to +0.0366704 m. It wrote 16 128×128 PNGs and a JSON file containing 360 per-step query samples. The frames were inspected; the moving sphere and wake are visible.
- Auto-selection smoke check: 32², 12 steps; selected llvmpipe and passed both audits. Breathing smoke check: 64², 180 steps with `--breathing=on`; passed both audits.
- Negative device-selection control: `--device=gpu` returned an error because the sandbox has no accessible EGL DRM render node.
- Existing fast verification tier passed: 278.9 seconds, ending in `SUMMARY PASS`. Its build and logs are under `Saved/Linux/l162/verify/` and `Saved/Linux/l162/`.
- The llvmpipe `GL_TIME_ELAPSED` samples are preserved in JSON but are software-driver query values, not GPU timings. The requested hardware timing pass remains to be run outside the sandbox.
- No Unreal build was run; no Unreal `Source/` files changed.
## Intel GPU pass
Run these from the plugin root as `jahni`. The frame run writes 512×512 PNGs to `Saved/Linux/l162/gpu-frames-512/`; the two timing runs write `timing.json` with one GPU query per step and skip PNG capture. They still read the field for the first-step and final-state correctness audits.
Before the timing runs, check `uptime` and run them on a quiet machine (load below about 1.5, with no other build active).
```sh
uptime
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=512 --steps=1800 --capture-every=12 --out=Saved/Linux/l162/gpu-frames-512
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=512 --steps=1800 --capture-every=0 --out=Saved/Linux/l162/gpu-timing-512
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=1024 --steps=1800 --capture-every=0 --out=Saved/Linux/l162/gpu-timing-1024
```
Encode the captured sequence as MP4 with:
```sh
ffmpeg -y -framerate 30 -i Saved/Linux/l162/gpu-frames-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l162/living-plane-512.mp4
```
## Files and remaining risk
The only changed file outside `Experiments/LivingPlane/` is `.gitignore`, which now allows `Experiments/` and its contents. GPU timings and Intel render-node selection remain unverified until the commands above run on the host outside this sandbox.
## Intel GPU pass (run by the orchestrator outside the sandbox, 2026-11-02)
- The device was selected correctly: `Mesa Intel(R) Graphics (ADL GT2)`, OpenGL 4.6, `/dev/dri/renderD128`. 512²,
1,800 steps, 151 frames; the equation audit passes. MP4: `Saved/Linux/l162/living-plane-512.mp4` (local).
- **The `GL_TIME_ELAPSED` per-dispatch values (~0.0001 ms) are not credible** for 262k cells, so they aren't
reported as timings.
- Wall-time difference between 1,800 and 200 steps, divided by the 1,600 steps (start-up cancels out; this includes
submission, so it's an upper bound): **0.080 ms/step at 512²** and **0.292 ms/step at 1024²**.
- Visual read: the effect is very subtle in the current render (a slight dip under the sphere, faint rings). It needs
a larger push amplitude, grazing light or height colouring, and a closer camera to read.
## L163 — readable, living plane
The plane now reads as rolling terrain, and the objects leave visible troughs and wakes. `alive` adds travelling
breathing and a viscoelastic imprint that remains after lift-off. The simulation still runs in one GPU dispatch per
step; CPU work only supplies the two object states.
`LivingPlane.cpp` now keeps the dynamic displacement, velocity, and imprint in an `RGBA32F` ping-pong field. The
surface shader adds broad static terrain relief, calculates normals from relief plus displacement, and uses grazing
light, elevation colours, slope tint, and 7.5 cm contour bands. The camera follows the first sphere at a closer angle.
The wake scene rolls the first sphere, pauses and wobbles it, lifts it, then sends a second sphere through the wake.
The alive scene runs that same sequence with low-frequency travelling swells and a decaying contact imprint. Its
defaults are breathing strength 4.8 m/s², imprint capture 0.15 m/s, recovery time 6 s, and maximum imprint depth
0.34 m. These values have command-line overrides.
The timer query check found no early read or reuse in L162: it allocated a unique query per step, called `glFinish`,
then read every result. The saved Intel/Mesa 25.0.7 samples are 0 or 52 ns for most dispatches (512² median 0,
p95 52 ns; largest sample 17.552 µs), which cannot be a credible per-dispatch cost. The old usage did not reuse
query objects, so lifetime is not the cause; the saved data point to Intel/Mesa's timer-query reporting or timing
scope, but the sandbox cannot isolate which. L163 uses a 64-query ring, reads each query before reusing its slot,
checks availability after `glFinish`, and records `GL_QUERY_COUNTER_BITS`. On llvmpipe the availability check passed
with 64 counter bits and zero waits. The Intel query result still needs the GPU diagnostic command below.
### Measurements and checks
- llvmpipe wake: 128², 600 steps (`dt=9.56814 ms`, 5.741 s simulation window), 151 frames at 30 fps. First-step
equation audit and timer-query lifetime check passed; final displacement range was −0.177488 m to +0.104854 m.
- llvmpipe alive: same grid, steps, and window; equation audit and timer-query lifetime check passed. Final
displacement range was −0.200437 m to +0.115904 m, with a −0.111667 m stored imprint.
- llvmpipe cost: 512², 1,800 steps, `--queries=off`, no frame capture: **0.767473 ms/step** in one timed
simulation-loop window. `timing.json` includes dispatch submission, the step-0 equation readback, and final
`glFinish`, but excludes setup and frame capture. This is a software-renderer measurement, not the Intel GPU cost.
- Both five-second llvmpipe previews are in `Saved/Linux/l163/llvmpipe-wake.mp4` and
`Saved/Linux/l163/llvmpipe-alive.mp4`. They are 128² review previews; use the GPU commands below for the owner
videos.
- Equation-audit negative control: a temporary copy with its expected first-step height forced to zero exited FAIL
at actual −0.0488777 m. The final source passes that same check.
- The fast verification tier passed in 278.5 s. Build and logs are under `Saved/Linux/l163/verify/`.
- No Unreal `Source/` files changed and no Unreal build ran.
### Intel GPU run
Run from the plugin root as `jahni`. First build the runner. For timed runs, check `uptime` and keep the machine
quiet. The wall-time field is valid only when `--capture-every=0`; use `--queries=off` for the step-cost measurement.
```sh
bash Experiments/LivingPlane/build.sh
uptime
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=wake --size=512 --steps=2400 --capture-every=16 --queries=off --out=Saved/Linux/l163/gpu-wake-512
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=2400 --capture-every=16 --queries=off --out=Saved/Linux/l163/gpu-alive-512
ffmpeg -y -framerate 30 -i Saved/Linux/l163/gpu-wake-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l163/living-plane-wake-512.mp4
ffmpeg -y -framerate 30 -i Saved/Linux/l163/gpu-alive-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l163/living-plane-alive-512.mp4
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=1800 --capture-every=0 --queries=off --out=Saved/Linux/l163/gpu-timing-512
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=1800 --capture-every=0 --queries=on --out=Saved/Linux/l163/gpu-query-check-512
```
`gpu-timing-512/timing.json` reports the 512² batch wall time per step, including dispatch submission and completion.
`gpu-query-check-512/timing.json` contains query counter bits, waits before slot reuse, availability status, and raw
per-step query samples. The sandbox has no accessible Intel DRM node, so the current Intel step cost and new query
diagnostic remain unmeasured here.
### L163 Intel GPU pass (run by the orchestrator, 2026-11-02)
- Intel ADL GT2 (Mesa 25.0.7, `renderD128`): wake and alive scenes, 512², 2,400 steps, 151 frames each; equation
audits PASS. Videos (local): `Saved/Linux/l163/living-plane-wake-512.mp4`, `living-plane-alive-512.mp4`.
- Step cost, alive scene, 512², 1,800 steps, no capture: **0.172 ms/step** with queries off, 0.152 ms/step with
queries on (batch wall time per step).
- The Intel `GL_TIME_ELAPSED` median is still 0 with 64 counter bits and the ring/availability checks passing. That
looks like a Mesa/Intel reporting issue; use wall time.
- **Open issue:** the same scene with the same steps gave different final fields (min −0.637 m with queries off vs
−0.327 m with queries on). Something depends on more than the step index (e.g. wall-clock time); investigate.
## L164 — repeatable field readback and UE runtime actor
**Result:** the repeatability check passes on llvmpipe, and the new `VoxelForgeLivingPlane` runtime module compiles
in the Linux UE 5.7 host. The surface actor is independent of VoxelForge generation and sends no field readback to
the CPU.
### Repeatability finding
`uTime` is `Step * dt`, and `GetBodiesAt` receives that same value. Neither the object schedule nor the breathing
force uses wall time or a rendered-frame counter. Every compute step already issued an image-access barrier before
the next ping-pong image read. The defect was on the diagnostic side: the step-zero and final `glGetTexImage`
readbacks followed shader image writes without `GL_TEXTURE_UPDATE_BARRIER_BIT`. The Khronos OpenGL specification
requires that barrier for `GetTexImage` to observe earlier shader writes ([§7.12](https://registry.khronos.org/OpenGL/specs/gl/glspec44.compatibility.pdf)). Query/capture pacing could therefore affect the reported field values without changing any simulation input. The barrier is now issued before both readbacks.
`check_repeatability.py` runs the same 512² alive scene for 1,800 steps twice: queries off with no captures, then
queries on with captures every 900 steps. The checksum covers every final RGBA32F field value using canonical
little-endian float bytes.
- llvmpipe, queries off/capture off: field range −0.637382 m to +0.109308 m; checksum
`1ba08f5dafc01f82`.
- llvmpipe, queries on/capture every 900: the same range and checksum; equation and timer-query audits pass; three
frames written.
- Negative control: changing one saved checksum made the comparison exit 1 with `FAIL field checksum mismatch`.
- The llvmpipe runs were each one 1,800-step simulation-loop window. Logs and JSON are under
`Saved/Linux/l164/repeatability/`.
The Intel run is not available in this sandbox. The owner should repeat the checksum comparison on the GPU to
confirm that the missing transfer-readback barrier accounts for the earlier Intel min mismatch:
```sh
python3 Experiments/LivingPlane/check_repeatability.py --device=gpu --out=Saved/Linux/l164/gpu-repeatability
```
### Unreal implementation and checks
The plugin descriptor adds a separate `VoxelForgeLivingPlane` runtime module. Its RDG global shader is in
`Shaders/LivingPlane.usf`; module startup maps `/Plugin/VoxelForgeLivingPlane` to the plugin shader directory. The
actor owns two UAV-capable RGBA32F render targets, advances them at the stable fixed step, and supplies the current
texture to a movable plane mesh material. Its Blueprint/C++ object input includes world position, radius, velocity,
base push, and active state. The CPU converts those values to plane-local metres and uploads uniforms; it does not
read the field back. The ignore rules now include `Shaders/` so that the shader ships in the worktree diff.
`Experiments/LivingPlane/UE-SETUP.md` gives level setup, the full material graph, and object driving steps.
- Linux UE 5.7 `VFHostEditor Linux Development`: **PASS**, final incremental build compiled
`VoxelForgeLivingPlaneActor.cpp` and linked `libUnrealEditor-VoxelForgeLivingPlane.so`. Command used
`-NoUBA -MaxParallelActions=4`, `taskset -c 0-11`, and no `prlimit`. Log:
`Saved/Linux/l164/ue-build-final.log`. The initial attempt exposed an incorrect RDG texture registration overload;
it had already compiled `VoxelForgeLivingPlaneModule.cpp` and the generated module code. The corrected final
build passed.
- The host `PluginDeps/VoxelForge/Source` and `.uplugin` links were temporarily pointed at this worktree for the
build and restored to `/home/jahni/VoxelM/Plugins/VoxelForge` afterward. The temporary shader link was removed.
- Standalone build, final repeatability comparison, checksum-mismatch negative control, plugin JSON parse, and
`git diff --check` passed.
- Not run: fast generation verification (generation sources are untouched), Intel GPU checksum comparison, or a UE
editor render/shader-runtime check. The Linux UBT build verifies C++ and module integration, not first-use shader
compilation or material appearance.
New Unreal source and shader files for the owner's Windows build:
- `Source/VoxelForgeLivingPlane/VoxelForgeLivingPlane.Build.cs`
- `Source/VoxelForgeLivingPlane/Public/VoxelForgeLivingPlaneActor.h`
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneActor.cpp`
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneModule.cpp`
- `Shaders/LivingPlane.usf`
No VoxelForge generation module files changed. Remaining risks are the Intel checksum confirmation and first-use UE
shader/material rendering, which require the owner's GPU/editor run.
+55
View File
@@ -0,0 +1,55 @@
# Living Plane in Unreal Engine 5.7
`VoxelForgeLivingPlane` is a separate runtime module. It does not call the VoxelForge terrain generators and it does not read the simulated texture back to the CPU. The actor owns two transient `RGBA32f` render targets and advances them with an RDG compute pass. The CPU sends object state and fixed-step uniforms only.
## Add it to a level
1. Enable the VoxelForge plugin and restart the editor so the `VoxelForgeLivingPlane` runtime module and its shader mapping load.
2. Create a material named `M_LivingPlane` using the graph below.
3. Place `VoxelForgeLivingPlaneActor` in the level. Assign `M_LivingPlane` to **Plane Material**.
4. Keep the actor on the world XY plane with unit actor scale. `Plane Length Meters` controls the mesh width and simulation width; the built-in plane mesh is scaled to that size.
5. The actor creates and clears its two GPU state targets on `BeginPlay`. The mesh has collision disabled; this surface is render-only and does not change VoxelForge density or collision queries.
## Material graph
Use a Surface, Opaque material. The actor binds these parameters on its dynamic material instance:
- `LivingField`: Texture Object Parameter. The texture is the current RGBA32f simulation state; **R** is displacement in metres.
- `PlaneLengthMeters`: Scalar Parameter.
- `FieldSize`: Scalar Parameter, equal to the render target width and height.
For World Position Offset:
1. Read `LivingField` at `TextureCoordinate[0]` with a Texture Sample.
2. Multiply its **R** output by `100` to convert metres to Unreal centimetres.
3. Append `(0, 0)` to that value, making `(0, 0, heightCm)`, and connect it to **World Position Offset**.
For the displaced normal, sample the height at four UV offsets using the same texture object:
- Left/right UV: `TexCoord + (-1/FieldSize, 0)` and `TexCoord + (1/FieldSize, 0)`.
- Down/up UV: `TexCoord + (0, -1/FieldSize)` and `TexCoord + (0, 1/FieldSize)`.
- `dHdx = (Right.R - Left.R) * FieldSize / (2 * PlaneLengthMeters)`.
- `dHdy = (Up.R - Down.R) * FieldSize / (2 * PlaneLengthMeters)`.
- Make and normalize the tangent-space vector `(-dHdx, -dHdy, 1)`, then connect it to **Normal**.
The built-in plane's U/V tangent directions align with local X/Y. Use a neutral base colour and roughness around `0.7` to start. The graph assumes a horizontal plane; add a local-to-world transform if the plane needs arbitrary rotation.
## Drive objects
`ObjectInputs` has two slots. Each `FVFLivingPlaneObjectInput` contains world position in centimetres, world velocity in centimetres per second, radius in centimetres, base push acceleration in metres per second squared, and an active flag. Blueprint can use **Set Object State** or set the array entry; C++ can call the same method. Set the object's center about one radius above the plane while it contacts it, and mark the slot inactive after lift-off.
The compute shader uses the radius to scale the Gaussian contact footprint. The original wave equation keeps the L163 wave speed, damping, foundation spring, travelling breathing force, and recovering imprint. The base push acceleration supplies the object force; lateral speed adds `0.25 m/s²` per `1 m/s`, and downward speed adds `0.5 m/s²` per `1 m/s`. Upward velocity does not add force. Object positions and velocities are transformed into the plane's local frame and converted from centimetres to metres before upload.
Each step uses `dt = 1.30 / sqrt(8*c*c/(dx*dx) + foundationStiffness)`, with `dx = PlaneLengthMeters / FieldSize`. Simulation time passed to breathing is the fixed step index times `dt`. Game-thread delta time only determines how many fixed steps are queued; no GPU field data is read back in the normal path.
## Build files
The owner's Windows build needs every added source and shader file:
- `Source/VoxelForgeLivingPlane/VoxelForgeLivingPlane.Build.cs`
- `Source/VoxelForgeLivingPlane/Public/VoxelForgeLivingPlaneActor.h`
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneActor.cpp`
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneModule.cpp`
- `Shaders/LivingPlane.usf`
The `VoxelForge.uplugin` runtime-module entry must ship with them.
+33
View File
@@ -0,0 +1,33 @@
# Unreal mapping note
The prototype stores height, vertical velocity, a recovering imprint, and a spare
channel in a pair of `RGBA32F` textures. A world-owned Unreal component can keep
equivalent transient render graph textures and advance them with a compute pass. It
should own simulation time and clear or restore its buffers when the world is recreated.
The sphere path becomes a world-space interaction event: position, contact radius,
and impulse strength are supplied to the compute pass. For multiplayer or deterministic
replay, replicate the compact events and simulation tick, or record the resulting field;
do not assume that independent GPU floating-point runs will produce byte-identical
textures.
The rendered top surface can remain a render-only displaced mesh. If it must participate
in voxel collision or queries, its height must also reach the density evaluator. For a
height `h(x,y)`, the plugin's density convention is negative inside solid and positive
in air, so the plane's signed density is `z - h(x,y)`. Keep this calculation in metres
until the existing voxel/Core boundary converts with the active voxel size (25 cm by
default); do not mix metre distances and voxel counts in the wave solver.
The current sample is an isolated visual field. Wiring this into `AVoxelWorld` would
need an explicit owner for simulation lifetime, collision/update cadence, save/load,
and the replication policy before it could affect shipped terrain.
## L164 render-only Unreal implementation
`Source/VoxelForgeLivingPlane/` now implements this render-only path as a separate
runtime module. `AVoxelForgeLivingPlaneActor` owns two UAV-capable RGBA32F render
targets, advances the wave, breathing, and imprint equations with an RDG global shader,
and binds the current texture to its plane material. Blueprint/C++ supplies object
position, radius, velocity, push, and active state. The actor does not call the
generation modules and does not read the field back. Level setup and the material graph
are in [UE-SETUP.md](UE-SETUP.md).
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
OUTPUT_DIR="${PLUGIN_ROOT}/Saved/Linux/l164"
CXX="${CXX:-g++}"
command -v "${CXX}" >/dev/null || { echo "C++ compiler not found: ${CXX}" >&2; exit 2; }
pkg-config --exists egl gl libpng || {
echo "EGL, desktop OpenGL, and libpng development files are required" >&2
exit 2
}
mkdir -p "${OUTPUT_DIR}"
read -r -a GRAPHICS_FLAGS <<<"$(pkg-config --cflags --libs egl gl libpng)"
taskset -c 0-11 prlimit --as=6442450944 -- \
"${CXX}" -std=c++20 -O2 -g -Wall -Wextra -Wpedantic \
"${SCRIPT_DIR}/LivingPlane.cpp" -o "${OUTPUT_DIR}/living-plane" \
"${GRAPHICS_FLAGS[@]}"
echo "built ${OUTPUT_DIR}/living-plane"
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Compare final field checksums across different query and capture settings."""
from __future__ import annotations
import argparse
import json
import pathlib
import shutil
import subprocess
import sys
PLUGIN_ROOT = pathlib.Path(__file__).resolve().parents[2]
def read_checksum(path: pathlib.Path) -> str:
try:
with path.open(encoding="utf-8") as source:
result = json.load(source)
except (OSError, json.JSONDecodeError) as error:
raise RuntimeError(f"could not read {path}: {error}") from error
checksum = result.get("field_checksum_fnv1a64")
if not isinstance(checksum, str) or len(checksum) != 16:
raise RuntimeError(f"{path} has no 16-digit field checksum")
return checksum
def compare(left: pathlib.Path, right: pathlib.Path) -> bool:
left_checksum = read_checksum(left)
right_checksum = read_checksum(right)
if left_checksum != right_checksum:
print(
"FAIL field checksum mismatch: "
f"{left}={left_checksum}, {right}={right_checksum}",
file=sys.stderr,
)
return False
print(f"PASS field checksum matches: {left_checksum}")
return True
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--binary",
type=pathlib.Path,
default=PLUGIN_ROOT / "Saved/Linux/l164/living-plane",
)
parser.add_argument(
"--out",
type=pathlib.Path,
default=PLUGIN_ROOT / "Saved/Linux/l164/repeatability",
)
parser.add_argument("--device", choices=("auto", "cpu", "gpu"), default="cpu")
parser.add_argument("--size", type=int, default=512)
parser.add_argument("--steps", type=int, default=1800)
parser.add_argument(
"--compare-only",
nargs=2,
type=pathlib.Path,
metavar=("LEFT_JSON", "RIGHT_JSON"),
help="compare existing timing.json files; used to exercise the failure path",
)
options = parser.parse_args()
if options.compare_only:
return 0 if compare(*options.compare_only) else 1
if not options.binary.is_file():
parser.error(f"binary not found: {options.binary}; run Experiments/LivingPlane/build.sh")
options.out.mkdir(parents=True, exist_ok=True)
off_dir = options.out / "queries-off_capture-off"
on_dir = options.out / "queries-on_capture-every-900"
for directory in (off_dir, on_dir):
if directory.exists():
shutil.rmtree(directory)
common = [
f"--device={options.device}",
"--scene=alive",
f"--size={options.size}",
f"--steps={options.steps}",
]
runs = (
("--capture-every=0", "--queries=off", off_dir),
("--capture-every=900", "--queries=on", on_dir),
)
for capture, queries, output in runs:
command = [
"taskset",
"-c",
"0-11",
"prlimit",
"--as=6442450944",
"--",
"timeout",
"1740s",
str(options.binary.resolve()),
*common,
capture,
queries,
f"--out={output}",
]
print("RUN " + " ".join(command), flush=True)
log_path = options.out / f"{output.name}.log"
with log_path.open("w", encoding="utf-8") as log_file:
try:
subprocess.run(
command,
cwd=PLUGIN_ROOT,
check=True,
stdout=log_file,
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError:
print(log_path.read_text(encoding="utf-8"), file=sys.stderr)
raise
print(log_path.read_text(encoding="utf-8").rstrip())
left = off_dir / "timing.json"
right = on_dir / "timing.json"
return 0 if compare(left, right) else 1
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
print(f"FAIL repeatability check: {error}", file=sys.stderr)
raise SystemExit(1)
+85
View File
@@ -0,0 +1,85 @@
Texture2D<float4> SourceState;
RWTexture2D<float4> TargetState;
int2 GridSize;
float Dt;
float Dx;
float PlaneLength;
float WaveSpeed;
float Damping;
float FoundationStiffness;
float TimeSeconds;
float2 BreathingSettings;
float4 ImprintSettings;
float4 ObjectPositionRadius[2];
float4 ObjectVelocityPush[2];
[numthreads(8, 8, 1)]
void MainCS(uint3 DispatchThreadId : SV_DispatchThreadID)
{
int2 Pixel = int2(DispatchThreadId.xy);
if (Pixel.x >= GridSize.x || Pixel.y >= GridSize.y)
{
return;
}
int2 Lo = int2(0, 0);
int2 Hi = GridSize - 1;
float4 State = SourceState.Load(int3(Pixel, 0));
float Height = State.r;
float Velocity = State.g;
float Imprint = State.b;
float Left = SourceState.Load(int3(max(Pixel + int2(-1, 0), Lo), 0)).r;
float Right = SourceState.Load(int3(min(Pixel + int2(1, 0), Hi), 0)).r;
float Down = SourceState.Load(int3(max(Pixel + int2(0, -1), Lo), 0)).r;
float Up = SourceState.Load(int3(min(Pixel + int2(0, 1), Hi), 0)).r;
float Laplacian = (Left + Right + Down + Up - 4.0 * Height) / max(Dx * Dx, 1.0e-8);
float2 World = (float2(Pixel) + 0.5) * Dx - PlaneLength * 0.5;
float Interaction = 0.0;
float Contact = 0.0;
for (int ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
{
float4 PositionRadius = ObjectPositionRadius[ObjectIndex];
if (PositionRadius.w <= 0.0)
{
continue;
}
float4 VelocityPush = ObjectVelocityPush[ObjectIndex];
float Radius = PositionRadius.w;
float Sigma = max(Radius * (0.58 / 0.43), Dx);
float2 Offset = World - PositionRadius.xy;
float Weight = exp(-dot(Offset, Offset) / max(2.0 * Sigma * Sigma, 1.0e-8));
float HeightAboveContact = max(PositionRadius.z - Radius, 0.0);
float ContactHeight = 1.0 - smoothstep(0.0, max(Radius, Dx), HeightAboveContact);
float ImpactSpeed = length(VelocityPush.xy) * 0.25 + max(-VelocityPush.z, 0.0) * 0.5;
float Push = max(VelocityPush.w, 0.0) + ImpactSpeed;
Interaction -= Push * Weight * ContactHeight;
if (Push > 0.0)
{
Contact = max(Contact, Weight * ContactHeight);
}
}
float Breathing = 0.0;
if (BreathingSettings.x != 0.0)
{
float Swell = sin(World.x * 0.48 - TimeSeconds * 0.22)
* cos(World.y * 0.42 + TimeSeconds * 0.16);
Breathing = BreathingSettings.y * Swell * sin(TimeSeconds * 0.48);
}
float NextImprint = Imprint;
if (ImprintSettings.x != 0.0)
{
NextImprint = clamp(Imprint * exp(-Dt / max(ImprintSettings.z, 1.0e-4))
- Contact * ImprintSettings.y * Dt, -ImprintSettings.w, 0.0);
}
float FoundationTarget = ImprintSettings.x != 0.0 ? Imprint : 0.0;
float Acceleration = WaveSpeed * WaveSpeed * Laplacian
- FoundationStiffness * (Height - FoundationTarget) + Interaction + Breathing;
float NextVelocity = (Velocity + Acceleration * Dt) * exp(-Damping * Dt);
float NextHeight = Height + NextVelocity * Dt;
TargetState[Pixel] = float4(NextHeight, NextVelocity, NextImprint, 0.0);
}
@@ -0,0 +1,303 @@
#include "VoxelForgeLivingPlaneActor.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/TextureRenderTarget.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "RenderGraphBuilder.h"
#include "RenderGraphUtils.h"
#include "RHICommandList.h"
#include "RHIResources.h"
#include "DataDrivenShaderPlatformInfo.h"
#include "GlobalShader.h"
#include "ShaderParameterStruct.h"
#include "UObject/ConstructorHelpers.h"
#include "Components/StaticMeshComponent.h"
namespace
{
constexpr int32 LivingPlaneThreadGroupSize = 8;
struct FLivingPlaneStepInput
{
FVector4f ObjectPositionRadius[2] = {FVector4f(0, 0, 0, -1), FVector4f(0, 0, 0, -1)};
FVector4f ObjectVelocityPush[2] = {FVector4f(0, 0, 0, 0), FVector4f(0, 0, 0, 0)};
float TimeSeconds = 0.0f;
};
class FVFLivingPlaneStepCS final : public FGlobalShader
{
DECLARE_GLOBAL_SHADER(FVFLivingPlaneStepCS);
SHADER_USE_PARAMETER_STRUCT(FVFLivingPlaneStepCS, FGlobalShader);
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER(FIntPoint, GridSize)
SHADER_PARAMETER(float, Dt)
SHADER_PARAMETER(float, Dx)
SHADER_PARAMETER(float, PlaneLength)
SHADER_PARAMETER(float, WaveSpeed)
SHADER_PARAMETER(float, Damping)
SHADER_PARAMETER(float, FoundationStiffness)
SHADER_PARAMETER(float, TimeSeconds)
SHADER_PARAMETER(FVector2f, BreathingSettings)
SHADER_PARAMETER(FVector4f, ImprintSettings)
SHADER_PARAMETER_ARRAY(FVector4f, ObjectPositionRadius, [2])
SHADER_PARAMETER_ARRAY(FVector4f, ObjectVelocityPush, [2])
SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D<float4>, SourceState)
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D<float4>, TargetState)
END_SHADER_PARAMETER_STRUCT()
public:
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}
};
IMPLEMENT_GLOBAL_SHADER(FVFLivingPlaneStepCS,
"/Plugin/VoxelForgeLivingPlane/LivingPlane.usf", "MainCS", SF_Compute)
UTextureRenderTarget2D* CreateFieldTarget(UObject* Outer, int32 Size, FName Name)
{
UTextureRenderTarget2D* Target = NewObject<UTextureRenderTarget2D>(Outer, Name, RF_Transient);
Target->RenderTargetFormat = RTF_RGBA32f;
Target->bCanCreateUAV = true;
Target->bAutoGenerateMips = false;
Target->ClearColor = FLinearColor::Transparent;
Target->Filter = TF_Bilinear;
Target->AddressX = TA_Clamp;
Target->AddressY = TA_Clamp;
Target->InitAutoFormat(Size, Size);
Target->UpdateResourceImmediate(true);
return Target;
}
}
AVoxelForgeLivingPlaneActor::AVoxelForgeLivingPlaneActor()
{
PrimaryActorTick.bCanEverTick = true;
PlaneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LivingPlaneMesh"));
SetRootComponent(PlaneMesh);
PlaneMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PlaneMesh->SetMobility(EComponentMobility::Movable);
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneAsset(
TEXT("/Engine/BasicShapes/Plane.Plane"));
if (PlaneAsset.Succeeded())
{
PlaneMesh->SetStaticMesh(PlaneAsset.Object);
}
ObjectInputs.SetNum(2);
}
void AVoxelForgeLivingPlaneActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (PlaneMesh)
{
// /Engine/BasicShapes/Plane is 100 cm square; one Unreal scale unit
// therefore corresponds to one metre of authored plane length.
PlaneMesh->SetRelativeScale3D(FVector(PlaneLengthMeters, PlaneLengthMeters, 1.0));
}
}
float AVoxelForgeLivingPlaneActor::GetStableStepSeconds() const
{
const float Dx = PlaneLengthMeters / static_cast<float>(FMath::Max(GridSize, 16));
const float EigenFrequencySquared = 8.0f * WaveSpeedMetersPerSecond * WaveSpeedMetersPerSecond
/ FMath::Max(Dx * Dx, 1.0e-8f) + FoundationStiffnessPerSecondSquared;
return 1.30f / FMath::Sqrt(FMath::Max(EigenFrequencySquared, 1.0e-8f));
}
void AVoxelForgeLivingPlaneActor::BeginPlay()
{
Super::BeginPlay();
GridSize = FMath::Clamp(GridSize, 16, 2048);
PlaneLengthMeters = FMath::Max(PlaneLengthMeters, 1.0f);
FixedStepSeconds = GetStableStepSeconds();
CreateFieldTargets();
if (PlaneMaterial && PlaneMesh)
{
DynamicPlaneMaterial = PlaneMesh->CreateDynamicMaterialInstance(0, PlaneMaterial);
if (DynamicPlaneMaterial)
{
DynamicPlaneMaterial->SetScalarParameterValue(TEXT("PlaneLengthMeters"), PlaneLengthMeters);
DynamicPlaneMaterial->SetScalarParameterValue(TEXT("FieldSize"), static_cast<float>(GridSize));
}
}
UpdateMaterialField();
}
void AVoxelForgeLivingPlaneActor::CreateFieldTargets()
{
FieldTargets[0] = CreateFieldTarget(this, GridSize, TEXT("LivingPlaneStateA"));
FieldTargets[1] = CreateFieldTarget(this, GridSize, TEXT("LivingPlaneStateB"));
CurrentFieldIndex = 0;
SimulationStepCount = 0;
SimulationAccumulatorSeconds = 0.0;
}
void AVoxelForgeLivingPlaneActor::SetObjectState(int32 ObjectIndex,
const FVFLivingPlaneObjectInput& State)
{
if (!ObjectInputs.IsValidIndex(ObjectIndex) || ObjectIndex >= 2)
{
return;
}
ObjectInputs[ObjectIndex] = State;
}
void AVoxelForgeLivingPlaneActor::ResetSimulation()
{
if (!IsInGameThread() || !FieldTargets[0] || !FieldTargets[1])
{
return;
}
CreateFieldTargets();
FixedStepSeconds = GetStableStepSeconds();
UpdateMaterialField();
}
void AVoxelForgeLivingPlaneActor::UpdateMaterialField()
{
if (DynamicPlaneMaterial && FieldTargets[CurrentFieldIndex])
{
DynamicPlaneMaterial->SetTextureParameterValue(TEXT("LivingField"), FieldTargets[CurrentFieldIndex]);
}
}
void AVoxelForgeLivingPlaneActor::EnqueueSimulationSteps(int32 NumSteps)
{
if (NumSteps <= 0 || !FieldTargets[0] || !FieldTargets[1])
{
return;
}
FTextureRenderTargetResource* ResourceA = FieldTargets[0]->GameThread_GetRenderTargetResource();
FTextureRenderTargetResource* ResourceB = FieldTargets[1]->GameThread_GetRenderTargetResource();
if (!ResourceA || !ResourceB)
{
return;
}
TArray<FLivingPlaneStepInput> Steps;
Steps.Reserve(NumSteps);
const FTransform PlaneTransform = GetActorTransform();
for (int32 Step = 0; Step < NumSteps; ++Step)
{
FLivingPlaneStepInput& Input = Steps.AddDefaulted_GetRef();
Input.TimeSeconds = static_cast<float>(SimulationStepCount + Step) * FixedStepSeconds;
for (int32 ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
{
if (!ObjectInputs.IsValidIndex(ObjectIndex) || !ObjectInputs[ObjectIndex].bActive)
{
continue;
}
const FVFLivingPlaneObjectInput& Object = ObjectInputs[ObjectIndex];
const FVector LocalPosition = PlaneTransform.InverseTransformPosition(Object.WorldPositionCm);
const FVector LocalVelocity = PlaneTransform.InverseTransformVector(Object.WorldVelocityCmPerSecond);
const float RadiusMeters = FMath::Max(Object.RadiusCm * 0.01f, 0.001f);
Input.ObjectPositionRadius[ObjectIndex] = FVector4f(
static_cast<float>(LocalPosition.X * 0.01),
static_cast<float>(LocalPosition.Y * 0.01),
static_cast<float>(LocalPosition.Z * 0.01),
RadiusMeters);
Input.ObjectVelocityPush[ObjectIndex] = FVector4f(
static_cast<float>(LocalVelocity.X * 0.01),
static_cast<float>(LocalVelocity.Y * 0.01),
static_cast<float>(LocalVelocity.Z * 0.01),
FMath::Max(Object.PushAccelerationMetersPerSecondSquared, 0.0f));
}
}
const int32 Resolution = GridSize;
const int32 InitialFieldIndex = CurrentFieldIndex;
const float Dt = static_cast<float>(FixedStepSeconds);
const float Dx = PlaneLengthMeters / static_cast<float>(Resolution);
const float PlaneLength = PlaneLengthMeters;
const float WaveSpeed = WaveSpeedMetersPerSecond;
const float Damping = DampingPerSecond;
const float Foundation = FoundationStiffnessPerSecondSquared;
const float BreathingStrength = bEnableBreathing ? BreathingStrengthMetersPerSecondSquared : 0.0f;
const FVector2f BreathingSettings(bEnableBreathing ? 1.0f : 0.0f, BreathingStrength);
const FVector4f ImprintSettings(bEnableImprint ? 1.0f : 0.0f,
ImprintCaptureMetersPerSecond, FMath::Max(ImprintRecoverySeconds, 1.0e-4f),
FMath::Max(ImprintMaxDepthMeters, 0.0f));
ENQUEUE_RENDER_COMMAND(VoxelForgeLivingPlaneStep)(
[ResourceA, ResourceB, Steps = MoveTemp(Steps), InitialFieldIndex, Resolution, Dt, Dx, PlaneLength,
WaveSpeed, Damping, Foundation, BreathingSettings, ImprintSettings]
(FRHICommandListImmediate& RHICmdList) mutable
{
FRDGBuilder GraphBuilder(RHICmdList, RDG_EVENT_NAME("VoxelForgeLivingPlane"));
FRDGTextureRef Fields[2] = {
RegisterExternalTexture(GraphBuilder, ResourceA->GetRenderTargetTexture().GetReference(), TEXT("LivingPlaneStateA")),
RegisterExternalTexture(GraphBuilder, ResourceB->GetRenderTargetTexture().GetReference(), TEXT("LivingPlaneStateB"))};
TShaderMapRef<FVFLivingPlaneStepCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
int32 CurrentIndex = InitialFieldIndex;
for (const FLivingPlaneStepInput& StepInput : Steps)
{
const int32 TargetIndex = 1 - CurrentIndex;
FVFLivingPlaneStepCS::FParameters* PassParameters =
GraphBuilder.AllocParameters<FVFLivingPlaneStepCS::FParameters>();
PassParameters->GridSize = FIntPoint(Resolution, Resolution);
PassParameters->Dt = Dt;
PassParameters->Dx = Dx;
PassParameters->PlaneLength = PlaneLength;
PassParameters->WaveSpeed = WaveSpeed;
PassParameters->Damping = Damping;
PassParameters->FoundationStiffness = Foundation;
PassParameters->TimeSeconds = StepInput.TimeSeconds;
PassParameters->BreathingSettings = BreathingSettings;
PassParameters->ImprintSettings = ImprintSettings;
for (int32 ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
{
PassParameters->ObjectPositionRadius[ObjectIndex] = StepInput.ObjectPositionRadius[ObjectIndex];
PassParameters->ObjectVelocityPush[ObjectIndex] = StepInput.ObjectVelocityPush[ObjectIndex];
}
PassParameters->SourceState = GraphBuilder.CreateSRV(Fields[CurrentIndex]);
PassParameters->TargetState = GraphBuilder.CreateUAV(Fields[TargetIndex]);
FComputeShaderUtils::AddPass(GraphBuilder,
RDG_EVENT_NAME("LivingPlaneStep"),
ComputeShader, PassParameters,
FIntVector(FMath::DivideAndRoundUp(Resolution, LivingPlaneThreadGroupSize),
FMath::DivideAndRoundUp(Resolution, LivingPlaneThreadGroupSize), 1));
CurrentIndex = TargetIndex;
}
GraphBuilder.Execute();
});
CurrentFieldIndex = (InitialFieldIndex + NumSteps) & 1;
SimulationStepCount += NumSteps;
UpdateMaterialField();
}
void AVoxelForgeLivingPlaneActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (FixedStepSeconds <= 0.0 || !FieldTargets[0] || !FieldTargets[1])
{
return;
}
SimulationAccumulatorSeconds += FMath::Max(static_cast<double>(DeltaSeconds), 0.0);
const int32 StepsToRun = FMath::Min(
static_cast<int32>(SimulationAccumulatorSeconds / FixedStepSeconds), 32);
if (StepsToRun > 0)
{
SimulationAccumulatorSeconds -= StepsToRun * FixedStepSeconds;
EnqueueSimulationSteps(StepsToRun);
}
}
void AVoxelForgeLivingPlaneActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
DynamicPlaneMaterial = nullptr;
FieldTargets[0] = nullptr;
FieldTargets[1] = nullptr;
}
@@ -0,0 +1,25 @@
#include "Modules/ModuleManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "ShaderCore.h"
class FVoxelForgeLivingPlaneModule final : public IModuleInterface
{
public:
virtual void StartupModule() override
{
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("VoxelForge"));
if (!Plugin.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("VoxelForgeLivingPlane could not find its owning plugin."));
return;
}
AddShaderSourceDirectoryMapping(
TEXT("/Plugin/VoxelForgeLivingPlane"),
FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders")));
}
};
IMPLEMENT_MODULE(FVoxelForgeLivingPlaneModule, VoxelForgeLivingPlane)
@@ -0,0 +1,114 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "VoxelForgeLivingPlaneActor.generated.h"
class UMaterialInterface;
class UMaterialInstanceDynamic;
class UStaticMeshComponent;
class UTextureRenderTarget2D;
USTRUCT(BlueprintType)
struct FVFLivingPlaneObjectInput
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
FVector WorldPositionCm = FVector(0.0, 0.0, 43.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
FVector WorldVelocityCmPerSecond = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object", meta=(ClampMin="0.1"))
float RadiusCm = 43.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object", meta=(ClampMin="0.0"))
float PushAccelerationMetersPerSecondSquared = 36.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
bool bActive = false;
};
/** A render-only, GPU-stepped heightfield plane. Object state is uploaded; the field is never read back. */
UCLASS(BlueprintType, Blueprintable)
class VOXELFORGELIVINGPLANE_API AVoxelForgeLivingPlaneActor : public AActor
{
GENERATED_BODY()
public:
AVoxelForgeLivingPlaneActor();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
virtual void OnConstruction(const FTransform& Transform) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
UFUNCTION(BlueprintCallable, Category="Living Plane|Object")
void SetObjectState(int32 ObjectIndex, const FVFLivingPlaneObjectInput& State);
UFUNCTION(BlueprintCallable, Category="Living Plane")
void ResetSimulation();
UFUNCTION(BlueprintPure, Category="Living Plane")
int64 GetSimulationStepCount() const { return SimulationStepCount; }
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Living Plane")
TObjectPtr<UStaticMeshComponent> PlaneMesh;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Living Plane|Rendering")
TObjectPtr<UMaterialInterface> PlaneMaterial;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="16", ClampMax="2048"))
int32 GridSize = 256;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="1.0", ClampMax="100.0"))
float PlaneLengthMeters = 12.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
float WaveSpeedMetersPerSecond = 4.5f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
float DampingPerSecond = 1.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
float FoundationStiffnessPerSecondSquared = 28.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation")
bool bEnableBreathing = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0", ClampMax="30.0"))
float BreathingStrengthMetersPerSecondSquared = 4.8f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint")
bool bEnableImprint = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.0", ClampMax="1.0"))
float ImprintCaptureMetersPerSecond = 0.15f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.1", ClampMax="60.0"))
float ImprintRecoverySeconds = 6.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.0", ClampMax="1.0"))
float ImprintMaxDepthMeters = 0.34f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
TArray<FVFLivingPlaneObjectInput> ObjectInputs;
private:
void CreateFieldTargets();
void EnqueueSimulationSteps(int32 NumSteps);
void UpdateMaterialField();
float GetStableStepSeconds() const;
UPROPERTY(Transient)
TObjectPtr<UTextureRenderTarget2D> FieldTargets[2];
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> DynamicPlaneMaterial;
int32 CurrentFieldIndex = 0;
int64 SimulationStepCount = 0;
double SimulationAccumulatorSeconds = 0.0;
double FixedStepSeconds = 0.0;
};
@@ -0,0 +1,28 @@
using UnrealBuildTool;
public class VoxelForgeLivingPlane : ModuleRules
{
public VoxelForgeLivingPlane(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
if (Target.Platform == UnrealTargetPlatform.Linux)
{
bUseUnity = false;
}
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Projects",
"RenderCore",
"RHI"
});
}
}
@@ -1,7 +0,0 @@
# L133 — native floating islands
Stopped after the second route design. The native v9 plan binds the owner’s resolved mouths and passes raw route audit, strict layout (0 issues), 3/3 interface audits, shell (0 floor violations), and ripple (3/3). The candidate dispatches FI natively.
The composed FI walk still fails: 2 capsule collisions, 40 unsupported centers, 0 grade failures; mouth binding passes. The return lane is carved by `passage/0-1/0` near `(19.755, 9.605, -733.553) m` (4.25 m from its 4.5 m tube). At `passage/1-2/0`, the approach has collisions at `(14.402, -10.552, -732.556) m` and `(14.242, -10.712, -732.556) m`; the departure landing probe `(19.166, -14.650, -733.331) m` gets 0/5 support probes. The exact endpoints alone are not enough to place a route clear of both composed passage volumes and retain a supported landing.
No captures, timing matrix, compiler determinism matrix, baseline update, or UE host build were run after the required composed walk gate failed. The owner’s default import remains on its legacy FI fallback. Logs and candidate fixtures are under `Saved/Linux/l133/`.
@@ -1,511 +0,0 @@
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
index cb2b2dd..5a94ab8 100644
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
@@ -496,8 +496,9 @@ namespace VoxelForgeCore
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
// Version 226 unifies the passage influence reach in metres and converts it to core
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
- // support width/6 when deriving that reach.
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
+ // support width/6 when deriving that reach. Version 228 permits the native FI route
+ // planner to bind its fixed endpoints to the resolved passage mouths.
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228;
enum class FieldChannel : std::uint32_t
{
@@ -788,6 +789,15 @@ namespace VoxelForgeCore
std::uint64_t contentHash = 0;
};
+ // The two resolved inter-strate walk points owned by a FloatingIslands slot.
+ // The route planner treats these as fixed player-capsule centers and places
+ // the island chain between them.
+ struct FloatingIslandRouteEndpointsV1
+ {
+ std::array<std::int64_t, 3> arrivalSubvoxels{};
+ std::array<std::int64_t, 3> departureSubvoxels{};
+ };
+
struct WorldAssemblyFeatureGroupBoundsV1
{
std::array<std::int64_t, 3> minimumSubvoxels{};
@@ -1377,6 +1387,9 @@ namespace VoxelForgeCore
// Planner output is ordinary reviewed data; clients can load it without re-running the planner.
VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
const FloatingIslandRecipeV1& recipe, std::string& error);
+ VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
+ const FloatingIslandRecipeV1& recipe,
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error);
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
std::uint64_t seed = 0x49534c414e444231ull);
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
index 33d97d6..42f855a 100644
--- a/Standalone/Core/src/FloatingIslandsInternal.h
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
@@ -285,7 +285,7 @@ namespace floating_island_detail
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
+ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 9
|| (recipe.geometryVersion == 1
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|| recipe.unitsPerVoxel != Q
@@ -1473,7 +1473,8 @@ namespace floating_island_detail
static_cast<double>(anchorIsland->centerSubvoxels[0]),
static_cast<double>(anchorIsland->centerSubvoxels[1]));
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
- ||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
+ ||(recipe.geometryVersion<8
+ &&recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels)
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
>=anchorIsland->radiusSubvoxels)
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
@@ -1607,7 +1608,7 @@ namespace floating_island_detail
||to.ordinal==recipe.anchorIslandOrdinal);
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
- const double routeDeckLift=recipe.geometryVersion>=2
+ const double routeDeckLift=recipe.geometryVersion>=9?0.0:recipe.geometryVersion>=2
?(WorldAssemblyEvaluatorVersion>=178
?(westernSupportConnector
?(WorldAssemblyEvaluatorVersion>=181?-1.0
@@ -2264,6 +2265,335 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
return manifest;
}
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
+ const FloatingIslandRecipeV1& inputRecipe,
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error)
+{
+ FloatingIslandManifestV1 manifest;
+ manifest.recipe = inputRecipe;
+ auto& recipe = manifest.recipe;
+ recipe.geometryVersion = 9;
+ recipe.primaryIslandCount = 9;
+ recipe.anchorIslandOrdinal = recipe.primaryIslandCount - 1;
+ constexpr std::int64_t step = 192 * floating_island_detail::Q;
+ recipe.spacingSubvoxels = step;
+ if (recipe.routeConstraints.version == 0)
+ {
+ recipe.routeConstraints.version = 1;
+ recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
+ recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
+ recipe.routeConstraints.maximumGradeQ16 = 17560;
+ recipe.routeConstraints.sweepSpacingSubvoxels = 128;
+ recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
+ recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
+ recipe.routeConstraints.minimumClearanceSubvoxels = 128;
+ recipe.routeConstraints.minimumSupportProbes = 3;
+ }
+ for (const auto& value : endpoints.arrivalSubvoxels)
+ if (!floating_island_detail::ValidScalar(value))
+ { error = "resolved floating-island arrival mouth is outside the fixed-point range"; return std::nullopt; }
+ for (const auto& value : endpoints.departureSubvoxels)
+ if (!floating_island_detail::ValidScalar(value))
+ { error = "resolved floating-island departure mouth is outside the fixed-point range"; return std::nullopt; }
+
+ const double qPerMetre = static_cast<double>(floating_island_detail::Q)
+ * 1000000.0 / recipe.coreVoxelSizeMicrons;
+ const double dx = static_cast<double>(endpoints.departureSubvoxels[0]
+ - endpoints.arrivalSubvoxels[0]);
+ const double dy = static_cast<double>(endpoints.departureSubvoxels[1]
+ - endpoints.arrivalSubvoxels[1]);
+ const double endpointDistance = std::hypot(dx, dy);
+ const std::int64_t maximumMouthOffset = 64 * floating_island_detail::Q
+ - recipe.routeConstraints.capsuleRadiusSubvoxels - floating_island_detail::Q;
+ const std::int64_t desiredMouthOffset = std::max(recipe.anchorMouthOffsetSubvoxels,
+ static_cast<std::int64_t>(std::llround(qPerMetre * 6.6)));
+ recipe.anchorMouthOffsetSubvoxels = std::min({desiredMouthOffset,
+ maximumMouthOffset, static_cast<std::int64_t>(std::llround(endpointDistance))});
+ if (recipe.anchorMouthOffsetSubvoxels <= 0)
+ { error = "resolved island mouths cannot provide a bounded departure approach"; return std::nullopt; }
+ recipe.anchorRootRadiusSubvoxels = std::max(recipe.anchorRootRadiusSubvoxels,
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
+ + floating_island_detail::Q);
+ recipe.anchorIslandMinimumRadiusSubvoxels = std::max(recipe.minimumRadiusSubvoxels,
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
+ + floating_island_detail::Q + 1);
+ // Keep the resolved fixed mouth on a capsule-grade ramp at the anchor island lip.
+ recipe.topFlattenQ16 = std::max<std::int64_t>(recipe.topFlattenQ16, 52429);
+ recipe.bridgeHalfWidthSubvoxels = std::max(recipe.bridgeHalfWidthSubvoxels,
+ recipe.routeConstraints.capsuleRadiusSubvoxels + 2 * floating_island_detail::Q);
+ if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt;
+
+ auto& islands = manifest.plan.islands;
+ auto& edges = manifest.plan.clusterEdges;
+ auto& route = manifest.plan.route;
+ constexpr std::uint32_t count = 9;
+ double tangentX = endpointDistance > 1.0
+ ? dx / endpointDistance : 1.0;
+ double tangentY = endpointDistance > 1.0
+ ? dy / endpointDistance : 0.0;
+ if (endpointDistance <= 1.0)
+ {
+ const double angle = static_cast<double>(floating_island_detail::UnitQ16(
+ recipe.seed, 0x900000000ull, 0x524f555445ull))
+ * (2.0 * floating_island_detail::Pi / 65536.0);
+ tangentX = std::cos(angle);
+ tangentY = std::sin(angle);
+ }
+ const double normalX = -tangentY;
+ const double normalY = tangentX;
+ const auto localPoint = [&](double along, double side)
+ {
+ return std::array<std::int64_t, 2>{
+ floating_island_detail::RoundQ(
+ static_cast<double>(endpoints.arrivalSubvoxels[0])
+ + tangentX * along + normalX * side),
+ floating_island_detail::RoundQ(
+ static_cast<double>(endpoints.arrivalSubvoxels[1])
+ + tangentY * along + normalY * side)};
+ };
+ std::array<std::array<std::int64_t, 2>, count> primaryXY{};
+ primaryXY[0] = {endpoints.arrivalSubvoxels[0], endpoints.arrivalSubvoxels[1]};
+ primaryXY[1] = localPoint(0.0, static_cast<double>(step));
+ primaryXY[2] = localPoint(static_cast<double>(step), static_cast<double>(step));
+ primaryXY[3] = localPoint(static_cast<double>(step), 0.0);
+ primaryXY[4] = localPoint(static_cast<double>(step), -static_cast<double>(step));
+ primaryXY[5] = localPoint(0.0, -static_cast<double>(step));
+ primaryXY[6] = localPoint(-static_cast<double>(step), -static_cast<double>(step));
+ // Keep the return lane outside the arrival passage's spiral and on the negative
+ // side of the first connector, so the two bridges cannot cross in plan view.
+ // Keep the final approach radial at the passage mouth. This lets the shared
+ // passage resolver keep its authored route inside the placement disk.
+ primaryXY[7] = localPoint(-0.85 * static_cast<double>(step),
+ -0.23 * static_cast<double>(step));
+ const double departureRadial = std::hypot(
+ static_cast<double>(endpoints.departureSubvoxels[0]),
+ static_cast<double>(endpoints.departureSubvoxels[1]));
+ const double anchorScale = departureRadial > 1.0
+ ? std::max(0.0, (departureRadial - recipe.anchorMouthOffsetSubvoxels) / departureRadial)
+ : 0.0;
+ primaryXY[8] = {
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[0] * anchorScale),
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[1] * anchorScale)};
+
+ std::array<double, count> cumulative{};
+ for (std::uint32_t i = 1; i < count; ++i)
+ {
+ const double segmentX = static_cast<double>(primaryXY[i][0] - primaryXY[i - 1][0]);
+ const double segmentY = static_cast<double>(primaryXY[i][1] - primaryXY[i - 1][1]);
+ const double length = std::hypot(segmentX, segmentY);
+ if (!(length > 0.0) || length > static_cast<double>(256 * floating_island_detail::Q))
+ { error = "resolved island mouths require an unsupported serpentine segment length"; return std::nullopt; }
+ cumulative[i] = cumulative[i - 1] + length;
+ }
+ const double totalLength = cumulative.back();
+ if (!(totalLength > 0.0))
+ { error = "resolved floating-island mouths do not define a route"; return std::nullopt; }
+ const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
+ const std::int64_t routeOffset = capsuleHalf
+ + recipe.routeConstraints.minimumClearanceSubvoxels
+ + floating_island_detail::Q / 8;
+ const std::int64_t startTop = endpoints.arrivalSubvoxels[2] - routeOffset;
+ const std::int64_t endTop = endpoints.departureSubvoxels[2] - routeOffset;
+ const std::uint64_t anchorSite = 0x700000000ull + recipe.anchorIslandOrdinal;
+ auto anchorShape = floating_island_detail::MakeIsland(recipe,
+ recipe.anchorIslandOrdinal, primaryXY.back()[0], primaryXY.back()[1], endTop, anchorSite);
+ anchorShape.radiusSubvoxels = std::max(anchorShape.radiusSubvoxels,
+ recipe.anchorIslandMinimumRadiusSubvoxels);
+ anchorShape.topHalfSubvoxels = std::max<std::int64_t>(1,
+ anchorShape.radiusSubvoxels / 5);
+ anchorShape.centerSubvoxels[2] = endTop - anchorShape.topHalfSubvoxels;
+ std::int64_t anchorTop = endTop;
+ for (int iteration = 0; iteration < 4; ++iteration)
+ {
+ anchorShape.topZSubvoxels = anchorTop;
+ anchorShape.centerSubvoxels[2] = anchorTop - anchorShape.topHalfSubvoxels;
+ const double mouthSurface = floating_island_detail::IslandTopSurface(recipe,
+ anchorShape, static_cast<double>(endpoints.departureSubvoxels[0]),
+ static_cast<double>(endpoints.departureSubvoxels[1]));
+ anchorTop = endTop + floating_island_detail::RoundQ(
+ static_cast<double>(anchorTop) - mouthSurface);
+ }
+ std::array<std::int64_t, count> tops{};
+ for (std::uint32_t i = 0; i < count; ++i)
+ {
+ const double t = cumulative[i] / totalLength;
+ tops[i] = floating_island_detail::RoundQ(static_cast<double>(startTop)
+ + static_cast<double>(anchorTop - startTop) * t);
+ }
+
+ islands.reserve(count);
+ for (std::uint32_t i = 0; i < count; ++i)
+ {
+ const std::uint64_t site = 0x700000000ull + i;
+ auto island = floating_island_detail::MakeIsland(recipe, i,
+ primaryXY[i][0], primaryXY[i][1], tops[i], site);
+ if (i == recipe.anchorIslandOrdinal)
+ {
+ island.radiusSubvoxels = std::max(island.radiusSubvoxels,
+ recipe.anchorIslandMinimumRadiusSubvoxels);
+ }
+ const std::int64_t interiorBottom = recipe.layoutMinimumZSubvoxels
+ + recipe.boundarySealSubvoxels;
+ while (island.radiusSubvoxels > recipe.minimumRadiusSubvoxels
+ && floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
+ < interiorBottom)
+ --island.radiusSubvoxels;
+ const std::int64_t requiredRadius = i == recipe.anchorIslandOrdinal
+ ? recipe.anchorIslandMinimumRadiusSubvoxels : recipe.minimumRadiusSubvoxels;
+ if (island.radiusSubvoxels < requiredRadius
+ || floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
+ < interiorBottom)
+ {
+ error = "resolved-mouth island " + std::to_string(i) + " cannot fit above sealed bedrock"
+ + " (radius=" + std::to_string(island.radiusSubvoxels)
+ + ", required=" + std::to_string(requiredRadius)
+ + ", top=" + std::to_string(tops[i])
+ + ", bottom=" + std::to_string(floating_island_detail::IslandBottom(
+ recipe, tops[i], island.radiusSubvoxels))
+ + ", interior_bottom=" + std::to_string(interiorBottom)
+ + ", anchor_top=" + std::to_string(anchorTop)
+ + ", mouth_offset=" + std::to_string(recipe.anchorMouthOffsetSubvoxels) + ")";
+ return std::nullopt;
+ }
+ island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
+ island.topZSubvoxels = tops[i];
+ island.centerSubvoxels[2] = tops[i] - island.topHalfSubvoxels;
+ island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
+ island.topZSubvoxels, island.radiusSubvoxels);
+ floating_island_detail::SetIslandBounds(recipe, island);
+ islands.push_back(std::move(island));
+ }
+ for (std::uint32_t i = 0; i + 1 < count; ++i)
+ edges.push_back(floating_island_detail::MakeConnector(recipe,
+ islands[i], islands[i + 1], 0x600000000ull + i));
+
+ FloatingIslandFieldV1 planningField(recipe, manifest.plan);
+ if (!planningField.isValid())
+ { error = "resolved-mouth island field did not pass the Core field contract"; return std::nullopt; }
+ const std::int64_t routeRadius = 4 * floating_island_detail::Q;
+ std::uint32_t ordinal = 0;
+ for (std::uint32_t segment = 0; segment + 1 < count; ++segment)
+ {
+ const double segmentX = static_cast<double>(primaryXY[segment + 1][0]
+ - primaryXY[segment][0]);
+ const double segmentY = static_cast<double>(primaryXY[segment + 1][1]
+ - primaryXY[segment][1]);
+ const double length = std::hypot(segmentX, segmentY);
+ const std::uint32_t pieces = static_cast<std::uint32_t>(std::max(1.0,
+ std::ceil(length / (4.0 * floating_island_detail::Q))));
+ for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece)
+ {
+ const double t = static_cast<double>(piece) / pieces;
+ const auto x = floating_island_detail::RoundQ(
+ static_cast<double>(primaryXY[segment][0]) + segmentX * t);
+ const auto y = floating_island_detail::RoundQ(
+ static_cast<double>(primaryXY[segment][1]) + segmentY * t);
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
+ recipe, manifest.plan, x, y);
+ if (!surface)
+ { error = "resolved-mouth island chain has no capsule-width support along a connector"; return std::nullopt; }
+ WorldAssemblyRoutePointV1 point;
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
+ 0x800000000ull + ordinal, 0x524f555445ull);
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
+ point.radiusSubvoxels = routeRadius;
+ if (segment == 0 && piece == 0)
+ point.positionSubvoxels = endpoints.arrivalSubvoxels;
+ route.push_back(point);
+ ++ordinal;
+ }
+ }
+ const double mouthLegX = static_cast<double>(endpoints.departureSubvoxels[0]
+ - primaryXY.back()[0]);
+ const double mouthLegY = static_cast<double>(endpoints.departureSubvoxels[1]
+ - primaryXY.back()[1]);
+ const double mouthLegLength = std::hypot(mouthLegX, mouthLegY);
+ const std::uint32_t mouthLegPieces = static_cast<std::uint32_t>(std::max(1.0,
+ std::ceil(mouthLegLength / (4.0 * floating_island_detail::Q))));
+ for (std::uint32_t piece = 1; piece <= mouthLegPieces; ++piece)
+ {
+ const double t = static_cast<double>(piece) / mouthLegPieces;
+ const auto x = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[0])
+ + mouthLegX * t);
+ const auto y = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[1])
+ + mouthLegY * t);
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
+ recipe, manifest.plan, x, y);
+ if (!surface)
+ { error = "resolved island passage mouth has no capsule-width support along its anchor landing at x="
+ + std::to_string(x) + ", y=" + std::to_string(y) + ", piece="
+ + std::to_string(piece) + "/" + std::to_string(mouthLegPieces)
+ + ", anchor_radius=" + std::to_string(anchorShape.radiusSubvoxels); return std::nullopt; }
+ WorldAssemblyRoutePointV1 point;
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
+ 0x800000000ull + ordinal, 0x524f555445ull);
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
+ point.radiusSubvoxels = routeRadius;
+ if (piece == mouthLegPieces)
+ point.positionSubvoxels = endpoints.departureSubvoxels;
+ route.push_back(point);
+ ++ordinal;
+ }
+
+ const double maximumGrade = floating_island_detail::Q16ToDouble(
+ recipe.routeConstraints.maximumGradeQ16) * 0.95;
+ std::vector<std::int64_t> baseHeights;
+ std::vector<double> routeDistance(route.size(), 0.0);
+ baseHeights.reserve(route.size());
+ for (std::size_t i = 0; i < route.size(); ++i)
+ {
+ baseHeights.push_back(route[i].positionSubvoxels[2]);
+ if (i > 0)
+ {
+ const double dx = static_cast<double>(route[i].positionSubvoxels[0]
+ - route[i - 1].positionSubvoxels[0]);
+ const double dy = static_cast<double>(route[i].positionSubvoxels[1]
+ - route[i - 1].positionSubvoxels[1]);
+ routeDistance[i] = routeDistance[i - 1] + std::hypot(dx, dy);
+ }
+ }
+ for (std::size_t i = 0; i < route.size(); ++i)
+ {
+ double profile = static_cast<double>(baseHeights[i]);
+ std::size_t profileSource = i;
+ for (std::size_t j = 0; j < route.size(); ++j)
+ {
+ const double pathDistance = std::abs(routeDistance[i] - routeDistance[j]);
+ const double candidate = static_cast<double>(baseHeights[j])
+ - maximumGrade * pathDistance;
+ if (candidate > profile)
+ {
+ profile = candidate;
+ profileSource = j;
+ }
+ }
+ const std::int64_t plannedHeight = floating_island_detail::RoundQ(profile);
+ const bool fixedEndpoint = i == 0 || i + 1 == route.size();
+ if (fixedEndpoint && plannedHeight != baseHeights[i])
+ {
+ error = "resolved fixed island mouth endpoint " + std::to_string(i)
+ + " needs " + std::to_string(plannedHeight - baseHeights[i])
+ + " extra subvoxels from route sample " + std::to_string(profileSource)
+ + " at z=" + std::to_string(baseHeights[profileSource])
+ + " and path distance=" + std::to_string(static_cast<std::int64_t>(
+ std::abs(routeDistance[i] - routeDistance[profileSource])))
+ + " (arrival_z="
+ + std::to_string(endpoints.arrivalSubvoxels[2]) + ", departure_z="
+ + std::to_string(endpoints.departureSubvoxels[2]) + ", path_length="
+ + std::to_string(static_cast<std::int64_t>(routeDistance.back())) + ")";
+ return std::nullopt;
+ }
+ route[i].positionSubvoxels[2] = plannedHeight;
+ }
+ if (!manifest.finalize(error)) return std::nullopt;
+ const std::string encoded = manifest.ToJson();
+ const auto roundTrip = FloatingIslandManifestV1::FromJson(encoded, error);
+ if (!roundTrip || roundTrip->ToJson() != encoded
+ || roundTrip->contentHash != manifest.contentHash)
+ { if (error.empty()) error = "resolved-mouth floating-island plan did not canonical-round-trip"; return std::nullopt; }
+ return manifest;
+}
+
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
{}
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
index a0cb0a7..468e315 100644
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
@@ -1826,12 +1826,59 @@ int main(int argc,char** argv)
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
- if(((!useExportedCorePlan&&options.FloatingIslandsRecipe)
- ||(options.NativeOwnerFloatingIslandsRecipe
- &&worldAssembly.floatingIslands.empty()))
- &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
- *ownerWorld,ownerJsonRoot,worldAssembly,error))
- {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
+ const bool attachFloatingIslandPlan=(!useExportedCorePlan&&options.FloatingIslandsRecipe)
+ ||(options.NativeOwnerFloatingIslandsRecipe&&worldAssembly.floatingIslands.empty());
+ if(attachFloatingIslandPlan)
+ {
+ const FStrateSlot* islandSlot=nullptr;
+ if(ownerWorld->Manager)
+ for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
+ if(slot.Definition&&slot.Definition->GeneratorType==ECaveGeneratorType::FloatingIslands)
+ {islandSlot=&slot;break;}
+ if(!islandSlot)
+ {std::fprintf(stderr,"Floating-island endpoint resolution found no FloatingIslands strate.\n");return 2;}
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 mouthLayout;
+ if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
+ *ownerWorld,worldAssembly,"floating-islands",mouthLayout,error))
+ {std::fprintf(stderr,"Pre-plan passage layout failed: %s\n",error.c_str());return 2;}
+ VoxelForgeCore::FloatingIslandRouteEndpointsV1 endpoints;
+ bool hasArrival=false,hasDeparture=false;
+ for(const auto& passage:mouthLayout.passages)
+ {
+ const std::size_t lastSlash=passage.stableId.rfind('/');
+ if(lastSlash==std::string::npos||passage.stableId.substr(lastSlash+1)!="0")continue;
+ if(passage.lowerStrateIndex==islandSlot->StrateIndex&&!hasArrival)
+ {
+ endpoints.arrivalSubvoxels=passage.lowerMouthSubvoxels;
+ hasArrival=true;
+ }
+ if(passage.upperStrateIndex==islandSlot->StrateIndex&&!hasDeparture)
+ {
+ endpoints.departureSubvoxels=passage.upperMouthSubvoxels;
+ hasDeparture=true;
+ }
+ }
+ if(!hasArrival||!hasDeparture)
+ {std::fprintf(stderr,"Resolved passage layout does not provide both FI route mouths (arrival=%s departure=%s).\n",
+ hasArrival?"yes":"no",hasDeparture?"yes":"no");return 2;}
+ std::printf("[FloatingIslandResolvedMouths] strate=%d arrival_m=(%.3f,%.3f,%.3f) departure_m=(%.3f,%.3f,%.3f) status=PASS\n",
+ islandSlot->StrateIndex,
+ endpoints.arrivalSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
+ endpoints.arrivalSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
+ endpoints.arrivalSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
+ endpoints.departureSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
+ endpoints.departureSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
+ endpoints.departureSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
+ if(!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
+ *ownerWorld,ownerJsonRoot,worldAssembly,endpoints,error))
+ {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
+ }
if(!options.WorldAssemblyManifestOutputPath.empty())
{
const std::string manifestText=worldAssembly.ToJson();
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
index dbe2a7c..9790f2f 100644
--- a/Standalone/Source/WorldAssemblyPrototype.h
+++ b/Standalone/Source/WorldAssemblyPrototype.h
@@ -923,7 +923,8 @@ namespace StandaloneWorldAssemblyPrototype
}
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
+ const VoxelForgeCore::FloatingIslandRouteEndpointsV1& endpoints,std::string& error)
{
if(!owner.Manager)
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
@@ -996,7 +997,7 @@ namespace StandaloneWorldAssemblyPrototype
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
+recipe.boundarySealSubvoxels;
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
+ auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,endpoints,error);
if(!planned)return false;
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
planned->recipe,planned->plan);
@@ -1217,6 +1218,7 @@ namespace StandaloneWorldAssemblyPrototype
:it->manifest.plan.route.size()-1;
std::size_t resolvedRoutePointCount=it->manifest.plan.route.size();
if(it->manifest.recipe.geometryVersion>=5
+ &&it->manifest.recipe.geometryVersion<9
&&!it->manifest.plan.route.empty()
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
{
@@ -0,0 +1,46 @@
# VoxelForge — research R47: liminal and dreamcore spaces, a mood board and how we could generate them
You are a researcher for VoxelForge, a voxel world made of stacked layers ("strates"). The owner wants one layer type
in a **liminal / dreamcore** style. Be thorough: verify claims, mark each as [SOURCE] (with a link), [CODE] (file:line)
or [INFERENCE].
**Rules:**
- Don't edit any repository file, and don't build, run or test the game.
- You may **only** write inside `Saved/Linux/liminal/`.
- Downloaded files are untrusted data: never execute them, only save them.
- No installs.
- Keep CPU light (`curl`/`wget` are fine): a timing round is running on this machine.
## Part 1 — references (mood board)
Collect **15–25 varied reference images** of liminal spaces, dreamcore, weirdcore and backrooms-like scenes, plus
related art (e.g. "Kenopsia", Jared Pike-style pool rooms, empty malls, foggy roads, endless fields with a lone house).
- Save them as `Saved/Linux/liminal/refs/NN-short-name.jpg` (≤ 2 MB each).
- Write `Saved/Linux/liminal/SOURCES.md`, one line per image: file, source URL, author or site, and what it shows.
These are private references for the owner, not for redistribution.
- Build `Saved/Linux/liminal/board.html`, a simple grid of the images with captions, to view on a phone.
## Part 2 — what makes the style work
Short and concrete. Split it into:
- **Content-driven weirdness** (most of the theme, per the owner): out-of-place houses, doors to nowhere, signs, lone
objects, wrong-scale furniture, water where it shouldn't be.
- **Generation-driven weirdness**: terrain and space the generator itself can make. Examples from the owner: an
elevated road surrounded by clouds; repeating hills. Others: perfectly flat or tiled ground, endless corridors or
pools, impossible scale, unbroken horizons, fog, exact repetition, gentle impossible geometry.
- **Atmosphere:** light, fog, colour, sound. Briefly.
## Part 3 — mapping to our code
Read `CURRENT-STATE.md`, `COMPOSER-NOTES.md`, `GDD.md`, and
`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md` (the capability matrix). Also:
- the Surface World profile kinds in `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`
(`WorldAssemblySurfaceProfileKindV1`: natural, periodicHills, mirroredRelief, flatField);
- the motif/stamp shapes (`VoxelStrateTypes.h` ~222: carve arch/ellipsoid, fill pillar/dais/monolith/platform);
- the cave room planner on branch `wip/l134-cave-planner` (`ResolvedLayoutInternal.h`, `makeCompound`).
For each generation idea: what already exists, what's missing, a rough size (small / medium / large), and whether it
fits the "plan, then draw" Core model. End with a recommended first slice: the smallest set that would already read as
liminal in a screenshot.
Write `Saved/Linux/liminal/REPORT.md` (≤ ~1,200 words) and print a short summary at the end.
@@ -0,0 +1,30 @@
# VoxelForge R47 — liminal / dreamcore
## Result
[INFERENCE] Start with a rare, content-led anomaly on a flat SurfaceWorld: one ordinary, recognizable object placed where it makes no sense, framed by a quiet empty field and mild horizon fog. This can read in a screenshot without a new generator family.
[CODE] The requested references describe different snapshots: `CURRENT-STATE.md` labels the tree L132/Core v227 (`CURRENT-STATE.md:3,7`), while the live Core header says evaluator v236 (`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:513`). R46 calls itself a review of an earlier `assembler/world` snapshot (`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md:3`). The mapping below follows the live source; R46 is historical context.
## 1. What makes it work
- **Content-driven weirdness (the main signal).** [INFERENCE] Keep most geometry ordinary, then break one semantic expectation: a lone house in an indoor hall; a door standing in a field; a sign that points nowhere; a child-sized chair in a huge room; or still pool water in a dry corridor. Repeat one object exactly or vary its scale to make the wrongness legible. [SOURCE] ETH Zürich describes dreamcore as familiar scenes that are hard to place, including impossible proportions; Jared Pike’s *Dream Pools* frames endless pool environments as eerie nostalgia. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/) · [Jared Pike](https://www.jaredpike.art/projects/dream-pools/)
- **Generation-driven weirdness.** [INFERENCE] Use perfectly flat ground, exact repeated hills, a corridor or pool that returns to itself, an elevated road over clouds, oversized rooms, or a horizon with no visible edge. Favor one simple rule repeated cleanly over unrelated noise. [SOURCE] Liminal spaces often use familiar passage places with their expected people or activity absent, making them uncanny. [Liminal space](https://en.wikipedia.org/wiki/Liminal_space) [SOURCE] ETH Zürich describes dreamcore as recognizable but ambiguous scenes. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
- **Atmosphere.** [INFERENCE] Try cool fluorescent light, a weak warm pool of color, low horizon fog and sparse room tone (fluorescent hum, wind, or a looping distant splash); leave large areas quiet. [SOURCE] ETH Zürich notes that looping sound and ambiguous cues help make dreamcore recognizable but not fully explainable. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
## 2. Mapping to VoxelForge
| Idea | Already exists / missing | Size and “plan, then draw” fit |
|---|---|---|
| Wrong-place props and landmarks | [CODE] Shared decorations and rare landmarks can spawn actors or instanced meshes with deterministic placements (`ARCHITECTURE.md:267-349`; `Source/VoxelForge/Public/VoxelStrateTypes.h:2407,2471`). [CODE] Generated schema-2 slots currently carry no content/visual record, and theme draws for materials, creatures and audio remain open (`COMPOSER-NOTES.md:115-117,1484-1485`). | [INFERENCE] **Small** for one authored prop kit; **medium** to make the composer select/store coherent content. [INFERENCE] Fits a resolved placement plan and later Unreal draw; these props are not Core density geometry. |
| Flat ground and repeating hills | [CODE] Surface profile kinds include `flatField`, `periodicHills` and `mirroredRelief`; seeded phases are resolved during planning (`VoxelForgeCore.h:560-610`). No profile kind describes tiled ground materials. | [INFERENCE] **Small** to select a flat or periodic profile; **medium** for a dedicated tiled material/pattern pass. [CODE] Yes: profile parameters are recipe/plan data consumed by the Core field. |
| Raised road among clouds | [CODE] Floating Islands is an existing family (`VoxelStrateTypes.h:196-197`); persistent ceiling/floor actors can make a cloud sea (`ARCHITECTURE.md:457-464`), and landmarks can carry a mesh bridge. [INFERENCE] There is no demonstrated Core road/path structure that couples its geometry to the island route. | [INFERENCE] **Medium** for a finite authored bridge or platform chain; **large** for a robust generated road network. [INFERENCE] A finite road can be planned as records and drawn as mesh/content; cloud actors stay in Unreal presentation, outside Core density evaluation. |
| Endless corridors and pools | [CODE] The cave planner makes a deterministic finite room-and-tunnel graph between passage mouths (`VoxelForgeCore.h:1314-1324`); `makeCompound` emits two to four overlapping room primitives (`ResolvedLayoutInternal.h:3037-3045`). Core exposes water channels (`VoxelForgeCore.h:515-519`). [INFERENCE] The planner is not an endless loop grammar and does not specify a tiled pool basin/waterline. | [INFERENCE] **Medium** for a finite repeated room/pool set-piece; **large** for continuous repeat with reliable traversal and water presentation. [CODE] Finite rooms/edges fit plan-then-draw; truly unbounded repetition needs a new deterministic local-repeat field/streaming rule. |
| Impossible scale, horizon and geometry | [CODE] Resolved stamps already include carve ellipsoid/arch and fill pillar/dais/monolith/platform with bounded, stable placement (`VoxelForgeCore.h:912-941`); stamps are composed after boundaries/passages (`ARCHITECTURE.md:2242-2265`). [INFERENCE] They can sell oversized objects or a standalone arch, but not non-Euclidean topology or a genuinely impossible connection. | [INFERENCE] **Small** for bounded silhouette changes; **large** for topology-changing spaces. [CODE] Bounded stamp records fit plan-then-draw; topology changes do not fit the current finite stamp vocabulary. |
| Fog, color and sound | [CODE] Per-strate fog, skylight tint/intensity and persistent layer actors exist (`ARCHITECTURE.md:457-470`; `VoxelStrateDefinition.h:283-362`). [CODE] Composer theme draws for audio are still open (`COMPOSER-NOTES.md:1484-1485`). | [INFERENCE] **Small** to reuse fog/light settings; **medium** to author a coherent audio/theme selection. [INFERENCE] Fits a presentation record selected before draw, but remains outside Core density planning. |
[CODE] The game design requires every strate to keep a findable open-air entrance and exit and says anomalies should be rare and stand apart from grounded neighbours (`GDD.md:132-153`). [CODE] The `motif_family_override` prototype path is restricted to resolved SurfaceWorld/flatField recipes (`Standalone/Source/WorldAssemblyPrototype.h:921-943`); the shared stamp vocabulary itself is broader.
## Recommended first slice
[INFERENCE] Author one rare SurfaceWorld `flatField` layer with a single familiar doorway or small house mesh placed alone at an implausible scale, plus low horizon fog and one soft light tint. Keep the rest empty; the viewer should recognize the object before noticing the place has no sensible context. This uses existing profile, placement and atmosphere hooks, so the screenshot slice is **small** and does not require a new density family. [CODE] The mesh placement/presentation remains separate from Core’s density plan, and generated schema-2 slots do not yet carry its visual record (`COMPOSER-NOTES.md:115-117`); a composer-ready version will need that record and theme tags.
@@ -0,0 +1,28 @@
# Reference image sources
Private visual references for the VoxelForge owner. This is not a redistribution set; consult each source page and its rights statement before reusing an image. These are local display copies; some were resized to meet the per-image size limit.
- `01-backrooms-origin.jpg` — [SOURCE] [Bill Magritz (creator) / Bob Mazza (copyright holder)](https://commons.wikimedia.org/wiki/File:HobbyTown_USA_Oshkosh_interior_under_construction_2002_(The_Backrooms).jpg); HobbyTown USA Oshkosh interior under construction 2002, the image associated with “The Backrooms”.
- `02-empty-office.jpg` — [SOURCE] [Ged Carroll](https://commons.wikimedia.org/wiki/File:Empty_office.jpg); Empty office interior.
- `03-empty-mall-hallway.jpg` — [SOURCE] [Michael Rivera (Mjrmtg)](https://commons.wikimedia.org/wiki/File:Empty_Hallway,_Tallahassee_Mall.JPG); Tallahassee Mall hallway.
- `04-mall-source.jpg` — [SOURCE] [Jtalledo](https://commons.wikimedia.org/wiki/File:Mall_at_The_Source_empty.jpg); Mall at The Source, with people present.
- `05-ruoholahti-evening.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_Ruoholahti_shopping_centre_empty_of_people_on_an_evening_in_September_2022.jpg); Ruoholahti shopping centre interior, September 2022.
- `06-city-center-night.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_City-Center_empty_of_people_on_a_night_in_January_2025.jpg); City-Center shopping-centre interior at night, January 2025.
- `07-school-hallway.jpg` — [SOURCE] [Warren LeMay](https://commons.wikimedia.org/wiki/File:Hallway,_Log_Cabin_Association_Elementary_School,_Barkers_Creek,_NC_(39684209853).jpg); Hallway in Log Cabin Association Elementary School, Barkers Creek, North Carolina.
- `08-central-high-hallway.jpg` — [SOURCE] [Thomas J. O’Halloran](https://commons.wikimedia.org/wiki/File:Mostly_empty_hallway_at_Central_High_School_in_Little_Rock.tif); Mostly empty hallway at Central High School, Little Rock.
- `09-hearst-pool.jpg` — [SOURCE] [Mike McBey](https://commons.wikimedia.org/wiki/File:Indoor_pool,_Hearst_Castle_(32062000267).jpg); Indoor pool at Hearst Castle.
- `10-ymca-pool.jpg` — [SOURCE] [Joe Mabel](https://commons.wikimedia.org/wiki/File:Vancouver_-_Robert_Lee_YMCA_pool_01.jpg); Robert Lee YMCA pool, Vancouver.
- `11-jerma-hotel-pool.jpg` — [SOURCE] [Anonymous uploader](https://commons.wikimedia.org/wiki/File:Jerma_Hotel_Indoor_Pool_(first_angle).jpg); Jerma Hotel indoor pool.
- `12-dream-pools-01.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
- `13-dream-pools-02.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
- `14-foggy-road.jpg` — [SOURCE] [Dennis Bruijn](https://commons.wikimedia.org/wiki/File:Foggy_Road_(67154193).jpeg); Foggy Road (500px image).
- `15-driving-through-mist.jpg` — [SOURCE] [Tomasz Zagórski (Unsplash contributor)](https://commons.wikimedia.org/wiki/File:Driving_through_mist_(Unsplash).jpg); Driving through mist.
- `16-foggy-campus.jpg` — [SOURCE] [Ramesh Lalwani](https://commons.wikimedia.org/wiki/File:Foggy_Morning_in_IIT.jpg); Foggy Morning in IIT.
- `17-empty-playground.jpg` — [SOURCE] [Ian Paterson](https://commons.wikimedia.org/wiki/File:Empty_playground_-_geograph.org.uk_-_4118218.jpg); Empty playground, Geograph image 4118218.
- `18-playground-park.jpg` — [SOURCE] [mycurrency.com (uploader credit)](https://commons.wikimedia.org/wiki/File:Empty_Playground_in_a_park.jpg); Empty playground in a park.
- `19-lone-house.jpg` — [SOURCE] [Mayqel](https://commons.wikimedia.org/wiki/File:Lone_House.jpg); Lone House, Blender render.
- `20-door-to-nowhere.jpg` — [SOURCE] [Benmckune](https://commons.wikimedia.org/wiki/File:Door_to_Nowhere_Winchester_House_6-5-06.jpg); “Door to Nowhere” at Winchester Mystery House.
- `21-abandoned-theater.jpg` — [SOURCE] [Noki Va](https://commons.wikimedia.org/wiki/File:Abandoned_Theater_(97702985).jpeg); Abandoned Theater.
- `22-empty-retail-space.jpg` — [SOURCE] [Wikimedia Commons (uploader credit on source page)](https://commons.wikimedia.org/wiki/File:Empty_Retail_Space.jpg); Empty retail space.
- `23-historic-rail-platform.jpg` — [SOURCE] [Ben Brooksbank](https://commons.wikimedia.org/wiki/File:Euston_railway_station_geograph-2197358-by-Ben-Brooksbank.jpg); Euston railway station, historic view.
- `24-empty-airport-terminal.jpg` — [SOURCE] [Matt @ PEK](https://commons.wikimedia.org/wiki/File:Empty_terminal_in_TPE_airport_(4287773502).jpg); Empty terminal in TPE airport.
+5
View File
@@ -31,6 +31,11 @@
"Type": "Runtime", "Type": "Runtime",
"LoadingPhase": "Default" "LoadingPhase": "Default"
}, },
{
"Name": "VoxelForgeLivingPlane",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{ {
"Name": "VoxelForgeEditor", "Name": "VoxelForgeEditor",
"Type": "Editor", "Type": "Editor",