@woosh/meep-engine 3.40.0 → 3.41.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/package.json +1 -1
- package/src/core/geom/3d/quaternion/swing_twist_clamp.d.ts +24 -0
- package/src/core/geom/3d/quaternion/swing_twist_clamp.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/swing_twist_clamp.js +62 -0
- package/src/core/geom/3d/quaternion/swing_twist_compose.d.ts +15 -0
- package/src/core/geom/3d/quaternion/swing_twist_compose.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/swing_twist_compose.js +36 -0
- package/src/core/geom/3d/quaternion/swing_twist_error.d.ts +39 -0
- package/src/core/geom/3d/quaternion/swing_twist_error.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/swing_twist_error.js +85 -0
- package/src/core/geom/vec3/v3_rigid_align_paired_unit_vectors.d.ts +11 -6
- package/src/core/geom/vec3/v3_rigid_align_paired_unit_vectors.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_rigid_align_paired_unit_vectors.js +27 -13
- package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.d.ts +25 -0
- package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.d.ts.map +1 -0
- package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.js +166 -0
- package/src/core/math/linalg/eigen/matrix_top_eigenvector_power_iteration.d.ts +5 -3
- package/src/core/math/linalg/eigen/matrix_top_eigenvector_power_iteration.d.ts.map +1 -1
- package/src/core/math/linalg/eigen/matrix_top_eigenvector_power_iteration.js +5 -3
- package/src/engine/animation/curve/AnimationCurve.d.ts +3 -0
- package/src/engine/animation/curve/AnimationCurve.d.ts.map +1 -1
- package/src/engine/animation/curve/AnimationCurve.js +6 -1
- package/src/engine/ecs/EntityManager.d.ts +9 -1
- package/src/engine/ecs/EntityManager.d.ts.map +1 -1
- package/src/engine/ecs/EntityManager.js +26 -27
- package/src/engine/ecs/gpu/GPUStateAuthority.d.ts +20 -0
- package/src/engine/ecs/gpu/GPUStateAuthority.d.ts.map +1 -1
- package/src/engine/ecs/gpu/GPUStateAuthority.js +21 -0
- package/src/engine/ecs/gpu/GPU_AUTHORITY_EVENT_HOLD_CHANGE.d.ts +13 -0
- package/src/engine/ecs/gpu/GPU_AUTHORITY_EVENT_HOLD_CHANGE.d.ts.map +1 -0
- package/src/engine/ecs/gpu/GPU_AUTHORITY_EVENT_HOLD_CHANGE.js +12 -0
- package/src/engine/ecs/gpu/gpu_authority_grant.d.ts +6 -0
- package/src/engine/ecs/gpu/gpu_authority_grant.d.ts.map +1 -1
- package/src/engine/ecs/gpu/gpu_authority_grant.js +7 -1
- package/src/engine/ecs/gpu/gpu_authority_held.d.ts +14 -0
- package/src/engine/ecs/gpu/gpu_authority_held.d.ts.map +1 -0
- package/src/engine/ecs/gpu/gpu_authority_held.js +23 -0
- package/src/engine/ecs/gpu/gpu_authority_hold.d.ts +23 -0
- package/src/engine/ecs/gpu/gpu_authority_hold.d.ts.map +1 -0
- package/src/engine/ecs/gpu/gpu_authority_hold.js +41 -0
- package/src/engine/ecs/gpu/gpu_authority_unhold.d.ts +14 -0
- package/src/engine/ecs/gpu/gpu_authority_unhold.d.ts.map +1 -0
- package/src/engine/ecs/gpu/gpu_authority_unhold.js +32 -0
- package/src/engine/ecs/ik/IKGoal.d.ts +59 -0
- package/src/engine/ecs/ik/IKGoal.d.ts.map +1 -0
- package/src/engine/ecs/ik/IKGoal.js +83 -0
- package/src/engine/ecs/ik/IKGoalSystem.d.ts +75 -0
- package/src/engine/ecs/ik/IKGoalSystem.d.ts.map +1 -0
- package/src/engine/ecs/ik/IKGoalSystem.js +1456 -0
- package/src/engine/ecs/ik/PLAN.md +179 -0
- package/src/engine/ecs/speaker/VoiceSystem.d.ts +2 -2
- package/src/engine/ecs/speaker/VoiceSystem.d.ts.map +1 -1
- package/src/engine/ecs/storage/BinaryBufferSerializer.d.ts.map +1 -1
- package/src/engine/ecs/storage/BinaryBufferSerializer.js +4 -5
- package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +7 -3
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +1 -2
- package/src/engine/ecs/system/orderSystemsByDeclaredAccess.d.ts +33 -0
- package/src/engine/ecs/system/orderSystemsByDeclaredAccess.d.ts.map +1 -0
- package/src/engine/ecs/system/orderSystemsByDeclaredAccess.js +282 -0
- package/src/engine/ecs/transform/T64WorldCache.js +139 -139
- package/src/engine/ecs/transform/t64_evaluate_local.d.ts +25 -0
- package/src/engine/ecs/transform/t64_evaluate_local.d.ts.map +1 -0
- package/src/engine/ecs/transform/t64_evaluate_local.js +35 -0
- package/src/engine/ecs/transform/t64_evaluate_world.d.ts +5 -3
- package/src/engine/ecs/transform/t64_evaluate_world.d.ts.map +1 -1
- package/src/engine/ecs/transform/t64_evaluate_world.js +128 -126
- package/src/engine/graphics3/AnimationGraphSystem.d.ts +2 -0
- package/src/engine/graphics3/AnimationGraphSystem.d.ts.map +1 -1
- package/src/engine/graphics3/AnimationGraphSystem.js +337 -329
- package/src/engine/graphics3/AnimationSystem.d.ts +2 -0
- package/src/engine/graphics3/AnimationSystem.d.ts.map +1 -1
- package/src/engine/graphics3/AnimationSystem.js +360 -352
- package/src/engine/physics/cloth/MEASUREMENTS.md +130 -0
- package/src/engine/physics/cloth/PLAN.md +16 -1
- package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +5 -6
- package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +5 -6
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +4 -7
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothInstance.js +4 -7
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +4 -3
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothSystem.js +4 -3
- package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothWorld.js +10 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +24 -24
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +46 -43
- package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts +1 -1
- package/src/engine/physics/cloth/ecs/cloth_write_back.js +1 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts +0 -35
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +1 -83
- package/src/engine/physics/ecs/ContactProbe.d.ts +73 -0
- package/src/engine/physics/ecs/ContactProbe.d.ts.map +1 -0
- package/src/engine/physics/ecs/ContactProbe.js +103 -0
- package/src/engine/physics/ecs/ContactProbeSystem.d.ts +44 -0
- package/src/engine/physics/ecs/ContactProbeSystem.d.ts.map +1 -0
- package/src/engine/physics/ecs/ContactProbeSystem.js +397 -0
- package/src/engine/physics/fluid/ecs/FluidComponent.d.ts +1 -1
- package/src/engine/physics/fluid/ecs/FluidComponent.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/inverse_kinematics/frame_fabrik/FrameFabrikProblem.d.ts +205 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/FrameFabrikProblem.d.ts.map +1 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/FrameFabrikProblem.js +245 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_limit_from_joint.d.ts +21 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_limit_from_joint.d.ts.map +1 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_limit_from_joint.js +63 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.d.ts +33 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.d.ts.map +1 -0
- package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.js +874 -0
- package/src/engine/physics/inverse_kinematics/joint_hinge_mid_axis.d.ts +27 -0
- package/src/engine/physics/inverse_kinematics/joint_hinge_mid_axis.d.ts.map +1 -0
- package/src/engine/physics/inverse_kinematics/joint_hinge_mid_axis.js +77 -0
- package/src/engine/physics/inverse_kinematics/two_bone_path_solve.d.ts +36 -0
- package/src/engine/physics/inverse_kinematics/two_bone_path_solve.d.ts.map +1 -0
- package/src/engine/physics/inverse_kinematics/two_bone_path_solve.js +97 -0
- package/src/engine/physics/inverse_kinematics/two_joint_ik.d.ts +33 -11
- package/src/engine/physics/inverse_kinematics/two_joint_ik.d.ts.map +1 -1
- package/src/engine/physics/inverse_kinematics/two_joint_ik.js +158 -67
- package/src/shade/renderer/animation/ANIMATION_TRACK_BINDING_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/animation/ANIMATION_TRACK_BINDING_STRUCT.js +10 -1
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts +10 -4
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +251 -23
- package/src/shade/renderer/animation/POSE_ACCUMULATOR_LAYOUT.d.ts +12 -4
- package/src/shade/renderer/animation/POSE_ACCUMULATOR_LAYOUT.d.ts.map +1 -1
- package/src/shade/renderer/animation/POSE_ACCUMULATOR_LAYOUT.js +12 -4
- package/src/shade/renderer/animation/chunk_pose_accumulator.d.ts.map +1 -1
- package/src/shade/renderer/animation/chunk_pose_accumulator.js +14 -1
- package/src/shade/renderer/animation/pose/PoseChannelIndex.d.ts +78 -0
- package/src/shade/renderer/animation/pose/PoseChannelIndex.d.ts.map +1 -0
- package/src/shade/renderer/animation/pose/PoseChannelIndex.js +274 -0
- package/src/shade/renderer/animation/pose/PosePlayback.d.ts +40 -3
- package/src/shade/renderer/animation/pose/PosePlayback.d.ts.map +1 -1
- package/src/shade/renderer/animation/pose/PosePlayback.js +77 -3
- package/src/shade/renderer/animation/pose/pose_evaluate_local.d.ts +21 -3
- package/src/shade/renderer/animation/pose/pose_evaluate_local.d.ts.map +1 -1
- package/src/shade/renderer/animation/pose/pose_evaluate_local.js +79 -11
- package/src/shade/renderer/animation/shader_animation_apply.js +1 -1
- package/src/shade/renderer/animation/shader_animation_pose_resolve.d.ts +3 -2
- package/src/shade/renderer/animation/shader_animation_pose_resolve.d.ts.map +1 -1
- package/src/shade/renderer/animation/shader_animation_pose_resolve.js +187 -176
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/object_property/chunk_write_object_property.d.ts +3 -1
- package/src/shade/renderer/object_property/chunk_write_object_property.d.ts.map +1 -1
- package/src/shade/renderer/object_property/chunk_write_object_property.js +5 -2
- package/src/shade/renderer/object_property/chunk_write_object_property_node3d.d.ts +4 -0
- package/src/shade/renderer/object_property/chunk_write_object_property_node3d.d.ts.map +1 -1
- package/src/shade/renderer/object_property/chunk_write_object_property_node3d.js +6 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/scene/optimization/geometry/geometry_deduplicate.d.ts +1 -1
- package/src/shade/renderer/scene/optimization/geometry/geometry_deduplicate.d.ts.map +1 -1
- package/src/shade/renderer/scene/optimization/geometry/geometry_deduplicate.js +135 -73
- package/src/shade/renderer/scene/optimization/geometry/internal/cover_greedy.d.ts +7 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/cover_greedy.d.ts.map +1 -1
- package/src/shade/renderer/scene/optimization/geometry/internal/cover_greedy.js +29 -1
- package/src/shade/renderer/scene/optimization/geometry/internal/face_neighbourhood_hash.d.ts +19 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/face_neighbourhood_hash.d.ts.map +1 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/face_neighbourhood_hash.js +167 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/neighbourhood_anchor_index.d.ts +25 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/neighbourhood_anchor_index.d.ts.map +1 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/neighbourhood_anchor_index.js +124 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/seed_correspondence_from_face_pair.d.ts +19 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/seed_correspondence_from_face_pair.d.ts.map +1 -0
- package/src/shade/renderer/scene/optimization/geometry/internal/seed_correspondence_from_face_pair.js +48 -0
- package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts +17 -1
- package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
- package/src/shade/renderer/scene/rows/GPUSceneRows.js +29 -3
- package/src/engine/ecs/ik/IKConstraint.d.ts +0 -52
- package/src/engine/ecs/ik/IKConstraint.d.ts.map +0 -1
- package/src/engine/ecs/ik/IKConstraint.js +0 -95
- package/src/engine/ecs/ik/IKProblem.d.ts +0 -45
- package/src/engine/ecs/ik/IKProblem.d.ts.map +0 -1
- package/src/engine/ecs/ik/IKProblem.js +0 -58
- package/src/engine/ecs/ik/IKSolver.d.ts +0 -8
- package/src/engine/ecs/ik/IKSolver.d.ts.map +0 -1
- package/src/engine/ecs/ik/IKSolver.js +0 -9
- package/src/engine/ecs/ik/InverseKinematics.d.ts +0 -48
- package/src/engine/ecs/ik/InverseKinematics.d.ts.map +0 -1
- package/src/engine/ecs/ik/InverseKinematics.js +0 -119
- package/src/engine/ecs/ik/InverseKinematicsSerializationAdapter.d.ts +0 -19
- package/src/engine/ecs/ik/InverseKinematicsSerializationAdapter.d.ts.map +0 -1
- package/src/engine/ecs/ik/InverseKinematicsSerializationAdapter.js +0 -72
- package/src/engine/ecs/ik/InverseKinematicsSystem.d.ts +0 -54
- package/src/engine/ecs/ik/InverseKinematicsSystem.d.ts.map +0 -1
- package/src/engine/ecs/ik/InverseKinematicsSystem.js +0 -212
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts +0 -8
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +0 -1
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +0 -153
- package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.d.ts +0 -5
- package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.d.ts.map +0 -1
- package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.js +0 -120
- package/src/engine/ecs/ik/ik_bone_chain.d.ts +0 -64
- package/src/engine/ecs/ik/ik_bone_chain.d.ts.map +0 -1
- package/src/engine/ecs/ik/ik_bone_chain.js +0 -142
- package/src/engine/ecs/ik/ik_probe_terrain_contact.d.ts +0 -42
- package/src/engine/ecs/ik/ik_probe_terrain_contact.d.ts.map +0 -1
- package/src/engine/ecs/ik/ik_probe_terrain_contact.js +0 -123
- package/src/engine/ecs/ik/ik_write_local_rotation.d.ts +0 -18
- package/src/engine/ecs/ik/ik_write_local_rotation.d.ts.map +0 -1
- package/src/engine/ecs/ik/ik_write_local_rotation.js +0 -32
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# IK — trees, goals on bones, joints and contacts from physics
|
|
2
|
+
|
|
3
|
+
> Revision 5, 2026-10-06. **S1–S4 are implemented** (2026-10-06); S5 is later. What implementing
|
|
4
|
+
> it measured or decided beyond this text is in §9. The solver (§2) was accepted at revision 2. Revisions 3–5 settle the components, limits, contacts and clip-driven bones. Each
|
|
5
|
+
> revision went through an adversarial review. Numbers come from one-off probes outside the repo,
|
|
6
|
+
> and every stage re-pins what it relies on with a failing spec first.
|
|
7
|
+
|
|
8
|
+
## 1. Where it stands (master `c01c0ebbd`)
|
|
9
|
+
|
|
10
|
+
- `physics/inverse_kinematics/fabrik/` solves one chain, and nothing calls it.
|
|
11
|
+
- The ECS IK (`2BIK`, `1BSA`) reads name-keyed `IKConstraint` lists on an `InverseKinematics`
|
|
12
|
+
component, and probes `Terrain` directly.
|
|
13
|
+
- **Holds** (`gpu_authority_hold`): a CPU writer takes a joint's `TransformAttachment`. The
|
|
14
|
+
animation manager mutes every clip binding that names the joint, and `t64_evaluate_world` /
|
|
15
|
+
`T64WorldCache` read the held offset. Cloth uses this.
|
|
16
|
+
- `two_joint_ik` takes the knee's hinge. The CPU pose twin matches the GPU resolve.
|
|
17
|
+
|
|
18
|
+
## 2. Solver: frame FABRIK over a tree
|
|
19
|
+
|
|
20
|
+
**Unchanged.**
|
|
21
|
+
- Every solved joint is a frame: rigid offsets to its children, with its rotation fitted inward
|
|
22
|
+
(from its old position) and outward. With one child this is the paper's pull.
|
|
23
|
+
- Fits start from the carried rotation, so nothing drifts.
|
|
24
|
+
- A goal's path runs from its effector to its `chain_root`. A link carries the weight of the goals
|
|
25
|
+
whose path holds both ends, and a `chain_root` takes rotation, never position.
|
|
26
|
+
- The target is `lerp(carried, goal, w)`. Weight 0 gives the same pose as no goal, and the result is
|
|
27
|
+
continuous in `w`.
|
|
28
|
+
- Two-bone paths use `two_joint_ik`. Twist is held from the animated pose, and the effector's
|
|
29
|
+
rotation is slerped toward the goal after the solve.
|
|
30
|
+
- Worst error at 16 iterations is 0.005–1.34 mm across the probe cases. The paper's point branches
|
|
31
|
+
miss by 8–49 mm once written back.
|
|
32
|
+
|
|
33
|
+
## 3. Components: on the bones, limits from physics
|
|
34
|
+
|
|
35
|
+
Data lives on the entity it describes, and the tree is the `TransformAttachment` hierarchy. Meep's
|
|
36
|
+
own rig setup puts these components on the bones when the instance is created.
|
|
37
|
+
|
|
38
|
+
**`IKGoal`, on the effector bone.**
|
|
39
|
+
|
|
40
|
+
| field | meaning |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `target` | entity id. Its world pose is the goal. |
|
|
43
|
+
| `chain_root` | entity id, an ancestor-or-self of the effector: the highest bone this goal rotates |
|
|
44
|
+
| `position_weight`, `rotation_weight` | 0..1 |
|
|
45
|
+
|
|
46
|
+
- References are fixed while attached.
|
|
47
|
+
- The goal removes itself when its target or any joint on its path is destroyed, as
|
|
48
|
+
`TransformAttachment` does when its parent dies.
|
|
49
|
+
- A rejected goal holds nothing. Rejection is a real branch plus an assert, because asserts are
|
|
50
|
+
stripped in production. A goal is rejected for:
|
|
51
|
+
- a `chain_root` that is not an ancestor-or-self of the effector;
|
|
52
|
+
- a target hanging below its own path;
|
|
53
|
+
- a two-bone path whose knee has no bend side.
|
|
54
|
+
- A target hanging below *another* goal's joints in the same tree (a grip on a weapon held by the
|
|
55
|
+
other hand) is read from the solver's current frames on every iteration. Convergence is measured
|
|
56
|
+
in S4.
|
|
57
|
+
- Across groups, dependencies order the solves, and a cycle rejects both groups.
|
|
58
|
+
|
|
59
|
+
**Joint limits are the physics `Joint` on the bone.** Physics already has this as a 6-DOF
|
|
60
|
+
description (PhysX D6): per-axis `DofMode`, limits, `localBasisA/B`, and two entity ids.
|
|
61
|
+
- On a bone, `entityA` is the parent bone and `entityB` is the bone itself.
|
|
62
|
+
- Asserted at link:
|
|
63
|
+
- `entityA` equals `TransformAttachment.parent`;
|
|
64
|
+
- `localBasisA = rest_local · localBasisB`;
|
|
65
|
+
- `swingTwist` is on. Without it, physics measures angles as 2 sin(θ/2), so limits above 114.6°
|
|
66
|
+
never engage and physics and IK would disagree.
|
|
67
|
+
- **Hinge** (`asHinge`) gives the knee's `mid_axis`. Its bend side comes from asymmetric limits.
|
|
68
|
+
- **Cone-twist** gives twist on X and separate Y/Z swing, and IK clamps to the same box physics
|
|
69
|
+
allows.
|
|
70
|
+
- IK ignores anchors, linear DOFs, SPRING and MOTOR.
|
|
71
|
+
- **Shared maths.** `swing_twist_error*` moves from `physics/constraint/solve_constraints.js` to
|
|
72
|
+
`core/geom/3d/quaternion/`; the clamp is new.
|
|
73
|
+
- **Contract.** Physics simulates a `Joint` only when both sides have a body (or `JOINT_WORLD`).
|
|
74
|
+
Otherwise it is a kinematic limit. Nothing observes `Joint` today.
|
|
75
|
+
|
|
76
|
+
**Solver settings** (`max_iterations`, `tolerance`) belong to the system.
|
|
77
|
+
|
|
78
|
+
## 4. Contacts come from physics
|
|
79
|
+
|
|
80
|
+
A foot is a joint, the ground is a collider, and contact points are physics' job. IK probes
|
|
81
|
+
nothing: today's terrain prober goes, and nothing here touches `Terrain` or `ClingToTerrain`.
|
|
82
|
+
|
|
83
|
+
- **A goal that should touch a surface** gets its target from a physics query: a ray or shape cast
|
|
84
|
+
(`PhysicsSystem.raycast`, `shapeCast`) from a reference the rig provides, returning a
|
|
85
|
+
`PhysicsSurfacePoint` (position, normal, entity).
|
|
86
|
+
- **The component lives in the physics package.** It is a generic contact-probe component that
|
|
87
|
+
places its entity at the hit and aligns it to the normal. It knows nothing about IK or feet, so
|
|
88
|
+
footsteps, decals and AI can use it too. Physics has no query component today;
|
|
89
|
+
`PhysicsSurfacePoint` was written with "a ground sensor" in mind.
|
|
90
|
+
- **Its contract:**
|
|
91
|
+
- it exact-evaluates a GPU-composed reference through `T64WorldCache`;
|
|
92
|
+
- it writes its own attachment offset;
|
|
93
|
+
- it skips the character's own colliders (same `ParentEntity` root);
|
|
94
|
+
- it runs in `update`, before IK;
|
|
95
|
+
- its reference must not be held by IK, or the target would chase IK's output (asserted by the
|
|
96
|
+
IK side, §3).
|
|
97
|
+
- Pelvis lowering comes later.
|
|
98
|
+
|
|
99
|
+
## 5. Bones a clip drives: IK holds them
|
|
100
|
+
|
|
101
|
+
Downstream code (`t64_evaluate_world`, `T64WorldCache`, sockets, cloth anchors, the GPU hierarchy)
|
|
102
|
+
reads the held offset exactly as it reads cloth's, and never learns that IK exists.
|
|
103
|
+
|
|
104
|
+
- **Link:** hold `TransformAttachment` on every joint of the path, capture the offsets, and write the
|
|
105
|
+
evaluated base pose at once, so no frame shows the bind pose.
|
|
106
|
+
- **Every tick:** write each held joint's full local: translation, rotation and scale, since a hold
|
|
107
|
+
mutes every channel. The base comes from `pose_evaluate_local`, never from a held offset.
|
|
108
|
+
- **Unlink:** write the captured offset back, then unhold, guarded by `entityExists`.
|
|
109
|
+
- **One holder per joint:** IK on a cloth-held joint trips the existing assert.
|
|
110
|
+
- **Cost:** 37 / 127 / 531 µs per character per tick at 2 / 8 / 30 registered clips (34 levels),
|
|
111
|
+
because every level scans every channel of every clip.
|
|
112
|
+
- **Ordering:** IK declares what it reads, `Animation` and `AnimationGraphController`. The drivers
|
|
113
|
+
declare that they write them. With the scheduler fixed (below), IK then runs after the drivers.
|
|
114
|
+
Cloth needs no order against IK: it runs in `fixedUpdate` on the previous update's consistent
|
|
115
|
+
state.
|
|
116
|
+
|
|
117
|
+
## 6. Prerequisites (separate tasks)
|
|
118
|
+
|
|
119
|
+
1. **Holds: LANDED** (`84f2f7267`, `c01c0ebbd`). A clip that binds during a hold takes the rest the holder
|
|
120
|
+
leaves when the hold ends, and that offset reaches the row.
|
|
121
|
+
2. **Pose evaluator:** index channels by target (the cost row above). Sockets and cloth gain too.
|
|
122
|
+
3. **Scheduler:** a system that reads X and writes Y sorts before the writer of X. Measured with
|
|
123
|
+
synthetic components: Producer (X) and Consumer (X → Y) order as `Consumer > Producer` in both
|
|
124
|
+
registration orders. `system_execution_order.spec.js` covers only a pure reader. The fix stays
|
|
125
|
+
inside derived ordering.
|
|
126
|
+
|
|
127
|
+
## 7. Stages
|
|
128
|
+
|
|
129
|
+
Each stage is red-then-green.
|
|
130
|
+
|
|
131
|
+
1. **S1 Frame FABRIK:** a pure solver over typed arrays.
|
|
132
|
+
2. **S2 Two-bone paths:** `two_joint_ik` with the hinge read from the `Joint`.
|
|
133
|
+
3. **S3 Limits:** the shared swing-twist maths, the clamp, and `Joint` frames inside the passes.
|
|
134
|
+
4. **S4 ECS, landing together:**
|
|
135
|
+
- `IKGoal` and its system (groups, holds, base evaluation, target reads, declarations);
|
|
136
|
+
- the physics contact-probe component;
|
|
137
|
+
- in the same change, `InverseKinematics`, `IKConstraint`, `2BIK`, `1BSA`, the terrain prober
|
|
138
|
+
and the adapter go (hard break).
|
|
139
|
+
5. **S5 Later:** pelvis lowering, twist distribution, goal priority, pole goals.
|
|
140
|
+
|
|
141
|
+
## 8. Sources
|
|
142
|
+
|
|
143
|
+
- [A] Aristidou, Chrysanthou, Lasenby, *Extending FABRIK with model constraints*, CAVW 2016
|
|
144
|
+
- [C] Aristidou, Lasenby, *FABRIK*, Graphical Models 73, 2011
|
|
145
|
+
- [D] Baerlocher, Boulic, *Parametrization and range of motion of the ball-and-socket joint*, 2001
|
|
146
|
+
- [G] UE 5.4 source: `Plugins/Experimental/FullBodyIK/Source/PBIK/`, `AnimNode_FootPlacement`
|
|
147
|
+
- [N] ozz-animation `ik_two_bone_job.h`, `samples/foot_ik/sample_foot_ik.cc`
|
|
148
|
+
|
|
149
|
+
## 9. After implementation (2026-10-06)
|
|
150
|
+
|
|
151
|
+
Measured while implementing S1–S4, and decided with the owner where it changed anything.
|
|
152
|
+
|
|
153
|
+
- **The fit** is 8 steps of power iteration warm-started from the carried rotation: 110–130 ns a fit
|
|
154
|
+
against 610–790 ns for the exact Jacobi fit, which also turns the chest about its shoulder line
|
|
155
|
+
from rounding (a 40–175° bend at the chest joint, converged, where the warm start bends 0–54°).
|
|
156
|
+
- **The 0/0** of this mechanism is a target on the effector's parent, at any bone lengths: the parent
|
|
157
|
+
is fitted toward a child sitting on its old position. A target at a limb's top with equal bones is
|
|
158
|
+
finite and reached. A straight arm turned only at the shoulder, its target exactly on the elbow,
|
|
159
|
+
has no side to fold to and stays (finite); 1e-12 m off, it reaches.
|
|
160
|
+
- **An effector inside another goal's path** (a goal on the elbow under a goal on the hand) is one
|
|
161
|
+
more proposal in its joint's mean, weighted by its goal's weight (owner's decision; pinning it at
|
|
162
|
+
its target moved the pose 161 mm at weight 0). Weight 0 is no goal, 0.001 moves it 0.05 mm, and two
|
|
163
|
+
equal goals that disagree meet half way.
|
|
164
|
+
- **`FrameFabrikProblem` takes `base_local_rotation`**: a goal's root is limited against its
|
|
165
|
+
parent's fixed frame, which world rotations alone do not give.
|
|
166
|
+
- **A hinge alone on a longer path can lock**: the arc fits never twist the shoulder to bring an
|
|
167
|
+
elbow's hinge plane round, so a target off it left the arm straight, 350 / 334 mm short; a
|
|
168
|
+
cone-twist on the shoulder unlocks it (1.4 mm at 16 iterations, 0 at 64). Hinges also slow a grip
|
|
169
|
+
pair down: 65 / 220 mm at 16 iterations, 0 at 64. Two-bone paths are analytic and unaffected.
|
|
170
|
+
Stated, not fixed.
|
|
171
|
+
- **"Below its own path"** means the deepest posed bone the target hangs from is a bone of the goal's
|
|
172
|
+
own path. A grip on a weapon in the other hand hangs below the gripping hand's own chest too, but
|
|
173
|
+
the other arm's bones carry it, so it is read from the solver's frames. Known only with every goal,
|
|
174
|
+
so it is found when the groups are formed; a target below the effector itself is refused at link.
|
|
175
|
+
- **The contact probe** casts a sphere of `radius`, a ray at 0. It and IK both read and write
|
|
176
|
+
`TransformAttachment`, so declared access leaves them a cycle and the scheduler keeps their
|
|
177
|
+
registration order: the probe is registered first (owner's decision). That its reference must not
|
|
178
|
+
be held by a CPU writer is documented on `ContactProbe`, not asserted (owner's decision): IK cannot
|
|
179
|
+
check it without knowing the probe's component.
|
|
@@ -9,7 +9,7 @@ export class VoiceSystem extends AbstractContextSystem<any> {
|
|
|
9
9
|
font_size: number;
|
|
10
10
|
});
|
|
11
11
|
dependencies: (typeof Voice)[];
|
|
12
|
-
components_used: (ResourceAccessSpecification<typeof Transform64> | ResourceAccessSpecification<typeof GUIElement> | ResourceAccessSpecification<typeof ViewportPosition> | ResourceAccessSpecification<typeof
|
|
12
|
+
components_used: (ResourceAccessSpecification<typeof Transform64> | ResourceAccessSpecification<typeof GUIElement> | ResourceAccessSpecification<typeof ViewportPosition> | ResourceAccessSpecification<typeof TransformAttachment> | ResourceAccessSpecification<typeof HeadsUpDisplay> | ResourceAccessSpecification<typeof SerializationMetadata> | ResourceAccessSpecification<typeof BehaviorComponent>)[];
|
|
13
13
|
/**
|
|
14
14
|
*
|
|
15
15
|
* @type {Localization}
|
|
@@ -105,8 +105,8 @@ import { Transform64 } from "../transform/Transform64.js";
|
|
|
105
105
|
import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
|
|
106
106
|
import GUIElement from "../gui/GUIElement.js";
|
|
107
107
|
import ViewportPosition from "../gui/position/ViewportPosition.js";
|
|
108
|
-
import HeadsUpDisplay from "../gui/hud/HeadsUpDisplay.js";
|
|
109
108
|
import { TransformAttachment } from "../transform-attachment/TransformAttachment.js";
|
|
109
|
+
import HeadsUpDisplay from "../gui/hud/HeadsUpDisplay.js";
|
|
110
110
|
import { SerializationMetadata } from "../components/SerializationMetadata.js";
|
|
111
111
|
import { BehaviorComponent } from "../../intelligence/behavior/ecs/BehaviorComponent.js";
|
|
112
112
|
//# sourceMappingURL=VoiceSystem.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/speaker/VoiceSystem.js"],"names":[],"mappings":"AA0KA;IA2CI;;;;OAIG;IACH;;;OA8CC;IA7FD,+BAAuB;IACvB,gZAQE;IAEF;;;OAGG;IACH,0BAAmB;IAEnB;;;OAGG;IACH,eAAW;IAEX;;;OAGG;IACH,4BAAa;IAEb;;;OAGG;IACH,8BAAY;IAEZ;;;;OAIG;IACH,iCAAqC;IAajC;;;OAGG;IACH,eAAoB;IAEpB;;;;OAIG;IACH,kBAAkC;IAGlC;;;;OAIG;IACH,eAAkB;IAElB;;;;OAIG;IACH,oBAAgC;IAEhC;;;OAGG;IACH,oBAAqC;IAErC;;;;OAIG;IACH,gBAAoB;IAGxB;;;OAGG;IACH,kBAFY,MAAM,CAIjB;IAEF,2CAqBE;IAED;;;;;OAKG;IACH,uBAJW,MAAM,UACN,MAAM,SACN,KAAK,QAsBf;IAED;;;;;;OAMG;IACH,wBAoBC;IAED;;;;;OAKG;IACH,gBAJW,MAAM,WACN,MAAM,SACN,KAAK,QAiIf;CACJ;sCA5cqC,oCAAoC;sBAKpD,YAAY;4BAFN,6BAA6B;4CAxBb,oDAAoD;uBAkBzE,sBAAsB;6BAEhB,qCAAqC;
|
|
1
|
+
{"version":3,"file":"VoiceSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/speaker/VoiceSystem.js"],"names":[],"mappings":"AA0KA;IA2CI;;;;OAIG;IACH;;;OA8CC;IA7FD,+BAAuB;IACvB,gZAQE;IAEF;;;OAGG;IACH,0BAAmB;IAEnB;;;OAGG;IACH,eAAW;IAEX;;;OAGG;IACH,4BAAa;IAEb;;;OAGG;IACH,8BAAY;IAEZ;;;;OAIG;IACH,iCAAqC;IAajC;;;OAGG;IACH,eAAoB;IAEpB;;;;OAIG;IACH,kBAAkC;IAGlC;;;;OAIG;IACH,eAAkB;IAElB;;;;OAIG;IACH,oBAAgC;IAEhC;;;OAGG;IACH,oBAAqC;IAErC;;;;OAIG;IACH,gBAAoB;IAGxB;;;OAGG;IACH,kBAFY,MAAM,CAIjB;IAEF,2CAqBE;IAED;;;;;OAKG;IACH,uBAJW,MAAM,UACN,MAAM,SACN,KAAK,QAsBf;IAED;;;;;;OAMG;IACH,wBAoBC;IAED;;;;;OAKG;IACH,gBAJW,MAAM,WACN,MAAM,SACN,KAAK,QAiIf;CACJ;sCA5cqC,oCAAoC;sBAKpD,YAAY;4BAFN,6BAA6B;4CAxBb,oDAAoD;uBAkBzE,sBAAsB;6BAEhB,qCAAqC;oCAG9B,gDAAgD;2BAJzD,8BAA8B;sCAJnB,wCAAwC;kCAP5C,sDAAsD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BinaryBufferSerializer.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/storage/BinaryBufferSerializer.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"BinaryBufferSerializer.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/storage/BinaryBufferSerializer.js"],"names":[],"mappings":";AAkDA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IACI;;;OAGG;IACH,sCAAgB;IAEhB;;;OAGG;IACH,QAFU,GAAG,GAAC,IAAI,CAEJ;IAEd;;;;OAIG;IACH,qEAmIC;CACJ"}
|
|
@@ -24,15 +24,14 @@ function isComponentClassSerializable(componentClass) {
|
|
|
24
24
|
* @returns {boolean}
|
|
25
25
|
*/
|
|
26
26
|
function isEntityTransient(entity, dataset, smComponentIndex, componentInstance) {
|
|
27
|
-
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
//check component instance flag
|
|
27
|
+
//check component instance flag, it does not depend on SerializationMetadata
|
|
32
28
|
if (componentInstance[COMPONENT_SERIALIZATION_TRANSIENT_FIELD] === true) {
|
|
33
29
|
return true;
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
if (smComponentIndex === -1) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
*
|
|
@@ -57,8 +57,8 @@ threw, no save containing these behaviours can exist, so there is no data to sta
|
|
|
57
57
|
|
|
58
58
|
## Adapters still without a spec
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
`ObjectBasedClassSerializationAdapter.js` (a helper) are not adapters — so **
|
|
60
|
+
28 files, of which `BinaryClassSerializationAdapter.js` (the base class) and
|
|
61
|
+
`ObjectBasedClassSerializationAdapter.js` (a helper) are not adapters — so **26 real ones**. Every entry is an
|
|
62
62
|
adapter nothing currently verifies.
|
|
63
63
|
|
|
64
64
|
Sixteen of these are the ones whose `serialize` threw under the old harness. That is not a coincidence and it
|
|
@@ -71,7 +71,6 @@ fixture rather than a default instance. `FogOfWar` needs correctly-sized height
|
|
|
71
71
|
| `Animation` | `engine/ecs/animation/AnimationSerializationAdapter.js` |
|
|
72
72
|
| `FogOfWar` | `engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js` |
|
|
73
73
|
| `GUIElement` | `engine/ecs/gui/GUIElementSerializationAdapter.js` |
|
|
74
|
-
| `InverseKinematics` | `engine/ecs/ik/InverseKinematicsSerializationAdapter.js` |
|
|
75
74
|
| `Book` | `engine/ecs/storage/binary/BinaryClassSerializationAdapter.js` |
|
|
76
75
|
| `?` | `engine/ecs/storage/binary/object/ObjectBasedClassSerializationAdapter.js` |
|
|
77
76
|
| `Terrain` | `engine/ecs/terrain/serialization/TerrainSerializationAdapter.js` |
|
|
@@ -106,6 +105,11 @@ The per-upgrader spec should assert **the restored field values**, not the byte
|
|
|
106
105
|
asserted. The byte count was a proxy it used because it had no idea what the values ought to be; a spec that
|
|
107
106
|
names one upgrader does know, and should say so.
|
|
108
107
|
|
|
108
|
+
Upgraders added since carry that spec from the start. The first was IK's, which wrote a version 0 constraint
|
|
109
|
+
field by field, ran it through the chain the engine registry hands a load, and asserted every restored field;
|
|
110
|
+
it went with the component it upgraded (2026-10-06, a hard break: IK's goals live on bones, which are never
|
|
111
|
+
saved).
|
|
112
|
+
|
|
109
113
|
### Goldens to port
|
|
110
114
|
|
|
111
115
|
```js
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"populateEngineSerializationRegistry.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/storage/populateEngineSerializationRegistry.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"populateEngineSerializationRegistry.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/storage/populateEngineSerializationRegistry.js"],"names":[],"mappings":"AAmLA;;;;;;;;GAQG;AACH,iGA8GC"}
|
|
@@ -17,7 +17,6 @@ import { HeadsUpDisplaySerializationAdapter } from "../gui/hud/HeadsUpDisplaySer
|
|
|
17
17
|
import {
|
|
18
18
|
ViewportPositionSerializationAdapter
|
|
19
19
|
} from "../gui/position/ViewportPositionSerializationAdapter.js";
|
|
20
|
-
import { InverseKinematicsSerializationAdapter } from "../ik/InverseKinematicsSerializationAdapter.js";
|
|
21
20
|
import { NameSerializationAdapter } from "../name/NameSerializationAdapter.js";
|
|
22
21
|
import { TeamSerializationAdapter } from "../team/TeamSerializationAdapter.js";
|
|
23
22
|
import ClingToTerrain from "../terrain/ecs/cling/ClingToTerrain.js";
|
|
@@ -237,7 +236,6 @@ export function populateEngineSerializationRegistry(registry) {
|
|
|
237
236
|
new GUIElementSerializationAdapter(),
|
|
238
237
|
new HeadsUpDisplaySerializationAdapter(),
|
|
239
238
|
new BehaviorComponentSerializationAdapter(),
|
|
240
|
-
new InverseKinematicsSerializationAdapter(),
|
|
241
239
|
new TooltipComponentSerializationAdapter(),
|
|
242
240
|
new DecalSerializationAdapter(),
|
|
243
241
|
new PickableSerializationAdapter(),
|
|
@@ -297,4 +295,5 @@ export function populateEngineSerializationRegistry(registry) {
|
|
|
297
295
|
registry.registerUpgrader(TopDownCameraController.typeName, new TopDownCameraControllerSerializationUpgrader_0_1());
|
|
298
296
|
|
|
299
297
|
registry.registerUpgrader(ClingToTerrain.typeName, new ClingToTerrainSerializationUpgrader_0_1());
|
|
298
|
+
|
|
300
299
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders systems by what they declare they touch: a system that writes (or creates) a component
|
|
3
|
+
* runs before every other system that reads it, when the two run in the same pass.
|
|
4
|
+
*
|
|
5
|
+
* A pass is `fixedUpdate` or `update`. Every fixed step runs before the update pass whatever this
|
|
6
|
+
* order says, so a write in one pass and a read in the other orders nothing, and is not counted —
|
|
7
|
+
* counting it could only close a cycle out of edges that are each real. One order serves both
|
|
8
|
+
* passes, so a system in both carries its edges into both.
|
|
9
|
+
*
|
|
10
|
+
* Without a cycle the result is a topological order of those edges. Where they leave a choice, the
|
|
11
|
+
* system with the higher priority runs first, and of equal priorities the one earlier in `systems`.
|
|
12
|
+
*
|
|
13
|
+
* A cycle — two systems that both read and write one component is the common one — has no order
|
|
14
|
+
* that respects every edge. So the rule is stated over cycles: the next system to run is the
|
|
15
|
+
* highest priority (then earliest) of those that wait on nothing but their own cycle, and a system
|
|
16
|
+
* on no cycle that waits on nothing qualifies as its own. Running a member of a cycle drops its
|
|
17
|
+
* edges from the rest of that cycle and nothing else. A system that waits on a cycle from outside
|
|
18
|
+
* it never qualifies before the cycle has run, so the only edges dropped are edges on a cycle.
|
|
19
|
+
*
|
|
20
|
+
* A cycle and a system that waits on nothing compete on those same terms, so a cycle runs where
|
|
21
|
+
* its best member's priority puts it, never held back behind systems no edge relates to it. Where
|
|
22
|
+
* sorting by priority alone already respects every edge, the result is that sort; and the result
|
|
23
|
+
* depends on nothing but the declarations, the passes, the priorities and the order of `systems`.
|
|
24
|
+
*
|
|
25
|
+
* O(n²) per system placed, O(n³) in all; it runs when the set of running systems changes.
|
|
26
|
+
*
|
|
27
|
+
* @param {System[]} systems sorted in place, the first `system_count` of them
|
|
28
|
+
* @param {number} system_count
|
|
29
|
+
* @param {Float64Array|number[]} priorities one per system, in the order `systems` arrives in; higher runs earlier
|
|
30
|
+
*/
|
|
31
|
+
export function orderSystemsByDeclaredAccess(systems: import("../System.js").System<any>[], system_count: number, priorities: Float64Array | number[]): void;
|
|
32
|
+
export type System = import("../System.js").System<any>;
|
|
33
|
+
//# sourceMappingURL=orderSystemsByDeclaredAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orderSystemsByDeclaredAccess.d.ts","sourceRoot":"","sources":["../../../../../src/engine/ecs/system/orderSystemsByDeclaredAccess.js"],"names":[],"mappings":"AA8JA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,sDAJW,oCAAQ,gBACR,MAAM,cACN,YAAY,GAAC,MAAM,EAAE,QA+F/B"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { noop } from "../../../core/function/noop.js";
|
|
2
|
+
import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import("../System.js").System} System
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PASS_FIXED_UPDATE = 1;
|
|
9
|
+
const PASS_UPDATE = 2;
|
|
10
|
+
|
|
11
|
+
const WRITES = ResourceAccessKind.Write | ResourceAccessKind.Create;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Which of the two passes over the execution order a system takes part in. `openFixedStep` and
|
|
15
|
+
* `closeFixedStep` are not counted: they frame a step rather than simulate in it, and run outside
|
|
16
|
+
* the derived order on purpose (see {@link System#openFixedStep}).
|
|
17
|
+
*
|
|
18
|
+
* @param {System} system
|
|
19
|
+
* @returns {number} bitmask of PASS_* flags
|
|
20
|
+
*/
|
|
21
|
+
function system_passes(system) {
|
|
22
|
+
let result = 0;
|
|
23
|
+
|
|
24
|
+
if (system.fixedUpdate !== noop) {
|
|
25
|
+
result |= PASS_FIXED_UPDATE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (system.update !== noop) {
|
|
29
|
+
result |= PASS_UPDATE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Map<Function, number[]>} map
|
|
37
|
+
* @param {Function} component
|
|
38
|
+
* @param {number} system_index
|
|
39
|
+
*/
|
|
40
|
+
function map_push(map, component, system_index) {
|
|
41
|
+
const list = map.get(component);
|
|
42
|
+
|
|
43
|
+
if (list === undefined) {
|
|
44
|
+
map.set(component, [system_index]);
|
|
45
|
+
} else {
|
|
46
|
+
list.push(system_index);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Groups the unplaced systems into their strongly connected components — a cycle, or one system on
|
|
52
|
+
* no cycle — and marks each component that an unplaced system outside it still feeds. A component
|
|
53
|
+
* nothing outside it feeds holds systems that wait on nothing but each other, if on anything.
|
|
54
|
+
*
|
|
55
|
+
* Kosaraju's: a post-order over the edges, then trees over the reversed edges in reverse post-order,
|
|
56
|
+
* one component per tree. Iterative, O(n²) over the adjacency matrix.
|
|
57
|
+
*
|
|
58
|
+
* @param {number} system_count
|
|
59
|
+
* @param {Uint8Array} edges `edges[a * system_count + b] !== 0`: `a` runs before `b`
|
|
60
|
+
* @param {Uint8Array} placed
|
|
61
|
+
* @param {Int32Array} component output: component id per unplaced system
|
|
62
|
+
* @param {Uint8Array} fed output: per component id, 1 when something outside it feeds it
|
|
63
|
+
* @param {Uint8Array} visited scratch
|
|
64
|
+
* @param {Int32Array} post_order scratch
|
|
65
|
+
* @param {Int32Array} frame_system scratch, the depth-first stack
|
|
66
|
+
* @param {Int32Array} frame_next scratch, per stack frame the next system to look at
|
|
67
|
+
*/
|
|
68
|
+
function label_components(
|
|
69
|
+
system_count, edges, placed,
|
|
70
|
+
component, fed,
|
|
71
|
+
visited, post_order, frame_system, frame_next
|
|
72
|
+
) {
|
|
73
|
+
visited.fill(0);
|
|
74
|
+
|
|
75
|
+
let finished = 0;
|
|
76
|
+
|
|
77
|
+
for (let root = 0; root < system_count; root++) {
|
|
78
|
+
if (placed[root] !== 0 || visited[root] !== 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
visited[root] = 1;
|
|
83
|
+
|
|
84
|
+
let depth = 0;
|
|
85
|
+
frame_system[0] = root;
|
|
86
|
+
frame_next[0] = 0;
|
|
87
|
+
|
|
88
|
+
while (depth >= 0) {
|
|
89
|
+
const a = frame_system[depth];
|
|
90
|
+
let b = frame_next[depth];
|
|
91
|
+
|
|
92
|
+
while (b < system_count && (placed[b] !== 0 || visited[b] !== 0 || edges[a * system_count + b] === 0)) {
|
|
93
|
+
b++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (b < system_count) {
|
|
97
|
+
frame_next[depth] = b + 1;
|
|
98
|
+
|
|
99
|
+
visited[b] = 1;
|
|
100
|
+
|
|
101
|
+
depth++;
|
|
102
|
+
frame_system[depth] = b;
|
|
103
|
+
frame_next[depth] = 0;
|
|
104
|
+
} else {
|
|
105
|
+
post_order[finished++] = a;
|
|
106
|
+
depth--;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
component.fill(-1);
|
|
112
|
+
|
|
113
|
+
let component_count = 0;
|
|
114
|
+
|
|
115
|
+
for (let k = finished - 1; k >= 0; k--) {
|
|
116
|
+
const root = post_order[k];
|
|
117
|
+
|
|
118
|
+
if (component[root] !== -1) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
component[root] = component_count;
|
|
123
|
+
|
|
124
|
+
// `frame_system` is free again, and serves as a plain stack
|
|
125
|
+
let top = 0;
|
|
126
|
+
frame_system[top++] = root;
|
|
127
|
+
|
|
128
|
+
while (top > 0) {
|
|
129
|
+
const b = frame_system[--top];
|
|
130
|
+
|
|
131
|
+
for (let a = 0; a < system_count; a++) {
|
|
132
|
+
if (placed[a] !== 0 || component[a] !== -1 || edges[a * system_count + b] === 0) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
component[a] = component_count;
|
|
137
|
+
frame_system[top++] = a;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
component_count++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fed.fill(0, 0, component_count);
|
|
145
|
+
|
|
146
|
+
for (let a = 0; a < system_count; a++) {
|
|
147
|
+
if (placed[a] !== 0) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (let b = 0; b < system_count; b++) {
|
|
152
|
+
if (placed[b] === 0 && edges[a * system_count + b] !== 0 && component[a] !== component[b]) {
|
|
153
|
+
fed[component[b]] = 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Orders systems by what they declare they touch: a system that writes (or creates) a component
|
|
161
|
+
* runs before every other system that reads it, when the two run in the same pass.
|
|
162
|
+
*
|
|
163
|
+
* A pass is `fixedUpdate` or `update`. Every fixed step runs before the update pass whatever this
|
|
164
|
+
* order says, so a write in one pass and a read in the other orders nothing, and is not counted —
|
|
165
|
+
* counting it could only close a cycle out of edges that are each real. One order serves both
|
|
166
|
+
* passes, so a system in both carries its edges into both.
|
|
167
|
+
*
|
|
168
|
+
* Without a cycle the result is a topological order of those edges. Where they leave a choice, the
|
|
169
|
+
* system with the higher priority runs first, and of equal priorities the one earlier in `systems`.
|
|
170
|
+
*
|
|
171
|
+
* A cycle — two systems that both read and write one component is the common one — has no order
|
|
172
|
+
* that respects every edge. So the rule is stated over cycles: the next system to run is the
|
|
173
|
+
* highest priority (then earliest) of those that wait on nothing but their own cycle, and a system
|
|
174
|
+
* on no cycle that waits on nothing qualifies as its own. Running a member of a cycle drops its
|
|
175
|
+
* edges from the rest of that cycle and nothing else. A system that waits on a cycle from outside
|
|
176
|
+
* it never qualifies before the cycle has run, so the only edges dropped are edges on a cycle.
|
|
177
|
+
*
|
|
178
|
+
* A cycle and a system that waits on nothing compete on those same terms, so a cycle runs where
|
|
179
|
+
* its best member's priority puts it, never held back behind systems no edge relates to it. Where
|
|
180
|
+
* sorting by priority alone already respects every edge, the result is that sort; and the result
|
|
181
|
+
* depends on nothing but the declarations, the passes, the priorities and the order of `systems`.
|
|
182
|
+
*
|
|
183
|
+
* O(n²) per system placed, O(n³) in all; it runs when the set of running systems changes.
|
|
184
|
+
*
|
|
185
|
+
* @param {System[]} systems sorted in place, the first `system_count` of them
|
|
186
|
+
* @param {number} system_count
|
|
187
|
+
* @param {Float64Array|number[]} priorities one per system, in the order `systems` arrives in; higher runs earlier
|
|
188
|
+
*/
|
|
189
|
+
export function orderSystemsByDeclaredAccess(systems, system_count, priorities) {
|
|
190
|
+
const passes = new Uint8Array(system_count);
|
|
191
|
+
|
|
192
|
+
/** @type {Map<Function, number[]>} */
|
|
193
|
+
const writers = new Map();
|
|
194
|
+
/** @type {Map<Function, number[]>} */
|
|
195
|
+
const readers = new Map();
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < system_count; i++) {
|
|
198
|
+
const system = systems[i];
|
|
199
|
+
|
|
200
|
+
passes[i] = system_passes(system);
|
|
201
|
+
|
|
202
|
+
const components = system.referenced_components;
|
|
203
|
+
const component_count = components.length;
|
|
204
|
+
|
|
205
|
+
for (let j = 0; j < component_count; j++) {
|
|
206
|
+
const component = components[j];
|
|
207
|
+
|
|
208
|
+
const access = system.getAccessForComponent(component);
|
|
209
|
+
|
|
210
|
+
if ((access & WRITES) !== 0) {
|
|
211
|
+
map_push(writers, component, i);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if ((access & ResourceAccessKind.Read) !== 0) {
|
|
215
|
+
map_push(readers, component, i);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// edges[a * system_count + b] !== 0: a runs before b
|
|
221
|
+
const edges = new Uint8Array(system_count * system_count);
|
|
222
|
+
|
|
223
|
+
for (const [component, component_writers] of writers) {
|
|
224
|
+
const component_readers = readers.get(component);
|
|
225
|
+
|
|
226
|
+
if (component_readers === undefined) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < component_writers.length; i++) {
|
|
231
|
+
const writer = component_writers[i];
|
|
232
|
+
|
|
233
|
+
for (let j = 0; j < component_readers.length; j++) {
|
|
234
|
+
const reader = component_readers[j];
|
|
235
|
+
|
|
236
|
+
if (writer === reader || (passes[writer] & passes[reader]) === 0) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
edges[writer * system_count + reader] = 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const placed = new Uint8Array(system_count);
|
|
246
|
+
const sorted = new Array(system_count);
|
|
247
|
+
|
|
248
|
+
const component = new Int32Array(system_count);
|
|
249
|
+
const fed = new Uint8Array(system_count);
|
|
250
|
+
const visited = new Uint8Array(system_count);
|
|
251
|
+
const post_order = new Int32Array(system_count);
|
|
252
|
+
const frame_system = new Int32Array(system_count);
|
|
253
|
+
const frame_next = new Int32Array(system_count);
|
|
254
|
+
|
|
255
|
+
for (let k = 0; k < system_count; k++) {
|
|
256
|
+
label_components(
|
|
257
|
+
system_count, edges, placed,
|
|
258
|
+
component, fed,
|
|
259
|
+
visited, post_order, frame_system, frame_next
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// the components form an acyclic graph, so at least one of them is fed by nothing
|
|
263
|
+
let next = -1;
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < system_count; i++) {
|
|
266
|
+
if (placed[i] !== 0 || fed[component[i]] !== 0) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (next === -1 || priorities[i] > priorities[next]) {
|
|
271
|
+
next = i;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
placed[next] = 1;
|
|
276
|
+
sorted[k] = systems[next];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (let k = 0; k < system_count; k++) {
|
|
280
|
+
systems[k] = sorted[k];
|
|
281
|
+
}
|
|
282
|
+
}
|