@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.
Files changed (261) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +132 -17
  5. package/dist/Shell3dPresentation.d.ts +5 -11
  6. package/dist/Shell3dPresentation.js +73 -11
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +20 -2
  10. package/dist/ShellHudPresentation.js +41 -6
  11. package/dist/audio/AudioComponents.js +6 -2
  12. package/dist/audio/audioEngine.d.ts +24 -0
  13. package/dist/audio/audioEngine.js +231 -83
  14. package/dist/audio/audioWire.d.ts +3 -0
  15. package/dist/audio/audioWire.js +9 -3
  16. package/dist/audio/loopParams.d.ts +39 -0
  17. package/dist/audio/loopParams.js +43 -0
  18. package/dist/audio/synthEngine.d.ts +7 -1
  19. package/dist/audio/synthEngine.js +103 -38
  20. package/dist/camera/GameCameraRig.js +3 -2
  21. package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
  22. package/dist/camera/GameFirstPersonCamera.js +68 -27
  23. package/dist/camera/Viewports.d.ts +34 -0
  24. package/dist/camera/Viewports.js +123 -0
  25. package/dist/camera/cameraRigs.js +76 -62
  26. package/dist/camera/chaseHeading.d.ts +9 -0
  27. package/dist/camera/chaseHeading.js +10 -0
  28. package/dist/camera/customMuzzle.d.ts +14 -0
  29. package/dist/camera/customMuzzle.js +68 -0
  30. package/dist/camera/index.d.ts +1 -0
  31. package/dist/camera/index.js +1 -0
  32. package/dist/camera/orbitCameraMath.js +2 -2
  33. package/dist/camera/rigMath.d.ts +82 -4
  34. package/dist/camera/rigMath.js +133 -15
  35. package/dist/camera/rigResolve.d.ts +3 -0
  36. package/dist/camera/rigResolve.js +11 -0
  37. package/dist/combat/ProjectileModels.d.ts +31 -0
  38. package/dist/combat/ProjectileModels.js +70 -0
  39. package/dist/commandSink.d.ts +1 -2
  40. package/dist/commandSink.js +3 -2
  41. package/dist/defineGame.d.ts +19 -3
  42. package/dist/defineGame.js +39 -7
  43. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  44. package/dist/devtools/DevtoolsOverlay.js +15 -167
  45. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  46. package/dist/devtools/DevtoolsRuntime.js +61 -0
  47. package/dist/devtools/agentBridge.d.ts +6 -0
  48. package/dist/devtools/agentBridge.js +16 -2
  49. package/dist/devtools/devtoolsReports.d.ts +68 -0
  50. package/dist/devtools/devtoolsReports.js +100 -0
  51. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  52. package/dist/devtools/latencyInstrumentation.js +11 -0
  53. package/dist/devtools/playtestProbe.d.ts +4 -0
  54. package/dist/devtools/playtestProbe.js +22 -0
  55. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  56. package/dist/diagnostics/FpsCounter.js +32 -0
  57. package/dist/drivers/FrameDriver.js +35 -20
  58. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  59. package/dist/drivers/FrameRateLimiter.js +39 -0
  60. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  61. package/dist/drivers/HudOnlyDriver.js +32 -14
  62. package/dist/environment/DayNightSky.js +5 -5
  63. package/dist/environment/Daylight.d.ts +35 -1
  64. package/dist/environment/Daylight.js +98 -28
  65. package/dist/environment/EnvironmentScene.js +11 -10
  66. package/dist/environment/VolumetricClouds.js +20 -2
  67. package/dist/environment/daylightCycle.d.ts +20 -0
  68. package/dist/environment/daylightCycle.js +59 -7
  69. package/dist/environment/sunShadowMath.d.ts +45 -0
  70. package/dist/environment/sunShadowMath.js +50 -0
  71. package/dist/input/gamepadPoll.d.ts +47 -0
  72. package/dist/input/gamepadPoll.js +142 -0
  73. package/dist/input/gamepadSource.d.ts +11 -1
  74. package/dist/input/gamepadSource.js +117 -71
  75. package/dist/input/mouseLook.js +2 -1
  76. package/dist/input/padHaptics.d.ts +17 -0
  77. package/dist/input/padHaptics.js +32 -0
  78. package/dist/input/pointerLock.d.ts +11 -0
  79. package/dist/input/pointerLock.js +19 -0
  80. package/dist/inputSink.d.ts +6 -6
  81. package/dist/inputSink.js +60 -21
  82. package/dist/lazyShell3dPresentation.d.ts +9 -0
  83. package/dist/lazyShell3dPresentation.js +24 -0
  84. package/dist/materialOverride.d.ts +42 -0
  85. package/dist/materialOverride.js +95 -36
  86. package/dist/multiplayer.d.ts +33 -3
  87. package/dist/multiplayer.js +105 -22
  88. package/dist/pointer/pointerService.d.ts +2 -0
  89. package/dist/pointer/pointerService.js +12 -2
  90. package/dist/postfx/PostProcessing.d.ts +11 -1
  91. package/dist/postfx/PostProcessing.js +14 -42
  92. package/dist/postfx/gradeShader.d.ts +1 -1
  93. package/dist/postfx/gradeShader.js +30 -2
  94. package/dist/postfx/shadowPrepass.d.ts +3 -0
  95. package/dist/postfx/shadowPrepass.js +18 -0
  96. package/dist/postfx/stylizePass.d.ts +23 -0
  97. package/dist/postfx/stylizePass.js +216 -0
  98. package/dist/presentationRecovery.d.ts +75 -0
  99. package/dist/presentationRecovery.js +204 -0
  100. package/dist/presentationRecovery3d.d.ts +4 -0
  101. package/dist/presentationRecovery3d.js +32 -0
  102. package/dist/presentationResolve.d.ts +8 -1
  103. package/dist/presentationResolve.js +5 -2
  104. package/dist/render/CascadedShadows.d.ts +7 -2
  105. package/dist/render/CascadedShadows.js +98 -20
  106. package/dist/render/EnvironmentLighting.js +74 -35
  107. package/dist/render/HairCards.d.ts +7 -0
  108. package/dist/render/HairCards.js +23 -0
  109. package/dist/render/SceneModels.js +52 -78
  110. package/dist/render/SkinnedInstances.d.ts +91 -0
  111. package/dist/render/SkinnedInstances.js +249 -0
  112. package/dist/render/TextureFiltering.d.ts +16 -0
  113. package/dist/render/TextureFiltering.js +64 -0
  114. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  115. package/dist/render/authoredSurfaceMaterial.js +108 -0
  116. package/dist/render/displayColor.d.ts +33 -0
  117. package/dist/render/displayColor.js +138 -0
  118. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  119. package/dist/render/materialAppearanceSignals.js +58 -0
  120. package/dist/render/materialAsset.d.ts +98 -0
  121. package/dist/render/materialAsset.js +454 -0
  122. package/dist/render/measureBounds.js +16 -3
  123. package/dist/render/measureCollisionMesh.js +8 -1
  124. package/dist/render/modelAssets.d.ts +8 -0
  125. package/dist/render/modelAssets.js +43 -0
  126. package/dist/render/modelBindPose.d.ts +5 -0
  127. package/dist/render/modelBindPose.js +35 -0
  128. package/dist/render/modelLoad.d.ts +32 -0
  129. package/dist/render/modelLoad.js +70 -1
  130. package/dist/render/modelNodeVisibility.d.ts +6 -0
  131. package/dist/render/modelNodeVisibility.js +18 -0
  132. package/dist/render/modelRender.d.ts +18 -1
  133. package/dist/render/modelRender.js +81 -3
  134. package/dist/render/projectedOverdraw.d.ts +35 -0
  135. package/dist/render/projectedOverdraw.js +204 -0
  136. package/dist/render/resolveModel.d.ts +12 -2
  137. package/dist/render/resolveModel.js +25 -4
  138. package/dist/render/rigNode.d.ts +26 -0
  139. package/dist/render/rigNode.js +32 -0
  140. package/dist/render/spriteRender.d.ts +4 -0
  141. package/dist/render/spriteRender.js +5 -0
  142. package/dist/render/staticShapeInstances.d.ts +34 -0
  143. package/dist/render/staticShapeInstances.js +78 -0
  144. package/dist/render/useFootIk.d.ts +53 -4
  145. package/dist/render/useFootIk.js +262 -70
  146. package/dist/render/useModelAnimation.d.ts +50 -0
  147. package/dist/render/useModelAnimation.js +245 -99
  148. package/dist/render/useModelInstance.d.ts +25 -0
  149. package/dist/render/useModelInstance.js +43 -0
  150. package/dist/scatter/scatterModels.d.ts +3 -2
  151. package/dist/scatter/scatterModels.js +15 -7
  152. package/dist/scene/AuthoredScene.d.ts +23 -6
  153. package/dist/scene/AuthoredScene.js +46 -9
  154. package/dist/scene/CityRenderer.js +6 -6
  155. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  156. package/dist/settings/QuickControls.js +1 -1
  157. package/dist/settings/SettingsMenu.js +11 -18
  158. package/dist/settings/SettingsRuntime.d.ts +5 -0
  159. package/dist/settings/SettingsRuntime.js +29 -2
  160. package/dist/settings/appliedSettings.d.ts +9 -3
  161. package/dist/settings/appliedSettings.js +6 -5
  162. package/dist/settings/settingsController.d.ts +3 -0
  163. package/dist/settings/settingsController.js +47 -3
  164. package/dist/shellControlSuspension.d.ts +18 -0
  165. package/dist/shellControlSuspension.js +28 -0
  166. package/dist/shellInputPublication.d.ts +28 -0
  167. package/dist/shellInputPublication.js +49 -0
  168. package/dist/shellPointerInput.d.ts +18 -0
  169. package/dist/shellPointerInput.js +49 -0
  170. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  171. package/dist/structures/BuildingKitBatch.js +35 -56
  172. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  173. package/dist/structures/BuildingRenderBudget.js +54 -0
  174. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  175. package/dist/structures/GeneratedBuilding.js +33 -22
  176. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  177. package/dist/structures/buildingKitMaterials.js +70 -0
  178. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  179. package/dist/structures/buildingSpatialBatch.js +45 -0
  180. package/dist/structures/buildingSurface.d.ts +21 -0
  181. package/dist/structures/buildingSurface.js +134 -0
  182. package/dist/terrain/GrassField.d.ts +2 -1
  183. package/dist/terrain/GrassField.js +59 -5
  184. package/dist/terrain/grassGeometry.d.ts +10 -0
  185. package/dist/terrain/grassGeometry.js +55 -0
  186. package/dist/terrain/grassMaterial.js +2 -2
  187. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  188. package/dist/terrain/terrainDetailMaterial.js +7 -7
  189. package/dist/touch/TouchControlsOverlay.js +4 -4
  190. package/dist/useShellMultiplayerSync.d.ts +1 -1
  191. package/dist/useShellMultiplayerSync.js +9 -3
  192. package/dist/vfx/ParticleField.d.ts +23 -20
  193. package/dist/vfx/ParticleField.js +168 -50
  194. package/dist/visibility/CullingProvider.js +2 -62
  195. package/dist/visibility/cullingDriver.d.ts +9 -0
  196. package/dist/visibility/cullingDriver.js +68 -0
  197. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  198. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  199. package/dist/weather/DustField.d.ts +2 -1
  200. package/dist/weather/DustField.js +17 -3
  201. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  202. package/dist/weather/FireSpreadLayer.js +5 -11
  203. package/dist/weather/LightningStrike.d.ts +4 -1
  204. package/dist/weather/LightningStrike.js +13 -17
  205. package/dist/weather/RainField.d.ts +2 -1
  206. package/dist/weather/RainField.js +17 -3
  207. package/dist/weather/RainImpactField.d.ts +16 -0
  208. package/dist/weather/RainImpactField.js +89 -0
  209. package/dist/weather/SnowField.d.ts +2 -1
  210. package/dist/weather/SnowField.js +17 -3
  211. package/dist/weather/WeatherLayer.d.ts +11 -2
  212. package/dist/weather/WeatherLayer.js +30 -6
  213. package/dist/weather/index.d.ts +2 -0
  214. package/dist/weather/index.js +2 -0
  215. package/dist/weather/weatherMath.d.ts +2 -0
  216. package/dist/weather/weatherMath.js +8 -0
  217. package/dist/weather/weatherShelter.d.ts +18 -0
  218. package/dist/weather/weatherShelter.js +75 -0
  219. package/dist/weather/weatherUniforms.d.ts +40 -1
  220. package/dist/weather/weatherUniforms.js +29 -9
  221. package/dist/world/AuthoredFlocks.d.ts +22 -0
  222. package/dist/world/AuthoredFlocks.js +87 -0
  223. package/dist/world/StaticObjectModels.d.ts +8 -0
  224. package/dist/world/StaticObjectModels.js +75 -0
  225. package/dist/world/WorldHud.d.ts +14 -6
  226. package/dist/world/WorldHud.js +25 -12
  227. package/dist/world/WorldParticles.d.ts +10 -23
  228. package/dist/world/WorldParticles.js +210 -62
  229. package/dist/world/WorldScene.d.ts +3 -1
  230. package/dist/world/WorldScene.js +41 -26
  231. package/dist/world/staticObjectBatches.d.ts +35 -0
  232. package/dist/world/staticObjectBatches.js +163 -0
  233. package/dist/world/worldBarSamples.d.ts +37 -33
  234. package/dist/world/worldBarSamples.js +117 -75
  235. package/package.json +4 -4
  236. package/skills/ce-handoff/SKILL.md +2 -2
  237. package/skills/game-design/SKILL.md +2 -2
  238. package/skills/jgengine/SKILL.md +28 -6
  239. package/skills/jgengine/api.md +139 -99
  240. package/skills/jgengine/capabilities.md +28 -0
  241. package/skills/jgengine/recipes/minimal-game.md +10 -2
  242. package/skills/jgengine-ui/SKILL.md +16 -17
  243. package/skills/jgengine-ui/api.md +603 -365
  244. package/skills/jgengine-ui/capabilities.md +131 -1
  245. package/skills/jgengine-ui/reference-primitives.md +179 -0
  246. package/skills/jgengine-ui/reference.md +4 -81
  247. package/skills/jgengine-world/SKILL.md +25 -4
  248. package/skills/jgengine-world/api.md +1045 -655
  249. package/skills/jgengine-world/capabilities.md +246 -9
  250. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  251. package/skills/jgengine-world/recipes/controls.md +48 -0
  252. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  253. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  254. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  255. package/skills/jgengine-world/reference.md +180 -4
  256. package/skills/jgengine-world/visibility.md +139 -0
  257. package/skills/level-design/SKILL.md +1 -1
  258. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
  259. package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
  260. package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
  261. package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
@@ -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
- function tuple(value) {
6
- return [value.x, value.y, value.z];
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 worldQuaternion(bone) {
9
- const result = new THREE.Quaternion();
10
- bone.getWorldQuaternion(result);
11
- return result;
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
- const parent = worldQuaternion(bone.parent).invert();
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, weight) {
23
- const current = currentTo.clone().sub(from).normalize();
24
- const solved = new THREE.Vector3(solvedTo[0] - from.x, solvedTo[1] - from.y, solvedTo[2] - from.z).normalize();
25
- if (current.lengthSq() < 1e-8 || solved.lengthSq() < 1e-8)
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 = new THREE.Quaternion().setFromUnitVectors(current, solved);
28
- const currentWorld = worldQuaternion(bone);
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
- /** Applies one frame of foot IK to a loaded rig. Exported for renderer tests and custom model hosts. */
34
- export function applyFootIk(scene, config, raycast, weight, cameraTarget) {
35
- let grounded = false;
36
- scene.updateMatrixWorld(true);
37
- for (const foot of config.feet) {
38
- const root = scene.getObjectByName(foot.root);
39
- const mid = scene.getObjectByName(foot.mid);
40
- const tip = scene.getObjectByName(foot.tip);
41
- if (root === undefined || mid === undefined || tip === undefined)
42
- continue;
43
- const rootPosition = root.getWorldPosition(new THREE.Vector3());
44
- const midPosition = mid.getWorldPosition(new THREE.Vector3());
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
- if (config.lookAt !== undefined && cameraTarget !== undefined) {
66
- const bone = scene.getObjectByName(config.lookAt.bone);
67
- if (bone !== undefined) {
68
- const from = bone.getWorldPosition(new THREE.Vector3());
69
- const q = lookAt({ from: tuple(from), target: cameraTarget, up: [0, 1, 0] });
70
- const desired = new THREE.Quaternion(q[0], q[1], q[2], q[3]);
71
- const current = worldQuaternion(bone);
72
- current.slerp(desired, weight);
73
- writeWorldQuaternion(bone, current);
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
- return grounded;
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
- /** Runs foot IK after the model animation mixer, fading the correction out while the entity is airborne. */
79
- export function useFootIk(scene, config, ctx, instanceId) {
80
- const camera = useThree((state) => state.camera);
81
- const weight = useRef(0);
82
- useFrame((_state, delta) => {
83
- if (config === undefined || ctx === null || instanceId === undefined)
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 grounded = applyFootIk(scene, config, (input) => ctx.scene.raycast({ ...input, excludeInstanceIds: [instanceId] }), weight.current, tuple(camera.getWorldPosition(new THREE.Vector3())));
86
- const target = grounded ? 1 : 0;
87
- weight.current += (target - weight.current) * Math.min(1, delta * (grounded ? 12 : 5));
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;