@aethervtt/shard-physics 0.0.1 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { characterSystem } from './character.js';
2
2
  export { BODY_KINDS, type BodyKind, CharacterController, CharacterGroundEvent, type CharacterGroundEventData, CharacterIntent, CharacterState, Collider, CollisionEvent, type CollisionEventData, ContactForceEvent, type ContactForceEventData, ExternalForce, ExternalImpulse, GravitySource, JOINT_KINDS, Joint, Mass, PhysicsConfig, type PhysicsConfigValue, PhysicsParked, PhysicsRange, type PhysicsRangeValue, RigidBody, SHAPES, type Shape, Velocity, } from './components.js';
3
3
  export { physicsMethods } from './methods.js';
4
- export { Physics, PhysicsSystems, physics, physics2dPlugin, physics3dPlugin } from './plugin.js';
5
- export { type BodyRecord, CHARACTER_OFFSET, type CharacterRecord, createRayHit, loadRapier, type PhysicsStats, PhysicsWorld, type QueryOptions, type QueryShape, type RayHit, } from './world.js';
4
+ export { Physics, type PhysicsPluginOptions, PhysicsSystems, physics, physics2dPlugin, physics3dPlugin, } from './plugin.js';
5
+ export { type LoadRapierOptions, loadRapier, type Rapier, type RapierVariant } from './rapier.js';
6
+ export { type BodyRecord, CHARACTER_OFFSET, type CharacterRecord, createRayHit, type MeshLookup, type PhysicsStats, PhysicsWorld, type QueryOptions, type QueryShape, type RayHit, } from './world.js';
package/dist/index.js CHANGED
@@ -22,11 +22,17 @@ import {
22
22
  Velocity
23
23
  } from "./components.js";
24
24
  import { physicsMethods } from "./methods.js";
25
- import { Physics, PhysicsSystems, physics, physics2dPlugin, physics3dPlugin } from "./plugin.js";
25
+ import {
26
+ Physics,
27
+ PhysicsSystems,
28
+ physics,
29
+ physics2dPlugin,
30
+ physics3dPlugin
31
+ } from "./plugin.js";
32
+ import { loadRapier } from "./rapier.js";
26
33
  import {
27
34
  CHARACTER_OFFSET,
28
35
  createRayHit,
29
- loadRapier,
30
36
  PhysicsWorld
31
37
  } from "./world.js";
32
38
  export {
package/dist/methods.js CHANGED
@@ -98,12 +98,13 @@ const physicsMethods = [
98
98
  },
99
99
  {
100
100
  name: "physics.describe",
101
- description: "Physics state: dimension, gravity, bodies by kind and how many sleep, colliders by shape, joints, contact pairs, colliders waiting for a mesh, bodies parked out of range (large worlds) and the range, the last step time, and every character controller with its state (grounded, ground, velocity, up).",
101
+ description: "Physics state: dimension, Rapier variant (regular or deterministic), gravity, bodies by kind and how many sleep, colliders by shape, joints, contact pairs, colliders waiting for a mesh, bodies parked out of range (large worlds) and the range, the last step time, and every character controller with its state (grounded, ground, velocity, up).",
102
102
  params: defineSchema("physics/DescribeParams", {}),
103
103
  handler: ({ world }) => {
104
104
  const physics = ready(world);
105
105
  return {
106
106
  dimension: physics.dim,
107
+ variant: physics.variant,
107
108
  gravity: physics.config.gravity,
108
109
  interpolate: physics.config.interpolate,
109
110
  paused: physics.config.paused,
package/dist/plugin.d.ts CHANGED
@@ -6,8 +6,14 @@ export { Physics };
6
6
  export declare const PhysicsSystems: import("@aethervtt/shard-core").SystemSetDef;
7
7
  /** The physics world, or `physics/not-ready` before the plugin's `ready` step. */
8
8
  export declare function physics(world: World): PhysicsWorld;
9
+ export interface PhysicsPluginOptions {
10
+ /**
11
+ * Run on Rapier's deterministic build: the same results on every machine and browser, at some
12
+ * cost in speed (0053). Default false.
13
+ */
14
+ deterministic?: boolean;
15
+ }
9
16
  /** 3D rigid bodies, colliders, joints, and scene queries (Rapier). */
10
- export declare const physics3dPlugin: Plugin;
17
+ export declare function physics3dPlugin(options?: PhysicsPluginOptions): Plugin;
11
18
  /** 2D rigid bodies, colliders, joints, and scene queries in the XY plane (Rapier 2D). */
12
- export declare const physics2dPlugin: Plugin;
13
- export declare const collidersOverlay: import("@aethervtt/shard-render").OverlayDef;
19
+ export declare function physics2dPlugin(options?: PhysicsPluginOptions): Plugin;
package/dist/plugin.js CHANGED
@@ -9,8 +9,7 @@ import {
9
9
  PostUpdate,
10
10
  ShardError
11
11
  } from "@aethervtt/shard-core";
12
- import { defineOverlay } from "@aethervtt/shard-render";
13
- import { FixedTime } from "@aethervtt/shard-runtime";
12
+ import { FixedTime, FrameDemand } from "@aethervtt/shard-runtime";
14
13
  import {
15
14
  GlobalTransform,
16
15
  GridCell,
@@ -23,7 +22,6 @@ import { characterSystem } from "./character.js";
23
22
  import * as componentsModule from "./components.js";
24
23
  import {
25
24
  CharacterController,
26
- CharacterState,
27
25
  Collider,
28
26
  CollisionEvent,
29
27
  ContactForceEvent,
@@ -39,8 +37,11 @@ import {
39
37
  import { createGravitySources, gatherGravitySources, sampleGravity } from "./gravity.js";
40
38
  import { physicsMethods } from "./methods.js";
41
39
  import * as pluginModule from "./plugin.js";
42
- import { isGrid, parentOf, rotate, worldToLocal } from "./pose.js";
43
- import { loadRapier, Physics, PhysicsWorld } from "./world.js";
40
+ import { isGrid, parentOf, worldToLocal } from "./pose.js";
41
+ import { loadRapier } from "./rapier.js";
42
+ import * as renderModule from "./render.js";
43
+ import { rendererMeshes } from "./render.js";
44
+ import { Physics, PhysicsWorld } from "./world.js";
44
45
  const PhysicsSystems = defineSystemSet("physics/PhysicsSystems");
45
46
  function physics(world) {
46
47
  const p = world.tryResource(Physics);
@@ -376,6 +377,8 @@ const syncOut = defineSystem({
376
377
  visit: void 0,
377
378
  world,
378
379
  tick: 0,
380
+ /** Awake bodies sync-out wrote this step: while any are, on-demand apps keep rendering. */
381
+ awake: 0,
379
382
  pos: new Float64Array(3),
380
383
  rot: new Float64Array(4),
381
384
  rootIdentity: true,
@@ -400,6 +403,7 @@ const syncOut = defineSystem({
400
403
  s.visit ??= (handle) => {
401
404
  const record = p.bodyByHandle.get(handle);
402
405
  if (!record || record.kind === KIND_FIXED) return;
406
+ s.awake++;
403
407
  const o = record.slot * 7;
404
408
  const curr = p.curr;
405
409
  for (let k = 0; k < 7; k++) p.prev[o + k] = curr[o + k];
@@ -450,7 +454,9 @@ const syncOut = defineSystem({
450
454
  c.velTicks[row] = s.tick;
451
455
  }
452
456
  };
457
+ s.awake = 0;
453
458
  p.raw.islands.forEachActiveRigidBodyHandle(s.visit);
459
+ world.tryResource(FrameDemand)?.set("physics", s.awake > 0);
454
460
  }
455
461
  });
456
462
  const interpolate = defineSystem({
@@ -552,10 +558,11 @@ function observe(world) {
552
558
  get()?.shiftOrigin(data.offset[0], data.offset[1], data.offset[2], onCollision);
553
559
  });
554
560
  }
555
- function physicsPlugin(dim) {
561
+ function physicsPlugin(dim, options) {
562
+ const deterministic = options?.deterministic === true;
556
563
  return {
557
564
  name: dim === 3 ? "physics3d" : "physics2d",
558
- provides: [componentsModule, pluginModule],
565
+ provides: [componentsModule, pluginModule, renderModule],
559
566
  dependencies: ["core/transform"],
560
567
  build(app) {
561
568
  const other = dimensionOf.get(app.world);
@@ -585,268 +592,26 @@ function physicsPlugin(dim) {
585
592
  app.addMethod(...physicsMethods);
586
593
  },
587
594
  async ready(app) {
588
- const R = await loadRapier(dim);
589
- app.world.insertResource(Physics, new PhysicsWorld(R, dim, app.world.resource(PhysicsConfig)));
595
+ const R = await loadRapier(dim, { deterministic });
596
+ const config = app.world.resource(PhysicsConfig);
597
+ const variant = deterministic ? "deterministic" : "regular";
598
+ app.world.insertResource(Physics, new PhysicsWorld(R, dim, config, variant, rendererMeshes));
599
+ },
600
+ dispose(app) {
601
+ app.world.tryResource(Physics)?.free();
602
+ app.world.removeResource(Physics);
590
603
  }
591
604
  };
592
605
  }
593
- const physics3dPlugin = physicsPlugin(3);
594
- const physics2dPlugin = physicsPlugin(2);
595
- const COLORS = [
596
- [1, 0.55, 0.2, 1],
597
- // dynamic
598
- [0.55, 0.55, 0.6, 1],
599
- // fixed
600
- [0.3, 0.8, 1, 1],
601
- // kinematic
602
- [0.3, 0.8, 1, 1]
603
- ];
604
- const SLEEPING = [0.5, 0.35, 0.2, 1];
605
- const SENSOR = [0.3, 1, 0.4, 1];
606
- const CONTACT = [1, 0.2, 0.3, 1];
607
- const UP = [1, 0.9, 0.2, 1];
608
- const pa = new Float64Array(3);
609
- const pb = new Float64Array(3);
610
- const q = new Float64Array(4);
611
- const center = new Float64Array(3);
612
- const collidersOverlay = defineOverlay({
613
- name: "colliders",
614
- description: "Physics collider outlines, colored by body kind (sleeping dimmed, sensors green, characters cyan with their up in yellow), and contact normals.",
615
- draw(world, g, passes) {
616
- const p = world.tryResource(Physics);
617
- if (!p) return;
618
- for (const [entity, collider] of p.colliders) {
619
- if (!passes(entity)) continue;
620
- const owner = p.colliderOwner.get(entity);
621
- const record = owner === void 0 ? void 0 : p.bodies.get(owner);
622
- const color = collider.isSensor() ? SENSOR : record?.body.isSleeping() ? SLEEPING : COLORS[record?.kind ?? KIND_FIXED];
623
- const t = collider.translation(p.v3);
624
- center[0] = t.x;
625
- center[1] = t.y;
626
- center[2] = p.dim === 3 ? t.z : 0;
627
- if (p.dim === 3) {
628
- const r = collider.rotation(p.q4);
629
- q[0] = r.x;
630
- q[1] = r.y;
631
- q[2] = r.z;
632
- q[3] = r.w;
633
- } else {
634
- const angle = collider.rotation();
635
- q[0] = 0;
636
- q[1] = 0;
637
- q[2] = Math.sin(angle / 2);
638
- q[3] = Math.cos(angle / 2);
639
- }
640
- drawCollider(p, g, collider, p.colliderShape.get(entity), color);
641
- }
642
- for (const [entity, c] of p.characters) {
643
- if (!passes(entity) || !world.has(entity, CharacterState)) continue;
644
- const up = world.get(entity, CharacterState).up;
645
- const t = c.collider.translation(p.v3);
646
- const tall = p.colliderShape.get(entity) === "capsule" ? c.collider.halfHeight() : 0;
647
- const reach = c.collider.radius() + tall + 0.5;
648
- pa[0] = t.x;
649
- pa[1] = t.y;
650
- pa[2] = p.dim === 3 ? t.z : 0;
651
- pb[0] = pa[0] + up[0] * reach;
652
- pb[1] = pa[1] + up[1] * reach;
653
- pb[2] = pa[2] + up[2] * reach;
654
- g.line(pa, pb, UP);
655
- }
656
- contactBudget = MAX_CONTACT_PAIRS;
657
- for (let i = 0; i < p.list.length && contactBudget > 0; i++) {
658
- const record = p.list[i];
659
- if (record.kind !== KIND_DYNAMIC || record.body.isSleeping()) continue;
660
- const n = record.body.numColliders();
661
- for (let c = 0; c < n; c++) drawContacts(p, g, record.body.collider(c));
662
- }
663
- }
664
- });
665
- const MAX_CONTACT_PAIRS = 1e3;
666
- let contactBudget = 0;
667
- function visitedFromOtherSide(other) {
668
- const body = other.parent();
669
- return body?.isDynamic() === true && !body.isSleeping();
670
- }
671
- function drawContacts(p, g, collider) {
672
- p.raw.contactPairsWith(collider, (other) => {
673
- if (contactBudget <= 0) return;
674
- if (other.handle < collider.handle && visitedFromOtherSide(other)) return;
675
- contactBudget--;
676
- p.raw.contactPair(collider, other, (manifold) => {
677
- const count = manifold.numSolverContacts();
678
- if (count === 0) return;
679
- const n = manifold.normal();
680
- for (let i = 0; i < count; i++) {
681
- const point = manifold.solverContactPoint(i);
682
- if (!point) continue;
683
- pa[0] = point.x;
684
- pa[1] = point.y;
685
- pa[2] = p.dim === 3 ? point.z : 0;
686
- pb[0] = pa[0] + n.x * 0.25;
687
- pb[1] = pa[1] + n.y * 0.25;
688
- pb[2] = pa[2] + (p.dim === 3 ? n.z : 0) * 0.25;
689
- g.line(pa, pb, CONTACT);
690
- }
691
- });
692
- });
693
- }
694
- function local(out, x, y, z) {
695
- rotate(out, 0, q, 0, x, y, z);
696
- out[0] = out[0] + center[0];
697
- out[1] = out[1] + center[1];
698
- out[2] = out[2] + center[2];
699
- return out;
700
- }
701
- const SEGMENTS = 24;
702
- const UNIT = (() => {
703
- const out = new Float64Array((SEGMENTS + 1) * 2);
704
- for (let i = 0; i <= SEGMENTS; i++) {
705
- const t = i / SEGMENTS * Math.PI * 2;
706
- out[i * 2] = Math.cos(t);
707
- out[i * 2 + 1] = Math.sin(t);
708
- }
709
- return out;
710
- })();
711
- function localCircle(g, axis, r, dy, color) {
712
- for (let i = 0; i <= SEGMENTS; i++) {
713
- const u = UNIT[i * 2] * r;
714
- const v = UNIT[i * 2 + 1] * r;
715
- if (axis === 0) local(pb, 0, u + dy, v);
716
- else if (axis === 1) local(pb, u, dy, v);
717
- else local(pb, u, v + dy, 0);
718
- if (i > 0) g.line(pa, pb, color);
719
- pa[0] = pb[0];
720
- pa[1] = pb[1];
721
- pa[2] = pb[2];
722
- }
723
- }
724
- function drawCollider(p, g, collider, shape, color) {
725
- const is2d = p.dim === 2;
726
- switch (shape) {
727
- case "ball": {
728
- const r = collider.radius();
729
- localCircle(g, 2, r, 0, color);
730
- if (!is2d) {
731
- localCircle(g, 0, r, 0, color);
732
- localCircle(g, 1, r, 0, color);
733
- }
734
- return;
735
- }
736
- case "cuboid": {
737
- const he = collider.halfExtents();
738
- const hz = is2d ? 0 : he.z;
739
- const size = [he.x * 2, he.y * 2, hz * 2];
740
- g.box(center, size, q, color);
741
- return;
742
- }
743
- case "capsule":
744
- case "cylinder":
745
- case "cone": {
746
- const r = collider.radius();
747
- const h = collider.halfHeight();
748
- if (is2d) {
749
- local(pa, -r, -h, 0);
750
- local(pb, -r, h, 0);
751
- g.line(pa, pb, color);
752
- local(pa, r, -h, 0);
753
- local(pb, r, h, 0);
754
- g.line(pa, pb, color);
755
- localCircle(g, 2, r, h, color);
756
- localCircle(g, 2, r, -h, color);
757
- return;
758
- }
759
- const top = shape === "cone" ? 0 : r;
760
- localCircle(g, 1, r, -h, color);
761
- if (top > 0) localCircle(g, 1, top, h, color);
762
- for (let k = 0; k < 4; k++) {
763
- const a = k / 4 * Math.PI * 2;
764
- local(pa, Math.cos(a) * r, -h, Math.sin(a) * r);
765
- local(pb, Math.cos(a) * top, h, Math.sin(a) * top);
766
- g.line(pa, pb, color);
767
- }
768
- if (shape === "capsule") {
769
- localCircle(g, 0, r, h, color);
770
- localCircle(g, 0, r, -h, color);
771
- localCircle(g, 2, r, h, color);
772
- localCircle(g, 2, r, -h, color);
773
- }
774
- return;
775
- }
776
- case "heightfield":
777
- drawHeightfield(p, g, collider, color);
778
- return;
779
- default: {
780
- const vertices = collider.vertices?.();
781
- const indices = collider.indices?.();
782
- if (!vertices || vertices.length === 0) return;
783
- const d = p.dim;
784
- const edge = (i, j) => {
785
- local(pa, vertices[i * d], vertices[i * d + 1], d === 3 ? vertices[i * d + 2] : 0);
786
- local(pb, vertices[j * d], vertices[j * d + 1], d === 3 ? vertices[j * d + 2] : 0);
787
- g.line(pa, pb, color);
788
- };
789
- if (indices && indices.length > 0) {
790
- const stride = shape === "polyline" || is2d ? 2 : 3;
791
- const limit = Math.min(indices.length, 3e4);
792
- for (let i = 0; i + stride - 1 < limit; i += stride) {
793
- for (let k = 0; k < stride; k++) {
794
- if (stride === 2 && k === 1) break;
795
- edge(indices[i + k], indices[i + (k + 1) % stride]);
796
- }
797
- }
798
- } else {
799
- const n = vertices.length / d;
800
- for (let i = 0; i + 1 < n; i++) edge(i, i + 1);
801
- if (shape === "convex" && n > 2) edge(n - 1, 0);
802
- }
803
- }
804
- }
606
+ function physics3dPlugin(options) {
607
+ return physicsPlugin(3, options);
805
608
  }
806
- function drawHeightfield(p, g, collider, color) {
807
- const hf = collider;
808
- const heights = hf.heightfieldHeights();
809
- const scale = hf.heightfieldScale();
810
- if (p.dim === 2) {
811
- const n = heights.length;
812
- for (let i = 0; i < n; i++) {
813
- local(pb, (i / (n - 1) - 0.5) * scale.x, heights[i] * scale.y, 0);
814
- if (i > 0) g.line(pa, pb, color);
815
- pa[0] = pb[0];
816
- pa[1] = pb[1];
817
- pa[2] = pb[2];
818
- }
819
- return;
820
- }
821
- const rows = hf.heightfieldNRows() + 1;
822
- const cols = hf.heightfieldNCols() + 1;
823
- const sz = scale.z ?? 1;
824
- const at = (out, r, c) => local(
825
- out,
826
- (c / (cols - 1) - 0.5) * scale.x,
827
- heights[c * rows + r] * scale.y,
828
- (r / (rows - 1) - 0.5) * sz
829
- );
830
- const step2 = Math.max(1, Math.ceil(Math.max(rows, cols) / 64));
831
- for (let r = 0; r < rows; r += step2) {
832
- for (let c = 0; c + step2 < cols; c += step2) {
833
- at(pa, r, c);
834
- at(pb, r, c + step2);
835
- g.line(pa, pb, color);
836
- }
837
- }
838
- for (let c = 0; c < cols; c += step2) {
839
- for (let r = 0; r + step2 < rows; r += step2) {
840
- at(pa, r, c);
841
- at(pb, r + step2, c);
842
- g.line(pa, pb, color);
843
- }
844
- }
609
+ function physics2dPlugin(options) {
610
+ return physicsPlugin(2, options);
845
611
  }
846
612
  export {
847
613
  Physics,
848
614
  PhysicsSystems,
849
- collidersOverlay,
850
615
  physics,
851
616
  physics2dPlugin,
852
617
  physics3dPlugin
@@ -0,0 +1,22 @@
1
+ import type RAPIER from '@dimforge/rapier3d-compat';
2
+ export type Rapier = typeof RAPIER;
3
+ /**
4
+ * Which Rapier build: `regular` (fastest, deterministic only on one build and platform) or
5
+ * `deterministic` (the same 0.20.0 API, with results that don't depend on the machine).
6
+ */
7
+ export type RapierVariant = 'regular' | 'deterministic';
8
+ export interface LoadRapierOptions {
9
+ /** Load the deterministic build (default false). */
10
+ deterministic?: boolean;
11
+ }
12
+ /**
13
+ * Loads and initializes Rapier's WASM for a dimension and variant, once per process. The variants
14
+ * are separate modules, so a page can run both (the game on the regular build, a dice worker on
15
+ * the deterministic one).
16
+ */
17
+ export declare function loadRapier(dim: 2 | 3, options?: LoadRapierOptions): Promise<Rapier>;
18
+ /**
19
+ * The deterministic 3D build alone. Tracks load it through this, so a track bundle references no
20
+ * other build.
21
+ */
22
+ export declare function loadDeterministic3d(): Promise<Rapier>;
package/dist/rapier.js ADDED
@@ -0,0 +1,27 @@
1
+ const modules = /* @__PURE__ */ new Map();
2
+ function cached(key, load) {
3
+ let loading = modules.get(key);
4
+ if (!loading) {
5
+ loading = (async () => {
6
+ const mod = await load();
7
+ const R = mod.default ?? mod;
8
+ await R.init();
9
+ return R;
10
+ })();
11
+ modules.set(key, loading);
12
+ }
13
+ return loading;
14
+ }
15
+ function loadRapier(dim, options) {
16
+ if (options?.deterministic) {
17
+ return dim === 3 ? loadDeterministic3d() : cached("2/deterministic", () => import("@dimforge/rapier2d-deterministic-compat"));
18
+ }
19
+ return dim === 3 ? cached("3/regular", () => import("@dimforge/rapier3d-compat")) : cached("2/regular", () => import("@dimforge/rapier2d-compat"));
20
+ }
21
+ function loadDeterministic3d() {
22
+ return cached("3/deterministic", () => import("@dimforge/rapier3d-deterministic-compat"));
23
+ }
24
+ export {
25
+ loadDeterministic3d,
26
+ loadRapier
27
+ };
@@ -0,0 +1,5 @@
1
+ import type { AssetRef, World } from '@aethervtt/shard-core';
2
+ import type { Mesh } from '@aethervtt/shard-mesh';
3
+ /** Collider meshes from the renderer's mesh store. */
4
+ export declare function rendererMeshes(world: World, ref: AssetRef): Mesh | undefined;
5
+ export declare const collidersOverlay: import("@aethervtt/shard-render").OverlayDef;