incanto 0.4.3 → 0.6.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.
Files changed (44) hide show
  1. package/dist/2d.d.ts +80 -6
  2. package/dist/2d.js +4 -4
  3. package/dist/3d.d.ts +200 -15
  4. package/dist/3d.js +4 -4
  5. package/dist/{audio-player-DqUR3XFs.d.ts → audio-player-DkBqRTs4.d.ts} +1 -1
  6. package/dist/{behavior-BAQq7HGM.d.ts → behavior-CWhW3oa6.d.ts} +54 -0
  7. package/dist/{create-game-CZHROKcT.js → create-game-B_sW_eiE.js} +59 -27
  8. package/dist/{create-game-DiTq3-fQ.js → create-game-DkbZCNaV.js} +292 -14
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DP2WPYom.js → duplicate-BEvGBtb_.js} +1 -1
  11. package/dist/{gameplay-Ccruc3Wd.js → gameplay-CDFgSG6z.js} +293 -24
  12. package/dist/gameplay.d.ts +113 -2
  13. package/dist/gameplay.js +2 -2
  14. package/dist/index.d.ts +139 -5
  15. package/dist/index.js +59 -6
  16. package/dist/{loader-CGs_G-r0.js → loader-B4OEXDZ8.js} +60 -0
  17. package/dist/{loader-Mo0KghCv.d.ts → loader-D2u0fVW2.d.ts} +1 -1
  18. package/dist/net.d.ts +1 -1
  19. package/dist/net.js +1 -1
  20. package/dist/{particle-sim-DYuSUxvK.js → particle-sim-CFkILGwh.js} +84 -3
  21. package/dist/{particle-sim-CbN4YUuH.d.ts → particle-sim-CwJ5rI_P.d.ts} +3 -0
  22. package/dist/{physics-2d-KuMWPTf6.js → physics-2d-Cgli1aju.js} +95 -10
  23. package/dist/{physics-3d-DmNCeh58.js → physics-3d-CBAQ12LY.js} +121 -43
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-DPEV9_9t.js → register-BJCfuuZ2.js} +79 -5
  27. package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
  28. package/dist/{register-BRy8FNox.js → register-Dd7Juujf.js} +389 -49
  29. package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-DryTXVd4.js → agent8-MciFwn0v.js} +1 -1
  33. package/editor/assets/index-mofVSmuA.js +7417 -0
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +980 -126
  37. package/skills/incanto-3d-models.md +24 -1
  38. package/skills/incanto-building-2d-games.md +6 -0
  39. package/skills/incanto-building-3d-games.md +46 -1
  40. package/skills/incanto-gameplay-behaviors.md +60 -0
  41. package/skills/incanto-hud.md +58 -0
  42. package/skills/incanto-node-reference.md +148 -0
  43. package/skills/incanto-physics-and-input.md +47 -0
  44. package/editor/assets/index-N8se-PhP.js +0 -7365
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { _ as registerBehavior, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-CGs_G-r0.js";
3
- import { a as Engine } from "./particle-sim-DYuSUxvK.js";
2
+ import { _ as registerBehavior, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-B4OEXDZ8.js";
3
+ import { a as Engine } from "./particle-sim-CFkILGwh.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { r as AudioPlayer } from "./register-BuUV1_KB.js";
5
+ import { s as AudioPlayer } from "./register-Dl_ixIJe.js";
6
6
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
8
- import { f as PhysicsBody2D, h as Node2D, n as UILayer, s as Camera2D, t as registerNodes2D } from "./register-DPEV9_9t.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-CDFgSG6z.js";
8
+ import { g as Node2D, n as UILayer, p as PhysicsBody2D, s as Camera2D, t as registerNodes2D } from "./register-BJCfuuZ2.js";
9
9
  import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
10
- import { n as enablePhysics2D } from "./physics-2d-KuMWPTf6.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-Cgli1aju.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -102,26 +102,41 @@ function worldFromScreen(view, sx, sy) {
102
102
  }
103
103
  //#endregion
104
104
  //#region src/2d/sync.ts
105
- /**
106
- * Mirror an Incanto node tree onto two three scenes:
107
- * - `world`: camera-space drawables
108
- * - `ui`: everything under a `UILayer` (screen space, ignores the camera);
109
- * each layer mounts through its own group, positioned by its `anchor`
110
- * against `uiSize` (origin when no size is known — headless).
111
- *
112
- * Same dirty-push contract as the 3D sync; pure scene-graph math, headless-testable.
113
- */
114
- function syncTree2D(root, world, ui, assets, uiSize) {
115
- const visited = /* @__PURE__ */ new Set();
116
- const cameras = [];
117
- walk(root, world, ui, false, visited, cameras, assets, uiSize);
118
- prune(world, visited);
119
- prune(ui, visited);
120
- return { activeCamera: cameras.find((c) => c.current) ?? cameras[0] ?? null };
105
+ function createSync2DScratch() {
106
+ return {
107
+ visited: /* @__PURE__ */ new Set(),
108
+ cameras: []
109
+ };
110
+ }
111
+ function syncTree2D(root, world, ui, assets, uiSize, scratch, opts) {
112
+ const s = scratch ?? createSync2DScratch();
113
+ s.visited.clear();
114
+ s.cameras.length = 0;
115
+ walk(root, world, ui, false, s.visited, s.cameras, assets, uiSize, opts?.ignoreStatic === true);
116
+ prune(world, s.visited);
117
+ prune(ui, s.visited);
118
+ let current = null;
119
+ for (let i = 0; i < s.cameras.length; i++) {
120
+ const c = s.cameras[i];
121
+ if (c.current) {
122
+ current = c;
123
+ break;
124
+ }
125
+ }
126
+ if (!current) current = s.cameras[0] ?? null;
127
+ return { activeCamera: current };
121
128
  }
122
- function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize) {
129
+ function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize, ignoreStatic) {
123
130
  let nextParent = parentObj;
124
131
  let nextInUi = inUi;
132
+ if (node instanceof Node2D && !(node instanceof UILayer)) {
133
+ const frozen = node._ensureObject2D();
134
+ if (node.static && !ignoreStatic && frozen.userData.incantoStaticSynced === true) {
135
+ frozen.userData.incantoStatic = true;
136
+ visited.add(frozen);
137
+ return;
138
+ }
139
+ }
125
140
  if (node instanceof UILayer) {
126
141
  const group = node._ensureGroup();
127
142
  if (group.parent !== ui) ui.add(group);
@@ -141,11 +156,25 @@ function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize) {
141
156
  if (node instanceof Camera2D && !inUi) cameras.push(node);
142
157
  nextParent = obj;
143
158
  }
144
- for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, assets, uiSize);
159
+ for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, assets, uiSize, ignoreStatic);
160
+ if (node instanceof Node2D && !(node instanceof UILayer)) {
161
+ const obj = node._ensureObject2D();
162
+ if (node.static && !ignoreStatic) {
163
+ obj.userData.incantoStaticSynced = true;
164
+ obj.userData.incantoStatic = true;
165
+ } else if (obj.userData.incantoStaticSynced === true) {
166
+ obj.userData.incantoStaticSynced = false;
167
+ obj.userData.incantoStatic = false;
168
+ }
169
+ }
145
170
  }
146
171
  function prune(obj, visited) {
147
- for (const child of [...obj.children]) if (child.userData.incantoNode && !visited.has(child)) obj.remove(child);
148
- else prune(child, visited);
172
+ const ch = obj.children;
173
+ for (let i = ch.length - 1; i >= 0; i--) {
174
+ const child = ch[i];
175
+ if (child.userData.incantoNode && !visited.has(child)) obj.remove(child);
176
+ else if (child.userData.incantoStatic !== true) prune(child, visited);
177
+ }
149
178
  }
150
179
  //#endregion
151
180
  //#region src/2d/renderer.ts
@@ -163,6 +192,9 @@ var Renderer2D = class {
163
192
  viewOverride = null;
164
193
  webgl;
165
194
  worldScene = new Scene();
195
+ syncScratch = createSync2DScratch();
196
+ /** Editors set this: keep syncing `static: true` subtrees every frame. */
197
+ ignoreStatic = false;
166
198
  uiScene = new Scene();
167
199
  worldCam = new OrthographicCamera(-1, 1, 1, -1, -1e3, 1e3);
168
200
  uiCam = new OrthographicCamera(-1, 1, 1, -1, -1e3, 1e3);
@@ -231,7 +263,7 @@ var Renderer2D = class {
231
263
  const { activeCamera } = syncTree2D(scene.root, this.worldScene, this.uiScene, this.assets, {
232
264
  width: uiW,
233
265
  height: uiH
234
- });
266
+ }, this.syncScratch);
235
267
  let center = vp ? {
236
268
  x: vp.design[0] / 2,
237
269
  y: vp.design[1] / 2
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { _ as registerBehavior, n as loadScene } from "./loader-CGs_G-r0.js";
3
- import { a as Engine } from "./particle-sim-DYuSUxvK.js";
2
+ import { _ as registerBehavior, n as loadScene } from "./loader-B4OEXDZ8.js";
3
+ import { a as Engine } from "./particle-sim-CFkILGwh.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { r as AudioPlayer } from "./register-BuUV1_KB.js";
5
+ import { s as AudioPlayer } from "./register-Dl_ixIJe.js";
6
6
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-CDFgSG6z.js";
8
8
  import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
9
- import { E as PhysicsBody3D, c as ModelInstance3D, k as Node3D, t as registerNodes3D, u as DirectionalLight3D, v as Camera3D } from "./register-BRy8FNox.js";
10
- import { n as enablePhysics3D } from "./physics-3d-DmNCeh58.js";
11
- import { ACESFilmicToneMapping, AmbientLight, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
9
+ import { O as PhysicsBody3D, c as ModelInstance3D, d as DirectionalLight3D, j as Node3D, t as registerNodes3D, y as Camera3D } from "./register-Dd7Juujf.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-CBAQ12LY.js";
11
+ import { ACESFilmicToneMapping, AmbientLight, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
12
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
14
14
  import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
@@ -127,6 +127,122 @@ var AssetStore3D = class {
127
127
  }
128
128
  };
129
129
  //#endregion
130
+ //#region src/3d/bloom/bloom-composite.ts
131
+ /**
132
+ * HDR bloom post pass:
133
+ *
134
+ * scene → offscreen HalfFloat target (raw linear HDR — three applies neither
135
+ * tone mapping nor the sRGB transform when rendering into a target)
136
+ * bright-pass at HALF resolution (keep only luminance above `threshold`;
137
+ * with HDR buffers, emissives and sun glints sit above 1.0)
138
+ * separable 9-tap Gaussian blur (H then V) at half res
139
+ * composite: scene + glow × strength, THEN the standard postprocessing tail
140
+ * (ACES tone mapping + linear→sRGB) — skipping that tail is exactly the
141
+ * dark/oversaturated bug the volumetric-clouds pipeline shipped with.
142
+ */
143
+ const VERT = `
144
+ varying vec2 vUv;
145
+ void main() {
146
+ vUv = uv;
147
+ gl_Position = vec4(position.xy, 0.0, 1.0);
148
+ }
149
+ `;
150
+ const BRIGHT_FRAG = `
151
+ precision highp float;
152
+ varying vec2 vUv;
153
+ uniform sampler2D tScene;
154
+ uniform float uThreshold;
155
+ void main() {
156
+ vec3 c = texture2D(tScene, vUv).rgb;
157
+ // HDR luminance knee: the scene buffer is raw linear HDR (tone mapping is
158
+ // applied later, in the composite), so emissives/sun glints sit ABOVE 1.0 —
159
+ // keep only the energy above the threshold, in proportion
160
+ float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
161
+ float k = max(l - uThreshold, 0.0);
162
+ gl_FragColor = vec4(c * (k / max(l, 1e-4)), 1.0);
163
+ }
164
+ `;
165
+ const BLUR_FRAG = `
166
+ precision highp float;
167
+ varying vec2 vUv;
168
+ uniform sampler2D tTex;
169
+ uniform vec2 uDir;
170
+ void main() {
171
+ // 9-tap Gaussian, sigma ~2 texels (same recipe as the cloud blur)
172
+ vec4 sum = texture2D(tTex, vUv) * 0.227027;
173
+ sum += texture2D(tTex, vUv + uDir * 1.3846) * 0.3162162;
174
+ sum += texture2D(tTex, vUv - uDir * 1.3846) * 0.3162162;
175
+ sum += texture2D(tTex, vUv + uDir * 3.2308) * 0.0702703;
176
+ sum += texture2D(tTex, vUv - uDir * 3.2308) * 0.0702703;
177
+ gl_FragColor = sum;
178
+ }
179
+ `;
180
+ const COMPOSITE_FRAG = `
181
+ precision highp float;
182
+ varying vec2 vUv;
183
+ uniform sampler2D tScene;
184
+ uniform sampler2D tBloom;
185
+ uniform float uStrength;
186
+ void main() {
187
+ vec3 base = texture2D(tScene, vUv).rgb;
188
+ vec3 glow = texture2D(tBloom, vUv).rgb;
189
+ gl_FragColor = vec4(base + glow * uStrength, 1.0);
190
+ // three skips BOTH tone mapping and the output colorspace transform when the
191
+ // scene renders into an offscreen target — the buffer holds raw linear HDR.
192
+ // This is the final to-screen pass, so apply the standard postprocessing
193
+ // tail here (ACES then linear→sRGB); without it the frame comes out dark
194
+ // and oversaturated (the volumetric-clouds pipeline bug, root-caused).
195
+ #include <tonemapping_fragment>
196
+ #include <colorspace_fragment>
197
+ }
198
+ `;
199
+ function quad(fragment, uniforms) {
200
+ const material = new ShaderMaterial({
201
+ vertexShader: VERT,
202
+ fragmentShader: fragment,
203
+ depthTest: false,
204
+ depthWrite: false,
205
+ uniforms
206
+ });
207
+ const mesh = new Mesh(new PlaneGeometry(2, 2), material);
208
+ mesh.frustumCulled = false;
209
+ return mesh;
210
+ }
211
+ /** Half-res bright-pass quad — keeps only pixels brighter than the threshold. */
212
+ function createBloomBrightQuad() {
213
+ const mesh = quad(BRIGHT_FRAG, {
214
+ tScene: { value: null },
215
+ uThreshold: { value: 1 }
216
+ });
217
+ return {
218
+ mesh,
219
+ uniforms: mesh.material.uniforms
220
+ };
221
+ }
222
+ /** Separable-blur quad (run twice: uDir = (1/w, 0) then (0, 1/h)). */
223
+ function createBloomBlurQuad() {
224
+ const mesh = quad(BLUR_FRAG, {
225
+ tTex: { value: null },
226
+ uDir: { value: new Vector2() }
227
+ });
228
+ return {
229
+ mesh,
230
+ uniforms: mesh.material.uniforms
231
+ };
232
+ }
233
+ /** Full-res composite quad — adds the blurred glow over the scene. */
234
+ function createBloomCompositeQuad() {
235
+ const mesh = quad(COMPOSITE_FRAG, {
236
+ tScene: { value: null },
237
+ tBloom: { value: null },
238
+ uStrength: { value: .85 }
239
+ });
240
+ return {
241
+ mesh,
242
+ uniforms: mesh.material.uniforms
243
+ };
244
+ }
245
+ //#endregion
130
246
  //#region src/3d/clouds/clouds-composite.ts
131
247
  /**
132
248
  * Volumetric clouds — a raymarched cloud DECK the camera flies through, drawn as
@@ -446,7 +562,12 @@ const CLOUD_KEYS = [
446
562
  "speed",
447
563
  "scale"
448
564
  ];
449
- const SHADOW_KEYS = ["mapSize", "radius"];
565
+ const BLOOM_KEYS = ["threshold", "strength"];
566
+ const SHADOW_KEYS = [
567
+ "mapSize",
568
+ "radius",
569
+ "static"
570
+ ];
450
571
  const SHADOW_MAP_SIZES = [1024, 2048];
451
572
  const DEFAULT_TURBIDITY = 2;
452
573
  const DEFAULT_RAYLEIGH = 1;
@@ -462,6 +583,7 @@ function parseEnvironment3D(env) {
462
583
  sky: parseSky(env?.sky),
463
584
  fog: parseFog(env?.fog, env?.sky !== void 0),
464
585
  clouds: parseClouds(env?.clouds),
586
+ bloom: parseBloom(env?.bloom),
465
587
  shadows: parseShadows(env?.shadows)
466
588
  };
467
589
  }
@@ -616,12 +738,30 @@ function parseClouds(value) {
616
738
  scale
617
739
  };
618
740
  }
741
+ function parseBloom(value) {
742
+ if (value === void 0) return null;
743
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.bloom must be an object ({ threshold?, strength? }), got ${JSON.stringify(value)}.`, { prop: "bloom" });
744
+ const b = value;
745
+ for (const key of Object.keys(b)) if (!BLOOM_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.bloom has unknown key '${key}'. Valid keys: [${BLOOM_KEYS.join(", ")}].`, {
746
+ prop: "bloom",
747
+ validOptions: BLOOM_KEYS
748
+ });
749
+ const threshold = numberOr(b.threshold, 1, "bloom.threshold");
750
+ if (threshold < 0 || threshold > 8) throw new IncantoError("BAD_FORMAT", `environment.bloom.threshold must be in [0, 8] (linear-HDR luminance; 1 = white), got ${threshold}.`, { prop: "bloom" });
751
+ const strength = numberOr(b.strength, .8, "bloom.strength");
752
+ if (strength < 0) throw new IncantoError("BAD_FORMAT", `environment.bloom.strength must be >= 0, got ${strength}.`, { prop: "bloom" });
753
+ return {
754
+ threshold,
755
+ strength
756
+ };
757
+ }
619
758
  function parseShadows(value) {
620
759
  if (value === void 0) return null;
621
760
  if (value === false) return false;
622
761
  if (value === true) return {
623
762
  mapSize: 2048,
624
- radius: 1
763
+ radius: 1,
764
+ static: false
625
765
  };
626
766
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.shadows must be true, false or an object ({ mapSize?, radius? }), got ${JSON.stringify(value)}.`, { prop: "shadows" });
627
767
  const shadows = value;
@@ -636,9 +776,12 @@ function parseShadows(value) {
636
776
  });
637
777
  const radius = numberOr(shadows.radius, 1, "shadows.radius");
638
778
  if (radius < 0) throw new IncantoError("BAD_FORMAT", `environment.shadows.radius must be >= 0, got ${radius}.`, { prop: "shadows" });
779
+ const staticShadows = shadows.static === void 0 ? false : shadows.static;
780
+ if (typeof staticShadows !== "boolean") throw new IncantoError("BAD_FORMAT", `environment.shadows.static must be a boolean, got ${JSON.stringify(shadows.static)}.`, { prop: "shadows" });
639
781
  return {
640
782
  mapSize,
641
- radius
783
+ radius,
784
+ static: staticShadows
642
785
  };
643
786
  }
644
787
  function numberOr(value, fallback, at) {
@@ -730,6 +873,10 @@ var Environment3D = class {
730
873
  get clouds() {
731
874
  return this.config.clouds;
732
875
  }
876
+ /** Parsed bloom config, or null when no bloom pass is declared. */
877
+ get bloom() {
878
+ return this.config.bloom;
879
+ }
733
880
  /** The live scene fog (color/near/far) the cloud composite fades into, or null. */
734
881
  get sceneFog() {
735
882
  return this.scene.fog instanceof Fog ? this.scene.fog : null;
@@ -748,6 +895,12 @@ var Environment3D = class {
748
895
  if (gl) {
749
896
  gl.toneMappingExposure = cfg.exposure;
750
897
  gl.shadowMap.enabled = cfg.shadows !== false;
898
+ if (cfg.shadows !== false && cfg.shadows !== null && cfg.shadows.static) {
899
+ if (gl.shadowMap.autoUpdate) {
900
+ gl.shadowMap.autoUpdate = false;
901
+ gl.shadowMap.needsUpdate = true;
902
+ }
903
+ } else if (!gl.shadowMap.autoUpdate) gl.shadowMap.autoUpdate = true;
751
904
  }
752
905
  this.applyHdri(env);
753
906
  this.applySky(cfg.sky, gl);
@@ -942,7 +1095,8 @@ function createSyncScratch() {
942
1095
  assets: void 0,
943
1096
  sunDirection: null,
944
1097
  sunLight: null,
945
- alpha: 1
1098
+ alpha: 1,
1099
+ ignoreStatic: false
946
1100
  }
947
1101
  };
948
1102
  }
@@ -966,6 +1120,7 @@ function syncTree(root, threeScene, assets, opts, scratch) {
966
1120
  const state = s.state;
967
1121
  state.assets = assets;
968
1122
  state.sunDirection = opts?.sunDirection ?? null;
1123
+ state.ignoreStatic = opts?.ignoreStatic === true;
969
1124
  state.sunLight = null;
970
1125
  state.alpha = opts?.alpha ?? 1;
971
1126
  walk(root, threeScene, state);
@@ -1037,6 +1192,11 @@ function walk(node, parentObj, state) {
1037
1192
  let nextParent = parentObj;
1038
1193
  if (node instanceof Node3D) {
1039
1194
  const obj = node._ensureObject3D();
1195
+ if (node.static && !state.ignoreStatic && obj.userData.incantoStaticSynced === true) {
1196
+ obj.userData.incantoStatic = true;
1197
+ state.visited.add(obj);
1198
+ return;
1199
+ }
1040
1200
  if (obj.parent !== parentObj) parentObj.add(obj);
1041
1201
  node._syncObject3D(state.alpha);
1042
1202
  if (state.assets && node instanceof ModelInstance3D) node._syncModel(state.assets);
@@ -1056,13 +1216,37 @@ function walk(node, parentObj, state) {
1056
1216
  parent: nextParent
1057
1217
  });
1058
1218
  for (const child of node.children) walk(child, nextParent, state);
1219
+ if (node instanceof Node3D) {
1220
+ const obj = node._ensureObject3D();
1221
+ if (node.static && !state.ignoreStatic) {
1222
+ obj.userData.incantoStaticSynced = true;
1223
+ obj.userData.incantoStatic = true;
1224
+ warnStaticHazards(node);
1225
+ } else if (obj.userData.incantoStaticSynced === true) {
1226
+ obj.userData.incantoStaticSynced = false;
1227
+ obj.userData.incantoStatic = false;
1228
+ }
1229
+ }
1230
+ }
1231
+ const warnedStatic = /* @__PURE__ */ new WeakSet();
1232
+ /** One-time warning when animated machinery is frozen under a static root. */
1233
+ function warnStaticHazards(root) {
1234
+ if (warnedStatic.has(root)) return;
1235
+ warnedStatic.add(root);
1236
+ const hazards = [];
1237
+ const scan = (n) => {
1238
+ if (typeof n._onRender3D === "function" || n instanceof Camera3D) hazards.push(`${n.name} (${n.constructor.typeName ?? "?"})`);
1239
+ for (const c of n.children) scan(c);
1240
+ };
1241
+ scan(root);
1242
+ if (hazards.length > 0) console.warn(`[incanto] static subtree '${root.name}' freezes animated/per-frame nodes: ${hazards.join(", ")} — they will stop updating. Unmark static or move them out.`);
1059
1243
  }
1060
1244
  function prune(obj, visited) {
1061
1245
  const ch = obj.children;
1062
1246
  for (let i = ch.length - 1; i >= 0; i--) {
1063
1247
  const child = ch[i];
1064
1248
  if (child.userData.incantoNode && !visited.has(child)) obj.remove(child);
1065
- else prune(child, visited);
1249
+ else if (child.userData.incantoStatic !== true) prune(child, visited);
1066
1250
  }
1067
1251
  }
1068
1252
  //#endregion
@@ -1209,6 +1393,8 @@ var Renderer3D = class {
1209
1393
  compiledScene = null;
1210
1394
  /** Reused per-frame walk scratch (instance-scoped — never a module global). */
1211
1395
  syncScratch = createSyncScratch();
1396
+ /** Editors set this: keep syncing `static: true` subtrees every frame. */
1397
+ ignoreStatic = false;
1212
1398
  /** Reused render-hook context — gl/scene are stable, camera reassigned/frame. */
1213
1399
  renderCtx = null;
1214
1400
  /** Underwater caustics pass (lazy — only created the first time submerged). */
@@ -1228,6 +1414,15 @@ var Renderer3D = class {
1228
1414
  cloudsBlurUniforms = null;
1229
1415
  cloudsCompositeScene = null;
1230
1416
  cloudsCompositeUniforms = null;
1417
+ bloomTarget = null;
1418
+ bloomBrightTarget = null;
1419
+ bloomBlurTarget = null;
1420
+ bloomBrightScene = null;
1421
+ bloomBrightUniforms = null;
1422
+ bloomBlurScene = null;
1423
+ bloomBlurUniforms = null;
1424
+ bloomCompositeScene = null;
1425
+ bloomCompositeUniforms = null;
1231
1426
  constructor(opts) {
1232
1427
  this.canvas = opts.canvas;
1233
1428
  this.engine = opts.engine;
@@ -1277,7 +1472,8 @@ var Renderer3D = class {
1277
1472
  this.environment.apply(scene.environment, this.webgl);
1278
1473
  const { activeCamera: sceneCamera, renderHooks, sunLight } = syncTree(scene.root, this.threeScene, this.assets, {
1279
1474
  sunDirection: this.environment.sunDirection,
1280
- alpha: this.engine.interpolationAlpha
1475
+ alpha: this.engine.interpolationAlpha,
1476
+ ignoreStatic: this.ignoreStatic
1281
1477
  }, this.syncScratch);
1282
1478
  let activeCamera = sceneCamera;
1283
1479
  if (this.viewOverride) {
@@ -1334,11 +1530,80 @@ var Renderer3D = class {
1334
1530
  }
1335
1531
  this.environment.applyUnderwater(underwater);
1336
1532
  const clouds = this.environment.clouds;
1533
+ const bloom = this.environment.bloom;
1337
1534
  if (underwater?.caustics.enabled) this.renderWithCaustics(activeCamera, underwater);
1338
1535
  else if (clouds && !underwater) this.renderWithClouds(activeCamera, clouds, threeLight);
1536
+ else if (bloom && !underwater) this.renderWithBloom(activeCamera, bloom);
1339
1537
  else this.webgl.render(this.threeScene, activeCamera);
1340
1538
  }
1341
1539
  /**
1540
+ * Bloom post pass (only when `environment.bloom` is declared). LDR and safe:
1541
+ * 1. scene → full-res offscreen (tone-mapped exactly like direct output)
1542
+ * 2. bright-pass at HALF res (keep pixels above threshold)
1543
+ * 3. separable Gaussian blur (H, V) at half res
1544
+ * 4. composite scene + glow×strength → screen (pure ADD — never re-touches
1545
+ * the scene's tone; the clouds pipeline taught us that lesson)
1546
+ * Caustics/clouds paths take precedence (composite pipelines don't stack yet).
1547
+ */
1548
+ renderWithBloom(camera, bloom) {
1549
+ const buf = this.webgl.getDrawingBufferSize(sizeScratch);
1550
+ const w = Math.max(1, buf.x);
1551
+ const h = Math.max(1, buf.y);
1552
+ const hw = Math.max(1, Math.ceil(w / 2));
1553
+ const hh = Math.max(1, Math.ceil(h / 2));
1554
+ if (this.bloomTarget && (this.bloomTarget.width !== w || this.bloomTarget.height !== h)) {
1555
+ this.bloomTarget.dispose();
1556
+ this.bloomTarget = null;
1557
+ this.bloomBrightTarget?.dispose();
1558
+ this.bloomBrightTarget = null;
1559
+ this.bloomBlurTarget?.dispose();
1560
+ this.bloomBlurTarget = null;
1561
+ }
1562
+ if (!this.bloomTarget) {
1563
+ this.bloomTarget = new WebGLRenderTarget(w, h, { type: HalfFloatType });
1564
+ this.bloomBrightTarget = new WebGLRenderTarget(hw, hh, { type: HalfFloatType });
1565
+ this.bloomBlurTarget = new WebGLRenderTarget(hw, hh, { type: HalfFloatType });
1566
+ }
1567
+ if (!this.bloomBrightScene) {
1568
+ const bright = createBloomBrightQuad();
1569
+ this.bloomBrightScene = new Scene();
1570
+ this.bloomBrightScene.add(bright.mesh);
1571
+ this.bloomBrightUniforms = bright.uniforms;
1572
+ const blur = createBloomBlurQuad();
1573
+ this.bloomBlurScene = new Scene();
1574
+ this.bloomBlurScene.add(blur.mesh);
1575
+ this.bloomBlurUniforms = blur.uniforms;
1576
+ const composite = createBloomCompositeQuad();
1577
+ this.bloomCompositeScene = new Scene();
1578
+ this.bloomCompositeScene.add(composite.mesh);
1579
+ this.bloomCompositeUniforms = composite.uniforms;
1580
+ }
1581
+ const brightTarget = this.bloomBrightTarget;
1582
+ const blurTarget = this.bloomBlurTarget;
1583
+ this.webgl.setRenderTarget(this.bloomTarget);
1584
+ this.webgl.render(this.threeScene, camera);
1585
+ const bu = this.bloomBrightUniforms;
1586
+ bu.tScene.value = this.bloomTarget.texture;
1587
+ bu.uThreshold.value = bloom.threshold;
1588
+ this.webgl.setRenderTarget(brightTarget);
1589
+ this.webgl.render(this.bloomBrightScene, camera);
1590
+ const blu = this.bloomBlurUniforms;
1591
+ blu.tTex.value = brightTarget.texture;
1592
+ blu.uDir.value.set(1 / hw, 0);
1593
+ this.webgl.setRenderTarget(blurTarget);
1594
+ this.webgl.render(this.bloomBlurScene, camera);
1595
+ blu.tTex.value = blurTarget.texture;
1596
+ blu.uDir.value.set(0, 1 / hh);
1597
+ this.webgl.setRenderTarget(brightTarget);
1598
+ this.webgl.render(this.bloomBlurScene, camera);
1599
+ const cu = this.bloomCompositeUniforms;
1600
+ cu.tScene.value = this.bloomTarget.texture;
1601
+ cu.tBloom.value = brightTarget.texture;
1602
+ cu.uStrength.value = bloom.strength;
1603
+ this.webgl.setRenderTarget(null);
1604
+ this.webgl.render(this.bloomCompositeScene, camera);
1605
+ }
1606
+ /**
1342
1607
  * Volumetric cloud pass (only when `environment.clouds` is declared — scenes
1343
1608
  * without it render exactly as before, zero cost). Three steps:
1344
1609
  * 1. scene → full-res offscreen target (color + depth)
@@ -1546,6 +1811,13 @@ var Renderer3D = class {
1546
1811
  forward: new Vector3(0, 0, -1).applyQuaternion(q)
1547
1812
  };
1548
1813
  }
1814
+ /**
1815
+ * Re-render the shadow pass once (static-shadow scenes,
1816
+ * `environment.shadows.static: true`) after a one-off world edit.
1817
+ */
1818
+ refreshShadows() {
1819
+ this.webgl.shadowMap.needsUpdate = true;
1820
+ }
1549
1821
  dispose() {
1550
1822
  this.disconnect();
1551
1823
  this.threeScene.traverse((obj) => {
@@ -1564,10 +1836,16 @@ var Renderer3D = class {
1564
1836
  this.cloudsTarget?.dispose();
1565
1837
  this.cloudsHalfTarget?.dispose();
1566
1838
  this.cloudsBlurTarget?.dispose();
1839
+ this.bloomTarget?.dispose();
1840
+ this.bloomBrightTarget?.dispose();
1841
+ this.bloomBlurTarget?.dispose();
1567
1842
  for (const s of [
1568
1843
  this.cloudsScene,
1569
1844
  this.cloudsBlurScene,
1570
- this.cloudsCompositeScene
1845
+ this.cloudsCompositeScene,
1846
+ this.bloomBrightScene,
1847
+ this.bloomBlurScene,
1848
+ this.bloomCompositeScene
1571
1849
  ]) {
1572
1850
  const m = s?.children[0];
1573
1851
  m?.geometry?.dispose();
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RendererStats, k as LogLevel, v as Engine } from "./behavior-BAQq7HGM.js";
1
+ import { C as RendererStats, k as LogLevel, v as Engine } from "./behavior-CWhW3oa6.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-CGs_G-r0.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-B4OEXDZ8.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;