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.
- package/README.md +22 -0
- package/bin/incanto-new.mjs +87 -0
- package/dist/2d.d.ts +2 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +219 -82
- package/dist/3d.js +74 -4
- package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
- package/dist/{create-game-C_xf4gm9.js → create-game-Ct86zczq.js} +5 -5
- package/dist/{create-game-B2fewJUy.js → create-game-OSntcIiM.js} +5 -5
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-BEvGBtb_.js → duplicate-BINwP0WG.js} +1 -1
- package/dist/{gameplay-Dn3yidJw.js → gameplay-sSqrUcnz.js} +75 -3
- package/dist/gameplay.d.ts +37 -2
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -4
- package/dist/{loader-B4OEXDZ8.js → loader-BZqOKfI2.js} +3 -1
- package/dist/{loader-BcUDfNHn.d.ts → loader-DILt9PGC.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{pathfinding-BSiXjd6W.d.ts → pathfinding-RWYkNKx9.d.ts} +1 -1
- package/dist/{physics-2d-B6D864oZ.js → physics-2d-BiIdl51r.js} +2 -2
- package/dist/{physics-3d-RLNaxRyw.js → physics-3d-D_kUdOjr.js} +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Cwc-yCYx.js → register-BFFE1Mh1.js} +2 -2
- package/dist/{register-ysLR3WQS.js → register-C9-16x_t.js} +562 -157
- package/dist/{register-BD6zgrv_.js → register-Djwckzgx.js} +2 -2
- package/dist/{register-DJZXxwAn.js → register-nObreUQR.js} +1 -1
- package/dist/test.d.ts +2 -2
- package/dist/test.js +10 -10
- package/editor/assets/{agent8-D_4QY8Aq.js → agent8-BDC4LhjC.js} +1 -1
- package/editor/assets/{index-s0DZuroe.js → index-7qOYg7t8.js} +74 -74
- package/editor/index.html +1 -1
- package/package.json +4 -2
- package/schemas/scene.schema.json +266 -6
- package/skills/incanto-3d-character.md +35 -0
- package/skills/incanto-building-3d-games.md +48 -0
- package/skills/incanto-environment.md +15 -16
- package/skills/incanto-gameplay-behaviors.md +23 -1
- package/skills/incanto-node-reference.md +54 -3
- package/templates-app/beacon-isle-3d/PROJECT/Context.md +44 -0
- package/templates-app/beacon-isle-3d/PROJECT/Requirements.md +16 -0
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +38 -0
- package/templates-app/beacon-isle-3d/PROJECT/Structure.md +24 -0
- package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +38 -0
- package/templates-app/beacon-isle-3d/generate-world.ts +812 -0
- package/templates-app/beacon-isle-3d/index.html +68 -0
- package/templates-app/beacon-isle-3d/package.json +26 -0
- package/templates-app/beacon-isle-3d/src/behaviors.ts +570 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +2969 -0
- package/templates-app/beacon-isle-3d/src/main.ts +33 -0
- package/templates-app/beacon-isle-3d/tsconfig.json +13 -0
- package/templates-app/beacon-isle-3d/verify.ts +285 -0
- package/templates-app/beacon-isle-3d/vite.config.ts +5 -0
- package/templates-app/tps-3d/PROJECT/Context.md +49 -0
- package/templates-app/tps-3d/PROJECT/Requirements.md +38 -0
- package/templates-app/tps-3d/PROJECT/Status.md +41 -0
- package/templates-app/tps-3d/PROJECT/Structure.md +44 -0
- package/templates-app/tps-3d/docs/project-3d-rules.md +38 -0
- package/templates-app/tps-3d/index.html +244 -0
- package/templates-app/tps-3d/package.json +25 -0
- package/templates-app/tps-3d/src/behaviors.ts +405 -0
- package/templates-app/tps-3d/src/game.scene.json +677 -0
- package/templates-app/tps-3d/src/main.ts +64 -0
- package/templates-app/tps-3d/tsconfig.json +13 -0
- package/templates-app/tps-3d/verify.ts +167 -0
- package/templates-app/tps-3d/vite.config.ts +5 -0
- package/templates-app/village-quest-3d/PROJECT/Context.md +39 -0
- package/templates-app/village-quest-3d/PROJECT/Requirements.md +36 -0
- package/templates-app/village-quest-3d/PROJECT/Status.md +26 -0
- package/templates-app/village-quest-3d/PROJECT/Structure.md +30 -0
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +38 -0
- package/templates-app/village-quest-3d/index.html +68 -0
- package/templates-app/village-quest-3d/package.json +25 -0
- package/templates-app/village-quest-3d/src/behaviors.ts +529 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +826 -0
- package/templates-app/village-quest-3d/src/main.ts +35 -0
- package/templates-app/village-quest-3d/src/quest.ts +18 -0
- package/templates-app/village-quest-3d/src/village.scene.json +680 -0
- package/templates-app/village-quest-3d/tsconfig.json +13 -0
- package/templates-app/village-quest-3d/verify.ts +254 -0
- package/templates-app/village-quest-3d/vite.config.ts +5 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { f as Node } from "./loader-
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
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:
|
|
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
|
-
/**
|
|
2977
|
-
*
|
|
2978
|
-
|
|
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
|
|
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 (
|
|
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$
|
|
3114
|
-
matrixScratch$2.compose(positionScratch$2, quaternionScratch$2, scaleScratch$
|
|
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$
|
|
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:
|
|
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
|
-
/**
|
|
4398
|
-
*
|
|
4399
|
-
|
|
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
|
|
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
|
-
|
|
4498
|
-
|
|
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 (
|
|
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$
|
|
4676
|
-
mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$
|
|
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$
|
|
4715
|
-
mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$
|
|
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$
|
|
4768
|
-
mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4855
|
-
matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$
|
|
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$
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5856
|
-
if (
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
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:
|
|
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
|
-
/**
|
|
8061
|
-
*
|
|
8062
|
-
*
|
|
8063
|
-
*
|
|
8064
|
-
|
|
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
|
|
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 (
|
|
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 {
|
|
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 };
|