incanto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-environment
|
|
3
|
+
description: Generating game environments in Incanto — the deterministic `incanto-env` CLI and `incanto/env` library. EXACTLY three themed 3D generators (arena with boxes/ruins/garden, Terrain3D heightfield worlds with island/alpine/plains/desert/meadow/forest/savanna/snow/wetland/volcanic themes, maze with stone/hedge/canyon) plus 2D mazes, roguelike dungeons and platform courses; insertIntoScene, the GENERATORS catalog + runGenerator; and the environment nodes — Terrain3D (biome texture splatting, heightAt, heightfield collider), shader-water Water3D (FBM waves, anisotropic wind ripple, sparse sun glints, crystal shallows, refraction + depth absorption, reflections, shoreline foam, splash signals + ripples), shader-grass Foliage3D (real curved blade meshes, clumps, rolling wind, distance LOD, character bend), Flowers3D flower plants (lush/sparse/none density dial, daisy/cosmos/bellflower varieties, Voronoi patches, head-bob sway) and ez-tree Tree3D groves (branchy trunks, textured wind-swayed leaves, budget-checked tiers). Use when a game needs a level, stage, arena, terrain, heightfield, island, meadow, grass field, flowers, flower bed, forest, trees, mountain, desert, savanna, snow, tundra, swamp, wetland, marsh, volcano, lava field, maze, dungeon, platform course, scattered props, clouds, water, or a splash effect.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Environment generation — levels from a seed
|
|
7
|
+
|
|
8
|
+
> Shipped inside the `incanto` npm package — this document always matches the
|
|
9
|
+
> installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
|
|
10
|
+
|
|
11
|
+
Generators are PURE and DETERMINISTIC: all randomness flows through a seeded
|
|
12
|
+
`Rng`, so the same `--seed` always emits byte-identical JSON. A generated
|
|
13
|
+
level is reproducible from its seed alone — keep the seed in your scene
|
|
14
|
+
notes/PROJECT docs, and change it (not the output) to reroll a layout.
|
|
15
|
+
|
|
16
|
+
## CLI
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx incanto-env --list # every generator + params
|
|
20
|
+
bunx incanto-env arena --seed 42 --theme ruins # print the NodeJson
|
|
21
|
+
bunx incanto-env terrain --seed 42 --theme island --json
|
|
22
|
+
bunx incanto-env terrain --seed 42 --theme meadow --size 120
|
|
23
|
+
bunx incanto-env maze --seed 7 --theme hedge --width 10 --depth 10
|
|
24
|
+
|
|
25
|
+
# Insert straight into a scene file (validates with incanto-check afterwards):
|
|
26
|
+
bunx incanto-env terrain --seed 42 --theme forest --into src/game.scene.json --at Root/Level
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `--seed` is REQUIRED — determinism is the contract.
|
|
30
|
+
- Subcommands and `--<param>` flags come from the `GENERATORS` metadata
|
|
31
|
+
(camelCase params become kebab-case flags: `wallHeight` → `--wall-height`;
|
|
32
|
+
boolean params are bare flags: `--water`). `--list` prints the full table.
|
|
33
|
+
- Without `--into` the pretty-printed NodeJson goes to stdout (`--json`
|
|
34
|
+
silences the stderr summary for piping).
|
|
35
|
+
- `--into <file>` reads the scene, appends the generated node under `--at`
|
|
36
|
+
(default: the scene root) and writes the file back pretty-printed.
|
|
37
|
+
- `--at` is the '/'-joined chain of node NAMES from the root, root included:
|
|
38
|
+
`Root/Level` targets the root's child `Level`. A missing path is a hard
|
|
39
|
+
`NODE_NOT_FOUND` listing the children that DO exist.
|
|
40
|
+
- `scatter` is library-only — its item templates don't fit CLI flags.
|
|
41
|
+
- The scene editor (`bunx incanto-editor`) drives the same catalog visually:
|
|
42
|
+
the ✦ Generate dialog builds its param form from `GENERATORS` and inserts
|
|
43
|
+
under the selected node as one undoable step (see the incanto-editor skill).
|
|
44
|
+
|
|
45
|
+
## The catalog: 3 themed 3D generators + 3 2D generators
|
|
46
|
+
|
|
47
|
+
The 3D set is EXACTLY `arena`, `terrain`, `maze` — the `theme` param does
|
|
48
|
+
the heavy lifting (one knob, big payoff). Every generator: same skeleton,
|
|
49
|
+
themed palette/dressing, deterministic child names, sun/fill lighting.
|
|
50
|
+
|
|
51
|
+
| generator | theme | what you get |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `terrain` | `island` (default) | Terrain3D island splat (beach→grass→cliff→snow), edge-wrapped rim, a `Sea` Water3D at a COMPUTED drownable level, conifers on the grass band, a `Clouds` group |
|
|
54
|
+
| `terrain` | `alpine` | rugged alpine splat (meadow valleys, stone, snow from 0.55), conifer treeline at mid heights, boulders in the snow band |
|
|
55
|
+
| `terrain` | `plains` | rolling plains splat (grass + dirt patches), scattered broadleaf trees + boulders |
|
|
56
|
+
| `terrain` | `desert` | dune splat (sand, gravel pans, stone mesas), dead trees + boulders |
|
|
57
|
+
| `terrain` | `meadow` (잔디밭) | LOW grassland splat (ez-tree ground set, noise dirt patches), dense `Foliage3D` TUFT carpets + sparse `Flowers3D` beds at flat spots, broadleaf groves, boulders, a HIGH HOT sun (sunlit-meadow stage) |
|
|
58
|
+
| `terrain` | `forest` | LAYERED woodland on a litter splat (ez-tree dark soil + noise moss patches): ~480 trees in 40 region-mixed groves (low-freq noise picks pine- vs broadleaf-dominant patches) + high-tier ELDERS, aspen/birch accent clumps and sapling fringes; bush undergrowth + FERN tuft beds near trunks; fallen logs (tipped dead trunks) near elders, stumps, mossy boulders; a meadow-treated central clearing (bright tuft carpets + sparse `Flowers3D`) |
|
|
59
|
+
| `terrain` | `savanna` | dry GOLDEN plains on a dry-grass splat (grass + sand pans + dirt patches): sparse wide-LOW-canopy acacias (broadleaf, gold-green tint, 3.5–5 m), cured gold tuft carpets, dry-scrub bush clumps, scattered boulders; warm hazy low sun (exposure 1.05), no water |
|
|
60
|
+
| `terrain` | `snow` | snow-dominant TUNDRA splat (snow at every height, rock on steep) — distinct from alpine's banded peaks: sparse cool-DARK conifers (snow-dusted tint), snow-dusted boulders, a LOW pale crisp sun (exposure 0.9), light fog, no water |
|
|
61
|
+
| `terrain` | `wetland` | SWAMP on a mossy-grass + dark-mud splat near a HIGH water table: a STRUCTURAL broad `Swamp` Water3D (always shipped — `quality: 'simple'`, murky green, not the blue fancy sea), lush `Foliage3D` REED beds at the waterline, mossy ground tufts, dead snags + bush undergrowth, mossy boulders; soft overcast green-grey haze |
|
|
62
|
+
| `terrain` | `volcanic` | dark basalt/ash splat (stone + gravel ash beds): bare DEAD snags, dark basalt boulders, 3 `Particles3D` smoke + ember vent pairs; ember-warm low sun + DENSE dark fog (the smoky read rides the fog + ember light — the atmosphere sky stays blue up high, see LEARNINGS), no water |
|
|
63
|
+
| `arena` | `boxes` (default) | the classic FPS stage: floor, 4 walls, colorful random crate obstacles |
|
|
64
|
+
| `arena` | `ruins` | stone palette, obstacles become broken colonnade rows (standing columns + toppled stumps) |
|
|
65
|
+
| `arena` | `garden` | hedge-green walls/blocks, `Foliage3D` grass patches, lush `Flowers3D` beds, a calm `Water3D` pool at the center |
|
|
66
|
+
| `maze` | `stone` (default) | brick-textured walls (`wall/blocks` color + normal maps) with coping caps, stone floor, west entrance → east exit |
|
|
67
|
+
| `maze` | `hedge` | foliage-look walls (grass map) with trimmed `Foliage3D` grass strips along the wall tops, lawn floor, corridor grass patches |
|
|
68
|
+
| `maze` | `canyon` | warm sandstone walls, sand floor, boulders perched on the wall rim |
|
|
69
|
+
|
|
70
|
+
**`terrain`** (3D heightfield world): emits the canonical physics recipe —
|
|
71
|
+
a `Ground` `StaticBody3D` with `collider: {shape: 'heightfield'}` and a
|
|
72
|
+
`Surface` `Terrain3D` child — plus the theme dressing above, ALL placed by
|
|
73
|
+
probing the exact heightmap the node rebuilds at runtime (`heightAt` for
|
|
74
|
+
tree/rock/grass ground contact, slope + height-band rejection so trees skip
|
|
75
|
+
sand/snow/steep cells). Options: `theme`, `size` (200 m square), `maxHeight`
|
|
76
|
+
(0 = theme default: island 4.5, alpine 8, plains 4, desert 5, meadow 1.2,
|
|
77
|
+
forest 2.5, savanna 3, snow 2.2, wetland 1.4, volcanic 4), `water` (adds a
|
|
78
|
+
low valley `Lake` to non-island themes; the island ALWAYS ships its sea and
|
|
79
|
+
the wetland ALWAYS ships its broad swamp water — see the sea-level math
|
|
80
|
+
below). Islands
|
|
81
|
+
auto-fit `maxHeight` DOWN when a seed realizes rim hills no drownable sea
|
|
82
|
+
could cover. Live (conifer/broadleaf) trees get a per-grove SPECIES color
|
|
83
|
+
drawn from the seed: conifers deep blue-green (pine/spruce/fir) with red-brown
|
|
84
|
+
bark, broadleaf mid-warm-green oak/maple, lighter yellow-green aspen/birch
|
|
85
|
+
(grey bark), plus a few autumnal gold/amber groves — grove-to-grove drift on
|
|
86
|
+
top of Tree3D's per-instance jitter, so a forest reads varied, not one flat
|
|
87
|
+
green. Same seed → byte-identical colors (deterministic); dead/bush keep their
|
|
88
|
+
node defaults.
|
|
89
|
+
|
|
90
|
+
**Themed atmosphere — `terrainEnvironment(theme, size?)`**: generators emit
|
|
91
|
+
node trees, so the matching scene `environment` header ships as a separate
|
|
92
|
+
one-liner. It composes the whole rendering stage per theme — physical `sky`
|
|
93
|
+
(island: bright maritime; forest: low WARM sun raking dappled pools under a
|
|
94
|
+
close green-grey haze, lifted ambient so shadowed floor stays readable; desert:
|
|
95
|
+
warm dusty; alpine: crisp far fog + cooler sun + 0.92 exposure so the snow
|
|
96
|
+
keeps texture; meadow/plains: pleasant mid-morning; savanna: warm hazy late
|
|
97
|
+
afternoon over the gold grass; snow: low pale crisp cold-blue light + 0.9
|
|
98
|
+
exposure; wetland: soft overcast green-grey humid haze + lifted ambient;
|
|
99
|
+
volcanic: ember-warm low sun + DENSE dark fog + 0.86 exposure — the smoky
|
|
100
|
+
mood rides the fog + ember light, NOT the sky dome, which stays Preetham-blue
|
|
101
|
+
up high), size-scaled `fog`, `shadows: true`, a LOW ambient (0.18 — the sky's
|
|
102
|
+
image-based ambience is the fill; the generator's `Sun` light runs at ~1.7
|
|
103
|
+
against it so shadows stay visible under ACES; deep-haze themes lift it:
|
|
104
|
+
forest/wetland 0.24–0.26, volcanic 0.22) and a per-theme `iblIntensity` (the
|
|
105
|
+
`?scene=tree` stand's lit-FORM dial: cuts the physical-sky IBL below full so
|
|
106
|
+
the hot sun carves each crown's lit-vs-shadow side instead of a flat-bright
|
|
107
|
+
wash — volcanic 0.5, forest 0.55, plains 0.58, savanna/meadow 0.6–0.62,
|
|
108
|
+
wetland 0.66, alpine/island/snow 0.72, desert 0.75; island stays mild so the
|
|
109
|
+
deep cut never dulls its sea). Spread it into your scene:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { runGenerator, terrainEnvironment } from 'incanto/env';
|
|
113
|
+
const scene = {
|
|
114
|
+
…,
|
|
115
|
+
environment: { background: '#9bc4e2', ...terrainEnvironment('island') },
|
|
116
|
+
root: { …, children: [camera, runGenerator('terrain', { seed: 7, theme: 'island' })] },
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**`arena`** (3D FPS stage): a `StaticBody3D` floor, 4 perimeter walls and N
|
|
121
|
+
obstacles — every body a box collider with a `MeshInstance3D` `Skin` child
|
|
122
|
+
(the canonical visible+collidable composition) — plus `Sun`/`FillLight`/
|
|
123
|
+
`Lamp` lights. Children are deterministically named (`Wall1`…`Wall4`,
|
|
124
|
+
`Obstacle1`…`ObstacleN`). Options: `theme`, `width`, `depth` (meters,
|
|
125
|
+
default 30), `wallHeight` (3), `obstacles` (8).
|
|
126
|
+
|
|
127
|
+
**`maze`** (3D): a recursive-backtracker maze — entrance on the west edge,
|
|
128
|
+
exit on the east, every cell reachable — as one-tile-thick `StaticBody3D`
|
|
129
|
+
wall boxes (merged into runs) over a floor slab, with lights. Every theme
|
|
130
|
+
textures its walls and floor (worldspace `repeat` tiling on the
|
|
131
|
+
`MeshInstance3D` skins) and adds structure: junction `Pillar`s where runs
|
|
132
|
+
meet, `Gate1`–`Gate4` posts framing the entrance/exit plus a tinted
|
|
133
|
+
`EntrancePath`/`ExitPath` floor tile; 'stone' caps every run with coping
|
|
134
|
+
trim, 'hedge' grows `HedgeTop` grass strips along the longest rims, 'canyon'
|
|
135
|
+
keeps its rim boulders. All dressing is decorative `MeshInstance3D` — the
|
|
136
|
+
COLLISION layout (walls only) is identical across themes, and the carved
|
|
137
|
+
LAYOUT is identical across themes for the same seed. Options: `theme`,
|
|
138
|
+
`width`/`depth` (corridor cells, default 8), `cellSize` (2 m), `wallHeight`
|
|
139
|
+
(2.5 m). The boolean grid is also available in the library as
|
|
140
|
+
`carveMaze(rng, cols, rows)` for pathing/item placement.
|
|
141
|
+
|
|
142
|
+
**Maze mood — `mazeEnvironment(theme, span?)`**: the terrainEnvironment
|
|
143
|
+
contract for mazes. Maze scenes should read MOODY at corridor level, not
|
|
144
|
+
showroom-bright: every theme gets a LOW hazy sun, exposure 0.85–0.95, dim
|
|
145
|
+
ambient (≤0.15) and a close fog window (scaled by `span` — pass
|
|
146
|
+
`(2·width+1)·cellSize` if you changed the size, default 34). 'stone' is the
|
|
147
|
+
coolest and foggiest (dungeon courtyard), 'hedge' an overcast garden,
|
|
148
|
+
'canyon' a warm dusk raking the sandstone. The generator's `Sun`/`FillLight`
|
|
149
|
+
are theme-graded to match — pair both halves:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { mazeEnvironment, runGenerator } from 'incanto/env';
|
|
153
|
+
const scene = {
|
|
154
|
+
…,
|
|
155
|
+
environment: { background: '#9bc4e2', ...mazeEnvironment('stone') },
|
|
156
|
+
root: { …, children: [camera, runGenerator('maze', { seed: 7, theme: 'stone' })] },
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**`maze2d`** (2D): the SAME algorithm as `ColorRect2D` floor +
|
|
161
|
+
`StaticBody2D` wall tiles with `ColorRect2D` skins (px, y-down, centered).
|
|
162
|
+
Options: `cols` (10), `rows` (8), `cellPx` (64).
|
|
163
|
+
|
|
164
|
+
**`dungeon2d`** (2D roguelike): rectangular rooms joined by 1-tile
|
|
165
|
+
L-corridors on a 32px tile grid — `ColorRect2D` floor rects (`Room1`…,
|
|
166
|
+
`Corridor1H`/`1V`…) ringed by merged `StaticBody2D` wall segments with
|
|
167
|
+
skins. Options: `rooms` (5), `size` (px; square number on the CLI,
|
|
168
|
+
`[width, height]` via the library; default [960, 720]).
|
|
169
|
+
|
|
170
|
+
**`platforms2d`** (2D course): `StaticBody2D` rect colliders with
|
|
171
|
+
`ColorRect2D` skins, left to right. Spacing is caller-tunable — keep `gapX`
|
|
172
|
+
and `stepY` inside your character's jump arc (px, y-DOWN: negative `stepY`
|
|
173
|
+
steps UP).
|
|
174
|
+
|
|
175
|
+
### Migrating from the removed generators
|
|
176
|
+
|
|
177
|
+
`meadow`, `forest`, `island`, `rocks` and `clouds` left the catalog — they
|
|
178
|
+
are terrain themes now (`runGenerator('meadow', …)` is a hard error that
|
|
179
|
+
says so):
|
|
180
|
+
|
|
181
|
+
| before | now |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `incanto-env meadow --seed 42` | `incanto-env terrain --seed 42 --theme meadow` |
|
|
184
|
+
| `incanto-env forest --seed 42` | `incanto-env terrain --seed 42 --theme forest` |
|
|
185
|
+
| `incanto-env island --seed 42` | `incanto-env terrain --seed 42 --theme island` (smooth Terrain3D island, computed sea level, heightfield collider — no longer voxel) |
|
|
186
|
+
| `incanto-env rocks` / `clouds` | themes scatter their own; the library still exports `generateRocks`/`generateClouds` (deprecated) for custom compositions |
|
|
187
|
+
| `incanto-env terrain` (voxel) | library-only `generateVoxelTerrain` — see the voxel worlds note |
|
|
188
|
+
|
|
189
|
+
The old functions stay exported from `incanto/env` (deprecated, see jsdoc)
|
|
190
|
+
so library code keeps compiling.
|
|
191
|
+
|
|
192
|
+
### Voxel worlds
|
|
193
|
+
|
|
194
|
+
The old voxel terrain lives on as **`generateVoxelTerrain`** (library-only,
|
|
195
|
+
NOT in the catalog, not deprecated): a `VoxelGrid3D` whose `voxels` prop
|
|
196
|
+
carries the heightfield as `[x, y, z, tile]` tuples (value noise; grass
|
|
197
|
+
top, dirt fill, bedrock base). Reach for it when BLOCKS are the point —
|
|
198
|
+
digging, building, minecraft-style games on `VoxelGrid3D` (colliders are
|
|
199
|
+
the game's job: chunk trimeshes near the player, see the minecraft
|
|
200
|
+
template). Options: `seed`, `size` (32 blocks), `height` (8), `water`. The
|
|
201
|
+
voxel `generateIsland` (radial dome falloff, sandy shore) also remains for
|
|
202
|
+
the same use case.
|
|
203
|
+
|
|
204
|
+
## Library
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import {
|
|
208
|
+
generateArena, generateTerrain, generateMaze, generateMaze2D,
|
|
209
|
+
generateDungeon2D, generatePlatforms2D, generateScatter,
|
|
210
|
+
generateVoxelTerrain, GENERATORS, runGenerator, carveMaze, insertIntoScene,
|
|
211
|
+
} from 'incanto/env';
|
|
212
|
+
|
|
213
|
+
const world = generateTerrain({ seed: 42, theme: 'forest', size: 160 });
|
|
214
|
+
|
|
215
|
+
// Every generator is also invokable uniformly by name — what the CLI and
|
|
216
|
+
// editor use. Unknown names are a hard IncantoError listing the valid ones:
|
|
217
|
+
const island = runGenerator('terrain', { seed: 7, theme: 'island' });
|
|
218
|
+
|
|
219
|
+
// GENERATORS is the machine-readable catalog (description, dimension, typed
|
|
220
|
+
// params with defaults/min/max/options) — render UIs and help from it. The
|
|
221
|
+
// theme params carry options arrays that drive the editor dropdowns:
|
|
222
|
+
console.log(GENERATORS.terrain.params.theme.options);
|
|
223
|
+
// ['island','alpine','plains','desert','meadow','forest','savanna','snow','wetland','volcanic']
|
|
224
|
+
|
|
225
|
+
// Scatter takes weighted node JSON templates — YOU say what to strew:
|
|
226
|
+
const props = generateScatter({
|
|
227
|
+
seed: 42,
|
|
228
|
+
count: 30,
|
|
229
|
+
area: [36, 20],
|
|
230
|
+
items: [
|
|
231
|
+
{ type: 'MeshInstance3D', props: { mesh: 'cylinder', size: [0.3, 2, 0.3],
|
|
232
|
+
material: { color: '#6b4a2b' } }, weight: 3 }, // trees (3x as likely)
|
|
233
|
+
{ type: 'MeshInstance3D', props: { mesh: 'sphere', size: [0.5, 0.5, 0.5],
|
|
234
|
+
material: { color: '#777777' } } }, // rocks
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
// Each instance gets a random position in `area`, y-rotation, and ±20% scale
|
|
238
|
+
// jitter; the template's position[1] survives as the ground offset.
|
|
239
|
+
|
|
240
|
+
// insertIntoScene is PURE — it returns a NEW scene, inputs untouched:
|
|
241
|
+
let scene = JSON.parse(readFileSync('src/game.scene.json', 'utf-8'));
|
|
242
|
+
scene = insertIntoScene(scene, meadow, 'Root/Level');
|
|
243
|
+
scene = insertIntoScene(scene, props); // default: under the root
|
|
244
|
+
writeFileSync('src/game.scene.json', JSON.stringify(scene, null, 2));
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Tune `generatePlatforms2D` spacing for reachability:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
generatePlatforms2D({
|
|
251
|
+
seed: 3,
|
|
252
|
+
count: 14,
|
|
253
|
+
width: [80, 160], // platform width range, px
|
|
254
|
+
gapX: [40, 120], // edge-to-edge horizontal gap, px
|
|
255
|
+
stepY: [-80, 40], // vertical step, px y-down — negative climbs
|
|
256
|
+
start: [0, 300], // first platform center
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Foliage3D
|
|
261
|
+
|
|
262
|
+
An instanced vegetation field (in `incanto/3d`, registered by
|
|
263
|
+
`registerNodes3D`): grass, flowers or reeds scattered over an XZ `area`.
|
|
264
|
+
Placement is deterministic from the `seed` PROP (never the engine rng), so
|
|
265
|
+
the same JSON grows the identical field on every machine.
|
|
266
|
+
|
|
267
|
+
Four looks, picked by `style`:
|
|
268
|
+
|
|
269
|
+
| style | what renders | when to pick it |
|
|
270
|
+
|---|---|---|
|
|
271
|
+
| `"tufts"` | ONE InstancedMesh of 3-crossed-quad CARDS (12 verts each) carrying a baked multi-blade alpha-cutout texture, per-instance clump-coherent green tints + dry patches, ported sin·cos sway — kind `grass` only | the dense 잔디밭 — the ez-tree demo's actual grass mechanism; overlapping textured fans read as a full meadow at a fraction of the instances. Layer a sparse `mesh` field on top for close-up blade silhouettes |
|
|
272
|
+
| `"mesh"` (default) | ONE InstancedMesh where EVERY instance is a real tapered blade (7-vertex strip) curved along a vertex-shader quadratic bezier — kind `grass` only | real per-blade geometry — best up close; reads thin at field scale |
|
|
273
|
+
| `"blades"` | TWO crossed quads per slot; the fragment shader draws 8 procedural SDF blades per quad — kind `grass` only; bends to the player (`interaction`) and receives scene `fog` (distant fields melt into the haze like the mesh grass / trees / water) | pinning the pre-mesh ported look |
|
|
274
|
+
| `"simple"` | legacy crossed quads, per-instance colorA→colorB lerp, whole-field shear sway | `flowers`/`reeds` (their only look); ultra-cheap set dressing |
|
|
275
|
+
|
|
276
|
+
What the **tufts** style does (the recon finding behind it: the famous
|
|
277
|
+
ez-tree meadow is NOT per-blade geometry — its `grass.glb` is three crossed
|
|
278
|
+
quads with one grayscale tall-grass cutout texture, instanced 5 000× with
|
|
279
|
+
random green instance colors):
|
|
280
|
+
|
|
281
|
+
- the card texture is **baked at runtime** (~80 tapered blade silhouettes
|
|
282
|
+
fanning from a clumped base, dark roots → bright tips) — no asset fetch,
|
|
283
|
+
headless-safe (no DOM → no texture, geometry still deterministic);
|
|
284
|
+
- count is `⌊area·density/6⌋` (cards are BIG); cards stand ~2.2× `height`
|
|
285
|
+
and ride the same regional height / dry-patch / coverage noise as mesh;
|
|
286
|
+
- per-tuft luminance varies ~2× (upstream-style) — that brightness
|
|
287
|
+
diversity is what carves depth into the canopy;
|
|
288
|
+
- tufts skip the character bend (a card fan has no blade tops to push);
|
|
289
|
+
generators emit `style: "tufts"` for their carpets — meadow density ≈ 30.
|
|
290
|
+
|
|
291
|
+
What the **mesh** style adds (all deterministic from `seed`):
|
|
292
|
+
|
|
293
|
+
- **Real curved blades** — each blade bends along a quadratic bezier whose
|
|
294
|
+
tip is displaced by resting lean + wind + character bend; the root never
|
|
295
|
+
moves. Blades are fully opaque: no alpha sorting, no transparency cost.
|
|
296
|
+
- **8× instances** — one blade per instance needs more instances to match
|
|
297
|
+
the SDF coverage, so mesh plants `⌊area·density·8⌋` (still capped at
|
|
298
|
+
`maxInstances`; past the cap the LOD keeps the NEAR field dense).
|
|
299
|
+
- **Clumps** — a Voronoi-ish cell id from the placement position gives each
|
|
300
|
+
tuft a SHARED hue shift and lean direction, so the field grows patchy
|
|
301
|
+
like a real meadow instead of uniformly green.
|
|
302
|
+
- **Regional height field** — low-frequency noise scales blade height
|
|
303
|
+
0.55–1.7× in ~6 m patches (tall meadow tufts beside short worn spots),
|
|
304
|
+
and tall blades lean/curve harder — they DROOP like real long grass.
|
|
305
|
+
- **Dry-hue patches** — a second noise channel lerps blades toward a dry
|
|
306
|
+
yellow-green ramp, mixing cured-grass hues through the green.
|
|
307
|
+
- **Coverage holes** — `coverage` < 1 (default 0.85) drops blades in
|
|
308
|
+
noise-carved patches so dirt breaks the carpet like a real field;
|
|
309
|
+
`coverage: 1` restores the wall-to-wall carpet (and the exact
|
|
310
|
+
`⌊area·density·8⌋` instance count).
|
|
311
|
+
- **Flowers** — DEPRECATED: `flowers` (0–0.2, default 0) renders that
|
|
312
|
+
fraction of instances as cheap 5-vertex heads riding the canopy. They
|
|
313
|
+
read as confetti — use a **`Flowers3D` node** (real plants, below)
|
|
314
|
+
instead; the generators already do. The prop keeps working for existing
|
|
315
|
+
scenes.
|
|
316
|
+
- **Base-to-tip ramp** — blades root in `groundColor` (soil, with DEEP
|
|
317
|
+
root occlusion — the inside of the volume reads near-black), pass through
|
|
318
|
+
`colorA` and tip out at `colorB`. **Pair `groundColor` with the ground
|
|
319
|
+
under the field** (the `MeshInstance3D` floor color or the Terrain3D
|
|
320
|
+
grass band) so roots melt into the terrain instead of floating on it.
|
|
321
|
+
- **Sun sheen** — fake-cylinder normals across each blade feed a cheap
|
|
322
|
+
Blinn-Phong vs the fixed `sunDirection`; tips glint as they sway. Match
|
|
323
|
+
it to the scene's key light direction.
|
|
324
|
+
- **Rolling wind** — 2-octave directional gusts (large rolling wave + slow
|
|
325
|
+
swell) plus per-blade flutter: waves visibly travel across the field.
|
|
326
|
+
- **Distance LOD** — blades shrink to zero between `fadeStart`..`fadeEnd`
|
|
327
|
+
meters from the camera, and HALF the blades (instance-hash) bow out by
|
|
328
|
+
the midpoint, halving far-field work. Far blades also widen slightly so
|
|
329
|
+
the carpet stays closed instead of aliasing away.
|
|
330
|
+
|
|
331
|
+
mesh + blades share the character bend: with `interaction: true` the
|
|
332
|
+
nearest ≤4 moving bodies (`CharacterBody3D` / `RigidBody3D`) push blade
|
|
333
|
+
tops radially away with a smooth falloff as a character wades through.
|
|
334
|
+
|
|
335
|
+
```json
|
|
336
|
+
{ "name": "Grass", "type": "Foliage3D",
|
|
337
|
+
"props": { "area": [30, 30], "density": 10, "seed": 7,
|
|
338
|
+
"groundColor": "#2e4a26" } }
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
| prop | default | meaning |
|
|
342
|
+
|---|---|---|
|
|
343
|
+
| `kind` | `"grass"` | `grass` \| `flowers` \| `reeds` — blade silhouette (load-time check) |
|
|
344
|
+
| `style` | `"mesh"` | `mesh` (curved real blades) \| `tufts` (ez-tree textured cards — the dense 잔디밭) \| `blades` (SDF quads) \| `simple` (legacy quads) |
|
|
345
|
+
| `tuftStyle` | `"grass"` | tufts only: `grass` (dense blade-fan bake) \| `fern` (fewer, wider, arching fronds with leaflet notches — forest undergrowth) |
|
|
346
|
+
| `area` | `[20, 20]` | [x, z] extent in meters, centered on the node |
|
|
347
|
+
| `density` | `12` | instances per m² (mesh plants 8× — total capped at `maxInstances`) |
|
|
348
|
+
| `maxInstances` | `50000` | instance cap (hard ceiling 200000 — split bigger fields) |
|
|
349
|
+
| `colorA` | `"#4d7232"` | bottom/body color (mesh/blades) / lerp start (simple) — warm sunlit meadow green |
|
|
350
|
+
| `colorB` | `"#9aab55"` | tip color (mesh/blades) / lerp end (simple) |
|
|
351
|
+
| `groundColor` | `"#1f3015"` | mesh: soil tint at the blade ROOT — match the ground/terrain color |
|
|
352
|
+
| `sunDirection` | `[0.5, 0.8, 0.3]` | mesh: fixed sun for the specular sheen — match the key light (load-time check: non-zero [x,y,z]) |
|
|
353
|
+
| `fadeStart` | `60` | mesh LOD: camera distance where blades start shrinking, meters |
|
|
354
|
+
| `fadeEnd` | `90` | mesh LOD: gone by here (load-time check: > `fadeStart`) |
|
|
355
|
+
| `height` | `0.25` | base blade height, meters (mesh scales it 0.55–1.7× regionally) |
|
|
356
|
+
| `sway` | `0.6` | wind strength; 0 disables wind entirely |
|
|
357
|
+
| `interaction` | `true` | mesh/blades styles: moving bodies bend nearby grass |
|
|
358
|
+
| `coverage` | `0.85` | mesh: carpet fill in (0, 1] — below 1 noise carves dirt patches (load-time check) |
|
|
359
|
+
| `flowers` | `0` | DEPRECATED — confetti heads; use a `Flowers3D` node (load-time check 0–0.2) |
|
|
360
|
+
| `seed` | `1` | placement seed — identical field across runs/machines |
|
|
361
|
+
| `drape` | `false` | **drape each blade onto a Terrain3D's surface** so a field follows rolling ground instead of floating on a flat plane — one big field covers a whole terrain |
|
|
362
|
+
| `terrain` | `""` | drape target node path; empty = **auto-find the first Terrain3D** in the tree (the zero-config default when `drape` is on) |
|
|
363
|
+
|
|
364
|
+
> **Rolling terrain?** Set `drape: true` and the field's blades root on the
|
|
365
|
+
> ground per-instance (it samples the Terrain3D's `heightAt`). Without draping a
|
|
366
|
+
> Foliage3D/Flowers3D is a FLAT carpet at the node's Y, so on hills it
|
|
367
|
+
> floats/sinks — either drape it, or place small patches at flat spots (the
|
|
368
|
+
> generators still do the latter for backward-compatible output).
|
|
369
|
+
|
|
370
|
+
## Flowers3D
|
|
371
|
+
|
|
372
|
+
A first-class flower FIELD (in `incanto/3d`, registered by
|
|
373
|
+
`registerNodes3D`): instanced procedural flower PLANTS — a curved stem,
|
|
374
|
+
2-3 leaf blades low on the stem, a 5-8 petal head cupped around a
|
|
375
|
+
contrasting center disc, 1-3 blooms per plant at staggered heights — not
|
|
376
|
+
billboard confetti. Deterministic from the `seed` PROP.
|
|
377
|
+
|
|
378
|
+
- **`density` is the vibe dial** — `'lush'` (풍성하게, 2.2 plants/m²),
|
|
379
|
+
`'sparse'` (듬성듬성, 0.35 — the default), `'none'` (없게, renders
|
|
380
|
+
nothing), or any number in plants/m². Load-time checks: valid
|
|
381
|
+
preset/number AND `area × density ≤ 10 000` plants (~1.3M tris budget).
|
|
382
|
+
- **Varieties** — `varieties` picks a unique subset of
|
|
383
|
+
`daisy` (8 flat petals, 3 blooms), `cosmos` (8 cupped petals, 2 tall
|
|
384
|
+
blooms), `bellflower` (5 petals in a deep nodding bell, 3 blooms);
|
|
385
|
+
`[]` (default) = all three. Each variety is ONE merged template geometry,
|
|
386
|
+
instanced per plant as a green-structure mesh + a petal mesh —
|
|
387
|
+
**≤3 varieties → ≤6 draw calls**.
|
|
388
|
+
- **Patches, not confetti** — placement gathers plants into jittered-Voronoi
|
|
389
|
+
patches (`clustering` 0 = uniform … 1 = tight patches, default 0.6), and
|
|
390
|
+
each patch blooms ONE variety in ONE palette color, like real meadows.
|
|
391
|
+
- **`palette`** — head colors as `'#rrggbb'` strings; `[]` (default) = the
|
|
392
|
+
reference white/yellow/violet trio. The center disc keeps its contrasting
|
|
393
|
+
per-variety color.
|
|
394
|
+
- **`height`** — plant height in meters (default 0.45, ±25% per-plant
|
|
395
|
+
jitter; petals scale along). Pick it ABOVE the surrounding grass canopy
|
|
396
|
+
(e.g. 0.6–0.7 over `height: 0.35` Foliage3D) so heads ride the grass.
|
|
397
|
+
- **`sway`** — gentle vertex-shader head bob, phase-hashed per plant; roots
|
|
398
|
+
never move. 0 holds still.
|
|
399
|
+
- Like Foliage3D the field is planar — on rolling terrain place small
|
|
400
|
+
patches at flat spots (the meadow/garden generators do this for you).
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{ "name": "Flowers", "type": "Flowers3D",
|
|
404
|
+
"props": { "area": [26, 26], "density": "sparse", "seed": 7,
|
|
405
|
+
"height": 0.7 } }
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
| prop | default | meaning |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `density` | `"sparse"` | `lush` \| `sparse` \| `none` \| number (plants/m²) — budget-checked at load |
|
|
411
|
+
| `area` | `[20, 20]` | [x, z] extent in meters, centered on the node |
|
|
412
|
+
| `seed` | `1` | placement seed — identical field across runs/machines |
|
|
413
|
+
| `varieties` | `[]` | unique subset of `daisy`/`cosmos`/`bellflower`; `[]` = all three (load-time check) |
|
|
414
|
+
| `palette` | `[]` | head colors, `'#rrggbb'` each; `[]` = white/yellow/violet (load-time check) |
|
|
415
|
+
| `height` | `0.45` | plant height, meters (±25% per-plant jitter) |
|
|
416
|
+
| `clustering` | `0.6` | 0 uniform … 1 tight Voronoi patches (load-time check) |
|
|
417
|
+
| `sway` | `0.5` | head-bob wind strength; 0 disables |
|
|
418
|
+
| `drape` | `false` | drape each plant onto a Terrain3D's surface (rolling terrain) — see the Foliage3D drape note above |
|
|
419
|
+
| `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D |
|
|
420
|
+
|
|
421
|
+
## Tree3D
|
|
422
|
+
Procedural trees grown by the ported ez-tree generator (MIT, Dan Greenheck;
|
|
423
|
+
in `incanto/3d`, registered by `registerNodes3D`) — recursive gnarled
|
|
424
|
+
branches plus alpha-cutout leaf billboards sampling the packaged leaf
|
|
425
|
+
textures. One node is a whole grove in ≤6 draw calls: up to 3 seed VARIANTS
|
|
426
|
+
(differently-grown trees), each one branches-InstancedMesh + one
|
|
427
|
+
leaves-InstancedMesh, `count` instances scattered over `area` and dealt
|
|
428
|
+
round-robin among the variants. Construction is deterministic from the
|
|
429
|
+
`seed` PROP: identical grove on every machine. Per-instance
|
|
430
|
+
hue/scale/rotation jitter keeps a patch from reading as copies; `height`
|
|
431
|
+
jitters ±20% per tree; leaves ride a ported simplex-wind vertex sway.
|
|
432
|
+
|
|
433
|
+
**Crown shading (automatic):** leaf billboards do NOT light like flat
|
|
434
|
+
quads — the generator bakes crown-VOLUME normals (deciduous crowns shade as
|
|
435
|
+
an ellipsoid, conifers as a cone) plus per-leaf occlusion vertex colors
|
|
436
|
+
(interior leaves darken toward 0.45) and per-cluster hue/value jitter, so
|
|
437
|
+
canopies read as lit/shaded three-dimensional masses with a dark heart, not
|
|
438
|
+
paper cut-outs. The leaf cutout threshold relaxes with camera distance
|
|
439
|
+
(1.0× → 0.25× over 40–240 m) and edges ride MSAA alpha-to-coverage, so far
|
|
440
|
+
groves keep solid crowns instead of mip-ghosting away (~300 m). Leaf albedo
|
|
441
|
+
is calibrated for the atmosphere-sky + ACES stage; `canopyColor` still
|
|
442
|
+
tints on top.
|
|
443
|
+
|
|
444
|
+
**Bark** (medium/high tiers): trunks and branches sample the packaged
|
|
445
|
+
ez-tree BARK sets — color + normal + roughness (1k, ambientcg CC0 packaged
|
|
446
|
+
by ez-tree) — tiled over the generator's ring UVs with the upstream
|
|
447
|
+
per-preset repeat, so trunks show ridged bark up close instead of smooth
|
|
448
|
+
plastic. The mapping follows upstream: oak/ash → oak bark, aspen → birch,
|
|
449
|
+
pine → pine; `dead` trees stay barkless weathered gray. Default texture
|
|
450
|
+
source is the agent8 CDN (the only sanctioned external host, like leaves and
|
|
451
|
+
the terrain splat); the same JPGs also ship in `incanto/assets/vegetation/bark/`
|
|
452
|
+
for offline serving.
|
|
453
|
+
|
|
454
|
+
**Tiers are a quality/cost dial** (ported preset → tris per tree):
|
|
455
|
+
|
|
456
|
+
| tier | what grows | tris/tree |
|
|
457
|
+
|---|---|---|
|
|
458
|
+
| `simple` | the original primitive low-poly trees (cones/blobs) — cheapest, unchanged | ~0.1–0.5k |
|
|
459
|
+
| `medium` (default) | light `*-forest` presets — use for FORESTS (many trees) | ≤3k |
|
|
460
|
+
| `high` | full ez-tree presets (oak/ash rotation, airy pine whorls) — hero trees | 7–20k |
|
|
461
|
+
|
|
462
|
+
**Perf budget (load-time check):** `count × tris-per-tree ≤ 1 500 000` —
|
|
463
|
+
the error states the math and the max count for the tier. 500 medium trees
|
|
464
|
+
fit; `high` caps at ~75 trees per node (worst variant ≈ 20k). Geometry
|
|
465
|
+
grows lazily once (1–30 ms per variant), then scatter is matrix-only.
|
|
466
|
+
```json
|
|
467
|
+
{ "name": "Pines", "type": "Tree3D",
|
|
468
|
+
"props": { "type": "conifer", "tier": "medium",
|
|
469
|
+
"count": 24, "area": [40, 40], "seed": 11 } }
|
|
470
|
+
```
|
|
471
|
+
| prop | default | meaning |
|
|
472
|
+
|---|---|---|
|
|
473
|
+
| `tier` | `"medium"` | `simple` \| `medium` \| `high` — see the dial above (load-time check) |
|
|
474
|
+
| `type` | `"conifer"` | `conifer` (pine whorls) \| `broadleaf` (oak/ash greens — autumn-gold aspen left out of the mix on purpose) \| `dead` (bare branches, no leaves) \| `bush` (ported bush_1 shrub — undergrowth; pair with height 2–4) |
|
|
475
|
+
| `seed` | `1` | construction + scatter seed |
|
|
476
|
+
| `height` | `6` | tree height, meters (±20% per-instance jitter) |
|
|
477
|
+
| `count` | `1` | instances; > 1 scatters a forest patch (max 500, budget-checked) |
|
|
478
|
+
| `area` | `[10, 10]` | [x, z] scatter extent when count > 1 |
|
|
479
|
+
| `trunkColor` | `"#7a5a3a"` | bark tint — the DEFAULT hands color to the upstream preset tint (the bark maps carry it); a custom value keeps its hue, max-channel-normalized so it tints the map instead of darkening it. Headless / `dead`: flat untextured tint |
|
|
480
|
+
| `canopyColor` | `"#4a7c3f"` | leaf tint — hue kept, value normalized over the leaf textures |
|
|
481
|
+
|
|
482
|
+
Every instance in a grove also gets a deterministic per-instance color jitter (±~20° hue, ±0.11 lightness off the base canopy/trunk color) so neighbouring trees read varied, not a flat wall of one green — the `terrain` generator stacks per-grove SPECIES colors on top (see above).
|
|
483
|
+
| `leafTexture` | `""` | leaf texture URL override; `""` = packaged per-type ez-tree texture (agent8 CDN — the only sanctioned external host; loads zero-setup). The same PNGs also ship in `incanto/assets/vegetation/` (`incanto-assets list`, kind `foliage`) — copy one next to your game and point here to serve offline |
|
|
484
|
+
| `leafFadeStart` | `0` | **leaf LOD** — leaf cards begin collapsing toward their branch at this camera distance (m). `0`/`0` disables (full leaves at any range) |
|
|
485
|
+
| `leafFadeEnd` | `0` | leaf LOD — cards fully collapsed (zero overdraw) by here; tune ≈ the scene `fog.far` so the thinning hides in fog (load-time check: `> leafFadeStart`) |
|
|
486
|
+
| `leafShadows` | `true` | `false` keeps trunk/branch shadows but drops the expensive alpha-cutout LEAF shadow pass — big FPS in dense forests |
|
|
487
|
+
| `drape` | `false` | **drape a scattered grove onto a Terrain3D**: each instance roots at the ground height under it instead of the node's single Y, so a `count > 1` patch on rolling/carved terrain never floats or buries |
|
|
488
|
+
| `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (the zero-config default when `drape` is on) |
|
|
489
|
+
|
|
490
|
+
> **Tree LOD / forest FPS.** Leaf overdraw + the leaf shadow pass dominate a
|
|
491
|
+
> dense-forest frame (~90% in profiling). For big groves set `leafFadeStart`/
|
|
492
|
+
> `leafFadeEnd` (e.g. `340`/`620` under fog far ~620) so far crowns shed their
|
|
493
|
+
> leaf cards (branch silhouette stays), and `leafShadows: false` to drop the leaf
|
|
494
|
+
> shadow pass. Both default off → existing scenes are unchanged.
|
|
495
|
+
|
|
496
|
+
> **Floating trees?** A scattered grove (`count > 1`) places every instance at
|
|
497
|
+
> the node's single Y, so on rolling or basin-carved terrain the far instances
|
|
498
|
+
> float or sink. Set `drape: true` (auto-finds the Terrain3D) so each instance
|
|
499
|
+
> roots on the ground under it. The drape is a pure heightAt lookup — no rng
|
|
500
|
+
> draw, so the seed/grove layout is byte-identical, only the Y shifts.
|
|
501
|
+
|
|
502
|
+
## Terrain3D
|
|
503
|
+
Procedural heightfield terrain with biome texture splatting (in `incanto/3d`,
|
|
504
|
+
registered by `registerNodes3D`), ported from the agent8 starter terrain:
|
|
505
|
+
seeded simplex octaves displace a plane grid ONCE on the CPU, and a patched
|
|
506
|
+
MeshStandardMaterial blends up to 4 biome textures by height and slope
|
|
507
|
+
(per-vertex weights in vertex colors, world-UV sampling with fbm anti-tiling
|
|
508
|
+
jitter — mip-correct: every sample passes the un-jittered UV's gradients via
|
|
509
|
+
`textureGrad` — normal maps via a screen-space TBN). Same `seed` → identical
|
|
510
|
+
terrain on every machine. Within 25 m of the camera every active layer is
|
|
511
|
+
re-sampled at 10× repeat and added as zero-mean grain (colors AND normals,
|
|
512
|
+
plus a crevice micro-AO), fading out by 60 m — the ground resolves into crisp
|
|
513
|
+
grain up close instead of bilinear mush without shifting tone; textures load
|
|
514
|
+
with anisotropy 8 so grazing angles stay sharp into the distance. All
|
|
515
|
+
automatic, no props.
|
|
516
|
+
{ "name": "Island", "type": "StaticBody3D",
|
|
517
|
+
"props": { "collider": { "shape": "heightfield" } },
|
|
518
|
+
"children": [
|
|
519
|
+
{ "name": "Terrain", "type": "Terrain3D",
|
|
520
|
+
"props": { "size": [200, 200], "maxHeight": 6, "seed": 1, "theme": "island" } }
|
|
521
|
+
] }
|
|
522
|
+
That snippet is the canonical PHYSICS recipe: the `heightfield` collider has
|
|
523
|
+
no params of its own — it pulls the height grid from its Terrain3D CHILD, so
|
|
524
|
+
visuals and collision can never drift apart. It is static-only (load-time
|
|
525
|
+
check) and hard-fails at physics start if the Terrain3D child is missing.
|
|
526
|
+
Skip the StaticBody3D wrapper when the terrain is scenery-only.
|
|
527
|
+
**Themes** preset the splat layers (and the island edge wrap) — load-time
|
|
528
|
+
check lists them on a typo:
|
|
529
|
+
| theme | layers (height × slope placement) | edge wrap |
|
|
530
|
+
| `island` | sand beach 0–0.12 · grass 0.1–0.7 · stone on slopes >45° · snow 0.8–1 | ON — borders curl down a quarter-circle to −50 m (sea floor) |
|
|
531
|
+
| `alpine` | grass valleys 0–0.45 · stone mid+high · gravel on slopes >36° · snow from 0.55 | off |
|
|
532
|
+
| `plains` | grass everywhere · dirt patches 0.35–0.65 · gravel on slopes >45° | off |
|
|
533
|
+
| `desert` | sand everywhere · gravel pans 0–0.15 · stone mesas on slopes >45° | off |
|
|
534
|
+
| `grassland` | the ez-tree demo GROUND set (mossy grass + dense crisp dirt, 30 m tiling): grass at EVERY height · dirt on slopes >45° AND in noise-carved flat patches (the demo's exact 100 m simplex / 0.7 patchiness shaping). With a custom `textureBase` it falls back to the `<base>/grass.png` + `dirt.png` contract | off |
|
|
535
|
+
| `forest` | grassland INVERTED — the LITTER floor: the ez-tree dark organic soil at every height/slope, moss-grass ONLY in noise-carved patches (55 m simplex, ~1/4 coverage). Custom `textureBase`: `<base>/dirt.png` + `grass.png` | off |
|
|
536
|
+
| `savanna` | dry GOLDEN plains: grass at every height · sand pans 0–0.22 · dirt on slopes >45° AND in noise-carved patches (the warm sun does the golden tinting) | off |
|
|
537
|
+
| `snow` | snow-dominant TUNDRA: snow at every height · stone (rock) on slopes >45° only — flat white field, distinct from alpine's banded peaks | off |
|
|
538
|
+
| `wetland` | SWAMP floor: mossy grass at every height · dark dirt (mud) on slopes >45° AND in frequent noise patches (the soft overcast light reads it humid) | off |
|
|
539
|
+
| `volcanic` | dark BASALT/ash: stone (basalt) at every height/slope · gravel (ash beds) pooling in the lows 0–0.28 | off |
|
|
540
|
+
| `custom` | YOUR `layers` (1–4 entries, load-time checked) | off |
|
|
541
|
+
Custom layers look like
|
|
542
|
+
`{ "texture": "url.png", "normalMap": "url_normal.png", "heightRange": [0, 0.3], "slopeRange": [0, 0.8], "repeat": 2, "roughness": 0.9 }`
|
|
543
|
+
— `heightRange` is normalized 0..1 over the realized height span, `slopeRange`
|
|
544
|
+
is radians of `atan(|∇h|)`. Default textures stream from the live agent8 CDN
|
|
545
|
+
(`textureBase` prop) — override it to self-host.
|
|
546
|
+
| `size` | `[200, 200]` | [width, depth] meters, centered on the node |
|
|
547
|
+
| `maxHeight` | `28` | height SCALE — realized heights land at ~3.5–8× this (the ported pipeline sums positive noise octaves); read real numbers off `heightAt` |
|
|
548
|
+
| `seed` | `1` | integer seed — deterministic terrain |
|
|
549
|
+
| `resolution` | `128` | grid segments per side (2–128, load-time check) |
|
|
550
|
+
| `theme` | `"island"` | see table — `custom` requires `layers` |
|
|
551
|
+
| `roughness` | `0.5` | detail-octave persistence (higher = more rugged) |
|
|
552
|
+
| `detail` | `4` | detail octave count |
|
|
553
|
+
| `flatThreshold` | `0.95` | snap-flatten heights within ±0.6 m of `maxHeight·flatThreshold` |
|
|
554
|
+
| `islandEdge` | `null` | `null` = theme decides; force the rim wrap on/off with a boolean |
|
|
555
|
+
| `layers` | `[]` | custom splat layers (theme `custom` only) |
|
|
556
|
+
| `textureBase` | agent8 CDN | base URL: `<base>/<name>.png` + `<base>/<name>_normal.png` |
|
|
557
|
+
| `basins` | `[]` | lake/pond bowls — `[{ x, z, radius, depth }]` (centered meters) carved into the surface (smooth, 0-slope rim); `heightAt`, the collider AND draped grass all see them |
|
|
558
|
+
|
|
559
|
+
**Carving a lake.** A flat Water3D plane on rolling/dome terrain reads as a wet
|
|
560
|
+
patch, not a lake. Carve a `basins` bowl so water fills a real depression with a
|
|
561
|
+
shoreline, then drop a `Water3D` at the floor + a little:
|
|
562
|
+
```ts
|
|
563
|
+
// Terrain3D prop: a 9 m-radius, 3 m-deep bowl at the interior point (20, -15)
|
|
564
|
+
"basins": [{ "x": 20, "z": -15, "radius": 9, "depth": 3 }]
|
|
565
|
+
// then place water just above the carved floor (heightAt sees the bowl):
|
|
566
|
+
const floor = terrain.heightAt(20, -15); // ≈ surroundingGround - 3
|
|
567
|
+
pond.position = [20, floor + 1.2, -15]; // ~60% of the bank submerged
|
|
568
|
+
```
|
|
569
|
+
Keep `radius`/`depth` gentle (depth/radius ≈ 0.3–0.5) so the bank stays walkable
|
|
570
|
+
and the shore splat reads. Interior ponds/lakes are cheapest as `Water3D
|
|
571
|
+
quality:'simple'` — the lake shader (fresnel sky tint + ripple normals + sun
|
|
572
|
+
glint + soft edge fade) with ZERO scene re-renders, vs fancy's ~2 full-scene
|
|
573
|
+
re-renders a frame. Give simple ponds a vivid water `color` (a muddy/dark hue
|
|
574
|
+
reads grey, since the surface IS mostly that color plus a sky sheen) and a
|
|
575
|
+
`sunDirection` matching the key light so the glint lands; its soft alpha edge
|
|
576
|
+
melts the rectangular plane into the shore (no hard rim poking out of the bowl).
|
|
577
|
+
**`heightAt(x, z)`** answers the surface height (world y) at WORLD x/z —
|
|
578
|
+
bilinear over the displaced grid, works headless and before any render.
|
|
579
|
+
Spawn points, prop placement, AI ground checks:
|
|
580
|
+
```ts
|
|
581
|
+
const terrain = scene.root.getNode('Island/Terrain') as Terrain3D;
|
|
582
|
+
crate.position = [x, terrain.heightAt(x, z) + 0.5, z];
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Splat bands follow the surface the eye sees: the island rim's wrap shoulder
|
|
586
|
+
descends through the bands (snow → grass → sand) and every shoreline ends in
|
|
587
|
+
the bottom layer, so beaches happen automatically at the waterline.
|
|
588
|
+
|
|
589
|
+
**Island sea level is a TWO-sided constraint** — probe heights with
|
|
590
|
+
`heightAt` (or `buildHeightmap`) instead of guessing:
|
|
591
|
+
|
|
592
|
+
- the edge wrap only drops the rim 20 m before the vertical skirt to −50 m,
|
|
593
|
+
so the waterline must sit ABOVE `(highest border height − 20 m)` or bare
|
|
594
|
+
cliff walls with stretched stripes poke out of the sea;
|
|
595
|
+
- it should also sit inside the sand band (bottom 12% of the realized span)
|
|
596
|
+
so the visible shore reads as beach.
|
|
597
|
+
|
|
598
|
+
Both fit comfortably at `maxHeight ≈ 4–5` with `size: [200, 200]` (e.g.
|
|
599
|
+
seed 1 → surface 10.4–32 m, cliffs top out at 12.0, sea at 12.8 works).
|
|
600
|
+
Tall islands (`maxHeight ≥ 8` at that size) have rim heights no drownable
|
|
601
|
+
sea level can cover — scale height with care, the wrap radius does not grow.
|
|
602
|
+
|
|
603
|
+
`runGenerator('terrain', { seed, theme: 'island' })` does ALL of this math
|
|
604
|
+
for you: it probes the border with `buildHeightmap`, places the sea above
|
|
605
|
+
every rim cliff top and inside the sand band, and auto-fits `maxHeight`
|
|
606
|
+
down for seeds whose rim is too tall. Hand-author only when you need a
|
|
607
|
+
non-default composition.
|
|
608
|
+
|
|
609
|
+
## Water3D
|
|
610
|
+
|
|
611
|
+
A commercial-quality water surface (in `incanto/3d`, registered by
|
|
612
|
+
`registerNodes3D`). The default `quality: 'fancy'` is a real water shader:
|
|
613
|
+
9-iteration simplex-FBM wave displacement with analytic normals, THREE
|
|
614
|
+
animated ANISOTROPIC detail-normal layers — wind-stretched (~2.8× along the
|
|
615
|
+
wind), ridge-sharpened fine ripple in crossing directions, each layer
|
|
616
|
+
distance-faded at its own range so the far field goes glassy instead of
|
|
617
|
+
aliasing — sun glints as SPARSE micro-facet sparks (extreme shininess +
|
|
618
|
+
noise-gated + weighted toward the sun's azimuth path and grazing views;
|
|
619
|
+
the rest of the surface stays broad calm color fields), per-channel
|
|
620
|
+
Beer's-law depth `absorption` (red dies first — turquoise shallows fade
|
|
621
|
+
WIDE into deep teal-blue, like a real sea), CRYSTAL shallows (inside the
|
|
622
|
+
first ~1.5 m of water the refracted bottom dominates, ripple-distorted —
|
|
623
|
+
you see the sand through the surface), screen-space `refraction`
|
|
624
|
+
(submerged geometry shimmers SOFTLY through the surface — the offset reads
|
|
625
|
+
the smooth gloss normal, fades to zero at object waterlines, mip-blurs the
|
|
626
|
+
grab pass and blends instead of snapping at above-water silhouettes, so
|
|
627
|
+
contact lines stay clean with no comb-pattern streaks), always-on wispy
|
|
628
|
+
noise-broken shoreline `foam` (the foam band's depth range scales with the
|
|
629
|
+
water's extent: ponds get a contact kiss, open seas get whole shallow
|
|
630
|
+
channels), a trough/surface/peak color ramp (teal sea-blue defaults),
|
|
631
|
+
fresnel-mixed CubeCamera reflections re-rendered every `reflectionInterval`
|
|
632
|
+
ms and an edge-fade ring. Fresnel/reflection read a SMOOTH long-wavelength
|
|
633
|
+
normal (never the ridged ripple — that's what kept turning HDR sky
|
|
634
|
+
reflections into white sequins), HDR-capped per sample. `quality: 'simple'`
|
|
635
|
+
is the cheap LAKE shader — fresnel sky-tint reflection (a procedural
|
|
636
|
+
horizon→zenith gradient, NO cube map), animated procedural ripple normals
|
|
637
|
+
(`detailStrength`), a sun glint+sheen (`sunDirection`/`sunColor`/
|
|
638
|
+
`sunIntensity`), a depth-free view-angle body tint (clearer looking down,
|
|
639
|
+
deeper/more-reflective grazing) and a soft alpha edge fade that melts the
|
|
640
|
+
plane's rim into the shore. It does ZERO scene re-renders (no CubeCamera, no
|
|
641
|
+
depth/refraction pre-pass — the three things that drop fancy to ~30 fps at a
|
|
642
|
+
pond), so it's the low-end/perf path AND the deterministic headless path. A
|
|
643
|
+
lake reads as ONE hue derived from `color` (deep = darkened, surface =
|
|
644
|
+
lifted, sky = mostly real sky-blue), so give it a vivid color — a dark/muddy
|
|
645
|
+
hue reads grey.
|
|
646
|
+
|
|
647
|
+
```json
|
|
648
|
+
{ "name": "Water", "type": "Water3D",
|
|
649
|
+
"props": { "size": [40, 40], "position": [0, 3.2, 0] } }
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
| prop | default | meaning |
|
|
653
|
+
|---|---|---|
|
|
654
|
+
| `size` | `[40, 40]` | [width, depth] meters — must be positive (load-time check) |
|
|
655
|
+
| `color` | `"#2a6fbe"` | THE lake hue for `simple` (deep/surface/sky derived from it — use a vivid color); if customized it also tints the fancy ramp |
|
|
656
|
+
| `opacity` | `0.8` | water-body density: lower = clearer (scales how much the refracted scene shows through) |
|
|
657
|
+
| `waveHeight` | `0.04` | wave intensity, meters-ish (0 = mirror-flat; 0.04 = calm sea; 0.08 = the older choppier look) |
|
|
658
|
+
| `waveSpeed` | `1` | animation speed multiplier |
|
|
659
|
+
| `quality` | `"fancy"` | `fancy` full shader water (CubeCamera + scene pre-pass) \| `simple` cheap lake shader, no scene re-renders (load-time check) |
|
|
660
|
+
| `colors` | `{}` | fancy ramp overrides: `{"trough", "surface", "peak"}` hex strings |
|
|
661
|
+
| `reflection` | `true` | live CubeCamera reflections (fancy only; needs a renderer) |
|
|
662
|
+
| `reflectionInterval` | `1000` | ms between CubeCamera reflection re-renders |
|
|
663
|
+
| `foam` | `true` | noise-broken shoreline foam band + crest foam near shores |
|
|
664
|
+
| `interaction` | `true` | character interaction: bodies (CharacterBody3D/RigidBody3D) inside the XZ footprint whose FEET dip below the surface count as in-water — so a WADING character (origin above water, feet below, sized from its collider) splashes, not only a fully-submerged one. Entry/exit emit `entered`/`exited` + a splash ripple; striding through water trails a WAKE (a ripple every ~0.45 m); floating bodies bob gently. Ripples drive both shaders' `uRipples[8]` (fancy) and CPU-displace the mesh (`simple`) |
|
|
665
|
+
| `splash` | `true` | built-in splash WHITEWATER (needs `interaction`): the shaders paint expanding **surface foam** where bodies interact — a **"풍덩"** froth bloom when a body plunges in (bigger the faster it falls) and a **foam trail ("물살")** behind a body wading/running through. Real frothy whitewater on the surface, not flying droplets; works on both `fancy` and `simple` quality. `false` keeps the ripples/waves but paints no foam. No wiring needed; for an extra custom burst add your own `Particles3D` on the `entered` signal |
|
|
666
|
+
| `sunDirection` | `[0.5, 0.8, 0.3]` | TOWARD the sun, for the specular glint — match the scene's key light (non-zero, load-time check) |
|
|
667
|
+
| `sunColor` | `"#fff5d6"` | glint/sheen tint |
|
|
668
|
+
| `sunIntensity` | `1` | glint/sheen strength (≥ 0; 0 = off) |
|
|
669
|
+
| `detailStrength` | `0.3` | animated detail-normal strength (≥ 0; 0 = vertex normals only) |
|
|
670
|
+
| `absorption` | `0.15` | Beer's-law constant per meter of water depth (≥ 0; lower = clearer/wider turquoise band; red absorbs ~3× faster than blue under the hood) |
|
|
671
|
+
| `refraction` | `true` | screen-space refraction of the submerged scene |
|
|
672
|
+
| `underwater` | `true` | submerged-camera look: ONLY when the camera EYE is below this surface (inside its XZ footprint) — wading with the camera above the water never tints the view — the scene switches to short-range underwater fog + tint, the sky is hidden, and animated **caustics** (depth-aware, projected onto the submerged floor/props) play. `false` disables it; an object `{ color?, visibility?, caustics? }` overrides the murk hue / view distance (m, default 22) / caustics. `caustics` is `true` (default) \| `false` \| `{ color?, intensity?, scale?, speed? }`. The murk color defaults to a darkened shade of `color`. Works for both qualities (the caustics composite runs only while the camera is genuinely underwater) |
|
|
673
|
+
|
|
674
|
+
Performance notes (fancy): beyond drawing the surface, each rendered frame
|
|
675
|
+
pays ONE half-resolution scene pre-pass (clamped at 1024px) that feeds the
|
|
676
|
+
depth texture (absorption + foam + soft intersections) AND the refraction
|
|
677
|
+
color grab; the CubeCamera reflection stays on its `reflectionInterval`
|
|
678
|
+
throttle. Measured on the generated island (sea 1600×1600 m, ~206k tris):
|
|
679
|
+
60 fps, frame times indistinguishable from the pre-v2 water. Headless runs
|
|
680
|
+
never invoke any of it. Multiple fancy waters each pay their own pre-pass —
|
|
681
|
+
prefer one large surface over many small ones.
|
|
682
|
+
|
|
683
|
+
Two placement rules keep the surface artifact-free:
|
|
684
|
+
|
|
685
|
+
- **Waves dip below the waterline.** Troughs reach ≈6.5× `waveHeight` meters
|
|
686
|
+
below `position.y` (the default 0.04 ≈ 0.26 m). Float the waterline at
|
|
687
|
+
least that far above any ground/terrain it overlaps — otherwise the ground
|
|
688
|
+
pokes through wave troughs as dark bites in the surface.
|
|
689
|
+
- **Reflections mirror DISTANT content only.** The cube map renders from the
|
|
690
|
+
water's center with everything inside the footprint near-plane-culled (a
|
|
691
|
+
nearby mesh would smear across the whole surface as a giant false-color
|
|
692
|
+
patch). Shoreline, terrain, and sky reflect; a boat floating ON the water
|
|
693
|
+
intentionally does not — its submerged hull shows through `refraction`
|
|
694
|
+
instead.
|
|
695
|
+
|
|
696
|
+
### Character interaction — `entered(body)` / `exited(body)`
|
|
697
|
+
|
|
698
|
+
With `interaction: true` the water scans the tree each frame for
|
|
699
|
+
`CharacterBody3D`/`RigidBody3D` inside its XZ footprint, compares their world
|
|
700
|
+
y against the waterline at their xz (wave offset included), and:
|
|
701
|
+
|
|
702
|
+
- emits `entered(body)` / `exited(body)` signals (the argument is the node),
|
|
703
|
+
- pushes a ripple impulse into a fixed 8-slot ring buffer that the shader
|
|
704
|
+
turns into expanding ring waves (faster falls splash harder; floating
|
|
705
|
+
bodies feed gentle bob rings on an interval),
|
|
706
|
+
- and (with `splash: true`, the default) paints expanding **surface FOAM** —
|
|
707
|
+
a **"풍덩"** froth bloom on entry whose size tracks the plunge speed, plus a
|
|
708
|
+
**foam trail ("물살")** behind a body wading/running through. This is real
|
|
709
|
+
whitewater on the water surface (driven by the same ripple buffer, in both
|
|
710
|
+
shaders), not flying droplets. Automatic; zero wiring. Set `splash: false`
|
|
711
|
+
to keep only the ripples/waves.
|
|
712
|
+
|
|
713
|
+
The surface has no bottom — anything below the waterline inside the
|
|
714
|
+
footprint counts as in-water. Keep using an `Area3D` for volumetric gameplay
|
|
715
|
+
(swim zones, drowning); the signals are for SPLASH moments. The built-in
|
|
716
|
+
`splash` particles already cover the common case — reach for a hand-wired
|
|
717
|
+
emitter only when you want a specific custom burst:
|
|
718
|
+
|
|
719
|
+
```json
|
|
720
|
+
{
|
|
721
|
+
"root": { "name": "World", "type": "Node3D", "children": [
|
|
722
|
+
{ "name": "Lake", "type": "Water3D",
|
|
723
|
+
"props": { "size": [60, 60], "position": [0, 2.8, 0] } },
|
|
724
|
+
{ "name": "Splash", "type": "Particles3D",
|
|
725
|
+
"props": { "position": [0, 3, 0], "preset": "explosion", "emitting": false,
|
|
726
|
+
"rate": 0, "burst": 40, "colorStart": "#dff3ff", "colorEnd": "#9bd8ff" } }
|
|
727
|
+
] },
|
|
728
|
+
"connections": [
|
|
729
|
+
{ "signal": "entered", "from": "Lake", "to": "Splash", "handler": "replay" }
|
|
730
|
+
]
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
(`replay` re-arms the one-shot burst; a small behavior can also move the
|
|
735
|
+
emitter to the body first — the signal hands you the node:
|
|
736
|
+
`onEntered(body) { splash.position = body.position; splash.replay(); }`.)
|
|
737
|
+
|
|
738
|
+
## Determinism, verified
|
|
739
|
+
|
|
740
|
+
`--seed` makes every run reproducible — keep the seed in your scene comments
|
|
741
|
+
or PROJECT docs. Regenerating with the same seed after an engine upgrade is
|
|
742
|
+
the cheap way to diff what changed; `bunx incanto-check` validates the scene
|
|
743
|
+
after every insert.
|