@jgengine/shell 0.18.1 → 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 +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- 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/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- 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 +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- 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/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- 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 +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- 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/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -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 +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -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 +32 -0
- package/dist/render/modelLoad.js +70 -1
- 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 +25 -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.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- 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 +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- 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 +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- 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/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -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.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- 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 +210 -62
- 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 +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
package/dist/render/useFootIk.js
CHANGED
|
@@ -1,89 +1,281 @@
|
|
|
1
1
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
|
-
import { useRef } from "react";
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
|
+
import { inferLegChains, placeFeet } from "@jgengine/core/anim/footPlacement";
|
|
4
5
|
import { lookAt, solveTwoBone } from "@jgengine/core/anim/ikSolver";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import { resolveRigNode } from "./rigNode.js";
|
|
7
|
+
const DEFAULT_MAX_ADJUST = 0.4;
|
|
8
|
+
const UP = new THREE.Vector3(0, 1, 0);
|
|
9
|
+
const scratch = {
|
|
10
|
+
root: new THREE.Vector3(),
|
|
11
|
+
mid: new THREE.Vector3(),
|
|
12
|
+
tip: new THREE.Vector3(),
|
|
13
|
+
a: new THREE.Vector3(),
|
|
14
|
+
b: new THREE.Vector3(),
|
|
15
|
+
q: new THREE.Quaternion(),
|
|
16
|
+
q2: new THREE.Quaternion(),
|
|
17
|
+
parent: new THREE.Quaternion(),
|
|
18
|
+
m: new THREE.Matrix4(),
|
|
19
|
+
};
|
|
20
|
+
const solved = { mid: [0, 0, 0], tip: [0, 0, 0] };
|
|
21
|
+
function bindPosition(skeleton, bone, out) {
|
|
22
|
+
const index = skeleton.bones.indexOf(bone);
|
|
23
|
+
if (index < 0)
|
|
24
|
+
return null;
|
|
25
|
+
return out.setFromMatrixPosition(scratch.m.copy(skeleton.boneInverses[index]).invert());
|
|
7
26
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
27
|
+
function restAnkleRatio(scene, leg) {
|
|
28
|
+
let soleY = Infinity;
|
|
29
|
+
let ratio = 0;
|
|
30
|
+
const root = new THREE.Vector3();
|
|
31
|
+
const mid = new THREE.Vector3();
|
|
32
|
+
const tip = new THREE.Vector3();
|
|
33
|
+
scene.traverse((object) => {
|
|
34
|
+
const mesh = object;
|
|
35
|
+
if (mesh.isSkinnedMesh !== true || !(leg.tip instanceof THREE.Bone) || !(leg.mid instanceof THREE.Bone) || !(leg.root instanceof THREE.Bone))
|
|
36
|
+
return;
|
|
37
|
+
if (bindPosition(mesh.skeleton, leg.tip, tip) === null || bindPosition(mesh.skeleton, leg.mid, mid) === null || bindPosition(mesh.skeleton, leg.root, root) === null)
|
|
38
|
+
return;
|
|
39
|
+
if (mesh.geometry.boundingBox === null)
|
|
40
|
+
mesh.geometry.computeBoundingBox();
|
|
41
|
+
const box = mesh.geometry.boundingBox.clone().applyMatrix4(mesh.bindMatrix);
|
|
42
|
+
soleY = Math.min(soleY, box.min.y);
|
|
43
|
+
const length = root.distanceTo(mid) + mid.distanceTo(tip);
|
|
44
|
+
if (length > 1e-6)
|
|
45
|
+
ratio = Math.max(0, tip.y - soleY) / length;
|
|
46
|
+
});
|
|
47
|
+
return ratio;
|
|
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
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves foot-IK bones on a loaded rig: explicit chains, or legs found by bone name for `"auto"`
|
|
58
|
+
* and configs without `feet`. Returns `null` when no leg resolves.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveFootIkRig(scene, config) {
|
|
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
|
+
};
|
|
88
|
+
let chains = options.feet;
|
|
89
|
+
if (chains === undefined) {
|
|
90
|
+
const bones = [];
|
|
91
|
+
scene.traverse((object) => {
|
|
92
|
+
if (object.isBone === true)
|
|
93
|
+
bones.push({ name: object.name, parent: object.parent?.isBone === true ? object.parent.name : null });
|
|
94
|
+
});
|
|
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.");
|
|
98
|
+
}
|
|
99
|
+
const legs = [];
|
|
100
|
+
const used = new Set();
|
|
101
|
+
for (const chain of chains) {
|
|
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.`);
|
|
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);
|
|
117
|
+
legs.push({ root, mid, tip, ankleRatio: restAnkleRatio(scene, { root, mid, tip }) });
|
|
118
|
+
}
|
|
119
|
+
const head = options.lookAt === undefined ? null : resolve(options.lookAt.bone, "Look-at bone");
|
|
120
|
+
if (legs.length === 0 && head === null)
|
|
121
|
+
return null;
|
|
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
|
+
}
|
|
127
|
+
const pelvis = found === scene ? null : found;
|
|
128
|
+
const bones = new Set(legs.flatMap((leg) => [leg.root, leg.mid, leg.tip]));
|
|
129
|
+
if (pelvis !== null)
|
|
130
|
+
bones.add(pelvis);
|
|
131
|
+
if (head !== null)
|
|
132
|
+
bones.add(head);
|
|
133
|
+
return {
|
|
134
|
+
legs,
|
|
135
|
+
pelvis,
|
|
136
|
+
head,
|
|
137
|
+
maxAdjust: options.maxAdjust ?? DEFAULT_MAX_ADJUST,
|
|
138
|
+
alignToGround: Math.max(0, Math.min(1, options.alignToGround ?? 1)),
|
|
139
|
+
poses: [...bones].map((bone) => ({
|
|
140
|
+
bone,
|
|
141
|
+
animated: { q: bone.quaternion.clone(), p: bone.position.clone() },
|
|
142
|
+
written: { q: bone.quaternion.clone(), p: bone.position.clone() },
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
12
145
|
}
|
|
13
146
|
function writeWorldQuaternion(bone, world) {
|
|
14
147
|
if (bone.parent === null)
|
|
15
148
|
bone.quaternion.copy(world);
|
|
16
|
-
else
|
|
17
|
-
|
|
18
|
-
bone.quaternion.copy(parent.multiply(world));
|
|
19
|
-
}
|
|
149
|
+
else
|
|
150
|
+
bone.quaternion.copy(bone.parent.getWorldQuaternion(scratch.parent).invert().multiply(world));
|
|
20
151
|
bone.updateMatrixWorld(true);
|
|
21
152
|
}
|
|
22
|
-
function aimSegment(bone, from, currentTo, solvedTo
|
|
23
|
-
const current =
|
|
24
|
-
const
|
|
25
|
-
if (current.lengthSq() < 1e-
|
|
153
|
+
function aimSegment(bone, from, currentTo, solvedTo) {
|
|
154
|
+
const current = scratch.a.copy(currentTo).sub(from);
|
|
155
|
+
const desired = scratch.b.set(solvedTo[0] - from.x, solvedTo[1] - from.y, solvedTo[2] - from.z);
|
|
156
|
+
if (current.lengthSq() < 1e-10 || desired.lengthSq() < 1e-10)
|
|
26
157
|
return;
|
|
27
|
-
const correction =
|
|
28
|
-
|
|
29
|
-
const desiredWorld = correction.multiply(currentWorld);
|
|
30
|
-
currentWorld.slerp(desiredWorld, weight);
|
|
31
|
-
writeWorldQuaternion(bone, currentWorld);
|
|
158
|
+
const correction = scratch.q.setFromUnitVectors(current.normalize(), desired.normalize());
|
|
159
|
+
writeWorldQuaternion(bone, correction.multiply(bone.getWorldQuaternion(scratch.q2)));
|
|
32
160
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const tipPosition = tip.getWorldPosition(new THREE.Vector3());
|
|
46
|
-
const hit = raycast({
|
|
47
|
-
origin: [tipPosition.x, tipPosition.y + 0.75, tipPosition.z],
|
|
48
|
-
direction: [0, -1, 0],
|
|
49
|
-
maxDistance: 2,
|
|
50
|
-
filter: { entities: false, objects: false, walls: false, terrain: true },
|
|
51
|
-
});
|
|
52
|
-
if (hit === null)
|
|
53
|
-
continue;
|
|
54
|
-
grounded = true;
|
|
55
|
-
const target = new THREE.Vector3(hit.point[0], hit.point[1], hit.point[2]);
|
|
56
|
-
const output = { mid: [0, 0, 0], tip: [0, 0, 0] };
|
|
57
|
-
solveTwoBone({ root: tuple(rootPosition), mid: tuple(midPosition), tip: tuple(tipPosition), target: tuple(target), pole: [rootPosition.x, rootPosition.y, rootPosition.z + 1] }, output);
|
|
58
|
-
aimSegment(root, rootPosition, midPosition, output.mid, weight);
|
|
59
|
-
root.updateMatrixWorld(true);
|
|
60
|
-
const solvedMid = mid.getWorldPosition(new THREE.Vector3());
|
|
61
|
-
// Rotating the root also moves the tip; aim the shin from that updated pose.
|
|
62
|
-
const currentTip = tip.getWorldPosition(new THREE.Vector3());
|
|
63
|
-
aimSegment(mid, solvedMid, currentTip, output.tip, weight);
|
|
161
|
+
function legLength(leg) {
|
|
162
|
+
leg.root.getWorldPosition(scratch.root);
|
|
163
|
+
leg.mid.getWorldPosition(scratch.mid);
|
|
164
|
+
leg.tip.getWorldPosition(scratch.tip);
|
|
165
|
+
return scratch.root.distanceTo(scratch.mid) + scratch.mid.distanceTo(scratch.tip);
|
|
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);
|
|
64
173
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Applies one frame of foot IK after the animation mixer: probes the ground under each foot,
|
|
177
|
+
* resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending
|
|
178
|
+
* toward the animated knee, and tilts planted feet to the ground. Returns whether the feet are
|
|
179
|
+
* grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
|
|
180
|
+
*/
|
|
181
|
+
export function applyFootIk(rig, originY, probe, state, delta, cameraTarget) {
|
|
182
|
+
restoreFootIkPose(rig);
|
|
183
|
+
for (const record of rig.poses) {
|
|
184
|
+
record.animated.q.copy(record.bone.quaternion);
|
|
185
|
+
record.animated.p.copy(record.bone.position);
|
|
186
|
+
}
|
|
187
|
+
const samples = [];
|
|
188
|
+
const normals = [];
|
|
189
|
+
let length = 0;
|
|
190
|
+
let ankleHeight = 0;
|
|
191
|
+
for (const leg of rig.legs) {
|
|
192
|
+
leg.root.updateWorldMatrix(true, true);
|
|
193
|
+
const legSize = legLength(leg);
|
|
194
|
+
length = Math.max(length, legSize);
|
|
195
|
+
ankleHeight = Math.max(ankleHeight, leg.ankleRatio * legSize);
|
|
196
|
+
const from = Math.max(scratch.tip.y, originY) + legSize * 0.5;
|
|
197
|
+
const hit = probe({ origin: [scratch.tip.x, from, scratch.tip.z], direction: [0, -1, 0], maxDistance: legSize * 2 });
|
|
198
|
+
samples.push({ ankleY: scratch.tip.y, groundY: hit === null ? null : hit.point[1] });
|
|
199
|
+
normals.push(hit === null ? null : hit.normal);
|
|
200
|
+
}
|
|
201
|
+
const placement = placeFeet({ feet: samples, originY, ankleHeight, maxAdjust: rig.maxAdjust * length, airborneGap: length * 0.3 });
|
|
202
|
+
const blend = Math.min(1, delta * (placement.grounded ? 12 : 6));
|
|
203
|
+
state.weight += ((placement.grounded ? 1 : 0) - state.weight) * blend;
|
|
204
|
+
state.pelvis += (placement.pelvisOffset - state.pelvis) * Math.min(1, delta * 15);
|
|
205
|
+
const weight = state.weight;
|
|
206
|
+
if (rig.pelvis !== null && weight > 1e-3 && Math.abs(state.pelvis) > 1e-6) {
|
|
207
|
+
const world = rig.pelvis.getWorldPosition(scratch.a);
|
|
208
|
+
world.y += state.pelvis * weight;
|
|
209
|
+
rig.pelvis.position.copy(rig.pelvis.parent === null ? world : rig.pelvis.parent.worldToLocal(world));
|
|
210
|
+
rig.pelvis.updateMatrixWorld(true);
|
|
211
|
+
}
|
|
212
|
+
for (let i = 0; i < rig.legs.length && weight > 1e-3; i += 1) {
|
|
213
|
+
const target = placement.ankleTargets[i];
|
|
214
|
+
if (target === null || target === undefined)
|
|
215
|
+
continue;
|
|
216
|
+
const leg = rig.legs[i];
|
|
217
|
+
const root = leg.root.getWorldPosition(scratch.root);
|
|
218
|
+
const mid = leg.mid.getWorldPosition(scratch.mid);
|
|
219
|
+
const tip = leg.tip.getWorldPosition(scratch.tip);
|
|
220
|
+
const bend = scratch.a.copy(root).add(tip).multiplyScalar(0.5);
|
|
221
|
+
bend.subVectors(mid, bend);
|
|
222
|
+
if (bend.lengthSq() < 1e-8)
|
|
223
|
+
bend.set(0, 0, 1).applyQuaternion(leg.root.parent?.getWorldQuaternion(scratch.q) ?? scratch.q.identity());
|
|
224
|
+
const pole = bend.normalize().multiplyScalar(length).add(mid);
|
|
225
|
+
const targetY = tip.y + (target - tip.y) * weight;
|
|
226
|
+
solveTwoBone({ root: [root.x, root.y, root.z], mid: [mid.x, mid.y, mid.z], tip: [tip.x, tip.y, tip.z], target: [tip.x, targetY, tip.z], pole: [pole.x, pole.y, pole.z] }, solved);
|
|
227
|
+
aimSegment(leg.root, root, mid, solved.mid);
|
|
228
|
+
// Rotating the thigh also moved the tip; aim the shin from that updated pose.
|
|
229
|
+
aimSegment(leg.mid, leg.mid.getWorldPosition(scratch.mid), leg.tip.getWorldPosition(scratch.tip), solved.tip);
|
|
230
|
+
const normal = normals[i];
|
|
231
|
+
if (normal !== null && normal !== undefined && rig.alignToGround > 0) {
|
|
232
|
+
const planted = 1 - Math.min(1, Math.max(0, samples[i].ankleY - ankleHeight - samples[i].groundY) / Math.max(ankleHeight, length * 0.05));
|
|
233
|
+
const tilt = scratch.q.setFromUnitVectors(UP, scratch.b.set(normal[0], normal[1], normal[2]).normalize());
|
|
234
|
+
const current = leg.tip.getWorldQuaternion(scratch.q2);
|
|
235
|
+
writeWorldQuaternion(leg.tip, current.slerp(tilt.multiply(current), planted * weight * rig.alignToGround));
|
|
74
236
|
}
|
|
75
237
|
}
|
|
76
|
-
|
|
238
|
+
if (rig.head !== null && cameraTarget !== undefined) {
|
|
239
|
+
const from = rig.head.getWorldPosition(scratch.a);
|
|
240
|
+
const q = lookAt({ from: [from.x, from.y, from.z], target: cameraTarget, up: [0, 1, 0] });
|
|
241
|
+
const current = rig.head.getWorldQuaternion(scratch.q2);
|
|
242
|
+
writeWorldQuaternion(rig.head, current.slerp(scratch.q.set(q[0], q[1], q[2], q[3]), Math.max(weight, rig.legs.length === 0 ? 1 : 0)));
|
|
243
|
+
}
|
|
244
|
+
for (const record of rig.poses) {
|
|
245
|
+
record.written.q.copy(record.bone.quaternion);
|
|
246
|
+
record.written.p.copy(record.bone.position);
|
|
247
|
+
}
|
|
248
|
+
return placement.grounded;
|
|
77
249
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Runs foot IK after the model's animation mixer for `ModelConfig.ik`. Ground probes hit terrain and
|
|
252
|
+
* blocking physical objects, never the model's own entity. `groundOffset` is the model-space height
|
|
253
|
+
* its soles rest on (`ModelConfig.y`).
|
|
254
|
+
*/
|
|
255
|
+
export function useFootIk(scene, config, ctx, instanceId, groundOffset = 0) {
|
|
256
|
+
const camera = useThree((three) => three.camera);
|
|
257
|
+
const rig = useMemo(() => (config === undefined ? null : resolveFootIkRig(scene, config)), [scene, config]);
|
|
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]);
|
|
268
|
+
useFrame((_three, delta) => {
|
|
269
|
+
if (rig === null || ctx === null || instanceId === undefined)
|
|
84
270
|
return;
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
271
|
+
const parent = scene.parent;
|
|
272
|
+
const originY = parent === null ? scene.position.y : parent.localToWorld(scratch.a.set(0, groundOffset, 0)).y;
|
|
273
|
+
const exclude = [instanceId];
|
|
274
|
+
applyFootIk(rig, originY, (input) => ctx.scene.raycast({
|
|
275
|
+
...input,
|
|
276
|
+
excludeInstanceIds: exclude,
|
|
277
|
+
filter: { entities: false, objects: true, walls: false, terrain: true },
|
|
278
|
+
accept: (hit) => hit.blocks && hit.purpose === "physical",
|
|
279
|
+
}), state.current, delta, rig.head === null ? undefined : camera.getWorldPosition(scratch.b).toArray());
|
|
88
280
|
});
|
|
89
281
|
}
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import type { ModelAnimationConfig } from "@jgengine/core/game/playableGame";
|
|
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>;
|
|
24
|
+
/**
|
|
25
|
+
* For a frame where a `rootMotion` state is current: pins the root bone's horizontal translation to
|
|
26
|
+
* its bind pose, so the clip plays in place, and returns that step's root travel as a world-space
|
|
27
|
+
* horizontal delta (through the rig's parent transform, so the entity's facing and the model's
|
|
28
|
+
* scale apply). The root bone's vertical motion stays in the clip.
|
|
29
|
+
*/
|
|
30
|
+
export declare function takeRootMotion(rootBone: THREE.Object3D, bind: THREE.Vector3, localDelta: readonly [number, number, number] | undefined, out: THREE.Vector3): THREE.Vector3;
|
|
31
|
+
/** A mixer set up to show {@link AnimGraph} output on a rig; see {@link createGraphPose}. */
|
|
32
|
+
export interface GraphPose {
|
|
33
|
+
/** Clip durations read from the rig, the `clips` argument `runtime.advance` expects. */
|
|
34
|
+
durations: Readonly<Record<string, {
|
|
35
|
+
duration: number;
|
|
36
|
+
rootTrack?: {
|
|
37
|
+
times: Float32Array;
|
|
38
|
+
values: Float32Array;
|
|
39
|
+
};
|
|
40
|
+
}>>;
|
|
41
|
+
/** Poses the rig with one advance's clip weights and times. */
|
|
42
|
+
apply(clips: readonly AnimClipOutput[], rootMotion?: boolean): void;
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Binds a graph's clips to a rig exactly as `useModelAnimation` does (masked layers get filtered
|
|
47
|
+
* clips, additive layers additive ones) so a host that runs its own `createAnimGraphRuntime`, such as
|
|
48
|
+
* the editor's graph preview, poses the rig from the runtime's output.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createGraphPose(scene: THREE.Object3D, graph: AnimGraph, clips: THREE.AnimationClip[]): GraphPose;
|
|
3
51
|
/**
|
|
4
52
|
* The engine's model animation driver as a standalone hook — the same mixer `EntityModel` runs,
|
|
5
53
|
* for games that render a cloned scene themselves (custom materials, procedural composition).
|
|
@@ -8,5 +56,7 @@ import type { ModelAnimationConfig } from "@jgengine/core/game/playableGame";
|
|
|
8
56
|
* `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
|
|
9
57
|
* With `animation.graph` set, the headless `AnimGraph` runtime owns every clip's time and weight
|
|
10
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.
|
|
11
61
|
*/
|
|
12
62
|
export declare function useModelAnimation(scene: THREE.Object3D, clips: THREE.AnimationClip[], animationInput: ModelAnimationConfig | "auto" | "none" | undefined, instanceId?: string): void;
|