incanto 0.12.0 → 0.14.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 (83) hide show
  1. package/README.md +22 -0
  2. package/bin/incanto-new.mjs +87 -0
  3. package/dist/2d.d.ts +2 -2
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +219 -82
  6. package/dist/3d.js +74 -4
  7. package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
  8. package/dist/{create-game-C_xf4gm9.js → create-game-Ct86zczq.js} +5 -5
  9. package/dist/{create-game-B2fewJUy.js → create-game-OSntcIiM.js} +5 -5
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/{duplicate-BEvGBtb_.js → duplicate-BINwP0WG.js} +1 -1
  12. package/dist/{gameplay-Dn3yidJw.js → gameplay-sSqrUcnz.js} +75 -3
  13. package/dist/gameplay.d.ts +37 -2
  14. package/dist/gameplay.js +2 -2
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +4 -4
  17. package/dist/{loader-B4OEXDZ8.js → loader-BZqOKfI2.js} +3 -1
  18. package/dist/{loader-BcUDfNHn.d.ts → loader-DILt9PGC.d.ts} +1 -1
  19. package/dist/net.d.ts +1 -1
  20. package/dist/net.js +3 -3
  21. package/dist/{pathfinding-BSiXjd6W.d.ts → pathfinding-RWYkNKx9.d.ts} +1 -1
  22. package/dist/{physics-2d-B6D864oZ.js → physics-2d-BiIdl51r.js} +2 -2
  23. package/dist/{physics-3d-RLNaxRyw.js → physics-3d-D_kUdOjr.js} +2 -2
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Cwc-yCYx.js → register-BFFE1Mh1.js} +2 -2
  27. package/dist/{register-ysLR3WQS.js → register-C9-16x_t.js} +562 -157
  28. package/dist/{register-BD6zgrv_.js → register-Djwckzgx.js} +2 -2
  29. package/dist/{register-DJZXxwAn.js → register-nObreUQR.js} +1 -1
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-D_4QY8Aq.js → agent8-BDC4LhjC.js} +1 -1
  33. package/editor/assets/{index-s0DZuroe.js → index-7qOYg7t8.js} +74 -74
  34. package/editor/index.html +1 -1
  35. package/package.json +4 -2
  36. package/schemas/scene.schema.json +266 -6
  37. package/skills/incanto-3d-character.md +35 -0
  38. package/skills/incanto-building-3d-games.md +48 -0
  39. package/skills/incanto-environment.md +15 -16
  40. package/skills/incanto-gameplay-behaviors.md +23 -1
  41. package/skills/incanto-node-reference.md +54 -3
  42. package/templates-app/beacon-isle-3d/PROJECT/Context.md +44 -0
  43. package/templates-app/beacon-isle-3d/PROJECT/Requirements.md +16 -0
  44. package/templates-app/beacon-isle-3d/PROJECT/Status.md +38 -0
  45. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +24 -0
  46. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +38 -0
  47. package/templates-app/beacon-isle-3d/generate-world.ts +812 -0
  48. package/templates-app/beacon-isle-3d/index.html +68 -0
  49. package/templates-app/beacon-isle-3d/package.json +26 -0
  50. package/templates-app/beacon-isle-3d/src/behaviors.ts +570 -0
  51. package/templates-app/beacon-isle-3d/src/game.scene.json +2969 -0
  52. package/templates-app/beacon-isle-3d/src/main.ts +33 -0
  53. package/templates-app/beacon-isle-3d/tsconfig.json +13 -0
  54. package/templates-app/beacon-isle-3d/verify.ts +285 -0
  55. package/templates-app/beacon-isle-3d/vite.config.ts +5 -0
  56. package/templates-app/tps-3d/PROJECT/Context.md +49 -0
  57. package/templates-app/tps-3d/PROJECT/Requirements.md +38 -0
  58. package/templates-app/tps-3d/PROJECT/Status.md +41 -0
  59. package/templates-app/tps-3d/PROJECT/Structure.md +44 -0
  60. package/templates-app/tps-3d/docs/project-3d-rules.md +38 -0
  61. package/templates-app/tps-3d/index.html +244 -0
  62. package/templates-app/tps-3d/package.json +25 -0
  63. package/templates-app/tps-3d/src/behaviors.ts +405 -0
  64. package/templates-app/tps-3d/src/game.scene.json +677 -0
  65. package/templates-app/tps-3d/src/main.ts +64 -0
  66. package/templates-app/tps-3d/tsconfig.json +13 -0
  67. package/templates-app/tps-3d/verify.ts +167 -0
  68. package/templates-app/tps-3d/vite.config.ts +5 -0
  69. package/templates-app/village-quest-3d/PROJECT/Context.md +39 -0
  70. package/templates-app/village-quest-3d/PROJECT/Requirements.md +36 -0
  71. package/templates-app/village-quest-3d/PROJECT/Status.md +26 -0
  72. package/templates-app/village-quest-3d/PROJECT/Structure.md +30 -0
  73. package/templates-app/village-quest-3d/docs/project-3d-rules.md +38 -0
  74. package/templates-app/village-quest-3d/index.html +68 -0
  75. package/templates-app/village-quest-3d/package.json +25 -0
  76. package/templates-app/village-quest-3d/src/behaviors.ts +529 -0
  77. package/templates-app/village-quest-3d/src/grove.scene.json +826 -0
  78. package/templates-app/village-quest-3d/src/main.ts +35 -0
  79. package/templates-app/village-quest-3d/src/quest.ts +18 -0
  80. package/templates-app/village-quest-3d/src/village.scene.json +680 -0
  81. package/templates-app/village-quest-3d/tsconfig.json +13 -0
  82. package/templates-app/village-quest-3d/verify.ts +254 -0
  83. package/templates-app/village-quest-3d/vite.config.ts +5 -0
@@ -1,12 +1,12 @@
1
- import { f as Node } from "./loader-B4OEXDZ8.js";
2
- import { t as registerCoreNodes } from "./register-DJZXxwAn.js";
1
+ import { f as Node } from "./loader-BZqOKfI2.js";
2
+ import { t as registerCoreNodes } from "./register-nObreUQR.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
7
  import { i as applyParticlePreset, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CyUU7HVU.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CroQPEER.js";
9
- import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
9
+ import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
11
11
  //#region src/3d/camera-rig.ts
12
12
  const QUARTER_PITCH = Math.atan(1 / Math.SQRT2);
@@ -1950,8 +1950,8 @@ var BoneAttachment3D = class extends Node3D {
1950
1950
  boneObj.updateWorldMatrix(true, false);
1951
1951
  parent.updateWorldMatrix(true, false);
1952
1952
  obj.matrixAutoUpdate = false;
1953
- boneObj.matrixWorld.decompose(bonePosScratch, boneQuatScratch, boneScaleScratch);
1954
- boneScratch.compose(bonePosScratch, boneQuatScratch, unitScale);
1953
+ boneObj.matrixWorld.decompose(bonePosScratch$1, boneQuatScratch, boneScaleScratch);
1954
+ boneScratch.compose(bonePosScratch$1, boneQuatScratch, unitScale);
1955
1955
  obj.updateMatrix();
1956
1956
  offsetScratch.copy(obj.matrix);
1957
1957
  obj.matrix.copy(parent.matrixWorld).invert().multiply(boneScratch).multiply(offsetScratch);
@@ -1963,11 +1963,125 @@ var BoneAttachment3D = class extends Node3D {
1963
1963
  };
1964
1964
  const offsetScratch = new Matrix4();
1965
1965
  const boneScratch = new Matrix4();
1966
- const bonePosScratch = new Vector3();
1966
+ const bonePosScratch$1 = new Vector3();
1967
1967
  const boneQuatScratch = new Quaternion();
1968
1968
  const boneScaleScratch = new Vector3();
1969
1969
  const unitScale = new Vector3(1, 1, 1);
1970
1970
  //#endregion
1971
+ //#region src/3d/nodes/bone-look-at-3d.ts
1972
+ /**
1973
+ * Look-at IK for one bone — the character's head (or chest, or turret) turns
1974
+ * toward a target node ON TOP of whatever the animation is doing. Runs after
1975
+ * the mixer each rendered frame, blends in/out smoothly, and DISENGAGES when
1976
+ * the target leaves the comfort cone (no owl necks).
1977
+ *
1978
+ * { "name": "HeadTrack", "type": "BoneLookAt3D",
1979
+ * "props": { "target": "../Skin", "bone": "Head", "lookAt": "%Player" } }
1980
+ *
1981
+ * `forwardAxis` names the bone's local facing axis (mixamo heads face +z,
1982
+ * the default). Purely visual — headless it's a no-op.
1983
+ */
1984
+ var BoneLookAt3D = class extends Node3D {
1985
+ static typeName = "BoneLookAt3D";
1986
+ static props = {
1987
+ /** Node path to the ModelInstance3D whose bone to steer. */
1988
+ target: { default: "" },
1989
+ /** Bone name ("Head", "Neck", "Spine2" — Mixamo spellings matched). */
1990
+ bone: { default: "Head" },
1991
+ /** Node path to LOOK AT ('' = idle). */
1992
+ lookAt: { default: "" },
1993
+ /** Comfort cone half-angle, degrees — beyond it the head returns to the anim. */
1994
+ maxAngleDeg: { default: 75 },
1995
+ /** Blend weight 0..1 (how far toward the target the bone turns). */
1996
+ weight: { default: .85 },
1997
+ /** The bone's local facing axis. */
1998
+ forwardAxis: {
1999
+ default: "+z",
2000
+ options: [
2001
+ "+x",
2002
+ "-x",
2003
+ "+y",
2004
+ "-y",
2005
+ "+z",
2006
+ "-z"
2007
+ ]
2008
+ }
2009
+ };
2010
+ target = "";
2011
+ bone = "Head";
2012
+ lookAt = "";
2013
+ maxAngleDeg = 75;
2014
+ weight = .85;
2015
+ forwardAxis = "+z";
2016
+ boneObj = null;
2017
+ lookedUpFor = "";
2018
+ /** 0..1 engage blend — eases in/out so tracking never snaps. */
2019
+ engage = 0;
2020
+ /** Test hook. */
2021
+ get attachedBone() {
2022
+ return this.boneObj;
2023
+ }
2024
+ _onRender3D(_ctx) {
2025
+ if (this.target === "" || this.bone === "") return;
2026
+ const key = `${this.target} ${this.bone}`;
2027
+ if (this.lookedUpFor !== key || this.boneObj === null || !this.boneObj.parent) {
2028
+ this.lookedUpFor = key;
2029
+ this.boneObj = null;
2030
+ const source = this.getNodeOrNull(this.target);
2031
+ if (source && typeof source.findBone === "function") this.boneObj = source.findBone(this.bone);
2032
+ }
2033
+ const bone = this.boneObj;
2034
+ if (!bone?.parent) return;
2035
+ const targetNode = this.lookAt === "" ? null : this.getNodeOrNull(this.lookAt);
2036
+ let engaged = false;
2037
+ if (targetNode && Array.isArray(targetNode.position)) {
2038
+ bone.updateWorldMatrix(true, false);
2039
+ bone.getWorldPosition(bonePosScratch);
2040
+ targetScratch.set(targetNode.position[0] ?? 0, targetNode.position[1] ?? 0, targetNode.position[2] ?? 0);
2041
+ dirScratch$1.copy(targetScratch).sub(bonePosScratch);
2042
+ if (dirScratch$1.lengthSq() > 1e-6) {
2043
+ dirScratch$1.normalize();
2044
+ axisScratch.copy(AXES[this.forwardAxis] ?? PLUS_Z);
2045
+ animForwardScratch.copy(axisScratch).applyQuaternion(bone.getWorldQuaternion(quatScratch$2));
2046
+ if (animForwardScratch.angleTo(dirScratch$1) < this.maxAngleDeg * Math.PI / 180) {
2047
+ engaged = true;
2048
+ lookMatrixScratch.lookAt(bonePosScratch, targetScratch, UP$1);
2049
+ desiredScratch.setFromRotationMatrix(lookMatrixScratch);
2050
+ correctionScratch.setFromUnitVectors(axisScratch, MINUS_Z);
2051
+ desiredScratch.multiply(correctionScratch);
2052
+ bone.parent.getWorldQuaternion(parentScratch).invert();
2053
+ desiredScratch.premultiply(parentScratch);
2054
+ const k = this.weight * this.engage;
2055
+ bone.quaternion.slerp(desiredScratch, k);
2056
+ }
2057
+ }
2058
+ }
2059
+ this.engage += ((engaged ? 1 : 0) - this.engage) * ENGAGE_RATE;
2060
+ }
2061
+ };
2062
+ const ENGAGE_RATE = .18;
2063
+ const AXES = {
2064
+ "+x": new Vector3(1, 0, 0),
2065
+ "-x": new Vector3(-1, 0, 0),
2066
+ "+y": new Vector3(0, 1, 0),
2067
+ "-y": new Vector3(0, -1, 0),
2068
+ "+z": new Vector3(0, 0, 1),
2069
+ "-z": new Vector3(0, 0, -1)
2070
+ };
2071
+ const UP$1 = new Vector3(0, 1, 0);
2072
+ const PLUS_Z = new Vector3(0, 0, 1);
2073
+ const MINUS_Z = new Vector3(0, 0, -1);
2074
+ const bonePosScratch = new Vector3();
2075
+ const targetScratch = new Vector3();
2076
+ const dirScratch$1 = new Vector3();
2077
+ const axisScratch = new Vector3();
2078
+ const animForwardScratch = new Vector3();
2079
+ const lookMatrixScratch = new Matrix4();
2080
+ const desiredScratch = new Quaternion();
2081
+ const correctionScratch = new Quaternion();
2082
+ const parentScratch = new Quaternion();
2083
+ const quatScratch$2 = new Quaternion();
2084
+ //#endregion
1971
2085
  //#region src/3d/nodes/camera-3d.ts
1972
2086
  /**
1973
2087
  * A perspective camera. Mark exactly one camera `current: true`; with none
@@ -2022,7 +2136,7 @@ const CAM_GROUND_CLEARANCE = .3;
2022
2136
  * near-plane band around the boom (the Emberwood hedge-maze lesson). */
2023
2137
  const CAM_PROBE_RADIUS = .25;
2024
2138
  const eulerScratch = new Euler();
2025
- const quatScratch = new Quaternion();
2139
+ const quatScratch$1 = new Quaternion();
2026
2140
  const matrixScratch$3 = new Matrix4();
2027
2141
  const camVecScratch = new Vector3();
2028
2142
  const eyeVecScratch = new Vector3();
@@ -2309,12 +2423,12 @@ var CharacterController3D = class extends Node3D {
2309
2423
  camVecScratch.set(camera.position[0] ?? 0, camera.position[1] ?? 0, camera.position[2] ?? 0);
2310
2424
  eyeVecScratch.set(eye[0], eye[1], eye[2]);
2311
2425
  matrixScratch$3.lookAt(camVecScratch, eyeVecScratch, UP_VEC);
2312
- quatScratch.setFromRotationMatrix(matrixScratch$3);
2313
- eulerScratch.setFromQuaternion(quatScratch, "XYZ");
2426
+ quatScratch$1.setFromRotationMatrix(matrixScratch$3);
2427
+ eulerScratch.setFromQuaternion(quatScratch$1, "XYZ");
2314
2428
  } else {
2315
2429
  eulerScratch.set(pose.rotation[0], pose.rotation[1], pose.rotation[2], "YXZ");
2316
- quatScratch.setFromEuler(eulerScratch);
2317
- eulerScratch.setFromQuaternion(quatScratch, "XYZ");
2430
+ quatScratch$1.setFromEuler(eulerScratch);
2431
+ eulerScratch.setFromQuaternion(quatScratch$1, "XYZ");
2318
2432
  }
2319
2433
  camera.rotation = [
2320
2434
  eulerScratch.x * RAD2DEG,
@@ -2952,7 +3066,7 @@ var Flowers3D = class extends Node3D {
2952
3066
  height: { default: .45 },
2953
3067
  clustering: { default: .6 },
2954
3068
  sway: { default: .5 },
2955
- drape: { default: false },
3069
+ drape: { default: null },
2956
3070
  terrain: { default: "" }
2957
3071
  };
2958
3072
  /** 'lush' | 'sparse' | 'none' | plants per m² (load-time check + budget). */
@@ -2973,9 +3087,13 @@ var Flowers3D = class extends Node3D {
2973
3087
  clustering = .6;
2974
3088
  /** Gentle head-bob wind strength — 0 holds still. */
2975
3089
  sway = .5;
2976
- /** Drape each plant onto a Terrain3D's surface so flowers root on the ground
2977
- * instead of a flat plane (lets ONE bed cover rolling terrain). */
2978
- drape = false;
3090
+ /**
3091
+ * null (the default) is AUTO: whenever the scene has a Terrain3D, instances
3092
+ * root on it — flat ground behaves as before, rolling terrain just works.
3093
+ * true forces it (keeps retrying until a terrain appears), false disables
3094
+ * draping entirely.
3095
+ */
3096
+ drape = null;
2979
3097
  /** Drape target: a node path to the Terrain3D. Empty = auto-find the first
2980
3098
  * Terrain3D in the tree (the zero-config default when `drape` is on). */
2981
3099
  terrain = "";
@@ -3005,7 +3123,7 @@ var Flowers3D = class extends Node3D {
3005
3123
  if (!(f.height > 0)) throw new IncantoError("BAD_FORMAT", `Flowers3D '${node.name}' height must be > 0 meters, got ${f.height}.`, { prop: "height" });
3006
3124
  if (!(f.clustering >= 0) || !(f.clustering <= 1)) throw new IncantoError("BAD_FORMAT", `Flowers3D '${node.name}' clustering must be in [0, 1] (0 uniform … 1 tight patches), got ${f.clustering}.`, { prop: "clustering" });
3007
3125
  if (!(f.sway >= 0)) throw new IncantoError("BAD_FORMAT", `Flowers3D '${node.name}' sway must be >= 0, got ${f.sway}.`, { prop: "sway" });
3008
- if (typeof f.drape !== "boolean") throw new IncantoError("BAD_FORMAT", `Flowers3D '${node.name}' drape must be a boolean, got ${JSON.stringify(f.drape)}.`, { prop: "drape" });
3126
+ if (f.drape !== null && typeof f.drape !== "boolean") throw new IncantoError("BAD_FORMAT", `Flowers3D '${node.name}' drape must be true, false or null (null = auto: drape whenever the scene has a Terrain3D), got ${JSON.stringify(f.drape)}.`, { prop: "drape" });
3009
3127
  if (typeof f.terrain !== "string") throw new IncantoError("BAD_FORMAT", `Flowers3D '${node.name}' terrain must be a node-path string (empty = auto-find), got ${JSON.stringify(f.terrain)}.`, { prop: "terrain" });
3010
3128
  }
3011
3129
  time = 0;
@@ -3058,7 +3176,7 @@ var Flowers3D = class extends Node3D {
3058
3176
  /** Resolve the drape target + cache the bed's world position for `bedY`. */
3059
3177
  resolveDrape() {
3060
3178
  this.drapeTerrain = null;
3061
- if (!this.drape) return;
3179
+ if (this.drape === false) return;
3062
3180
  const terrain = findDrapeTerrain(this, this.terrain);
3063
3181
  if (terrain) {
3064
3182
  this.drapeTerrain = terrain;
@@ -3110,8 +3228,8 @@ var Flowers3D = class extends Node3D {
3110
3228
  const p = plants[i];
3111
3229
  positionScratch$2.set(p.x, this.bedY(p.x, p.z), p.z);
3112
3230
  quaternionScratch$2.setFromAxisAngle(Y_AXIS$2, p.rotY);
3113
- scaleScratch$2.setScalar(this.height * p.scale);
3114
- matrixScratch$2.compose(positionScratch$2, quaternionScratch$2, scaleScratch$2);
3231
+ scaleScratch$3.setScalar(this.height * p.scale);
3232
+ matrixScratch$2.compose(positionScratch$2, quaternionScratch$2, scaleScratch$3);
3115
3233
  structureMesh.setMatrixAt(i, matrixScratch$2);
3116
3234
  petalMesh.setMatrixAt(i, matrixScratch$2);
3117
3235
  const pick = Math.min(Math.floor(p.color01 * palette.length), palette.length - 1);
@@ -3183,7 +3301,7 @@ const Y_AXIS$2 = new Vector3(0, 1, 0);
3183
3301
  const matrixScratch$2 = new Matrix4();
3184
3302
  const positionScratch$2 = new Vector3();
3185
3303
  const quaternionScratch$2 = new Quaternion();
3186
- const scaleScratch$2 = new Vector3();
3304
+ const scaleScratch$3 = new Vector3();
3187
3305
  const colorScratch$1 = new Color();
3188
3306
  /**
3189
3307
  * Nearest-k benders whose influence circle touches the field rectangle,
@@ -4335,7 +4453,7 @@ var Foliage3D = class Foliage3D extends Node3D {
4335
4453
  coverage: { default: .85 },
4336
4454
  flowers: { default: 0 },
4337
4455
  seed: { default: 1 },
4338
- drape: { default: false },
4456
+ drape: { default: null },
4339
4457
  terrain: { default: "" },
4340
4458
  renderOrder: { default: 2 }
4341
4459
  };
@@ -4394,9 +4512,14 @@ var Foliage3D = class Foliage3D extends Node3D {
4394
4512
  flowers = 0;
4395
4513
  /** Placement seed — the field is identical across runs and machines. */
4396
4514
  seed = 1;
4397
- /** Drape each instance onto a Terrain3D's surface so blades root on the
4398
- * ground instead of a flat plane (lets ONE field cover rolling terrain). */
4399
- drape = false;
4515
+ /**
4516
+ * Drape each instance onto a Terrain3D's surface so blades root on the
4517
+ * ground instead of a flat plane. null (the default) is AUTO: whenever the
4518
+ * scene has a Terrain3D, instances root on it — flat ground behaves as
4519
+ * before, rolling terrain just works. true forces it (keeps retrying until
4520
+ * a terrain appears), false disables draping entirely.
4521
+ */
4522
+ drape = null;
4400
4523
  /** Drape target: a node path to the Terrain3D. Empty = auto-find the first
4401
4524
  * Terrain3D in the tree (the zero-config default when `drape` is on). */
4402
4525
  terrain = "";
@@ -4430,7 +4553,7 @@ var Foliage3D = class Foliage3D extends Node3D {
4430
4553
  if (!Array.isArray(sun) || sun.length !== 3 || !sun.every((v) => Number.isFinite(v)) || Math.hypot(sun[0] ?? 0, sun[1] ?? 0, sun[2] ?? 0) === 0) throw new IncantoError("BAD_FORMAT", `Foliage3D '${node.name}' sunDirection must be a non-zero [x, y, z] vector, got ${JSON.stringify(sun)}.`, { prop: "sunDirection" });
4431
4554
  if (!(f.coverage > 0) || !(f.coverage <= 1)) throw new IncantoError("BAD_FORMAT", `Foliage3D '${node.name}' coverage must be in (0, 1] (carpet fill — 1 is solid, lower carves dirt patches), got ${f.coverage}.`, { prop: "coverage" });
4432
4555
  if (!(f.flowers >= 0) || !(f.flowers <= .2)) throw new IncantoError("BAD_FORMAT", `Foliage3D '${node.name}' flowers must be in [0, 0.2] (fraction of instances rendered as flower heads — for a whole flower FIELD use kind: 'flowers'), got ${f.flowers}.`, { prop: "flowers" });
4433
- if (typeof f.drape !== "boolean") throw new IncantoError("BAD_FORMAT", `Foliage3D '${node.name}' drape must be a boolean, got ${JSON.stringify(f.drape)}.`, { prop: "drape" });
4556
+ if (f.drape !== null && typeof f.drape !== "boolean") throw new IncantoError("BAD_FORMAT", `Foliage3D '${node.name}' drape must be true, false or null (null = auto: drape whenever the scene has a Terrain3D), got ${JSON.stringify(f.drape)}.`, { prop: "drape" });
4434
4557
  if (typeof f.terrain !== "string") throw new IncantoError("BAD_FORMAT", `Foliage3D '${node.name}' terrain must be a node-path string (empty = auto-find), got ${JSON.stringify(f.terrain)}.`, { prop: "terrain" });
4435
4558
  }
4436
4559
  time = 0;
@@ -4476,16 +4599,33 @@ var Foliage3D = class Foliage3D extends Node3D {
4476
4599
  update(dt) {
4477
4600
  this.time += dt;
4478
4601
  this.pickedBenders = this.computeBenders();
4602
+ this.writeAnimationUniforms();
4603
+ }
4604
+ /** Wind time/strength + the ≤4 character-bend slots (no-op before first build). */
4605
+ writeAnimationUniforms() {
4606
+ if (this.isShaderField && this.bladesMaterial) {
4607
+ const u = this.bladesMaterial.uniforms;
4608
+ if (u.time) u.time.value = this.time;
4609
+ if (u.windStrength) u.windStrength.value = this.sway * (this.isMesh ? MESH_SWAY_TO_WIND : SWAY_TO_WIND);
4610
+ const slots = this.benderSlots;
4611
+ if (slots) for (let i = 0; i < 4; i++) {
4612
+ const b = this.pickedBenders[i];
4613
+ if (b) slots[i]?.set(b.x, b.y, b.z, b.radius);
4614
+ else slots[i]?.set(0, 0, 0, 0);
4615
+ }
4616
+ } else if (this.isTufts) {
4617
+ if (this.tuftTime) this.tuftTime.value = this.time;
4618
+ if (this.tuftStrength) this.tuftStrength.value = this.sway * TUFT_SWAY_TO_WIND;
4619
+ }
4479
4620
  }
4480
4621
  _syncObject3D() {
4481
4622
  super._syncObject3D();
4482
4623
  this.rebuildIfNeeded();
4483
4624
  const pivot = this.pivot;
4625
+ this.writeAnimationUniforms();
4484
4626
  if (this.isShaderField) {
4485
4627
  pivot.matrix.identity();
4486
4628
  const u = this.bladesMaterial.uniforms;
4487
- if (u.time) u.time.value = this.time;
4488
- if (u.windStrength) u.windStrength.value = this.sway * (this.isMesh ? MESH_SWAY_TO_WIND : SWAY_TO_WIND);
4489
4629
  (u.bottomColor?.value).set(this.colorA);
4490
4630
  (u.topColor?.value).set(this.colorB);
4491
4631
  if (this.isMesh) {
@@ -4494,17 +4634,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4494
4634
  if (u.fadeStart) u.fadeStart.value = this.fadeStart;
4495
4635
  if (u.fadeEnd) u.fadeEnd.value = this.fadeEnd;
4496
4636
  }
4497
- const slots = this.benderSlots;
4498
- for (let i = 0; i < 4; i++) {
4499
- const b = this.pickedBenders[i];
4500
- if (b) slots[i]?.set(b.x, b.y, b.z, b.radius);
4501
- else slots[i]?.set(0, 0, 0, 0);
4502
- }
4503
- } else if (this.isTufts) {
4504
- pivot.matrix.identity();
4505
- if (this.tuftTime) this.tuftTime.value = this.time;
4506
- if (this.tuftStrength) this.tuftStrength.value = this.sway * TUFT_SWAY_TO_WIND;
4507
- } else if (this.sway > 0) {
4637
+ } else if (this.isTufts) pivot.matrix.identity();
4638
+ else if (this.sway > 0) {
4508
4639
  const leanX = Math.sin(this.time * 1.7) * SWAY_LEAN * this.sway;
4509
4640
  const leanZ = Math.sin(this.time * 1.3 + 1.7) * SWAY_LEAN * this.sway * .6;
4510
4641
  pivot.matrix.makeShear(0, 0, leanX, leanZ, 0, 0);
@@ -4531,7 +4662,7 @@ var Foliage3D = class Foliage3D extends Node3D {
4531
4662
  computeBenders() {
4532
4663
  if (!this.interaction || !this.isShaderField) return [];
4533
4664
  const [fx, fy, fz] = worldXZY(this);
4534
- if (this.drape && !this.drapeTerrain) this.resolveDrape();
4665
+ if (this.drape === true && !this.drapeTerrain) this.resolveDrape();
4535
4666
  const terrain = this.drapeTerrain;
4536
4667
  const candidates = [];
4537
4668
  for (const b of movingBodiesFor(this)) {
@@ -4592,7 +4723,7 @@ var Foliage3D = class Foliage3D extends Node3D {
4592
4723
  /** Resolve the drape target + cache the field's world position for `bedY`. */
4593
4724
  resolveDrape() {
4594
4725
  this.drapeTerrain = null;
4595
- if (!this.drape) return;
4726
+ if (this.drape === false) return;
4596
4727
  const terrain = findDrapeTerrain(this, this.terrain);
4597
4728
  if (terrain) {
4598
4729
  this.drapeTerrain = terrain;
@@ -4672,8 +4803,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4672
4803
  const bladeHeight = this.height * regional * (.55 + p.scale * .6);
4673
4804
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4674
4805
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4675
- scaleScratch$1.set(widthJitter, bladeHeight, widthJitter);
4676
- mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4806
+ scaleScratch$2.set(widthJitter, bladeHeight, widthJitter);
4807
+ mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4677
4808
  const leanDir = clump.leanDir + rng.range(-.6, .6);
4678
4809
  const lean = (MESH_LEAN_BASE + MESH_LEAN_RANGE * (.5 * clump.lean + .5 * rng.next())) * (1 + MESH_TALL_LEAN_BOOST * tall);
4679
4810
  blade[i * 4] = Math.cos(leanDir) * lean;
@@ -4711,8 +4842,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4711
4842
  const stem = this.height * regional * (.85 + rng.next() * .35);
4712
4843
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4713
4844
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4714
- scaleScratch$1.set(1, stem, 1);
4715
- mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4845
+ scaleScratch$2.set(1, stem, 1);
4846
+ mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4716
4847
  const pick = rng.next();
4717
4848
  const color = FLOWER_COLORS[pick < .45 ? 0 : pick < .75 ? 1 : 2];
4718
4849
  mesh.setColorAt(i, colorScratchC$1.set(color));
@@ -4764,8 +4895,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4764
4895
  const cardW = cardH * TUFT_ASPECT * rng.range(TUFT_WIDTH_JITTER[0], TUFT_WIDTH_JITTER[1]);
4765
4896
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4766
4897
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4767
- scaleScratch$1.set(cardW, cardH, cardW);
4768
- mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4898
+ scaleScratch$2.set(cardW, cardH, cardW);
4899
+ mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4769
4900
  const clump = clumpAt(p.x, p.z);
4770
4901
  const t = Math.min(1, Math.max(0, .5 + .35 * clump.hue + (rng.next() - .5) * .3));
4771
4902
  const dry = Math.min(1, Math.max(0, dryFieldAt(p.x, p.z, this.seed) * .8 - .1));
@@ -4822,11 +4953,11 @@ var Foliage3D = class Foliage3D extends Node3D {
4822
4953
  for (let i = 0; i < count; i++) {
4823
4954
  const p = placements[i];
4824
4955
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4825
- scaleScratch$1.setScalar(p.scale);
4956
+ scaleScratch$2.setScalar(p.scale);
4826
4957
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4827
- first.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4958
+ first.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4828
4959
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY + Math.PI / 2);
4829
- second.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4960
+ second.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4830
4961
  }
4831
4962
  first.instanceMatrix.needsUpdate = true;
4832
4963
  second.instanceMatrix.needsUpdate = true;
@@ -4851,8 +4982,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4851
4982
  const sz = rng.range(-halfZ, halfZ);
4852
4983
  positionScratch$1.set(sx, this.bedY(sx, sz), sz);
4853
4984
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, rng.range(0, Math.PI * 2));
4854
- scaleScratch$1.set(1, this.height * rng.range(HEIGHT_JITTER$1[0], HEIGHT_JITTER$1[1]), 1);
4855
- matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1);
4985
+ scaleScratch$2.set(1, this.height * rng.range(HEIGHT_JITTER$1[0], HEIGHT_JITTER$1[1]), 1);
4986
+ matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2);
4856
4987
  mesh.setMatrixAt(i, matrixScratch$1);
4857
4988
  mesh.setColorAt(i, colorScratchC$1.copy(colorA).lerp(colorB, rng.next()));
4858
4989
  }
@@ -5001,88 +5132,12 @@ const Y_AXIS$1 = new Vector3(0, 1, 0);
5001
5132
  const matrixScratch$1 = new Matrix4();
5002
5133
  const positionScratch$1 = new Vector3();
5003
5134
  const quaternionScratch$1 = new Quaternion();
5004
- const scaleScratch$1 = new Vector3();
5135
+ const scaleScratch$2 = new Vector3();
5005
5136
  const colorScratchA$1 = new Color();
5006
5137
  const colorScratchB$1 = new Color();
5007
5138
  const colorScratchC$1 = new Color();
5008
5139
  const tuftDryScratch = new Color();
5009
5140
  //#endregion
5010
- //#region src/3d/nodes/lights-3d.ts
5011
- /** Sun-style light. Direction comes from the node's rotation. */
5012
- var DirectionalLight3D = class extends Node3D {
5013
- static typeName = "DirectionalLight3D";
5014
- static props = {
5015
- color: { default: "#ffffff" },
5016
- intensity: { default: 1 },
5017
- castShadow: { default: false },
5018
- shadowArea: { default: 75 },
5019
- shadowMapSize: { default: 2048 },
5020
- shadowFollowsCamera: { default: false }
5021
- };
5022
- color = "#ffffff";
5023
- intensity = 1;
5024
- castShadow = false;
5025
- /** Half-extent of the directional shadow frustum (template default ±75). */
5026
- shadowArea = 75;
5027
- shadowMapSize = 2048;
5028
- /**
5029
- * Re-center the shadow frustum on the camera every frame. A directional sun's
5030
- * shadow map covers a fixed ortho box; in a big roaming world that box must be
5031
- * huge (and so low-resolution that the player's own shadow nearly vanishes).
5032
- * With this on, a SMALL `shadowArea` tracks the camera, so contact shadows stay
5033
- * crisp wherever you go. The renderer supplies the camera position.
5034
- */
5035
- shadowFollowsCamera = false;
5036
- _createObject3D() {
5037
- return new DirectionalLight();
5038
- }
5039
- _syncObject3D() {
5040
- super._syncObject3D();
5041
- const light = this._ensureObject3D();
5042
- light.color.set(this.color);
5043
- light.intensity = this.intensity;
5044
- light.castShadow = this.castShadow;
5045
- if (this.castShadow && "shadow" in light) {
5046
- const cam = light.shadow.camera;
5047
- if ("left" in cam) {
5048
- cam.left = -this.shadowArea;
5049
- cam.right = this.shadowArea;
5050
- cam.top = this.shadowArea;
5051
- cam.bottom = -this.shadowArea;
5052
- cam.near = .5;
5053
- cam.far = 500;
5054
- cam.updateProjectionMatrix();
5055
- }
5056
- light.shadow.mapSize.set(this.shadowMapSize, this.shadowMapSize);
5057
- light.shadow.bias = -2e-4;
5058
- light.shadow.normalBias = .002;
5059
- }
5060
- }
5061
- };
5062
- /** Point light with a range (Godot's OmniLight3D). */
5063
- var OmniLight3D = class extends Node3D {
5064
- static typeName = "OmniLight3D";
5065
- static props = {
5066
- color: { default: "#ffffff" },
5067
- intensity: { default: 1 },
5068
- range: { default: 0 }
5069
- };
5070
- color = "#ffffff";
5071
- intensity = 1;
5072
- /** 0 = unlimited (three `distance` semantics). */
5073
- range = 0;
5074
- _createObject3D() {
5075
- return new PointLight();
5076
- }
5077
- _syncObject3D() {
5078
- super._syncObject3D();
5079
- const light = this._ensureObject3D();
5080
- light.color.set(this.color);
5081
- light.intensity = this.intensity;
5082
- light.distance = this.range;
5083
- }
5084
- };
5085
- //#endregion
5086
5141
  //#region src/3d/nodes/mesh-instance-3d.ts
5087
5142
  /** Every key `material` accepts — validation lists these on a typo. */
5088
5143
  const MATERIAL_KEYS = [
@@ -5165,7 +5220,7 @@ var MeshInstance3D = class extends Node3D {
5165
5220
  const key = `${this.mesh}:${JSON.stringify(this.size)}`;
5166
5221
  if (key !== this.geometryKey) {
5167
5222
  m.geometry.dispose();
5168
- m.geometry = buildGeometry(this.mesh, this.size);
5223
+ m.geometry = buildMeshGeometry(this.mesh, this.size);
5169
5224
  this.geometryKey = key;
5170
5225
  }
5171
5226
  const mat = m.material;
@@ -5284,7 +5339,8 @@ function validateMeshMaterial(material, name, typeName = "MeshInstance3D") {
5284
5339
  if (value !== void 0 && typeof value !== "boolean") fail(`material.${key} must be a boolean, got ${JSON.stringify(value)}.`);
5285
5340
  }
5286
5341
  }
5287
- function buildGeometry(kind, size) {
5342
+ /** Geometry factory shared by MeshInstance3D and InstancedMesh3D. */
5343
+ function buildMeshGeometry(kind, size) {
5288
5344
  const x = size[0] ?? 1;
5289
5345
  const y = size[1] ?? 1;
5290
5346
  const z = size[2] ?? 1;
@@ -5303,6 +5359,206 @@ function buildGeometry(kind, size) {
5303
5359
  }
5304
5360
  }
5305
5361
  //#endregion
5362
+ //#region src/3d/nodes/instanced-mesh-3d.ts
5363
+ /**
5364
+ * Hundreds of copies of one mesh in ONE draw call — scattered rocks, fence
5365
+ * posts, pillars, crates, gravestones. Same `mesh`/`size`/`material` surface
5366
+ * as MeshInstance3D; `transforms` places the copies:
5367
+ *
5368
+ * { "name": "Rocks", "type": "InstancedMesh3D",
5369
+ * "props": { "mesh": "gem", "size": [0.7, 0.5, 0.7],
5370
+ * "material": { "color": "#8a8f98", "roughness": 0.9, "flatShading": true },
5371
+ * "transforms": [[12, 0.2, -8, 40, 1.3], [-6, 0.1, 14, 210, 0.8]] } }
5372
+ *
5373
+ * Each row is `[x, y, z, yawDeg?, scale?]` (yaw 0 and scale 1 when omitted) —
5374
+ * dense enough to generate hundreds inline. REPLACE the array to update
5375
+ * (mutations are not watched).
5376
+ */
5377
+ var InstancedMesh3D = class extends Node3D {
5378
+ static typeName = "InstancedMesh3D";
5379
+ static props = {
5380
+ mesh: {
5381
+ default: "box",
5382
+ options: [
5383
+ "box",
5384
+ "sphere",
5385
+ "capsule",
5386
+ "plane",
5387
+ "cylinder",
5388
+ "gem"
5389
+ ]
5390
+ },
5391
+ size: { default: [
5392
+ 1,
5393
+ 1,
5394
+ 1
5395
+ ] },
5396
+ material: { default: {} },
5397
+ /** `[x, y, z, yawDeg?, scale?]` per instance. */
5398
+ transforms: { default: [] },
5399
+ castShadow: { default: false },
5400
+ receiveShadow: { default: false }
5401
+ };
5402
+ mesh = "box";
5403
+ size = [
5404
+ 1,
5405
+ 1,
5406
+ 1
5407
+ ];
5408
+ material = {};
5409
+ castShadow = false;
5410
+ receiveShadow = false;
5411
+ _transforms = [];
5412
+ transformsDirty = true;
5413
+ get transforms() {
5414
+ return this._transforms;
5415
+ }
5416
+ set transforms(value) {
5417
+ this._transforms = value;
5418
+ this.transformsDirty = true;
5419
+ }
5420
+ instanced = null;
5421
+ builtKey = "";
5422
+ static validateJson(node) {
5423
+ const self = node;
5424
+ validateMeshMaterial(self.material, node.name);
5425
+ for (const [i, row] of self._transforms.entries()) if (!Array.isArray(row) || row.length < 3 || row.length > 5 || row.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `InstancedMesh3D '${node.name}': transforms[${i}] must be [x, y, z, yawDeg?, scale?] (numbers), got ${JSON.stringify(row)}.`);
5426
+ }
5427
+ /** Test hook: the live instance count. */
5428
+ get instanceCount() {
5429
+ return this.instanced?.count ?? 0;
5430
+ }
5431
+ _syncObject3D(alpha) {
5432
+ super._syncObject3D(alpha);
5433
+ const root = this._ensureObject3D();
5434
+ const key = `${this.mesh} ${JSON.stringify(this.size)} ${this._transforms.length}`;
5435
+ if (key !== this.builtKey) {
5436
+ this.builtKey = key;
5437
+ if (this.instanced) {
5438
+ root.remove(this.instanced);
5439
+ this.instanced.geometry.dispose();
5440
+ this.instanced.material.dispose();
5441
+ this.instanced = null;
5442
+ }
5443
+ if (this._transforms.length > 0) {
5444
+ const geometry = buildMeshGeometry(this.mesh, this.size);
5445
+ const material = new MeshPhysicalMaterial();
5446
+ this.instanced = new InstancedMesh(geometry, material, this._transforms.length);
5447
+ this.instanced.instanceMatrix.setUsage(DynamicDrawUsage);
5448
+ root.add(this.instanced);
5449
+ }
5450
+ this.transformsDirty = true;
5451
+ }
5452
+ const inst = this.instanced;
5453
+ if (!inst) return;
5454
+ inst.castShadow = this.castShadow;
5455
+ inst.receiveShadow = this.receiveShadow;
5456
+ applyMeshMaterial(inst.material, this.material);
5457
+ if (this.transformsDirty) {
5458
+ this.transformsDirty = false;
5459
+ for (let i = 0; i < this._transforms.length; i++) {
5460
+ const row = this._transforms[i];
5461
+ posScratch.set(row[0] ?? 0, row[1] ?? 0, row[2] ?? 0);
5462
+ quatScratch.setFromAxisAngle(UP, (row[3] ?? 0) * Math.PI / 180);
5463
+ const s = row[4] ?? 1;
5464
+ scaleScratch$1.set(s, s, s);
5465
+ matScratch.compose(posScratch, quatScratch, scaleScratch$1);
5466
+ inst.setMatrixAt(i, matScratch);
5467
+ }
5468
+ inst.instanceMatrix.needsUpdate = true;
5469
+ inst.computeBoundingSphere();
5470
+ }
5471
+ }
5472
+ free() {
5473
+ if (this.instanced) {
5474
+ this.instanced.geometry.dispose();
5475
+ this.instanced.material.dispose();
5476
+ }
5477
+ super.free();
5478
+ }
5479
+ };
5480
+ const UP = new Vector3(0, 1, 0);
5481
+ const posScratch = new Vector3();
5482
+ const quatScratch = new Quaternion();
5483
+ const scaleScratch$1 = new Vector3();
5484
+ const matScratch = new Matrix4();
5485
+ //#endregion
5486
+ //#region src/3d/nodes/lights-3d.ts
5487
+ /** Sun-style light. Direction comes from the node's rotation. */
5488
+ var DirectionalLight3D = class extends Node3D {
5489
+ static typeName = "DirectionalLight3D";
5490
+ static props = {
5491
+ color: { default: "#ffffff" },
5492
+ intensity: { default: 1 },
5493
+ castShadow: { default: false },
5494
+ shadowArea: { default: 75 },
5495
+ shadowMapSize: { default: 2048 },
5496
+ shadowFollowsCamera: { default: false }
5497
+ };
5498
+ color = "#ffffff";
5499
+ intensity = 1;
5500
+ castShadow = false;
5501
+ /** Half-extent of the directional shadow frustum (template default ±75). */
5502
+ shadowArea = 75;
5503
+ shadowMapSize = 2048;
5504
+ /**
5505
+ * Re-center the shadow frustum on the camera every frame. A directional sun's
5506
+ * shadow map covers a fixed ortho box; in a big roaming world that box must be
5507
+ * huge (and so low-resolution that the player's own shadow nearly vanishes).
5508
+ * With this on, a SMALL `shadowArea` tracks the camera, so contact shadows stay
5509
+ * crisp wherever you go. The renderer supplies the camera position.
5510
+ */
5511
+ shadowFollowsCamera = false;
5512
+ _createObject3D() {
5513
+ return new DirectionalLight();
5514
+ }
5515
+ _syncObject3D() {
5516
+ super._syncObject3D();
5517
+ const light = this._ensureObject3D();
5518
+ light.color.set(this.color);
5519
+ light.intensity = this.intensity;
5520
+ light.castShadow = this.castShadow;
5521
+ if (this.castShadow && "shadow" in light) {
5522
+ const cam = light.shadow.camera;
5523
+ if ("left" in cam) {
5524
+ cam.left = -this.shadowArea;
5525
+ cam.right = this.shadowArea;
5526
+ cam.top = this.shadowArea;
5527
+ cam.bottom = -this.shadowArea;
5528
+ cam.near = .5;
5529
+ cam.far = 500;
5530
+ cam.updateProjectionMatrix();
5531
+ }
5532
+ light.shadow.mapSize.set(this.shadowMapSize, this.shadowMapSize);
5533
+ light.shadow.bias = -2e-4;
5534
+ light.shadow.normalBias = .002;
5535
+ }
5536
+ }
5537
+ };
5538
+ /** Point light with a range (Godot's OmniLight3D). */
5539
+ var OmniLight3D = class extends Node3D {
5540
+ static typeName = "OmniLight3D";
5541
+ static props = {
5542
+ color: { default: "#ffffff" },
5543
+ intensity: { default: 1 },
5544
+ range: { default: 0 }
5545
+ };
5546
+ color = "#ffffff";
5547
+ intensity = 1;
5548
+ /** 0 = unlimited (three `distance` semantics). */
5549
+ range = 0;
5550
+ _createObject3D() {
5551
+ return new PointLight();
5552
+ }
5553
+ _syncObject3D() {
5554
+ super._syncObject3D();
5555
+ const light = this._ensureObject3D();
5556
+ light.color.set(this.color);
5557
+ light.intensity = this.intensity;
5558
+ light.distance = this.range;
5559
+ }
5560
+ };
5561
+ //#endregion
5306
5562
  //#region src/3d/nodes/loft-mesh-3d.ts
5307
5563
  /**
5308
5564
  * A smooth swept hull ("loft"): superellipse cross-sections interpolated
@@ -5482,6 +5738,42 @@ function buildLoftGeometry(sections, slices, smooth) {
5482
5738
  return geo;
5483
5739
  }
5484
5740
  //#endregion
5741
+ //#region src/3d/animation/layering.ts
5742
+ /**
5743
+ * Clip masking for two-layer animation (upper-body attack over lower-body
5744
+ * locomotion). three has no per-bone action weights, so layering is done at
5745
+ * the CLIP level: the upper action gets only the tracks under the layer root
5746
+ * (e.g. Spine), and the base action gets everything else — no track overlap,
5747
+ * no half-blended bones.
5748
+ */
5749
+ /** Bone name a track drives — "mixamorigSpine.quaternion" → "mixamorigSpine". */
5750
+ function trackBone(trackName) {
5751
+ const dot = trackName.lastIndexOf(".");
5752
+ return dot === -1 ? trackName : trackName.slice(0, dot);
5753
+ }
5754
+ /**
5755
+ * A copy of `clip` with only the tracks whose bone IS (mode 'keep') or is
5756
+ * NOT (mode 'drop') in `bones`. Track arrays are shared (cheap); the clip
5757
+ * duration is preserved so layered actions stay time-aligned.
5758
+ */
5759
+ function maskClip(clip, bones, mode) {
5760
+ const tracks = clip.tracks.filter((t) => {
5761
+ const inSet = bones.has(trackBone(t.name));
5762
+ return mode === "keep" ? inSet : !inSet;
5763
+ });
5764
+ return new AnimationClip(`${clip.name}#${mode}`, clip.duration, tracks);
5765
+ }
5766
+ /** Collect the bone-name subtree from `root` (inclusive) — the layer mask. */
5767
+ function boneSubtree(root) {
5768
+ const out = /* @__PURE__ */ new Set();
5769
+ const walk = (o) => {
5770
+ out.add(o.name);
5771
+ for (const c of o.children) walk(c);
5772
+ };
5773
+ walk(root);
5774
+ return out;
5775
+ }
5776
+ //#endregion
5485
5777
  //#region src/3d/animation/rigmap.ts
5486
5778
  /**
5487
5779
  * Mixamo rig name → VRM humanoid bone name (the vibe-starter-3d /
@@ -5615,6 +5907,8 @@ function retargetClipToVrm(clip, sourceScene, vrm) {
5615
5907
  //#region src/3d/nodes/model-instance-3d.ts
5616
5908
  /** Crossfade duration (s) between animation clips — smooths idle↔walk↔run↔jump. */
5617
5909
  const ANIM_CROSSFADE = .2;
5910
+ /** Upper-layer fade — snappier than locomotion blends (attacks must feel instant). */
5911
+ const UPPER_FADE = .12;
5618
5912
  /**
5619
5913
  * A GLB/glTF/VRM model. `model` is a `$key` into the scene's assets header
5620
5914
  * ({type:"model", url}) or a direct URL. `targetHeight` (>0) uniformly scales
@@ -5637,6 +5931,12 @@ var ModelInstance3D = class extends Node3D {
5637
5931
  targetHeight: { default: 0 },
5638
5932
  castShadow: { default: false },
5639
5933
  animationLoop: { default: true },
5934
+ /** Upper-body layer clip ('' = off). One-shots auto-clear on finish. */
5935
+ animationUpper: { default: "" },
5936
+ /** Loop the upper layer (false = one-shot: attack, wave, throw). */
5937
+ animationUpperLoop: { default: false },
5938
+ /** Bone whose subtree the upper layer drives (forgiving lookup). */
5939
+ upperBodyRoot: { default: "Spine" },
5640
5940
  receiveShadow: { default: false },
5641
5941
  tint: { default: "" },
5642
5942
  metalness: { default: -1 },
@@ -5648,6 +5948,16 @@ var ModelInstance3D = class extends Node3D {
5648
5948
  castShadow = false;
5649
5949
  /** false = play the clip once, then emit `animationFinished`. */
5650
5950
  animationLoop = true;
5951
+ /**
5952
+ * Two-layer animation: play this clip on the UPPER BODY ONLY (the
5953
+ * `upperBodyRoot` bone subtree) while `animation` keeps driving the rest —
5954
+ * attack while running, wave while walking. One-shot by default
5955
+ * (`animationUpperLoop: false`): finishes, emits `animationFinished` with
5956
+ * this key, and auto-clears back to ''.
5957
+ */
5958
+ animationUpper = "";
5959
+ animationUpperLoop = false;
5960
+ upperBodyRoot = "Spine";
5651
5961
  receiveShadow = false;
5652
5962
  /** Hex colour multiplied into every material — RESKIN one shared GLB into many
5653
5963
  * variants (e.g. a sickly-green zombie from the base human). '' = untinted. */
@@ -5666,6 +5976,12 @@ var ModelInstance3D = class extends Node3D {
5666
5976
  mixer = null;
5667
5977
  playing = "";
5668
5978
  currentAction = null;
5979
+ playingUpper = "";
5980
+ upperAction = null;
5981
+ /** Which variant the BASE action is playing ('lower' while a layer is on). */
5982
+ baseMode = "full";
5983
+ /** Masked-clip cache: source clip uuid + layer root → {upper, lower}. */
5984
+ maskCache = /* @__PURE__ */ new Map();
5669
5985
  /** action → the `animation` key that played it, so `animationFinished` reports
5670
5986
  * the clip that ACTUALLY finished (a crossfaded-away one-shot can finish a few
5671
5987
  * frames after `this.animation` already moved on). */
@@ -5759,6 +6075,12 @@ var ModelInstance3D = class extends Node3D {
5759
6075
  this.mixer.addEventListener("finished", (e) => {
5760
6076
  const action = e.action;
5761
6077
  const key = (action && this.actionKeys.get(action)) ?? this.animation;
6078
+ if (action && action === this.upperAction) {
6079
+ action.fadeOut(UPPER_FADE);
6080
+ this.upperAction = null;
6081
+ this.playingUpper = "";
6082
+ this.animationUpper = "";
6083
+ }
5762
6084
  this.emit("animationFinished", key);
5763
6085
  });
5764
6086
  this.playing = "";
@@ -5801,6 +6123,7 @@ var ModelInstance3D = class extends Node3D {
5801
6123
  this.applyMaterialMods();
5802
6124
  this.applyPoses();
5803
6125
  this.syncAnimation(assets);
6126
+ this.syncUpperAnimation(assets);
5804
6127
  }
5805
6128
  /** Apply per-instance material mods — `tint` (multiplied into colour) and the
5806
6129
  * `metalness`/`roughness` overrides — by cloning the captured originals
@@ -5844,33 +6167,68 @@ var ModelInstance3D = class extends Node3D {
5844
6167
  if (obj) obj.rotation.set(rot[0] * DEG, rot[1] * DEG, rot[2] * DEG);
5845
6168
  }
5846
6169
  }
6170
+ /** Resolve a clip ref: embedded name → '$asset' / URL (VRM-retargeted when
6171
+ * needed). 'loading' = asset still fetching, try again next frame. */
6172
+ resolveClip(ref, assets) {
6173
+ if (!this.entry) return null;
6174
+ const embedded = this.entry.animations.find((c) => c.name === ref);
6175
+ if (embedded) return embedded;
6176
+ if (!(ref.startsWith("$") || /\.(glb|gltf)$/i.test(ref))) return null;
6177
+ const anim = assets.getAnimation(ref);
6178
+ if (anim.status === "loading") return "loading";
6179
+ if (anim.status !== "ready" || !anim.scene) return null;
6180
+ const source = (anim.clip ? anim.clips.find((c) => c.name === anim.clip) : void 0) ?? anim.clips[0];
6181
+ if (!source) return null;
6182
+ if (this.entry.isVrm && this.entry.vrm) {
6183
+ const cached = this.entry.retargeted.get(ref);
6184
+ if (cached) return cached;
6185
+ const result = retargetClipToVrm(source, anim.scene, this.entry.vrm);
6186
+ if (result.skippedBones.length > 0) console.warn(`incanto: retarget '${ref}' skipped bones: ${result.skippedBones.join(", ")}`);
6187
+ this.entry.retargeted.set(ref, result.clip);
6188
+ return result.clip;
6189
+ }
6190
+ return source;
6191
+ }
6192
+ /** Masked upper/lower variants of a clip for two-layer playback (cached). */
6193
+ layerVariants(clip) {
6194
+ const rootBone = this.findBone(this.upperBodyRoot);
6195
+ if (!rootBone) return null;
6196
+ const key = `${clip.uuid} ${rootBone.name}`;
6197
+ let v = this.maskCache.get(key);
6198
+ if (!v) {
6199
+ const bones = boneSubtree(rootBone);
6200
+ v = {
6201
+ upper: maskClip(clip, bones, "keep"),
6202
+ lower: maskClip(clip, bones, "drop")
6203
+ };
6204
+ this.maskCache.set(key, v);
6205
+ }
6206
+ return v;
6207
+ }
6208
+ upperLayerActive() {
6209
+ return this.animationUpper !== "" || this.upperAction !== null;
6210
+ }
5847
6211
  syncAnimation(assets) {
5848
- if (!this.mixer || !this.entry || this.playing === this.animation) return;
6212
+ if (!this.mixer || !this.entry) return;
6213
+ const mode = this.upperLayerActive() ? "lower" : "full";
6214
+ if (this.playing === this.animation && this.baseMode === mode) return;
5849
6215
  if (this.animation === "") {
5850
6216
  if (this.currentAction) this.currentAction.fadeOut(ANIM_CROSSFADE);
5851
6217
  this.currentAction = null;
5852
6218
  this.playing = "";
6219
+ this.baseMode = mode;
5853
6220
  return;
5854
6221
  }
5855
- let clip = this.entry.animations.find((c) => c.name === this.animation) ?? null;
5856
- if (!clip && (this.animation.startsWith("$") || /\.(glb|gltf)$/i.test(this.animation))) {
5857
- const anim = assets.getAnimation(this.animation);
5858
- if (anim.status === "loading") return;
5859
- if (anim.status === "ready" && anim.scene) {
5860
- const source = (anim.clip ? anim.clips.find((c) => c.name === anim.clip) : void 0) ?? anim.clips[0];
5861
- if (source) if (this.entry.isVrm && this.entry.vrm) {
5862
- const cached = this.entry.retargeted.get(this.animation);
5863
- if (cached) clip = cached;
5864
- else {
5865
- const result = retargetClipToVrm(source, anim.scene, this.entry.vrm);
5866
- if (result.skippedBones.length > 0) console.warn(`incanto: retarget '${this.animation}' skipped bones: ${result.skippedBones.join(", ")}`);
5867
- this.entry.retargeted.set(this.animation, result.clip);
5868
- clip = result.clip;
5869
- }
5870
- } else clip = source;
5871
- }
6222
+ const resolved = this.resolveClip(this.animation, assets);
6223
+ if (resolved === "loading") return;
6224
+ let clip = resolved;
6225
+ if (clip && mode === "lower") {
6226
+ const variants = this.layerVariants(clip);
6227
+ if (variants) clip = variants.lower;
5872
6228
  }
6229
+ const samePlaying = this.playing === this.animation;
5873
6230
  this.playing = this.animation;
6231
+ this.baseMode = mode;
5874
6232
  if (clip) {
5875
6233
  const action = this.mixer.clipAction(clip);
5876
6234
  action.reset();
@@ -5883,12 +6241,50 @@ var ModelInstance3D = class extends Node3D {
5883
6241
  action.play();
5884
6242
  this.actionKeys.set(action, this.animation);
5885
6243
  if (this.currentAction && this.currentAction !== action) {
6244
+ if (samePlaying && clip.duration > 0) action.time = this.currentAction.time % clip.duration;
5886
6245
  const warp = clip.duration > 0 && this.currentAction.getClip().duration > 0;
5887
6246
  action.crossFadeFrom(this.currentAction, ANIM_CROSSFADE, warp);
5888
6247
  }
5889
6248
  this.currentAction = action;
5890
6249
  } else console.warn(`incanto: model '${this.model}' has no animation '${this.animation}' — embedded: ${this.entry.animations.map((c) => c.name).join(", ") || "(none)"}; assets: ${assets.animationRefs().join(", ") || "(none)"}`);
5891
6250
  }
6251
+ /** The upper-body layer: masked clip on the `upperBodyRoot` subtree. */
6252
+ syncUpperAnimation(assets) {
6253
+ if (!this.mixer || !this.entry || this.playingUpper === this.animationUpper) return;
6254
+ if (this.animationUpper === "") {
6255
+ this.upperAction?.fadeOut(UPPER_FADE);
6256
+ this.upperAction = null;
6257
+ this.playingUpper = "";
6258
+ return;
6259
+ }
6260
+ const resolved = this.resolveClip(this.animationUpper, assets);
6261
+ if (resolved === "loading") return;
6262
+ if (!resolved) {
6263
+ console.warn(`incanto: model '${this.model}' has no animation '${this.animationUpper}' for the upper layer.`);
6264
+ this.playingUpper = this.animationUpper;
6265
+ return;
6266
+ }
6267
+ const variants = this.layerVariants(resolved);
6268
+ if (!variants) {
6269
+ console.warn(`incanto: upperBodyRoot '${this.upperBodyRoot}' not found in '${this.model}' — upper layer skipped.`);
6270
+ this.playingUpper = this.animationUpper;
6271
+ return;
6272
+ }
6273
+ const action = this.mixer.clipAction(variants.upper);
6274
+ action.reset();
6275
+ if (this.animationUpperLoop) action.setLoop(LoopRepeat, Infinity);
6276
+ else {
6277
+ action.setLoop(LoopOnce, 1);
6278
+ action.clampWhenFinished = true;
6279
+ }
6280
+ action.setEffectiveWeight(1);
6281
+ action.fadeIn(UPPER_FADE);
6282
+ action.play();
6283
+ this.actionKeys.set(action, this.animationUpper);
6284
+ if (this.upperAction && this.upperAction !== action) this.upperAction.fadeOut(UPPER_FADE);
6285
+ this.upperAction = action;
6286
+ this.playingUpper = this.animationUpper;
6287
+ }
5892
6288
  update(dt) {
5893
6289
  this.mixer?.update(dt);
5894
6290
  if (this.entry?.isVrm && this.entry.claimedBy === this) this.entry.vrm?.update(dt);
@@ -5906,6 +6302,10 @@ var ModelInstance3D = class extends Node3D {
5906
6302
  this.fitGroup = null;
5907
6303
  this.mixer = null;
5908
6304
  this.currentAction = null;
6305
+ this.upperAction = null;
6306
+ this.playingUpper = "";
6307
+ this.baseMode = "full";
6308
+ this.maskCache.clear();
5909
6309
  this.entry = null;
5910
6310
  this.mountedRef = "";
5911
6311
  this.playing = "";
@@ -8028,7 +8428,7 @@ var Tree3D = class Tree3D extends Node3D {
8028
8428
  leafFadeStart: { default: 0 },
8029
8429
  leafFadeEnd: { default: 0 },
8030
8430
  leafShadows: { default: true },
8031
- drape: { default: false },
8431
+ drape: { default: null },
8032
8432
  terrain: { default: "" }
8033
8433
  };
8034
8434
  /** 'simple' (primitive low-poly) | 'medium' (light ez-trees) | 'high' (full). */
@@ -8057,11 +8457,14 @@ var Tree3D = class Tree3D extends Node3D {
8057
8457
  /** Let leaves cast shadows. false keeps trunk/branch shadows but drops the
8058
8458
  * expensive alpha-cutout leaf shadow pass (the dappled floor) for big FPS. */
8059
8459
  leafShadows = true;
8060
- /** Drape the grove over a Terrain3D: each scattered instance roots at the
8061
- * ground height under it (not the node's single Y), so a patch on rolling or
8062
- * carved terrain never floats/buries. No effect on a count:1 tree placed by
8063
- * hand unless it sits off the ground. */
8064
- drape = false;
8460
+ /**
8461
+ * Drape the grove over a Terrain3D: each scattered instance roots at the
8462
+ * ground height under it (not the node's single Y), so a patch on rolling
8463
+ * or carved terrain never floats/buries. null (the default) is AUTO:
8464
+ * whenever the scene has a Terrain3D the grove drapes onto it. true forces
8465
+ * it (keeps retrying until a terrain appears), false disables draping.
8466
+ */
8467
+ drape = null;
8065
8468
  /** Node path to the Terrain3D to drape onto ('' = auto-find the first one). */
8066
8469
  terrain = "";
8067
8470
  /** Resolved drape target + the grove's world position, set per rebuild. */
@@ -8080,7 +8483,7 @@ var Tree3D = class Tree3D extends Node3D {
8080
8483
  if (!((t.area[0] ?? 0) > 0) || !((t.area[1] ?? 0) > 0)) throw new IncantoError("BAD_FORMAT", `Tree3D '${node.name}' area must be positive [x, z] meters, got ${JSON.stringify(t.area)}.`, { prop: "area" });
8081
8484
  if (t.leafFadeEnd !== 0 && !(t.leafFadeEnd > t.leafFadeStart && t.leafFadeStart >= 0)) throw new IncantoError("BAD_FORMAT", `Tree3D '${node.name}' needs leafFadeEnd > leafFadeStart >= 0 (the leaf LOD window) or 0/0 to disable, got start ${t.leafFadeStart}, end ${t.leafFadeEnd}.`, { prop: "leafFadeStart" });
8082
8485
  if (typeof t.leafShadows !== "boolean") throw new IncantoError("BAD_FORMAT", `Tree3D '${node.name}' leafShadows must be a boolean, got ${JSON.stringify(t.leafShadows)}.`, { prop: "leafShadows" });
8083
- if (typeof t.drape !== "boolean") throw new IncantoError("BAD_FORMAT", `Tree3D '${node.name}' drape must be a boolean, got ${JSON.stringify(t.drape)}.`, { prop: "drape" });
8486
+ if (t.drape !== null && typeof t.drape !== "boolean") throw new IncantoError("BAD_FORMAT", `Tree3D '${node.name}' drape must be true, false or null (null = auto: drape whenever the scene has a Terrain3D), got ${JSON.stringify(t.drape)}.`, { prop: "drape" });
8084
8487
  if (typeof t.terrain !== "string") throw new IncantoError("BAD_FORMAT", `Tree3D '${node.name}' terrain must be a node-path string (empty = auto-find), got ${JSON.stringify(t.terrain)}.`, { prop: "terrain" });
8085
8488
  if (t.tier === "simple") return;
8086
8489
  const perTree = maxVariantTriangles(t.tier, t.type);
@@ -8157,7 +8560,7 @@ var Tree3D = class Tree3D extends Node3D {
8157
8560
  /** Resolve the drape target + cache the grove's world position for `bedY`. */
8158
8561
  resolveDrape() {
8159
8562
  this.drapeTerrain = null;
8160
- if (!this.drape) return;
8563
+ if (this.drape === false) return;
8161
8564
  const terrain = findDrapeTerrain(this, this.terrain);
8162
8565
  if (terrain) {
8163
8566
  this.drapeTerrain = terrain;
@@ -11716,6 +12119,8 @@ function registerNodes3D() {
11716
12119
  registerNode(Joint3D);
11717
12120
  registerNode(Trail3D);
11718
12121
  registerNode(BoneAttachment3D);
12122
+ registerNode(BoneLookAt3D);
12123
+ registerNode(InstancedMesh3D);
11719
12124
  registerNode(Terrain3D);
11720
12125
  registerNode(Water3D);
11721
12126
  registerNode(Foliage3D);
@@ -11731,4 +12136,4 @@ function registerNodes3D() {
11731
12136
  registerNode(CharacterBody3D);
11732
12137
  }
11733
12138
  //#endregion
11734
- export { PhysicsBody3D as A, Terrain3D as C, Joint3D as D, terrainThemeLayers as E, QUARTER_PITCH as F, cameraRelative as I, keyboardIntensity as L, StaticBody3D as M, Node3D as N, Area3D as O, validateCollider3D as P, movementState as R, Billboard3D as S, TERRAIN_THEMES as T, resolveFlowerDensity as _, VoxelGrid3D as a, Camera3D as b, Particles3D as c, MeshInstance3D as d, DirectionalLight3D as f, Flowers3D as g, DENSITY_PRESETS as h, VOXEL_PALETTE as i, RigidBody3D as j, CharacterBody3D as k, ModelInstance3D as l, Foliage3D as m, Water3D as n, Tree3D as o, OmniLight3D as p, WATER_MAX_RIPPLES as r, Trail3D as s, registerNodes3D as t, LoftMesh3D as u, FLOWER_VARIETIES as v, DEFAULT_TERRAIN_TEXTURE_BASE as w, BoneAttachment3D as x, CharacterController3D as y, rigPose as z };
12139
+ export { Area3D as A, movementState as B, BoneAttachment3D as C, TERRAIN_THEMES as D, DEFAULT_TERRAIN_TEXTURE_BASE as E, Node3D as F, validateCollider3D as I, QUARTER_PITCH as L, PhysicsBody3D as M, RigidBody3D as N, terrainThemeLayers as O, StaticBody3D as P, cameraRelative as R, BoneLookAt3D as S, Terrain3D as T, rigPose as V, Flowers3D as _, VoxelGrid3D as a, CharacterController3D as b, Particles3D as c, DirectionalLight3D as d, OmniLight3D as f, DENSITY_PRESETS as g, Foliage3D as h, VOXEL_PALETTE as i, CharacterBody3D as j, Joint3D as k, ModelInstance3D as l, MeshInstance3D as m, Water3D as n, Tree3D as o, InstancedMesh3D as p, WATER_MAX_RIPPLES as r, Trail3D as s, registerNodes3D as t, LoftMesh3D as u, resolveFlowerDensity as v, Billboard3D as w, Camera3D as x, FLOWER_VARIETIES as y, keyboardIntensity as z };