The stylized tree's foliage was a stack of ellipsoid lobes (canopy) or loose
per-tip cards (cards): the first reads as a bunch of grapes, the second clumps
and leaves holes because every card is drawn independently.
Add a cluster foliage mode built the way stylized foliage is normally authored:
* leaf centres on a plane are blue noise (Poisson disk), so the distribution
has no clumps and no bare patches;
* only the leaves emit geometry, which is how a CPU mesh expresses
'leaves opaque, everything else transparent' without alpha sorting or
transparent overdraw;
* 8-10 such planes are placed around the cluster's Y axis with a small random
tilt and offset, plus a few near-horizontal planes that close the poles;
* vertex normals are transferred from the cluster sphere instead of the leaf's
own plane, so a bundle of flat cards still shades as a rounded mass;
* both windings are emitted, because the engine's mesh3d pipelines cull back
faces unless a Material opts into two-sided rasterization.
Cluster centres are picked from the existing foliage anchors in a shuffled order
under a Poisson separation test, so they cover the crown evenly, stay inside a
hard geometry budget, and the twigs visibly enter the foliage.
FoliageCluster is a standalone implementation unit with its own
FoliageClusterDesc so other recipes can reuse it, and its disk sampling goes
through the module's existing Bridson sampler (poissonDiskPoints) instead of a
second blue-noise implementation.
Also fix the mesh.tree schema, which advertised clusters and colonization as
leafMode/branchAlgorithm choices while the generator rejected both, and was
missing canopy.
Tests live in their own translation unit (the shared procgen.cpp is already
~4500 lines): property tests for the sampler, the sphere normal transfer and
two-sided emission, a case asserting every cluster knob is exposed in the
schema and actually changes the mesh, and an opt-in step-by-step render dump
(EVENGINE_TREE_STEPS_DIR) that draws each construction stage.
Verified with ctest -R 'mesh\.|poisson' (136/136).
Introduce examples/level-designer: a 3D level design tool assembled from the
existing composable editor, procgen, physics and animation components.
- Terrain is referenced rather than embedded. Levels either regenerate it from
sampler parameters, or point at a terrain asset on disk. Both encodings the
runtime persists are decodable: the chunked EVTR archive and the raw EVTRN
float32 heightfield written next to an eve.terrain/1 asset.
- Whitebox pieces come from the ProcgenKit recipe registry (75 prototype
pieces). A level stores only the recipe id plus parameter values, so the mesh,
renderable and play-mode collider are all derived and no geometry is
duplicated into the level file. Piece parameters are driven by the recipe's
own schema.
- Player spawn points are ordinary scene nodes carrying facing plus the
character and motion set play mode should start with.
- Play mode builds a runtime world from the same authored data (terrain height
field plus one static box per piece) and drives the player with the Motion
Matching controller, using capsule collision for movement and a third-person
follow camera. Authored data is never mutated by a play session.
Editor view control is an orbit camera around a pannable focus point: right
drag orbits, middle drag pans along the view's ground basis, Q/E zoom, and F
focuses the selection or frames the whole terrain. Play-mode view is the
third-person follow camera; camera yaw is also the locomotion basis, so mouse
look steers where WASD runs.
Add procgen terrain file decoding:
- TerrainFile.h/.cpp decode EVTR and EVTRN into a Heightmap plus spacing, height
range and format metadata. Failures return structured diagnostics
(not_found, parse_error, unsupported) and never a partially filled grid.
- procgen.loadTerrainFile(path, format) and procgen.loadTerrainBytes(bytes,
format) expose it to script through the existing owning-handle proxy path.
EVTR stores no metres-per-cell, so hasSpacing reports whether the decoded
spacing is authoritative.
Verification: examples/level-designer/smoke.nut drives the live engine over MCP
(56 checks: terrain sampling and snapping, whitebox placement and parameter
rebuild, spawn resolution, panel construction in every tool state, play-mode
assembly and landing on the height field, camera-relative locomotion, character
and motion set switching, level document round trip and rejection, asset
terrain loading and rebuild atomicity). The new decoder has a unit test with 58
assertions covering both encodings, magic mismatches, truncated payloads and
dimension/size disagreement.
- EVENGINE_ENABLE_STRICT_WARNINGS option: MSVC /W4 /WX, GCC/Clang
-Wall -Wextra -Werror, with documented noise suppressions and
-isystem/-external:I for third-party headers; enabled on the
Windows/Linux debug CI jobs.
- Fix warnings across engine, modules and tests exposed by the gate,
including real bugs (incomplete-type deletes, Timespan microsecond
overflow, overload hiding, enum switch gaps, dead code).
- Add Linux ASan+UBSan job, example smoke step, python-tools test
job, ruff config (scripts+tools), weekly Linux coverage job and a
CodeQL workflow.
* chore(ci): add clang-format gate, CODEOWNERS and agent collaboration rules
* refactor(modules): clear remaining back-edges and split the largest TUs
Three known dependency back-edges are gone (module_depgraph.py KNOWN_BACK_EDGES
is now empty; --check passes with 0 violations, 0 cycles):
- window -> graphics: window queries the new IWindowSurfaceHost capability
(common/WindowSurfaceHost.h) instead of holding a Graphics pointer;
Window::setGraphics was removed and all call sites updated.
- window -> image: Window::setIcon was replaced by setIconRGBA(const uint8_t*,
w, h); the window no longer decodes ImageData.
- scene -> graphics: pickScreenAt / collectFrustumIdsAt implementations moved
to graphics/ScenePicking.cpp (graphics -> scene is a legal downward edge);
scene now depends only on spatial.
TU splits (pure moves, verified identical method sets and brace balance):
- vulkan/Graphics.cpp (5118 lines) -> Graphics.cpp (lifecycle), GraphicsPipeline,
Graphics2D, Graphics3D, GraphicsMesh + shared GraphicsInternal.h.
- scene/Scene.cpp -> Scene.cpp (script layer / host lifecycle / serialization)
+ SceneNodes.cpp + shared SceneInternal.h.
Verified with a real build (MSVC 19.51, prebuilt third-party): full build green;
window.* and Scene.* suites pass, including scene picking/culling and GPU cases.
* refactor(graphics): narrow Graphics interfaces and clear public-header leaks
Step 4 — interface segregation:
- New lean interfaces: IGraphics2D, IGraphics3D, IResourceFactory, IPostFX
(pure virtuals, forward-declared types only; graphics/Color.h extracted from
Canvas.h so modules that only need the color type stop pulling Canvas.h).
- Graphics now implements all four facets; consumers can depend on the narrow
interface they use instead of the 129-method god class.
- Migrated voxel (VoxelWorld::drawVisible -> IGraphics3D*) and model3d
(ModelRenderer::buildRenderable(s) -> IResourceFactory&).
- Graphics.h slims from 24 sub-header includes to a handful of small type
headers + forward declarations; the destructor moved out-of-line so
unique_ptr effect members are destroyed where types are complete.
Step 5 — header leaks (module_depgraph.py '*' marks: 14 -> 7):
- map/TileLayer.h, particles/ParticleEmitter.h, avatar/AvatarInstance.h,
animation/SpineAnim.h, map/TileSystem.h, model3d/ModelData.h,
model3d/ModelRenderer.h, audio/Audio.h + Source.h, image/ImageData.h +
Image.h, dialogue/Dialogue.h, procgen Palette/OutputSpec/TextureRecipe/
PbrMaterial -> forward declarations, includes moved to .cpp.
- voxel/VoxelWorld.h: terrain sampler Pimpl'd (unique_ptr + out-of-line
destructor), removing the procgen header dependency.
- gpgpu/vulkan backend headers stop pulling graphics/vulkan/Graphics.h.
- Remaining '*' marks are deliberate: tiny POD/enum headers (GridConfig,
MapObject, BlendMode, Color), an internal backend header
(GraphicsInternal.h -> filesystem), and lean interface references.
Verified with a full clean build (MSVC) and tests: window/Scene/voxel/
RenderSystem/Quad/shader/TextureSampler/resourceLifetime pass; broad suite
only shows the pre-existing PHYSFS-init filesystem-environment failures.
* feat(resources): revive ResourceManager as unified IAssetReloader-backed cache
* fix(resources): leak ResourceManager singleton to avoid exit-time teardown crashes
Cached CPU resources may own third-party handles (FreeType faces, Assimp
scenes, image decode handlers) whose libraries are torn down at process
exit in an unspecified TU order; destroying cached entries from the
singleton destructor can segfault at exit (seen on CI Windows in
model3d.materialApi.objMtl). Keep the singleton alive until OS reclaim;
explicit unload()/clear() still release entries during the run.
* refactor(devtools): consume business modules through capability interfaces
DevTools / cmdline no longer include business-module headers, so trimming
scene / physics / procgen / particles / audio / ui / graphics no longer
requires disabling devtools, and devtools can be switched off with
-DEVENGINE_MODULE_DEVTOOLS=OFF.
New capability interfaces in common/:
- ISceneQuery (scene) - host/node inspection + mutation + transform sync
- IRenderCapture (graphics) - render status, PNG capture, camera control,
visible-entity snapshot, entity-ID mask, G-buffer PNGs
- IPhysicsQuery (physics) - 2D world lifecycle + ray casts
- IProcgenQuery (procgen) - recipe listing, map/mesh generation
- IParticlesQuery (particles) - emitter status + one-shot emit
- IAudioQuery (audio) - volume / stop-all
- IEditorHost (ui) - declarative editor host (used by cmdline too)
Providers register in their module constructors. DevTools rewires McpServer
(render/scene/procgen/physics/particles/audio/host tools), SceneInspect
(now delegates to ISceneQuery + IRenderCapture) and RenderVision (uses
IRenderCapture::capturePngDataUrl); cmdline/Mcp.cpp uses IEditorHost.
manifest: devtools DEPS graphics scene physics procgen particles audio ui
image event filesystem -> event; cmdline DEPS filesystem ui -> filesystem.
Verified: full build green; devtools.mcp.* tests pass; broad suite shows
only the pre-existing PHYSFS-init filesystem-environment failures.
* style: apply clang-format to changed lines after dev merge
* fix(webgpu): include RenderControl/TextureSampler explicitly after Graphics.h slimming
* fix(webgpu): provide newFont/print stubs to satisfy IGraphics2D/IResourceFactory vtables
* fix(common): make ProcgenQuery.h self-sufficient (cstdint/utility for GCC)
Re-add test/hair.cpp and test/runtime.cpp (fixing the missing zeroerr include and enum comparisons), add graphics.resourceLifetime tests for the new release APIs, a test-source manifest check, module declared-layer check with unit tests, and a raw-string-literal regression test.
- graphics.water.render.planar (env-gated): render the scene from a mirror
camera into an offscreen canvas via begin3DFrameToCanvas, then have the
water plane sample it at screen UV. Confirms the mirror-camera capture
(red box in the reflection canvas) and a coherent final frame (sky above,
sky-reflecting water below).
- graphics.render3d.toCanvas: read the canvas once via newImageData instead of
per-pixel getPixel (each getPixel did a waitIdle + full image copy + staging
alloc, making the loop O(N) readbacks and looking like a hang).
The render-to-canvas path (already committed) is what makes this possible; the
offscreen render + subsequent present works once the readback is done once.
Add begin3DFrameToCanvas / end3DFrameToCanvas on Graphics: open a 3D render
pass targeting an offscreen Canvas (color + D32 depth) at the canvas size,
using setMesh3DViewProj/View/CameraPos/Env as the camera. Rendered on a
dedicated command buffer and submitted directly to the graphics queue, so it
does not touch the swapchain / present flow. Enables render targets such as a
mirror-camera planar reflection (reflection captured, though compositing it
into a live scene is a follow-up).
Vulkan: offscreen3D render pass + mesh3d pipeline + pool/cb/fence, canvas
gets a depth attachment + color+depth framebuffer; currentPresentCb/frameToken
route to the offscreen cb during the pass. WebGPU: unsupported (throws).
Test: graphics.render3d.toCanvas renders a red box into a canvas and reads it
back to confirm the scene lands in the offscreen target.
The engine's 3D path cannot render into a user Canvas (begin3DFrame refuses
while a Canvas is active), and 3D always targets the fixed-size scene-color
target, so a mirror-camera planar reflection into an offscreen texture is not
achievable without a renderer-level render-to-texture-with-custom-camera
feature. Remove the fragile demo that relied on double-rendering to screen +
scene-color capture (final present came out black). SSR (committed) and env
cubemap reflection remain the water reflection options.
Add a screen-space reflection (SSR) fullscreen pass and hook it into the Water
shader as an optional, off-by-default reflection overlay.
- ScreenSpaceReflection: ray-marches the reflected ray over the lit scene color
+ hardware depth, reconstructing the world normal from depth (mirrors the
SSGI pass). Outputs reflected color with A = hit validity into a canvas.
Graphics::newScreenSpaceReflection() + script binding; quality knobs
(strength/steps/stepLen/thickness/distance) and enabled toggle.
- Water::setScreenSpaceReflection(enabled, strength) + setViewport: the water
shader samples the reflection texture (bound via the height slot, binding 6)
at screen UV and blends it over the env-cubemap reflection. Off by default.
- New ssr.frag + compile script; vulkan SPIR-V regenerated.
Note: SSR reflects only geometry that is on-screen. For a horizontal water plane
viewed from above the reflected geometry sits in the void below the plane (no
depth there), so SSR finds no hits there; it works for on-screen-reflective
surfaces (glossy floors/walls, grazing water). Env-cubemap reflection remains
the default; SSR is additive when enabled.
The middle-drop ripples were a hard-edged expanding ring (band ~0.055, radius
growing to cover half the plane), which read as an ugly big circle and did not
look like water. Replace with an expanding damped wavelet: a smooth gaussian
envelope times a crest+trough cosine, fading as it grows. Tune scene amplitudes
lower (wave 0.30, ripple 0.55) for calmer, more natural water.
Render a proper skybox: a huge sphere centered on the camera (raised far
plane so it isn't clipped) shaded purely by the env cubemap, so the sky is
clearly visible behind the water plane. Use a vertical-gradient cubemap
(pale horizon -> deep blue zenith) instead of a flat blue. The frame now
reads as bright sky (top) over a sky-reflecting water plane (bottom), with
no dark background.
Add a Waterfall helper (graphics/Waterfall.{h,cpp}) built on the custom
Mesh3D shader path that renders a vertical sheet as falling water:
- Downward flow: scrolling fractal noise + elongated vertical streaks
(streakScale) to convey falling velocity.
- White-water foam: turbulent crests in the body plus foam bands at the
top lip (topFoam) and the bottom splash pool (bottomFoam).
- Sky reflection: Fresnel-weighted reflection of the environment cubemap
plus a specular glint from the primary directional light.
- Bottom transparency fade so the sheet melts into the pool.
Push-constant params drive time, flow speed, turbulence, streak
count/scale, top/bottom foam, foam amount, water color, reflection
intensity and sun intensity. Exposed via Graphics::newWaterfall() and
the Squirrel binding (Waterfall class). Also adds Graphics::saveFramePng
to dump the presented frame to a PNG.
Includes a runnable demo (examples/waterfall-demo) and tests: params
round-trip plus a GPU render test proving the waterfall renders, changes
over time (flowing), and differs between calm and foamy configurations.
Replace the tiled-cube grid with one flat water plane carrying the water
shader (createPlane). A water surface is a plane; the cube tiling was
unnecessary. Renders brighter and cleaner (mean luma ~90, still strong
ripple/Fresnel variation) and fills the frame without block-gap gaps.
- Boost the water shader's sky-reflection blend baseline so the surface
clearly reflects the sky even at non-grazing angles (better first impression).
- Add an env-gated render test (EVENGINE_WATER_RENDER_PNG) that tiles a grid of
unit-cube meshes, each carrying the water shader, and saves a PNG so the water
scene can be previewed. The rendered frame reads as blue sky-reflected water
with visible ripple/block variation (grid std ~53, mean luma ~73).
Add a Water helper (graphics/Water.{h,cpp}) built on the custom Mesh3D
shader path that renders a plane as dynamic water:
- Sky reflection: Fresnel-weighted reflection of the environment cubemap,
blurred more at grazing angles; LOD clamped to available mip levels so a
non-mipmapped env cubemap stays safe.
- Edge ripples: animated shore waves strongest near the water boundary and
fading inward.
- Middle ripples: occasional expanding ring drops near the centre, staggered
by an interval, fading over time.
- Sun glint from the primary directional light + soft foam at the edge.
Push-constant params drive time, wave speed/amplitude/scale, ripple
amplitude/count/interval, edge falloff, water color, reflection tint/intensity
and sun intensity. Exposed via Graphics::newWater() and the Squirrel binding
(Water class). Water::draw() plus mesh+shader accessors let it be attached to a
Renderable3D for the scene graph.
Tests: params round-trip and a GPU render test proving the water renders,
changes over time (dynamic ripples), and differs between flat and rippled
configurations. Verified no regression on procgen + graphics render tests.