@jgengine/core 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/ai/decisionGraph.d.ts +50 -4
- package/dist/ai/decisionGraph.js +159 -23
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +43 -1
- package/dist/anim/animGraph.js +326 -10
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/locomotionGraph.d.ts +18 -1
- package/dist/anim/locomotionGraph.js +42 -3
- package/dist/audio/audioFalloff.d.ts +6 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +20 -0
- package/dist/combat/abilityKit.js +41 -4
- package/dist/combat/death.d.ts +11 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +119 -18
- package/dist/editor/environment.js +8 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/types.d.ts +15 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +54 -2
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +11 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +134 -35
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +36 -2
- package/dist/input/gamepadModel.js +69 -30
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +444 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +63 -1
- package/dist/movement/movementModel.js +136 -26
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +48 -2
- package/dist/movement/playerMovement.js +493 -87
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +11 -1
- package/dist/movement/solidObstacles.js +48 -3
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +10 -10
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +40 -5
- package/dist/physics/kinematicVehicle.js +17 -2
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +52 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +375 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +54 -3
- package/dist/runtime/cameraDirector.js +187 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +12 -4
- package/dist/runtime/context/combat.js +179 -11
- package/dist/runtime/context/deathLoot.d.ts +11 -8
- package/dist/runtime/context/deathLoot.js +17 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +86 -13
- package/dist/runtime/gameContextTypes.d.ts +33 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +22 -4
- package/dist/runtime/inputSnapshot.js +41 -3
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +38 -7
- package/dist/scene/behaviorRuntime.js +293 -30
- package/dist/scene/behaviors.d.ts +35 -2
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +41 -3
- package/dist/scene/vehicleSeat.js +94 -3
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +11 -1
- package/dist/sensor/perception.js +57 -10
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +2 -0
- package/dist/settings/graphicsProfile.js +12 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +10 -3
- package/dist/stats/eventMeter.js +6 -3
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +11 -4
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +32 -5
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +62 -0
- package/dist/world/footprintGrid.js +217 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +9 -1
- package/dist/world/terraform.js +53 -18
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +95 -0
- package/dist/world/worldSolids.js +351 -0
- package/dist/world.d.ts +6 -6
- package/dist/world.js +5 -5
- package/package.json +1 -1
- 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-combat/SKILL.md +13 -2
- package/skills/jgengine-combat/api.md +150 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +583 -463
- package/skills/jgengine-gameplay/capabilities.md +92 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +123 -0
- 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/dist/anim/animGraph.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveOneShotClip } from "../game/modelAnimation.js";
|
|
1
2
|
const DEFAULT_FADE = 0.2;
|
|
2
3
|
/** Entity blackboard key the shell reads extra graph parameters from: `ctx.scene.entity.blackboard.set(id, ANIM_PARAMS_KEY, { aiming: true })`. */
|
|
3
4
|
export const ANIM_PARAMS_KEY = "anim.params";
|
|
@@ -126,7 +127,7 @@ function clipTimeFor(time, duration, loop) {
|
|
|
126
127
|
*
|
|
127
128
|
* @capability anim-graph data-first animation state machine with blend trees, crossfades, layers, and clip events
|
|
128
129
|
*/
|
|
129
|
-
export function createAnimGraphRuntime(initial) {
|
|
130
|
+
export function createAnimGraphRuntime(initial, options = {}) {
|
|
130
131
|
let graph = initial;
|
|
131
132
|
let triggers = new Set();
|
|
132
133
|
let layers = {};
|
|
@@ -136,6 +137,14 @@ export function createAnimGraphRuntime(initial) {
|
|
|
136
137
|
layers[layer.id] = { current: layer.entry, time: 0, transition: null };
|
|
137
138
|
}
|
|
138
139
|
reset();
|
|
140
|
+
function chooseClip(state, def) {
|
|
141
|
+
if (def.kind === "clip" && def.variants !== undefined && def.variants.length > 0) {
|
|
142
|
+
state.selectedClip = resolveOneShotClip({ choice: def.variants }, "choice", options.rng?.() ?? 0) ?? def.clip;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
delete state.selectedClip;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
139
148
|
function pickTransition(layer, state, params, normalized) {
|
|
140
149
|
for (const transition of layer.transitions) {
|
|
141
150
|
if (transition.from !== "*" && transition.from !== state.current)
|
|
@@ -162,8 +171,7 @@ export function createAnimGraphRuntime(initial) {
|
|
|
162
171
|
continue;
|
|
163
172
|
const at = event.atSec;
|
|
164
173
|
if (loop) {
|
|
165
|
-
const
|
|
166
|
-
const hit = wrapped ? at > before || at <= after : at > before && at <= after;
|
|
174
|
+
const hit = at >= 0 && at <= duration && Math.floor((after - at) / duration) > Math.floor((before - at) / duration);
|
|
167
175
|
if (hit)
|
|
168
176
|
out.push({ name: event.name, clip });
|
|
169
177
|
}
|
|
@@ -197,6 +205,7 @@ export function createAnimGraphRuntime(initial) {
|
|
|
197
205
|
advance(dt, params, clips) {
|
|
198
206
|
const output = { clips: [], events: [] };
|
|
199
207
|
const rootDelta = [0, 0, 0];
|
|
208
|
+
let rootMotion = false;
|
|
200
209
|
for (const layer of graph.layers) {
|
|
201
210
|
const state = layers[layer.id];
|
|
202
211
|
if (state === undefined)
|
|
@@ -204,16 +213,24 @@ export function createAnimGraphRuntime(initial) {
|
|
|
204
213
|
const def = layer.states[state.current];
|
|
205
214
|
if (def === undefined)
|
|
206
215
|
continue;
|
|
216
|
+
if (def.kind === "clip" && def.variants !== undefined && state.selectedClip === undefined)
|
|
217
|
+
chooseClip(state, def);
|
|
207
218
|
const layerWeight = layer.weight ?? 1;
|
|
219
|
+
if (def.rootMotion === true && layerWeight > 0)
|
|
220
|
+
rootMotion = true;
|
|
208
221
|
const before = state.time;
|
|
209
222
|
state.time += dt * stateSpeed(def);
|
|
210
|
-
const weights =
|
|
223
|
+
const weights = def.kind === "clip" && state.selectedClip !== undefined && def.variants?.includes(state.selectedClip)
|
|
224
|
+
? { [state.selectedClip]: 1 }
|
|
225
|
+
: stateClipWeights(def, params);
|
|
211
226
|
const duration = stateDuration(weights, clips);
|
|
212
227
|
const loop = stateLoops(def);
|
|
213
228
|
const normalized = duration > 0 ? (loop ? (state.time % duration) / duration : Math.min(1, state.time / duration)) : 1;
|
|
214
229
|
for (const clip of Object.keys(weights)) {
|
|
230
|
+
if (!(weights[clip] * layerWeight > 0))
|
|
231
|
+
continue;
|
|
215
232
|
const duration = clipDuration(clips[clip]);
|
|
216
|
-
collectEvents(output.events, clip,
|
|
233
|
+
collectEvents(output.events, clip, before, state.time, duration, loop);
|
|
217
234
|
if (def.rootMotion) {
|
|
218
235
|
const info = clips[clip];
|
|
219
236
|
if (typeof info !== "number" && info?.rootTrack !== undefined) {
|
|
@@ -232,7 +249,13 @@ export function createAnimGraphRuntime(initial) {
|
|
|
232
249
|
const times = {};
|
|
233
250
|
for (const [clip, w] of Object.entries(transition.fromWeights)) {
|
|
234
251
|
merged[clip] = (merged[clip] ?? 0) + w * (1 - t);
|
|
235
|
-
|
|
252
|
+
const playback = transition.fromPlayback?.[clip];
|
|
253
|
+
if (w * (1 - t) * layerWeight > 0 && playback?.rootMotion === true)
|
|
254
|
+
rootMotion = true;
|
|
255
|
+
const time = transition.fromTimes[clip] ?? 0;
|
|
256
|
+
const nextTime = playback === undefined ? time : clipTimeFor(time + dt * playback.speed, clipDuration(clips[clip]), playback.loop);
|
|
257
|
+
transition.fromTimes[clip] = nextTime;
|
|
258
|
+
times[clip] = nextTime;
|
|
236
259
|
}
|
|
237
260
|
for (const [clip, w] of Object.entries(weights)) {
|
|
238
261
|
merged[clip] = (merged[clip] ?? 0) + w * t;
|
|
@@ -254,33 +277,326 @@ export function createAnimGraphRuntime(initial) {
|
|
|
254
277
|
}
|
|
255
278
|
}
|
|
256
279
|
const next = pickTransition(layer, state, params, normalized);
|
|
257
|
-
if (next !== null
|
|
280
|
+
if (next !== null) {
|
|
258
281
|
const fromWeights = {};
|
|
259
282
|
const fromTimes = {};
|
|
283
|
+
const fromPlayback = {};
|
|
260
284
|
for (const entry of output.clips) {
|
|
261
285
|
if (entry.layer !== layer.id)
|
|
262
286
|
continue;
|
|
263
287
|
fromWeights[entry.clip] = (fromWeights[entry.clip] ?? 0) + entry.weight / (layerWeight || 1);
|
|
264
288
|
fromTimes[entry.clip] = entry.time;
|
|
289
|
+
const previous = transition?.fromPlayback?.[entry.clip];
|
|
290
|
+
const outgoingRoot = previous?.rootMotion === true && transition !== null &&
|
|
291
|
+
transition.duration > 0 && transition.elapsed < transition.duration &&
|
|
292
|
+
(transition.fromWeights[entry.clip] ?? 0) > 0;
|
|
293
|
+
const playback = weights[entry.clip] === undefined && previous !== undefined
|
|
294
|
+
? previous : { speed: stateSpeed(def), loop };
|
|
295
|
+
// Same-clip states share one clock, but an interrupted fade must retain either
|
|
296
|
+
// influencing source's in-place policy until that source leaves the pose.
|
|
297
|
+
fromPlayback[entry.clip] = { ...playback, ...(def.rootMotion === true || outgoingRoot ? { rootMotion: true } : {}) };
|
|
265
298
|
}
|
|
266
299
|
state.current = next.to;
|
|
267
300
|
state.time = 0;
|
|
301
|
+
const target = layer.states[next.to];
|
|
302
|
+
if (target !== undefined)
|
|
303
|
+
chooseClip(state, target);
|
|
268
304
|
state.transition = {
|
|
269
305
|
to: next.to,
|
|
270
306
|
elapsed: 0,
|
|
271
307
|
duration: next.duration ?? DEFAULT_FADE,
|
|
272
308
|
fromWeights,
|
|
273
309
|
fromTimes,
|
|
310
|
+
fromPlayback,
|
|
274
311
|
};
|
|
275
312
|
}
|
|
276
|
-
else if (next !== null && next.to === state.current) {
|
|
277
|
-
state.time = 0;
|
|
278
|
-
}
|
|
279
313
|
}
|
|
280
314
|
triggers.clear();
|
|
281
315
|
if (rootDelta[0] !== 0 || rootDelta[1] !== 0 || rootDelta[2] !== 0)
|
|
282
316
|
output.rootDelta = rootDelta;
|
|
317
|
+
if (rootMotion)
|
|
318
|
+
output.rootMotion = true;
|
|
283
319
|
return output;
|
|
284
320
|
},
|
|
285
321
|
};
|
|
286
322
|
}
|
|
323
|
+
const COMPARES = [">", "<", ">=", "<=", "==", "!="];
|
|
324
|
+
function record(value) {
|
|
325
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
326
|
+
}
|
|
327
|
+
function optionalNumber(value) {
|
|
328
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
329
|
+
}
|
|
330
|
+
function parseState(value) {
|
|
331
|
+
const raw = record(value);
|
|
332
|
+
if (raw === null)
|
|
333
|
+
return null;
|
|
334
|
+
const extra = {
|
|
335
|
+
...(optionalNumber(raw.speed) === undefined ? {} : { speed: raw.speed }),
|
|
336
|
+
...(typeof raw.loop === "boolean" ? { loop: raw.loop } : {}),
|
|
337
|
+
...(typeof raw.rootMotion === "boolean" ? { rootMotion: raw.rootMotion } : {}),
|
|
338
|
+
};
|
|
339
|
+
if (raw.kind === "clip" && typeof raw.clip === "string") {
|
|
340
|
+
const variants = Array.isArray(raw.variants) ? raw.variants.filter((clip) => typeof clip === "string" && clip.length > 0) : undefined;
|
|
341
|
+
return { kind: "clip", clip: raw.clip, ...(variants === undefined || variants.length === 0 ? {} : { variants }), ...extra };
|
|
342
|
+
}
|
|
343
|
+
if (!Array.isArray(raw.points))
|
|
344
|
+
return null;
|
|
345
|
+
if (raw.kind === "blend1D" && typeof raw.param === "string") {
|
|
346
|
+
const points = raw.points.flatMap((point) => {
|
|
347
|
+
const entry = record(point);
|
|
348
|
+
return entry !== null && typeof entry.clip === "string" && optionalNumber(entry.at) !== undefined ? [{ at: entry.at, clip: entry.clip }] : [];
|
|
349
|
+
});
|
|
350
|
+
return { kind: "blend1D", param: raw.param, points, ...extra };
|
|
351
|
+
}
|
|
352
|
+
if (raw.kind === "blend2D" && Array.isArray(raw.params) && raw.params.length === 2 && raw.params.every((param) => typeof param === "string")) {
|
|
353
|
+
const points = raw.points.flatMap((point) => {
|
|
354
|
+
const entry = record(point);
|
|
355
|
+
const at = entry?.at;
|
|
356
|
+
return entry !== null && typeof entry.clip === "string" && Array.isArray(at) && at.length === 2 && at.every((n) => optionalNumber(n) !== undefined)
|
|
357
|
+
? [{ at: [at[0], at[1]], clip: entry.clip }]
|
|
358
|
+
: [];
|
|
359
|
+
});
|
|
360
|
+
return { kind: "blend2D", params: [raw.params[0], raw.params[1]], points, ...extra };
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
function parseTransition(value, states) {
|
|
365
|
+
const raw = record(value);
|
|
366
|
+
if (raw === null || typeof raw.from !== "string" || typeof raw.to !== "string")
|
|
367
|
+
return null;
|
|
368
|
+
if (states[raw.to] === undefined || (raw.from !== "*" && states[raw.from] === undefined))
|
|
369
|
+
return null;
|
|
370
|
+
const when = Array.isArray(raw.when)
|
|
371
|
+
? raw.when.flatMap((condition) => {
|
|
372
|
+
const entry = record(condition);
|
|
373
|
+
return entry !== null &&
|
|
374
|
+
typeof entry.param === "string" &&
|
|
375
|
+
COMPARES.includes(entry.op) &&
|
|
376
|
+
(typeof entry.value === "number" || typeof entry.value === "boolean")
|
|
377
|
+
? [{ param: entry.param, op: entry.op, value: entry.value }]
|
|
378
|
+
: [];
|
|
379
|
+
})
|
|
380
|
+
: undefined;
|
|
381
|
+
return {
|
|
382
|
+
from: raw.from,
|
|
383
|
+
to: raw.to,
|
|
384
|
+
...(when === undefined || when.length === 0 ? {} : { when }),
|
|
385
|
+
...(typeof raw.trigger === "string" ? { trigger: raw.trigger } : {}),
|
|
386
|
+
...(optionalNumber(raw.duration) === undefined ? {} : { duration: Math.max(0, raw.duration) }),
|
|
387
|
+
...(optionalNumber(raw.exitTime) === undefined ? {} : { exitTime: raw.exitTime }),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Validates untrusted JSON (a saved scene document, a network payload) as an {@link AnimGraph}.
|
|
392
|
+
* Malformed states, transitions to unknown states, and bad conditions are dropped; a layer whose
|
|
393
|
+
* entry state is missing is dropped; the result is `undefined` when no layer survives.
|
|
394
|
+
*
|
|
395
|
+
* @capability anim-graph load an animation graph from saved JSON without trusting its shape
|
|
396
|
+
*/
|
|
397
|
+
export function parseAnimGraph(value) {
|
|
398
|
+
const raw = record(value);
|
|
399
|
+
if (raw === null || !Array.isArray(raw.layers))
|
|
400
|
+
return undefined;
|
|
401
|
+
const layers = [];
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
for (const candidate of raw.layers) {
|
|
404
|
+
const layer = record(candidate);
|
|
405
|
+
if (layer === null || typeof layer.id !== "string" || seen.has(layer.id) || typeof layer.entry !== "string")
|
|
406
|
+
continue;
|
|
407
|
+
const rawStates = record(layer.states);
|
|
408
|
+
if (rawStates === null)
|
|
409
|
+
continue;
|
|
410
|
+
const states = {};
|
|
411
|
+
for (const [name, state] of Object.entries(rawStates)) {
|
|
412
|
+
const parsed = parseState(state);
|
|
413
|
+
if (parsed !== null)
|
|
414
|
+
states[name] = parsed;
|
|
415
|
+
}
|
|
416
|
+
if (states[layer.entry] === undefined)
|
|
417
|
+
continue;
|
|
418
|
+
const transitions = Array.isArray(layer.transitions)
|
|
419
|
+
? layer.transitions.flatMap((transition) => {
|
|
420
|
+
const parsed = parseTransition(transition, states);
|
|
421
|
+
return parsed === null ? [] : [parsed];
|
|
422
|
+
})
|
|
423
|
+
: [];
|
|
424
|
+
const mask = Array.isArray(layer.mask) ? layer.mask.filter((prefix) => typeof prefix === "string") : undefined;
|
|
425
|
+
seen.add(layer.id);
|
|
426
|
+
layers.push({
|
|
427
|
+
id: layer.id,
|
|
428
|
+
entry: layer.entry,
|
|
429
|
+
states,
|
|
430
|
+
transitions,
|
|
431
|
+
...(mask === undefined ? {} : { mask }),
|
|
432
|
+
...(typeof layer.additive === "boolean" ? { additive: layer.additive } : {}),
|
|
433
|
+
...(optionalNumber(layer.weight) === undefined ? {} : { weight: Math.min(1, Math.max(0, layer.weight)) }),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (layers.length === 0)
|
|
437
|
+
return undefined;
|
|
438
|
+
const events = Array.isArray(raw.events)
|
|
439
|
+
? raw.events.flatMap((event) => {
|
|
440
|
+
const entry = record(event);
|
|
441
|
+
return entry !== null && typeof entry.clip === "string" && typeof entry.name === "string" && optionalNumber(entry.atSec) !== undefined
|
|
442
|
+
? [{ clip: entry.clip, atSec: entry.atSec, name: entry.name }]
|
|
443
|
+
: [];
|
|
444
|
+
})
|
|
445
|
+
: [];
|
|
446
|
+
return { layers, ...(events.length === 0 ? {} : { events }) };
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Validate an authored graph without dropping broken combat states or weakening transition conditions.
|
|
450
|
+
* Known malformed fields and dangling references reject the whole graph with relative repair locations;
|
|
451
|
+
* valid data, including authored object/array order, is returned unchanged. Unknown extension fields are
|
|
452
|
+
* retained. Clip availability belongs to the imported-rig diagnostics. Use {@link parseAnimGraph} when
|
|
453
|
+
* permissive repair is intentional.
|
|
454
|
+
* @capability anim-graph validate authored animation graphs with located repair diagnostics without partial fallback
|
|
455
|
+
*/
|
|
456
|
+
export function validateAnimGraph(value) {
|
|
457
|
+
const diagnostics = [];
|
|
458
|
+
const issue = (path, message, repair) => { diagnostics.push({ path, message, repair }); };
|
|
459
|
+
const field = (path, key) => path === "" ? key : /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
|
|
460
|
+
const object = (candidate, path) => {
|
|
461
|
+
const raw = record(candidate);
|
|
462
|
+
if (raw === null)
|
|
463
|
+
issue(path, "Expected an animation graph object.", "Supply an object with the supported fields.");
|
|
464
|
+
return raw;
|
|
465
|
+
};
|
|
466
|
+
const text = (candidate, path, empty = false) => {
|
|
467
|
+
if (typeof candidate === "string" && (empty || candidate.trim().length > 0))
|
|
468
|
+
return true;
|
|
469
|
+
issue(path, "Expected a nonempty string.", "Set an explicit name from this graph or imported rig.");
|
|
470
|
+
return false;
|
|
471
|
+
};
|
|
472
|
+
const number = (candidate, path, min = -Infinity, max = Infinity) => {
|
|
473
|
+
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= min && candidate <= max)
|
|
474
|
+
return true;
|
|
475
|
+
issue(path, `Expected a finite number${min === -Infinity && max === Infinity ? "" : max === Infinity ? ` at least ${min}` : ` between ${min} and ${max}`}.`, "Set a finite value in the supported range.");
|
|
476
|
+
return false;
|
|
477
|
+
};
|
|
478
|
+
const boolean = (candidate, path) => {
|
|
479
|
+
if (typeof candidate !== "boolean")
|
|
480
|
+
issue(path, "Expected a boolean.", "Set true or false, or omit the optional field.");
|
|
481
|
+
};
|
|
482
|
+
const array = (candidate, path) => {
|
|
483
|
+
if (Array.isArray(candidate))
|
|
484
|
+
return candidate;
|
|
485
|
+
issue(path, "Expected an array.", "Supply an array of supported entries.");
|
|
486
|
+
return null;
|
|
487
|
+
};
|
|
488
|
+
const tuple = (candidate, path, numeric) => {
|
|
489
|
+
const entries = array(candidate, path);
|
|
490
|
+
if (entries === null)
|
|
491
|
+
return;
|
|
492
|
+
if (entries.length !== 2)
|
|
493
|
+
issue(path, "Expected exactly two entries.", "Supply the two blend axes in authored order.");
|
|
494
|
+
entries.forEach((entry, index) => numeric ? number(entry, `${path}[${index}]`) : text(entry, `${path}[${index}]`));
|
|
495
|
+
};
|
|
496
|
+
const state = (candidate, path) => {
|
|
497
|
+
const raw = object(candidate, path);
|
|
498
|
+
if (raw === null)
|
|
499
|
+
return;
|
|
500
|
+
if (!["clip", "blend1D", "blend2D"].includes(raw.kind)) {
|
|
501
|
+
issue(`${path}.kind`, "Unknown animation state kind.", "Choose clip, blend1D or blend2D.");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (raw.speed !== undefined)
|
|
505
|
+
number(raw.speed, `${path}.speed`);
|
|
506
|
+
for (const key of ["loop", "rootMotion"])
|
|
507
|
+
if (raw[key] !== undefined)
|
|
508
|
+
boolean(raw[key], `${path}.${key}`);
|
|
509
|
+
const allowed = raw.kind === "clip" ? ["clip", "variants"] : raw.kind === "blend1D" ? ["param", "points"] : ["params", "points"];
|
|
510
|
+
for (const key of ["clip", "variants", "param", "params", "points"])
|
|
511
|
+
if (raw[key] !== undefined && !allowed.includes(key)) {
|
|
512
|
+
issue(`${path}.${key}`, `Field ${key} is not used by a ${raw.kind} state.`, "Remove the field or choose the matching state kind.");
|
|
513
|
+
}
|
|
514
|
+
if (raw.kind === "clip") {
|
|
515
|
+
text(raw.clip, `${path}.clip`);
|
|
516
|
+
if (raw.variants !== undefined)
|
|
517
|
+
array(raw.variants, `${path}.variants`)?.forEach((clip, index) => text(clip, `${path}.variants[${index}]`));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (raw.kind === "blend1D")
|
|
521
|
+
text(raw.param, `${path}.param`);
|
|
522
|
+
else
|
|
523
|
+
tuple(raw.params, `${path}.params`, false);
|
|
524
|
+
array(raw.points, `${path}.points`)?.forEach((candidate, index) => {
|
|
525
|
+
const pointPath = `${path}.points[${index}]`, point = object(candidate, pointPath);
|
|
526
|
+
if (point === null)
|
|
527
|
+
return;
|
|
528
|
+
text(point.clip, `${pointPath}.clip`);
|
|
529
|
+
if (raw.kind === "blend1D")
|
|
530
|
+
number(point.at, `${pointPath}.at`);
|
|
531
|
+
else
|
|
532
|
+
tuple(point.at, `${pointPath}.at`, true);
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
const raw = object(value, "");
|
|
536
|
+
if (raw === null)
|
|
537
|
+
return { graph: undefined, diagnostics };
|
|
538
|
+
const layers = array(raw.layers, "layers"), seen = new Set();
|
|
539
|
+
if (layers?.length === 0)
|
|
540
|
+
issue("layers", "An animation graph needs at least one layer.", "Add a layer with an entry state.");
|
|
541
|
+
layers?.forEach((candidate, index) => {
|
|
542
|
+
const path = `layers[${index}]`, layer = object(candidate, path);
|
|
543
|
+
if (layer === null)
|
|
544
|
+
return;
|
|
545
|
+
if (text(layer.id, `${path}.id`)) {
|
|
546
|
+
if (seen.has(layer.id))
|
|
547
|
+
issue(`${path}.id`, "Layer id is duplicated.", "Give each layer a distinct id.");
|
|
548
|
+
seen.add(layer.id);
|
|
549
|
+
}
|
|
550
|
+
const validEntry = text(layer.entry, `${path}.entry`), states = object(layer.states, `${path}.states`);
|
|
551
|
+
if (states !== null) {
|
|
552
|
+
for (const [name, candidate] of Object.entries(states)) {
|
|
553
|
+
text(name, field(`${path}.states`, name));
|
|
554
|
+
state(candidate, field(`${path}.states`, name));
|
|
555
|
+
}
|
|
556
|
+
if (validEntry && !Object.hasOwn(states, layer.entry))
|
|
557
|
+
issue(`${path}.entry`, "Entry state does not exist in this layer.", "Choose a state declared in this layer.");
|
|
558
|
+
}
|
|
559
|
+
if (layer.weight !== undefined)
|
|
560
|
+
number(layer.weight, `${path}.weight`, 0, 1);
|
|
561
|
+
if (layer.additive !== undefined)
|
|
562
|
+
boolean(layer.additive, `${path}.additive`);
|
|
563
|
+
if (layer.mask !== undefined)
|
|
564
|
+
array(layer.mask, `${path}.mask`)?.forEach((prefix, i) => text(prefix, `${path}.mask[${i}]`, true));
|
|
565
|
+
array(layer.transitions, `${path}.transitions`)?.forEach((candidate, i) => {
|
|
566
|
+
const edgePath = `${path}.transitions[${i}]`, edge = object(candidate, edgePath);
|
|
567
|
+
if (edge === null)
|
|
568
|
+
return;
|
|
569
|
+
for (const key of ["from", "to"])
|
|
570
|
+
if (text(edge[key], `${edgePath}.${key}`) && states !== null && !(key === "from" && edge[key] === "*") && !Object.hasOwn(states, edge[key])) {
|
|
571
|
+
issue(`${edgePath}.${key}`, "Transition references an unknown state.", "Choose a state declared in this layer; only from may use *.");
|
|
572
|
+
}
|
|
573
|
+
if (edge.trigger !== undefined)
|
|
574
|
+
text(edge.trigger, `${edgePath}.trigger`);
|
|
575
|
+
if (edge.duration !== undefined)
|
|
576
|
+
number(edge.duration, `${edgePath}.duration`, 0);
|
|
577
|
+
if (edge.exitTime !== undefined)
|
|
578
|
+
number(edge.exitTime, `${edgePath}.exitTime`);
|
|
579
|
+
if (edge.when !== undefined)
|
|
580
|
+
array(edge.when, `${edgePath}.when`)?.forEach((candidate, i) => {
|
|
581
|
+
const conditionPath = `${edgePath}.when[${i}]`, condition = object(candidate, conditionPath);
|
|
582
|
+
if (condition === null)
|
|
583
|
+
return;
|
|
584
|
+
text(condition.param, `${conditionPath}.param`);
|
|
585
|
+
if (!COMPARES.includes(condition.op))
|
|
586
|
+
issue(`${conditionPath}.op`, "Unknown comparison operator.", "Choose >, <, >=, <=, == or !=.");
|
|
587
|
+
if (typeof condition.value !== "boolean")
|
|
588
|
+
number(condition.value, `${conditionPath}.value`);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
if (raw.events !== undefined)
|
|
593
|
+
array(raw.events, "events")?.forEach((candidate, index) => {
|
|
594
|
+
const path = `events[${index}]`, event = object(candidate, path);
|
|
595
|
+
if (event === null)
|
|
596
|
+
return;
|
|
597
|
+
text(event.clip, `${path}.clip`);
|
|
598
|
+
text(event.name, `${path}.name`);
|
|
599
|
+
number(event.atSec, `${path}.atSec`, 0);
|
|
600
|
+
});
|
|
601
|
+
return { graph: diagnostics.length === 0 ? value : undefined, diagnostics };
|
|
602
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** A clip to bake: its name and length in seconds. */
|
|
2
|
+
export interface BoneTextureClipInput {
|
|
3
|
+
name: string;
|
|
4
|
+
duration: number;
|
|
5
|
+
}
|
|
6
|
+
/** One baked clip's rows in a {@link BoneTextureLayout}. */
|
|
7
|
+
export interface BoneTextureClip {
|
|
8
|
+
name: string;
|
|
9
|
+
/** First texture row of the clip. */
|
|
10
|
+
startFrame: number;
|
|
11
|
+
/** Rows baked for the clip, first and last pose included. */
|
|
12
|
+
frameCount: number;
|
|
13
|
+
duration: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Where every bone matrix of every baked frame lives in a vertex-animation texture: one row per
|
|
17
|
+
* frame, four RGBA texels (matrix columns) per bone. Plain data, so a baked crowd can be cached,
|
|
18
|
+
* shipped or checked in a test without a renderer.
|
|
19
|
+
*/
|
|
20
|
+
export interface BoneTextureLayout {
|
|
21
|
+
boneCount: number;
|
|
22
|
+
fps: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
clips: BoneTextureClip[];
|
|
26
|
+
}
|
|
27
|
+
/** Inputs for {@link planBoneTexture}. */
|
|
28
|
+
export interface BoneTextureInput {
|
|
29
|
+
boneCount: number;
|
|
30
|
+
clips: readonly BoneTextureClipInput[];
|
|
31
|
+
/** Baked samples per second. Default 30. */
|
|
32
|
+
fps?: number;
|
|
33
|
+
/** Largest texture side the target GPU accepts. Default 4096. */
|
|
34
|
+
maxTextureSize?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Plans a bone-matrix texture for a rig's clips. Throws when the rig or the clip set does not fit
|
|
38
|
+
* `maxTextureSize`; lower `fps` or bake fewer clips.
|
|
39
|
+
*
|
|
40
|
+
* @capability animation bake many clips of a rig into one bone-matrix texture for instanced crowds
|
|
41
|
+
*/
|
|
42
|
+
export declare function planBoneTexture(input: BoneTextureInput): BoneTextureLayout;
|
|
43
|
+
/** Two texture rows around a playback time and the blend between them. */
|
|
44
|
+
export interface BoneTextureSample {
|
|
45
|
+
rowA: number;
|
|
46
|
+
rowB: number;
|
|
47
|
+
t: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Finds the baked rows around `time` in a clip, wrapping when `loop` and holding the last pose
|
|
51
|
+
* otherwise. This is the CPU mirror of the crowd shader's lookup.
|
|
52
|
+
*
|
|
53
|
+
* @capability animation look up the baked frames and blend for a crowd member's playback time
|
|
54
|
+
*/
|
|
55
|
+
export declare function sampleBoneTexture(clip: BoneTextureClip, fps: number, time: number, loop: boolean, out?: BoneTextureSample): BoneTextureSample;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plans a bone-matrix texture for a rig's clips. Throws when the rig or the clip set does not fit
|
|
3
|
+
* `maxTextureSize`; lower `fps` or bake fewer clips.
|
|
4
|
+
*
|
|
5
|
+
* @capability animation bake many clips of a rig into one bone-matrix texture for instanced crowds
|
|
6
|
+
*/
|
|
7
|
+
export function planBoneTexture(input) {
|
|
8
|
+
const fps = input.fps ?? 30;
|
|
9
|
+
const maxSize = input.maxTextureSize ?? 4096;
|
|
10
|
+
if (!(fps > 0))
|
|
11
|
+
throw new Error(`planBoneTexture: fps must be positive, got ${fps}`);
|
|
12
|
+
const width = Math.max(1, input.boneCount) * 4;
|
|
13
|
+
if (width > maxSize)
|
|
14
|
+
throw new Error(`planBoneTexture: ${input.boneCount} bones need ${width} texels per row, over the ${maxSize} limit`);
|
|
15
|
+
const clips = [];
|
|
16
|
+
let row = 0;
|
|
17
|
+
for (const clip of input.clips) {
|
|
18
|
+
const duration = Math.max(0, clip.duration);
|
|
19
|
+
const frameCount = Math.max(2, Math.ceil(duration * fps) + 1);
|
|
20
|
+
clips.push({ name: clip.name, startFrame: row, frameCount, duration });
|
|
21
|
+
row += frameCount;
|
|
22
|
+
}
|
|
23
|
+
if (row > maxSize)
|
|
24
|
+
throw new Error(`planBoneTexture: ${row} frames exceed the ${maxSize}-row limit; lower fps or bake fewer clips`);
|
|
25
|
+
return { boneCount: input.boneCount, fps, width, height: Math.max(1, row), clips };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds the baked rows around `time` in a clip, wrapping when `loop` and holding the last pose
|
|
29
|
+
* otherwise. This is the CPU mirror of the crowd shader's lookup.
|
|
30
|
+
*
|
|
31
|
+
* @capability animation look up the baked frames and blend for a crowd member's playback time
|
|
32
|
+
*/
|
|
33
|
+
export function sampleBoneTexture(clip, fps, time, loop, out) {
|
|
34
|
+
const result = out ?? { rowA: 0, rowB: 0, t: 0 };
|
|
35
|
+
const span = clip.frameCount - 1;
|
|
36
|
+
const raw = time * fps;
|
|
37
|
+
const frame = loop ? ((raw % span) + span) % span : Math.min(Math.max(raw, 0), span);
|
|
38
|
+
const a = Math.floor(frame);
|
|
39
|
+
result.rowA = clip.startFrame + a;
|
|
40
|
+
result.rowB = clip.startFrame + Math.min(a + 1, span);
|
|
41
|
+
result.t = frame - a;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** One foot's animated ankle height and the ground found under it, both in world units. */
|
|
2
|
+
export interface FootGroundSample {
|
|
3
|
+
/** Animated ankle (foot bone) height this frame, before any correction. */
|
|
4
|
+
ankleY: number;
|
|
5
|
+
/** Ground height under the foot, or `null` when the probe found nothing. */
|
|
6
|
+
groundY: number | null;
|
|
7
|
+
}
|
|
8
|
+
/** Inputs for {@link placeFeet}. All heights are world units. */
|
|
9
|
+
export interface FootPlacementInput {
|
|
10
|
+
feet: readonly FootGroundSample[];
|
|
11
|
+
/** Height of the model origin, which the clips were authored to stand on. */
|
|
12
|
+
originY: number;
|
|
13
|
+
/** Ankle height above the sole in the rig's rest pose; the sole never ends below the ground. */
|
|
14
|
+
ankleHeight: number;
|
|
15
|
+
/** Largest correction applied to a foot or the pelvis, up or down. */
|
|
16
|
+
maxAdjust: number;
|
|
17
|
+
/** A lowest sole this far above its ground reads as airborne. */
|
|
18
|
+
airborneGap: number;
|
|
19
|
+
}
|
|
20
|
+
/** Result of {@link placeFeet}: where each ankle should go and how far the pelvis drops. */
|
|
21
|
+
export interface FootPlacement {
|
|
22
|
+
/** Vertical pelvis shift, `<= 0`, so the lowest planted foot can reach its ground. */
|
|
23
|
+
pelvisOffset: number;
|
|
24
|
+
/** Target ankle height per foot, or `null` when that foot has no ground or no correction. */
|
|
25
|
+
ankleTargets: (number | null)[];
|
|
26
|
+
/** `false` while the feet are clear of the ground, so callers fade the correction out. */
|
|
27
|
+
grounded: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves foot placement for a rig whose clips were authored on flat ground at its origin.
|
|
31
|
+
* Each foot keeps its animated lift and moves by its ground's height above or below the origin,
|
|
32
|
+
* the sole is never left under the ground, and the pelvis drops by the deepest reach so both legs
|
|
33
|
+
* stay within length. Pure and allocation-light; the renderer applies the targets with
|
|
34
|
+
* {@link solveTwoBone}.
|
|
35
|
+
*
|
|
36
|
+
* @capability animation ground rigged feet on slopes and steps without hiding the swing leg
|
|
37
|
+
*/
|
|
38
|
+
export declare function placeFeet(input: FootPlacementInput, out?: FootPlacement): FootPlacement;
|
|
39
|
+
/** A bone as {@link inferLegChains} sees it: its name and its parent's name. */
|
|
40
|
+
export interface RigBone {
|
|
41
|
+
name: string;
|
|
42
|
+
parent: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** Thigh → shin → foot bone names for one leg. */
|
|
45
|
+
export interface LegChain {
|
|
46
|
+
root: string;
|
|
47
|
+
mid: string;
|
|
48
|
+
tip: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Finds leg chains on a humanoid rig by bone name: a thigh-like bone (`UpLeg`, `UpperLeg`, `Thigh`)
|
|
52
|
+
* with its first child and grandchild as shin and foot. Control, IK-target and twist bones are
|
|
53
|
+
* skipped. Returns chains in rig order; an empty array means the rig has no recognizable legs.
|
|
54
|
+
*
|
|
55
|
+
* @capability animation find thigh, shin and foot bones on a humanoid rig for foot IK
|
|
56
|
+
*/
|
|
57
|
+
export declare function inferLegChains(bones: readonly RigBone[]): LegChain[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function clamp(value, limit) {
|
|
2
|
+
return Math.max(-limit, Math.min(limit, value));
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Resolves foot placement for a rig whose clips were authored on flat ground at its origin.
|
|
6
|
+
* Each foot keeps its animated lift and moves by its ground's height above or below the origin,
|
|
7
|
+
* the sole is never left under the ground, and the pelvis drops by the deepest reach so both legs
|
|
8
|
+
* stay within length. Pure and allocation-light; the renderer applies the targets with
|
|
9
|
+
* {@link solveTwoBone}.
|
|
10
|
+
*
|
|
11
|
+
* @capability animation ground rigged feet on slopes and steps without hiding the swing leg
|
|
12
|
+
*/
|
|
13
|
+
export function placeFeet(input, out) {
|
|
14
|
+
const result = out ?? { pelvisOffset: 0, ankleTargets: [], grounded: false };
|
|
15
|
+
result.ankleTargets.length = input.feet.length;
|
|
16
|
+
const limit = Math.max(0, input.maxAdjust);
|
|
17
|
+
let lowestGap = Infinity;
|
|
18
|
+
let pelvis = 0;
|
|
19
|
+
for (let i = 0; i < input.feet.length; i += 1) {
|
|
20
|
+
const foot = input.feet[i];
|
|
21
|
+
if (foot.groundY === null) {
|
|
22
|
+
result.ankleTargets[i] = null;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
lowestGap = Math.min(lowestGap, foot.ankleY - input.ankleHeight - foot.groundY);
|
|
26
|
+
const followed = foot.ankleY + clamp(foot.groundY - input.originY, limit);
|
|
27
|
+
const target = Math.min(foot.ankleY + limit, Math.max(followed, foot.groundY + input.ankleHeight));
|
|
28
|
+
result.ankleTargets[i] = target;
|
|
29
|
+
pelvis = Math.min(pelvis, target - foot.ankleY);
|
|
30
|
+
}
|
|
31
|
+
result.grounded = lowestGap <= input.airborneGap;
|
|
32
|
+
result.pelvisOffset = result.grounded ? Math.max(-limit, pelvis) : 0;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const THIGH = /(upleg|upperleg|thigh|femur)/;
|
|
36
|
+
const HELPER = /(ik|control|ctrl|twist|pole|target|roll)/;
|
|
37
|
+
/**
|
|
38
|
+
* Finds leg chains on a humanoid rig by bone name: a thigh-like bone (`UpLeg`, `UpperLeg`, `Thigh`)
|
|
39
|
+
* with its first child and grandchild as shin and foot. Control, IK-target and twist bones are
|
|
40
|
+
* skipped. Returns chains in rig order; an empty array means the rig has no recognizable legs.
|
|
41
|
+
*
|
|
42
|
+
* @capability animation find thigh, shin and foot bones on a humanoid rig for foot IK
|
|
43
|
+
*/
|
|
44
|
+
export function inferLegChains(bones) {
|
|
45
|
+
const children = new Map();
|
|
46
|
+
for (const bone of bones) {
|
|
47
|
+
if (bone.parent === null)
|
|
48
|
+
continue;
|
|
49
|
+
const list = children.get(bone.parent);
|
|
50
|
+
if (list === undefined)
|
|
51
|
+
children.set(bone.parent, [bone.name]);
|
|
52
|
+
else
|
|
53
|
+
list.push(bone.name);
|
|
54
|
+
}
|
|
55
|
+
const firstChild = (name) => children.get(name)?.find((child) => !HELPER.test(child.toLowerCase()));
|
|
56
|
+
const chains = [];
|
|
57
|
+
for (const bone of bones) {
|
|
58
|
+
const key = bone.name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
59
|
+
if (!THIGH.test(key) || HELPER.test(key))
|
|
60
|
+
continue;
|
|
61
|
+
const mid = firstChild(bone.name);
|
|
62
|
+
const tip = mid === undefined ? undefined : firstChild(mid);
|
|
63
|
+
if (mid !== undefined && tip !== undefined)
|
|
64
|
+
chains.push({ root: bone.name, mid, tip });
|
|
65
|
+
}
|
|
66
|
+
return chains;
|
|
67
|
+
}
|