@jgengine/shell 0.19.0 → 0.21.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/CHANGELOG.md +346 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +12 -5
- package/dist/defineGame.js +24 -9
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +3 -0
- package/dist/render/modelLoad.js +9 -0
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +21 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +209 -61
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +204 -67
- package/skills/jgengine-ui/capabilities.md +115 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +200 -57
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +152 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { AuthoredSurfaceMaterial, authoredSurfaceKey } from "./authoredSurfaceMaterial.js";
|
|
5
|
+
import { ModelFallbackBoundary } from "./SceneModels.js";
|
|
6
|
+
import { useDisposable } from "./useDisposable.js";
|
|
7
|
+
/** Plan compatible material/shape/shadow groups without changing authored transforms. @internal */
|
|
8
|
+
export function groupStaticShapeInstances(instances) {
|
|
9
|
+
const groups = new Map();
|
|
10
|
+
for (const instance of instances) {
|
|
11
|
+
if (![...instance.position, ...instance.scale, ...(instance.rotation ?? [])].every(Number.isFinite) || instance.scale.some(value => value <= 0))
|
|
12
|
+
throw new Error("Static shape transforms must be finite with positive scale");
|
|
13
|
+
if ([instance.surface.displacementScale, instance.surface.displacementBias].some(value => value !== undefined && !Number.isFinite(value)))
|
|
14
|
+
throw new Error("Static shape displacement must be finite");
|
|
15
|
+
const radialSegments = instance.shape === "cylinder" ? instance.radialSegments ?? 16 : 0;
|
|
16
|
+
if (instance.shape === "cylinder" && (!Number.isInteger(radialSegments) || radialSegments < 3))
|
|
17
|
+
throw new Error("Cylinder radialSegments must be an integer >= 3");
|
|
18
|
+
const shadows = instance.shadows ?? "both";
|
|
19
|
+
const key = JSON.stringify([instance.shape, radialSegments, shadows, authoredSurfaceKey(instance.surface)]);
|
|
20
|
+
let group = groups.get(key);
|
|
21
|
+
if (group === undefined) {
|
|
22
|
+
group = { key, shape: instance.shape, radialSegments, surface: instance.surface, shadows, instances: [] };
|
|
23
|
+
groups.set(key, group);
|
|
24
|
+
}
|
|
25
|
+
group.instances.push(instance);
|
|
26
|
+
}
|
|
27
|
+
return [...groups.values()];
|
|
28
|
+
}
|
|
29
|
+
/** Upload static transforms and refresh conservative aggregate bounds after a prop change. @internal */
|
|
30
|
+
export function writeStaticShapeInstances(mesh, instances) {
|
|
31
|
+
const transform = new THREE.Object3D();
|
|
32
|
+
instances.forEach((instance, index) => {
|
|
33
|
+
transform.position.set(...instance.position);
|
|
34
|
+
transform.rotation.set(...(instance.rotation ?? [0, 0, 0]));
|
|
35
|
+
transform.scale.set(...instance.scale);
|
|
36
|
+
transform.updateMatrix();
|
|
37
|
+
mesh.setMatrixAt(index, transform.matrix);
|
|
38
|
+
});
|
|
39
|
+
mesh.count = instances.length;
|
|
40
|
+
if (mesh.geometry.boundingBox === null)
|
|
41
|
+
mesh.geometry.computeBoundingBox();
|
|
42
|
+
const heightSurface = instances[0]?.surface;
|
|
43
|
+
const displacedBounds = mesh.geometry.boundingBox.clone();
|
|
44
|
+
if (heightSurface?.maps?.height !== undefined) {
|
|
45
|
+
const bias = heightSurface.displacementBias ?? 0;
|
|
46
|
+
const scale = heightSurface.displacementScale ?? 1;
|
|
47
|
+
const displacement = Math.max(Math.abs(bias), Math.abs(bias + scale));
|
|
48
|
+
displacedBounds.expandByScalar(displacement + Number.EPSILON * Math.max(1, displacement) * 8);
|
|
49
|
+
}
|
|
50
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
51
|
+
mesh.boundingBox = new THREE.Box3();
|
|
52
|
+
const matrix = new THREE.Matrix4(), bounds = new THREE.Box3();
|
|
53
|
+
for (let index = 0; index < instances.length; index++) {
|
|
54
|
+
mesh.getMatrixAt(index, matrix);
|
|
55
|
+
mesh.boundingBox.union(bounds.copy(displacedBounds).applyMatrix4(matrix));
|
|
56
|
+
}
|
|
57
|
+
mesh.boundingSphere = mesh.boundingBox.getBoundingSphere(new THREE.Sphere());
|
|
58
|
+
// Three scales spheres by the longest matrix column. sqrt(3) also encloses affine parent shear.
|
|
59
|
+
mesh.boundingSphere.radius *= Math.sqrt(3);
|
|
60
|
+
}
|
|
61
|
+
function ShapeBatch({ batch }) {
|
|
62
|
+
const ref = useRef(null);
|
|
63
|
+
const geometry = useDisposable(() => batch.shape === "box" ? new THREE.BoxGeometry(1, 1, 1) : new THREE.CylinderGeometry(1, 1, 1, batch.radialSegments), [batch.shape, batch.radialSegments]);
|
|
64
|
+
useLayoutEffect(() => {
|
|
65
|
+
if (ref.current !== null)
|
|
66
|
+
writeStaticShapeInstances(ref.current, batch.instances);
|
|
67
|
+
}, [batch.instances]);
|
|
68
|
+
const fallback = _jsx(AuthoredSurfaceMaterial, { surface: { ...batch.surface, maps: undefined }, shape: batch.shape });
|
|
69
|
+
return _jsx("instancedMesh", { ref: ref, args: [geometry, undefined, batch.instances.length], castShadow: batch.shadows === "both" || batch.shadows === "cast", receiveShadow: batch.shadows === "both" || batch.shadows === "receive", children: _jsx(ModelFallbackBoundary, { fallback: fallback, url: Object.values(batch.surface.maps ?? {}).join(","), seam: "object", children: _jsx(Suspense, { fallback: fallback, children: _jsx(AuthoredSurfaceMaterial, { surface: batch.surface, shape: batch.shape }) }) }) });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
|
|
73
|
+
* @capability static-shape-instances render authored primitive transforms as static material-compatible instance batches
|
|
74
|
+
*/
|
|
75
|
+
export function StaticShapeInstances({ instances }) {
|
|
76
|
+
const batches = useMemo(() => groupStaticShapeInstances(instances), [instances]);
|
|
77
|
+
return _jsx(_Fragment, { children: batches.map(batch => _jsx(ShapeBatch, { batch: batch }, batch.key)) });
|
|
78
|
+
}
|
package/dist/render/useFootIk.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
|
-
import { useMemo, useRef } from "react";
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { inferLegChains, placeFeet } from "@jgengine/core/anim/footPlacement";
|
|
5
5
|
import { lookAt, solveTwoBone } from "@jgengine/core/anim/ikSolver";
|
|
6
|
+
import { resolveRigNode } from "./rigNode.js";
|
|
6
7
|
const DEFAULT_MAX_ADJUST = 0.4;
|
|
7
8
|
const UP = new THREE.Vector3(0, 1, 0);
|
|
8
9
|
const scratch = {
|
|
@@ -45,12 +46,45 @@ function restAnkleRatio(scene, leg) {
|
|
|
45
46
|
});
|
|
46
47
|
return ratio;
|
|
47
48
|
}
|
|
49
|
+
function isAncestor(ancestor, child) {
|
|
50
|
+
for (let parent = child.parent; parent !== null; parent = parent.parent) {
|
|
51
|
+
if (parent === ancestor)
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
48
56
|
/**
|
|
49
57
|
* Resolves foot-IK bones on a loaded rig: explicit chains, or legs found by bone name for `"auto"`
|
|
50
58
|
* and configs without `feet`. Returns `null` when no leg resolves.
|
|
51
59
|
*/
|
|
52
60
|
export function resolveFootIkRig(scene, config) {
|
|
53
61
|
const options = config === "auto" ? {} : config;
|
|
62
|
+
const named = new Map();
|
|
63
|
+
scene.traverse((object) => {
|
|
64
|
+
if (object.name.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const matches = named.get(object.name);
|
|
67
|
+
if (matches === undefined)
|
|
68
|
+
named.set(object.name, [object]);
|
|
69
|
+
else
|
|
70
|
+
matches.push(object);
|
|
71
|
+
});
|
|
72
|
+
const available = [...named.keys()].filter((name) => named.get(name).some((object) => object.isBone === true)).slice(0, 12).join(", ");
|
|
73
|
+
const warn = (message) => console.warn(`[jgengine foot IK] ${message}${available.length > 0 ? ` Available bones include: ${available}.` : ""}`);
|
|
74
|
+
const resolve = (name, role) => {
|
|
75
|
+
const matches = named.get(name);
|
|
76
|
+
if (matches?.length === 1)
|
|
77
|
+
return matches[0];
|
|
78
|
+
if (matches === undefined) {
|
|
79
|
+
const imported = resolveRigNode(scene, name);
|
|
80
|
+
if (imported.node !== undefined)
|
|
81
|
+
return imported.node;
|
|
82
|
+
warn(`${role} "${name}" ${imported.diagnostic.code === "ambiguous-node" ? "matches multiple original imported names; use a unique runtime name or repair duplicate imported names" : "was not found by runtime or original imported name; check the loaded rig names"}.`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
warn(`${role} "${name}" matches multiple objects; use a unique rig name.`);
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
54
88
|
let chains = options.feet;
|
|
55
89
|
if (chains === undefined) {
|
|
56
90
|
const bones = [];
|
|
@@ -59,20 +93,37 @@ export function resolveFootIkRig(scene, config) {
|
|
|
59
93
|
bones.push({ name: object.name, parent: object.parent?.isBone === true ? object.parent.name : null });
|
|
60
94
|
});
|
|
61
95
|
chains = inferLegChains(bones);
|
|
96
|
+
if (chains.length === 0 && options.lookAt === undefined)
|
|
97
|
+
warn("No thigh → shin → foot chain was recognized. Configure feet with the rig's bone names, or disable IK for this model.");
|
|
62
98
|
}
|
|
63
99
|
const legs = [];
|
|
100
|
+
const used = new Set();
|
|
64
101
|
for (const chain of chains) {
|
|
65
|
-
const root =
|
|
66
|
-
const mid =
|
|
67
|
-
const tip =
|
|
68
|
-
if (root ===
|
|
102
|
+
const root = resolve(chain.root, "Foot-chain root");
|
|
103
|
+
const mid = resolve(chain.mid, "Foot-chain middle");
|
|
104
|
+
const tip = resolve(chain.tip, "Foot-chain tip");
|
|
105
|
+
if (root === null || mid === null || tip === null)
|
|
106
|
+
continue;
|
|
107
|
+
if (!isAncestor(root, mid) || !isAncestor(mid, tip)) {
|
|
108
|
+
warn(`Skipped chain "${chain.root}" → "${chain.mid}" → "${chain.tip}": each joint must descend from the previous joint.`);
|
|
69
109
|
continue;
|
|
110
|
+
}
|
|
111
|
+
if ([root, mid, tip].some((bone) => used.has(bone))) {
|
|
112
|
+
warn(`Skipped chain "${chain.root}" → "${chain.mid}" → "${chain.tip}": a joint is already corrected by another foot chain.`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const bone of [root, mid, tip])
|
|
116
|
+
used.add(bone);
|
|
70
117
|
legs.push({ root, mid, tip, ankleRatio: restAnkleRatio(scene, { root, mid, tip }) });
|
|
71
118
|
}
|
|
72
|
-
const head = options.lookAt === undefined ? null : (
|
|
119
|
+
const head = options.lookAt === undefined ? null : resolve(options.lookAt.bone, "Look-at bone");
|
|
73
120
|
if (legs.length === 0 && head === null)
|
|
74
121
|
return null;
|
|
75
|
-
|
|
122
|
+
let found = options.pelvis !== undefined ? resolve(options.pelvis, "Pelvis") : (legs[0]?.root.parent ?? null);
|
|
123
|
+
if (options.pelvis === undefined) {
|
|
124
|
+
while (found !== null && !legs.every((leg) => isAncestor(found, leg.root)))
|
|
125
|
+
found = found.parent;
|
|
126
|
+
}
|
|
76
127
|
const pelvis = found === scene ? null : found;
|
|
77
128
|
const bones = new Set(legs.flatMap((leg) => [leg.root, leg.mid, leg.tip]));
|
|
78
129
|
if (pelvis !== null)
|
|
@@ -113,6 +164,14 @@ function legLength(leg) {
|
|
|
113
164
|
leg.tip.getWorldPosition(scratch.tip);
|
|
114
165
|
return scratch.root.distanceTo(scratch.mid) + scratch.mid.distanceTo(scratch.tip);
|
|
115
166
|
}
|
|
167
|
+
function restoreFootIkPose(rig) {
|
|
168
|
+
for (const record of rig.poses) {
|
|
169
|
+
if (record.bone.quaternion.equals(record.written.q))
|
|
170
|
+
record.bone.quaternion.copy(record.animated.q);
|
|
171
|
+
if (record.bone.position.equals(record.written.p))
|
|
172
|
+
record.bone.position.copy(record.animated.p);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
116
175
|
/**
|
|
117
176
|
* Applies one frame of foot IK after the animation mixer: probes the ground under each foot,
|
|
118
177
|
* resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending
|
|
@@ -120,11 +179,8 @@ function legLength(leg) {
|
|
|
120
179
|
* grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
|
|
121
180
|
*/
|
|
122
181
|
export function applyFootIk(rig, originY, probe, state, delta, cameraTarget) {
|
|
182
|
+
restoreFootIkPose(rig);
|
|
123
183
|
for (const record of rig.poses) {
|
|
124
|
-
if (record.bone.quaternion.equals(record.written.q))
|
|
125
|
-
record.bone.quaternion.copy(record.animated.q);
|
|
126
|
-
if (record.bone.position.equals(record.written.p))
|
|
127
|
-
record.bone.position.copy(record.animated.p);
|
|
128
184
|
record.animated.q.copy(record.bone.quaternion);
|
|
129
185
|
record.animated.p.copy(record.bone.position);
|
|
130
186
|
}
|
|
@@ -200,6 +256,15 @@ export function useFootIk(scene, config, ctx, instanceId, groundOffset = 0) {
|
|
|
200
256
|
const camera = useThree((three) => three.camera);
|
|
201
257
|
const rig = useMemo(() => (config === undefined ? null : resolveFootIkRig(scene, config)), [scene, config]);
|
|
202
258
|
const state = useRef({ weight: 0, pelvis: 0 });
|
|
259
|
+
useLayoutEffect(() => {
|
|
260
|
+
state.current = { weight: 0, pelvis: 0 };
|
|
261
|
+
return () => {
|
|
262
|
+
if (rig !== null) {
|
|
263
|
+
restoreFootIkPose(rig);
|
|
264
|
+
scene.updateMatrixWorld(true);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}, [rig, scene]);
|
|
203
268
|
useFrame((_three, delta) => {
|
|
204
269
|
if (rig === null || ctx === null || instanceId === undefined)
|
|
205
270
|
return;
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import type { ModelAnimationConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
-
import { type AnimClipOutput, type AnimGraph } from "@jgengine/core/anim/animGraph";
|
|
3
|
+
import { type AnimClipOutput, type AnimGraph, type AnimParamValue } from "@jgengine/core/anim/animGraph";
|
|
4
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContextTypes";
|
|
5
|
+
/** A repairable mismatch between a model animation config and its imported rig. */
|
|
6
|
+
export interface ModelAnimationDiagnostic {
|
|
7
|
+
code: "missing-clip" | "missing-root-track" | "empty-layer-mask" | "incomplete-locomotion" | "missing-auto-idle";
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks clip mappings, masked layers and root-motion assumptions without starting a mixer.
|
|
12
|
+
*
|
|
13
|
+
* @capability model-animation-diagnostics inspect imported clip mappings, layer masks and root-track compatibility
|
|
14
|
+
*/
|
|
15
|
+
export declare function diagnoseModelAnimation(scene: THREE.Object3D, animation: ModelAnimationConfig, clips: readonly THREE.AnimationClip[]): readonly ModelAnimationDiagnostic[];
|
|
16
|
+
/**
|
|
17
|
+
* Refreshes a caller-owned graph parameter dictionary from authored extras and known physical movement.
|
|
18
|
+
* Grounded, verticalSpeed and crouched use the shared motor's resolved state when available; custom movers
|
|
19
|
+
* retain their authored values. Speed always uses the supplied measured ground speed. Does not write game state.
|
|
20
|
+
*
|
|
21
|
+
* @capability animation-motion-params read resolved player movement into reusable animation graph parameters
|
|
22
|
+
*/
|
|
23
|
+
export declare function readModelAnimationParams(ctx: GameContext, instanceId: string, speed: number, out: Record<string, AnimParamValue>): Record<string, AnimParamValue>;
|
|
4
24
|
/**
|
|
5
25
|
* For a frame where a `rootMotion` state is current: pins the root bone's horizontal translation to
|
|
6
26
|
* its bind pose, so the clip plays in place, and returns that step's root travel as a world-space
|
|
@@ -13,9 +33,13 @@ export interface GraphPose {
|
|
|
13
33
|
/** Clip durations read from the rig, the `clips` argument `runtime.advance` expects. */
|
|
14
34
|
durations: Readonly<Record<string, {
|
|
15
35
|
duration: number;
|
|
36
|
+
rootTrack?: {
|
|
37
|
+
times: Float32Array;
|
|
38
|
+
values: Float32Array;
|
|
39
|
+
};
|
|
16
40
|
}>>;
|
|
17
41
|
/** Poses the rig with one advance's clip weights and times. */
|
|
18
|
-
apply(clips: readonly AnimClipOutput[]): void;
|
|
42
|
+
apply(clips: readonly AnimClipOutput[], rootMotion?: boolean): void;
|
|
19
43
|
dispose(): void;
|
|
20
44
|
}
|
|
21
45
|
/**
|
|
@@ -32,5 +56,7 @@ export declare function createGraphPose(scene: THREE.Object3D, graph: AnimGraph,
|
|
|
32
56
|
* `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
|
|
33
57
|
* With `animation.graph` set, the headless `AnimGraph` runtime owns every clip's time and weight
|
|
34
58
|
* and the mixer only applies them; clip events surface as `animation.event`.
|
|
59
|
+
* Equivalent same-order config data retains playback across rerenders; actual data edits reset it.
|
|
60
|
+
* Entity-bound playback holds while context game time is frozen; unbound previews keep playing.
|
|
35
61
|
*/
|
|
36
62
|
export declare function useModelAnimation(scene: THREE.Object3D, clips: THREE.AnimationClip[], animationInput: ModelAnimationConfig | "auto" | "none" | undefined, instanceId?: string): void;
|