@threlte/rapier 3.0.0-next.3 → 3.0.0-next.5

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.
@@ -10,7 +10,7 @@ import { useParentRigidbodyObject } from '../../lib/rigidBodyObjectContext';
10
10
  import { applyColliderActiveEvents } from '../../lib/applyColliderActiveEvents';
11
11
  import { createCollidersFromChildren } from '../../lib/createCollidersFromChildren';
12
12
  import { eulerToQuaternion } from '../../lib/eulerToQuaternion';
13
- let { shape = 'convexHull', restitution, restitutionCombineRule, friction, frictionCombineRule, sensor, contactForceEventThreshold, density, mass, centerOfMass, principalAngularInertia, angularInertiaLocalFrame, refresh = $bindable(() => create()), colliders = $bindable(), oncreate, oncollisionenter, oncollisionexit, oncontact, onsensorenter, onsensorexit, children, } = $props();
13
+ let { shape = 'convexHull', restitution, restitutionCombineRule, friction, frictionCombineRule, sensor, contactForceEventThreshold, density, mass, centerOfMass, principalAngularInertia, angularInertiaLocalFrame, refresh = $bindable(() => create()), colliders = $bindable(), oncreate, oncollisionenter, oncollisionexit, oncontact, onsensorenter, onsensorexit, children } = $props();
14
14
  const group = new Group();
15
15
  const { updateRef } = useCreateEvent(oncreate);
16
16
  const rigidBody = useRigidBody();
@@ -16,7 +16,7 @@ let { shape, args, type, restitution, restitutionCombineRule, friction, friction
16
16
  return;
17
17
  collider.setTranslation(getWorldPosition(object));
18
18
  collider.setRotation(getWorldQuaternion(object));
19
- }), oncreate, oncollisionenter, oncollisionexit, oncontact, onsensorenter, onsensorexit, children, } = $props();
19
+ }), oncreate, oncollisionenter, oncollisionexit, oncontact, onsensorenter, onsensorexit, children } = $props();
20
20
  const object = new Object3D();
21
21
  const { updateRef } = useCreateEvent(oncreate);
22
22
  const rigidBody = useRigidBody();
@@ -1,4 +1,3 @@
1
- import type { ColliderEvents } from '../../types/types'
2
1
  import type {
3
2
  CoefficientCombineRule,
4
3
  Collider as RapierCollider,
@@ -6,6 +5,7 @@ import type {
6
5
  } from '@dimforge/rapier3d-compat'
7
6
  import { SvelteComponent, type Snippet } from 'svelte'
8
7
  import type { Euler, Vector3 } from 'three'
8
+ import type { ColliderEvents } from '../../types/types'
9
9
 
10
10
  // ------------------ BASE ------------------
11
11
 
@@ -1,20 +1,23 @@
1
1
  <script lang="ts">import { SceneGraphObject } from '@threlte/core';
2
2
  import { onDestroy, setContext, tick } from 'svelte';
3
3
  import { Object3D, Vector3 } from 'three';
4
+ import { initializeRigidBodyUserData, setInitialRigidBodyState } from '../../lib/createPhysicsTasks';
4
5
  import { useRapier } from '../../hooks/useRapier';
5
6
  import { getWorldPosition, getWorldQuaternion, getWorldScale } from '../../lib/getWorldTransforms';
6
7
  import { parseRigidBodyType } from '../../lib/parseRigidBodyType';
7
8
  import { setParentRigidbodyObject } from '../../lib/rigidBodyObjectContext';
8
9
  import { useCreateEvent } from '../../lib/useCreateEvent';
10
+ import { overrideTeleportMethods } from './overrideTeleportMethods';
9
11
  const { world, rapier, addRigidBodyToContext, removeRigidBodyFromContext } = useRapier();
10
- let { linearVelocity, angularVelocity, type = 'dynamic', canSleep = true, gravityScale = 1, ccd = false, angularDamping = 0, linearDamping = 0, lockRotations = false, lockTranslations = false, enabledRotations = [true, true, true], enabledTranslations = [true, true, true], dominance = 0, enabled = true, userData = {}, rigidBody = $bindable(), oncreate, oncollisionenter, oncollisionexit, oncontact, onsensorenter, onsensorexit, onsleep, onwake } = $props();
12
+ let { linearVelocity, angularVelocity, type = 'dynamic', canSleep = true, gravityScale = 1, ccd = false, angularDamping = 0, linearDamping = 0, lockRotations = false, lockTranslations = false, enabledRotations = [true, true, true], enabledTranslations = [true, true, true], dominance = 0, enabled = true, userData = {}, rigidBody = $bindable(), oncreate, oncollisionenter, oncollisionexit, oncontact, onsensorenter, onsensorexit, onsleep, onwake, children } = $props();
11
13
  /**
12
14
  * Every RigidBody receives and forwards collision-related events
13
15
  */
14
16
  const { updateRef } = useCreateEvent(oncreate);
15
17
  const object = new Object3D();
18
+ initializeRigidBodyUserData(object);
16
19
  /**
17
- * isSleeping used for events "sleep" and "wake" in `useFrameHandler`
20
+ * isSleeping used for events "sleep" and "wake" in `createPhysicsTasks`
18
21
  */
19
22
  object.userData.isSleeping = false;
20
23
  /**
@@ -25,6 +28,7 @@ const desc = new rapier.RigidBodyDesc(parseRigidBodyType(type)).setCanSleep(canS
25
28
  * Temporary RigidBody init
26
29
  */
27
30
  let rigidBodyInternal = world.createRigidBody(desc);
31
+ overrideTeleportMethods(rigidBodyInternal, object);
28
32
  rigidBody = rigidBodyInternal;
29
33
  /**
30
34
  * Apply transforms after the parent component added "object" to itself
@@ -36,6 +40,7 @@ const initPosition = async () => {
36
40
  const parentWorldScale = object.parent ? getWorldScale(object.parent) : new Vector3(1, 1, 1);
37
41
  const worldPosition = getWorldPosition(object).multiply(parentWorldScale);
38
42
  const worldQuaternion = getWorldQuaternion(object);
43
+ setInitialRigidBodyState(object, worldPosition, worldQuaternion);
39
44
  rigidBodyInternal.setTranslation(worldPosition, true);
40
45
  rigidBodyInternal.setRotation(worldQuaternion, true);
41
46
  updateRef(rigidBodyInternal);
@@ -114,5 +119,5 @@ onDestroy(() => {
114
119
  </script>
115
120
 
116
121
  <SceneGraphObject {object}>
117
- <slot rigidBody={rigidBodyInternal} />
122
+ {@render children?.({ rigidBody: rigidBodyInternal })}
118
123
  </SceneGraphObject>
@@ -7,7 +7,7 @@ import type { RigidBodyEvents } from '../../types/types'
7
7
  export type Boolean3Array = [x: boolean, y: boolean, z: boolean]
8
8
 
9
9
  export type RigidBodyProps = {
10
- rigidBody?: RapierRigidBody
10
+ rigidBody?: RapierRigidBody | undefined
11
11
 
12
12
  /**
13
13
  * Specify the type of this rigid body
@@ -0,0 +1,14 @@
1
+ import type { RigidBody } from '@dimforge/rapier3d-compat';
2
+ import type { Object3D } from 'three';
3
+ export type ExtractMethods<ObjectType> = Pick<ObjectType, {
4
+ [Method in keyof ObjectType]: ObjectType[Method] extends (...args: any[]) => any ? Method : never;
5
+ }[keyof ObjectType]>;
6
+ /**
7
+ * When using a fixed framerate, Threlte is interpolating the position and
8
+ * rotation of RigidBody objects. Sometimes, this is not desirable, especially
9
+ * when using methods on the rigidbody that teleport the object, e.g. should
10
+ * lead to a sudden change in translation/rotation such as `rb.setTranslation`.
11
+ * These methods are overridden to reset the physics simulation position and
12
+ * rotation to the current object position and rotation.
13
+ */
14
+ export declare const overrideTeleportMethods: (rb: RigidBody, object: Object3D) => void;
@@ -0,0 +1,31 @@
1
+ const overrideMethods = {
2
+ position: ['setTranslation', 'setNextKinematicTranslation'],
3
+ rotation: ['setRotation', 'setNextKinematicRotation']
4
+ };
5
+ /**
6
+ * When using a fixed framerate, Threlte is interpolating the position and
7
+ * rotation of RigidBody objects. Sometimes, this is not desirable, especially
8
+ * when using methods on the rigidbody that teleport the object, e.g. should
9
+ * lead to a sudden change in translation/rotation such as `rb.setTranslation`.
10
+ * These methods are overridden to reset the physics simulation position and
11
+ * rotation to the current object position and rotation.
12
+ */
13
+ export const overrideTeleportMethods = (rb, object) => {
14
+ const originalMethods = {};
15
+ overrideMethods.position.forEach((method) => {
16
+ originalMethods[method] = rb[method].bind(rb);
17
+ const proxy = (...args) => {
18
+ object.userData.physics.resetPosition = true;
19
+ return originalMethods[method](...args);
20
+ };
21
+ rb[method] = proxy;
22
+ });
23
+ overrideMethods.rotation.forEach((method) => {
24
+ originalMethods[method] = rb[method].bind(rb);
25
+ const proxy = (...args) => {
26
+ object.userData.physics.resetRotation = true;
27
+ return originalMethods[method](...args);
28
+ };
29
+ rb[method] = proxy;
30
+ });
31
+ };
@@ -1,19 +1,41 @@
1
1
  <script lang="ts">import { onDestroy, setContext, tick } from 'svelte';
2
- import { useFrameHandler } from '../../hooks/useFrameHandler';
3
2
  import { createRapierContext } from '../../lib/createRapierContext';
4
- let { gravity = [0, -9.81, 0], rawIntegrationParameters, rawIslands, rawBroadPhase, rawNarrowPhase, rawBodies, rawColliders, rawImpulseJoints, rawMultibodyJoints, rawCCDSolver, rawQueryPipeline, rawPhysicsPipeline, rawSerializationPipeline, rawDebugRenderPipeline, stage } = $props();
5
- const rapierContext = createRapierContext({ x: gravity[0], y: gravity[1], z: gravity[2] }, rawIntegrationParameters, rawIslands, rawBroadPhase, rawNarrowPhase, rawBodies, rawColliders, rawImpulseJoints, rawMultibodyJoints, rawCCDSolver, rawQueryPipeline, rawPhysicsPipeline, rawSerializationPipeline, rawDebugRenderPipeline);
3
+ let { gravity = [0, -9.81, 0], rawIntegrationParameters, rawIslands, rawBroadPhase, rawNarrowPhase, rawBodies, rawColliders, rawImpulseJoints, rawMultibodyJoints, rawCCDSolver, rawQueryPipeline, rawPhysicsPipeline, rawSerializationPipeline, rawDebugRenderPipeline, framerate, autoStart = true, simulationStageOptions, synchronizationStageOptions, children } = $props();
4
+ const rapierContext = createRapierContext([
5
+ { x: gravity[0], y: gravity[1], z: gravity[2] },
6
+ rawIntegrationParameters,
7
+ rawIslands,
8
+ rawBroadPhase,
9
+ rawNarrowPhase,
10
+ rawBodies,
11
+ rawColliders,
12
+ rawImpulseJoints,
13
+ rawMultibodyJoints,
14
+ rawCCDSolver,
15
+ rawQueryPipeline,
16
+ rawPhysicsPipeline,
17
+ rawSerializationPipeline,
18
+ rawDebugRenderPipeline
19
+ ], {
20
+ framerate,
21
+ autoStart,
22
+ simulationStageOptions,
23
+ synchronizationStageOptions
24
+ });
6
25
  setContext('threlte-rapier-context', rapierContext);
7
26
  $effect.pre(() => {
8
27
  if (gravity !== undefined) {
9
28
  rapierContext.world.gravity = { x: gravity[0], y: gravity[1], z: gravity[2] };
10
29
  }
11
30
  });
12
- useFrameHandler(rapierContext, stage);
31
+ $effect.pre(() => {
32
+ if (framerate !== undefined)
33
+ rapierContext.framerate.set(framerate);
34
+ });
13
35
  onDestroy(async () => {
14
36
  await tick();
15
37
  rapierContext.world.free();
16
38
  });
17
39
  </script>
18
40
 
19
- <slot />
41
+ {@render children?.()}
@@ -1,17 +1,11 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import type { WorldProps } from './World.svelte';
3
3
  declare const __propDef: {
4
- props: WorldProps & {
5
- children?: ((this: void) => typeof import("svelte").SnippetReturn & {
6
- _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
7
- }) | undefined;
8
- };
4
+ props: WorldProps;
9
5
  events: {
10
6
  [evt: string]: CustomEvent<any>;
11
7
  };
12
- slots: {
13
- default: {};
14
- };
8
+ slots: {};
15
9
  };
16
10
  export type InnerWorldProps = typeof __propDef.props;
17
11
  export type InnerWorldEvents = typeof __propDef.events;
@@ -1,60 +1,12 @@
1
- <script
2
- context="module"
3
- lang="ts"
4
- >import RAPIER from '@dimforge/rapier3d-compat';
5
- import { onMount } from 'svelte';
6
- import { writable } from 'svelte/store';
7
- /**
8
- * RAPIER.init() should only be called once
9
- */
10
- const initialized = writable(false);
1
+ <script lang="ts">import { initRapier } from '../../lib/initRapier.svelte';
2
+ import InnerWorld from './InnerWorld.svelte';
3
+ let { fallback, children, ...rest } = $props();
11
4
  </script>
12
5
 
13
- <script lang="ts">import InnerWorld from './InnerWorld.svelte';
14
- let { gravity, rawIntegrationParameters, rawIslands, rawBroadPhase, rawNarrowPhase, rawBodies, rawColliders, rawImpulseJoints, rawMultibodyJoints, rawCCDSolver, rawQueryPipeline, rawPhysicsPipeline, rawSerializationPipeline, rawDebugRenderPipeline,
15
- /**
16
- * This is passed to the useTask handler.
17
- * Use this to control when the rapier physics engine is updating the scene.
18
- * @default undefined
19
- */
20
- stage, fallback, children } = $props();
21
- let error = $state(false);
22
- const init = async () => {
23
- if ($initialized)
24
- return;
25
- try {
26
- await RAPIER.init();
27
- $initialized = true;
28
- }
29
- catch (e) {
30
- error = true;
31
- }
32
- };
33
- onMount(init);
34
- </script>
35
-
36
- {#if $initialized}
37
- <InnerWorld
38
- {gravity}
39
- {rawIntegrationParameters}
40
- {rawIslands}
41
- {rawBroadPhase}
42
- {rawNarrowPhase}
43
- {rawBodies}
44
- {rawColliders}
45
- {rawImpulseJoints}
46
- {rawMultibodyJoints}
47
- {rawCCDSolver}
48
- {rawQueryPipeline}
49
- {rawPhysicsPipeline}
50
- {rawSerializationPipeline}
51
- {rawDebugRenderPipeline}
52
- {stage}
53
- >
6
+ {#await initRapier() then _}
7
+ <InnerWorld {...rest}>
54
8
  {@render children?.()}
55
9
  </InnerWorld>
56
- {/if}
57
-
58
- {#if error}
59
- {@render fallback?.()}
60
- {/if}
10
+ {:catch error}
11
+ {@render fallback?.(error)}
12
+ {/await}
@@ -18,6 +18,8 @@ import type { Vector3 } from 'three'
18
18
  import type { Key, Stage } from '@threlte/core'
19
19
 
20
20
  export type WorldProps = {
21
+ framerate?: number | 'varying'
22
+ autoStart?: boolean
21
23
  gravity?: Parameters<Vector3['set']>
22
24
  rawIntegrationParameters?: RawIntegrationParameters
23
25
  rawIslands?: RawIslandManager
@@ -32,10 +34,16 @@ export type WorldProps = {
32
34
  rawPhysicsPipeline?: RawPhysicsPipeline
33
35
  rawSerializationPipeline?: RawSerializationPipeline
34
36
  rawDebugRenderPipeline?: RawDebugRenderPipeline
35
- stage?: Key | Stage
36
-
37
+ simulationStageOptions?: {
38
+ before?: (Key | Stage) | (Key | Stage)[]
39
+ after?: (Key | Stage) | (Key | Stage)[]
40
+ }
41
+ synchronizationStageOptions?: {
42
+ before?: (Key | Stage) | (Key | Stage)[]
43
+ after?: (Key | Stage) | (Key | Stage)[]
44
+ }
37
45
  children?: Snippet
38
- fallback?: Snippet
46
+ fallback?: Snippet<[error: any]>
39
47
  }
40
48
 
41
49
  export default class World extends SvelteComponent<WorldProps> {}
@@ -1,3 +1,3 @@
1
- import type { ColliderEvents, RigidBodyEvents } from '../types/types';
2
1
  import { type Collider } from '@dimforge/rapier3d-compat';
2
+ import type { ColliderEvents, RigidBodyEvents } from '../types/types';
3
3
  export declare const applyColliderActiveEvents: (collider: Collider, colliderEvents?: ColliderEvents, rigidBodyEvents?: RigidBodyEvents) => void;
@@ -0,0 +1,15 @@
1
+ import { type CurrentWritable, type Key, type Stage } from '@threlte/core';
2
+ import type { Framerate } from '../types/types';
3
+ export declare const createPhysicsStages: (framerate: CurrentWritable<Framerate>, simulationOffset: CurrentWritable<number>, updateRigidBodySimulationData: CurrentWritable<boolean>, options?: {
4
+ simulationStageOptions?: {
5
+ before?: (Key | Stage) | (Key | Stage)[];
6
+ after?: (Key | Stage) | (Key | Stage)[];
7
+ };
8
+ synchronizationStageOptions?: {
9
+ before?: (Key | Stage) | (Key | Stage)[];
10
+ after?: (Key | Stage) | (Key | Stage)[];
11
+ };
12
+ }) => {
13
+ simulationStage: Stage;
14
+ synchronizationStage: Stage;
15
+ };
@@ -0,0 +1,43 @@
1
+ import { useStage, useThrelte } from '@threlte/core';
2
+ import { simulationKey, synchronizationKey } from './keys';
3
+ export const createPhysicsStages = (framerate, simulationOffset, updateRigidBodySimulationData, options) => {
4
+ let fixedStepTimeAccumulator = 0;
5
+ let simulationTime = 0;
6
+ let lastSimulationTime = 0;
7
+ const { renderStage } = useThrelte();
8
+ const simulationStage = useStage(simulationKey, {
9
+ after: options?.simulationStageOptions?.after,
10
+ before: options?.simulationStageOptions?.before,
11
+ callback(delta, runTasks) {
12
+ if (framerate.current === 'varying') {
13
+ runTasks();
14
+ }
15
+ else {
16
+ const rate = 1 / framerate.current;
17
+ simulationTime += delta;
18
+ fixedStepTimeAccumulator += delta;
19
+ const iterations = Math.ceil(fixedStepTimeAccumulator / rate);
20
+ for (let iteration = 0; iteration < iterations; iteration++) {
21
+ updateRigidBodySimulationData.set(iteration >= iterations - 2);
22
+ runTasks(rate);
23
+ fixedStepTimeAccumulator -= rate;
24
+ lastSimulationTime += rate;
25
+ }
26
+ simulationOffset.set((simulationTime - lastSimulationTime) / rate + 1);
27
+ }
28
+ }
29
+ });
30
+ const synchronizationStage = useStage(synchronizationKey, {
31
+ after: options?.synchronizationStageOptions?.after
32
+ ? Array.isArray(options.synchronizationStageOptions.after)
33
+ ? [...options.synchronizationStageOptions.after, simulationKey]
34
+ : [options.synchronizationStageOptions.after, simulationKey]
35
+ : simulationKey,
36
+ before: options?.synchronizationStageOptions?.before
37
+ ? Array.isArray(options.synchronizationStageOptions.before)
38
+ ? [...options.synchronizationStageOptions.before, renderStage]
39
+ : [options.synchronizationStageOptions.before, renderStage]
40
+ : renderStage
41
+ });
42
+ return { simulationStage, synchronizationStage };
43
+ };
@@ -0,0 +1,19 @@
1
+ import { type World } from '@dimforge/rapier3d-compat';
2
+ import { type CurrentWritable, type Stage } from '@threlte/core';
3
+ import { Object3D, Quaternion, Vector3 } from 'three';
4
+ import type { ColliderEvents, Framerate, RigidBodyEvents } from '../types/types';
5
+ type PhysicsUserData = {
6
+ currentPosition: Vector3;
7
+ currentQuaternion: Quaternion;
8
+ lastPosition: Vector3;
9
+ lastQuaternion: Quaternion;
10
+ resetPosition: boolean;
11
+ resetRotation: boolean;
12
+ };
13
+ export declare const initializeRigidBodyUserData: (obj: Object3D) => PhysicsUserData;
14
+ export declare const setInitialRigidBodyState: (obj: Object3D, initialPosition: Vector3, initialQuaternion: Quaternion) => void;
15
+ export declare const createPhysicsTasks: (world: World, framerate: CurrentWritable<Framerate>, simulationOffset: CurrentWritable<number>, rigidBodyObjects: Map<number, Object3D>, updateRigidBodySimulationData: CurrentWritable<boolean>, colliderEventDispatchers: Map<number, ColliderEvents>, rigidBodyEventDispatchers: Map<number, RigidBodyEvents>, simulationStage: Stage, synchronizationStage: Stage) => {
16
+ simulationTask: import("@threlte/core").Task;
17
+ synchronizationTask: import("@threlte/core").Task;
18
+ };
19
+ export {};
@@ -1,21 +1,17 @@
1
1
  import { EventQueue } from '@dimforge/rapier3d-compat';
2
2
  import { useTask } from '@threlte/core';
3
- import { derived } from 'svelte/store';
4
3
  import { Object3D, Quaternion, Vector3 } from 'three';
4
+ import { simulationKey, synchronizationKey } from './keys';
5
5
  const tempObject = new Object3D();
6
6
  const tempVector3 = new Vector3();
7
7
  const tempQuaternion = new Quaternion();
8
- const getEventDispatchers = (ctx, collider1, collider2) => {
9
- const collider1Events = ctx.colliderEventDispatchers.get(collider1.handle);
10
- const collider2Events = ctx.colliderEventDispatchers.get(collider2.handle);
8
+ const getEventDispatchers = (collider1, collider2, colliderEventDispatchers, rigidBodyEventDispatchers) => {
9
+ const collider1Events = colliderEventDispatchers.get(collider1.handle);
10
+ const collider2Events = colliderEventDispatchers.get(collider2.handle);
11
11
  const rigidBody1 = collider1.parent();
12
12
  const rigidBody2 = collider2.parent();
13
- const rigidBody1Events = rigidBody1
14
- ? ctx.rigidBodyEventDispatchers.get(rigidBody1.handle)
15
- : undefined;
16
- const rigidBody2Events = rigidBody2
17
- ? ctx.rigidBodyEventDispatchers.get(rigidBody2.handle)
18
- : undefined;
13
+ const rigidBody1Events = rigidBody1 ? rigidBodyEventDispatchers.get(rigidBody1.handle) : undefined;
14
+ const rigidBody2Events = rigidBody2 ? rigidBodyEventDispatchers.get(rigidBody2.handle) : undefined;
19
15
  return {
20
16
  collider1Events,
21
17
  collider2Events,
@@ -23,21 +19,48 @@ const getEventDispatchers = (ctx, collider1, collider2) => {
23
19
  rigidBody2Events
24
20
  };
25
21
  };
26
- export const useFrameHandler = (ctx, stage) => {
22
+ const objectHasPhysicsUserData = (obj) => {
23
+ return obj.userData.physics !== undefined;
24
+ };
25
+ export const initializeRigidBodyUserData = (obj) => {
26
+ const userData = {
27
+ currentPosition: new Vector3(),
28
+ currentQuaternion: new Quaternion(),
29
+ lastPosition: new Vector3(),
30
+ lastQuaternion: new Quaternion(),
31
+ resetPosition: false,
32
+ resetRotation: false
33
+ };
34
+ obj.userData.physics = userData;
35
+ return userData;
36
+ };
37
+ export const setInitialRigidBodyState = (obj, initialPosition, initialQuaternion) => {
38
+ if (!objectHasPhysicsUserData(obj)) {
39
+ initializeRigidBodyUserData(obj);
40
+ }
41
+ const userData = obj.userData.physics;
42
+ userData.currentPosition.copy(initialPosition);
43
+ userData.lastPosition.copy(initialPosition);
44
+ userData.currentQuaternion.copy(initialQuaternion);
45
+ userData.lastQuaternion.copy(initialQuaternion);
46
+ };
47
+ export const createPhysicsTasks = (world, framerate, simulationOffset, rigidBodyObjects, updateRigidBodySimulationData, colliderEventDispatchers, rigidBodyEventDispatchers, simulationStage, synchronizationStage) => {
27
48
  const eventQueue = new EventQueue(false);
28
- const { start, started, stop } = useTask((delta) => {
29
- // if (!eventQueue) return
30
- const { world } = ctx;
49
+ const simulation = useTask(simulationKey, (delta) => {
31
50
  // Set timestep to current delta, to allow for variable frame rates
32
51
  // We cap the delta at 100, so that the physics simulation doesn't get wild
33
- world.timestep = Math.min(0.1, delta);
52
+ if (framerate.current === 'varying') {
53
+ world.timestep = Math.min(delta, 0.1);
54
+ }
55
+ else {
56
+ world.timestep = delta;
57
+ }
34
58
  world.step(eventQueue);
35
- // Update meshes
36
- ctx.rigidBodyObjects.forEach((mesh, handle) => {
59
+ rigidBodyObjects.forEach((mesh, handle) => {
37
60
  const rigidBody = world.getRigidBody(handle);
38
61
  if (!rigidBody || !rigidBody.isValid())
39
62
  return;
40
- const events = ctx.rigidBodyEventDispatchers.get(handle);
63
+ const events = rigidBodyEventDispatchers.get(handle);
41
64
  if (events) {
42
65
  if (rigidBody.isSleeping() && !mesh.userData.isSleeping) {
43
66
  events.onsleep?.();
@@ -50,21 +73,35 @@ export const useFrameHandler = (ctx, stage) => {
50
73
  if (rigidBody.isSleeping() || rigidBody.isFixed() || !mesh.parent) {
51
74
  return;
52
75
  }
53
- // Position
54
- const { x, y, z } = rigidBody.translation();
55
- tempObject.position.set(x, y, z);
56
- // Rotation
57
- const rotation = rigidBody.rotation();
58
- tempQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);
59
- tempObject.rotation.setFromQuaternion(tempQuaternion);
60
- // Scale
61
- mesh.getWorldScale(tempVector3);
62
- tempObject.scale.copy(tempVector3);
63
- tempObject.updateMatrix();
64
- tempObject.applyMatrix4(mesh.parent.matrixWorld.clone().invert());
65
- tempObject.updateMatrix();
66
- mesh.position.setFromMatrixPosition(tempObject.matrix);
67
- mesh.rotation.setFromRotationMatrix(tempObject.matrix);
76
+ if (updateRigidBodySimulationData.current) {
77
+ const translation = rigidBody.translation();
78
+ const rotation = rigidBody.rotation();
79
+ if (objectHasPhysicsUserData(mesh)) {
80
+ const userData = mesh.userData.physics;
81
+ if (userData.resetPosition) {
82
+ userData.resetPosition = false;
83
+ userData.lastPosition.set(translation.x, translation.y, translation.z);
84
+ userData.currentPosition.set(translation.x, translation.y, translation.z);
85
+ }
86
+ else {
87
+ userData.lastPosition.copy(userData.currentPosition);
88
+ userData.currentPosition.set(translation.x, translation.y, translation.z);
89
+ }
90
+ if (userData.resetRotation) {
91
+ userData.resetRotation = false;
92
+ userData.lastQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);
93
+ userData.currentQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);
94
+ }
95
+ else {
96
+ userData.lastQuaternion.copy(userData.currentQuaternion);
97
+ userData.currentQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);
98
+ }
99
+ }
100
+ else {
101
+ initializeRigidBodyUserData(mesh);
102
+ setInitialRigidBodyState(mesh, tempVector3.set(translation.x, translation.y, translation.z), tempQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w));
103
+ }
104
+ }
68
105
  });
69
106
  eventQueue.drainContactForceEvents((e) => {
70
107
  const collider1 = world.getCollider(e.collider1());
@@ -73,7 +110,7 @@ export const useFrameHandler = (ctx, stage) => {
73
110
  if (!collider1 || !collider2) {
74
111
  return;
75
112
  }
76
- const { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events } = getEventDispatchers(ctx, collider1, collider2);
113
+ const { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events } = getEventDispatchers(collider1, collider2, colliderEventDispatchers, rigidBodyEventDispatchers);
77
114
  const rigidBody1 = collider1.parent();
78
115
  const rigidBody2 = collider2.parent();
79
116
  // Collider events
@@ -119,7 +156,7 @@ export const useFrameHandler = (ctx, stage) => {
119
156
  if (!collider1 || !collider2) {
120
157
  return;
121
158
  }
122
- const { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events } = getEventDispatchers(ctx, collider1, collider2);
159
+ const { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events } = getEventDispatchers(collider1, collider2, colliderEventDispatchers, rigidBodyEventDispatchers);
123
160
  if (!collider1Events && !collider2Events && !rigidBody1Events && !rigidBody2Events) {
124
161
  return;
125
162
  }
@@ -226,9 +263,41 @@ export const useFrameHandler = (ctx, stage) => {
226
263
  });
227
264
  }
228
265
  });
229
- }, { stage });
230
- // replacing the original pause and resume functions as well as the paused property
231
- ctx.pause = () => stop();
232
- ctx.resume = () => start();
233
- ctx.paused = derived(started, (started) => !started);
266
+ }, {
267
+ stage: simulationStage
268
+ });
269
+ const synchronization = useTask(synchronizationKey, () => {
270
+ rigidBodyObjects.forEach((mesh) => {
271
+ if (!objectHasPhysicsUserData(mesh))
272
+ return;
273
+ const userData = mesh.userData.physics;
274
+ if (framerate.current === 'varying') {
275
+ tempObject.position.copy(userData.currentPosition);
276
+ tempObject.quaternion.copy(userData.currentQuaternion);
277
+ }
278
+ else {
279
+ tempObject.position
280
+ .copy(userData.lastPosition)
281
+ .lerp(userData.currentPosition, simulationOffset.current);
282
+ tempObject.quaternion
283
+ .copy(userData.lastQuaternion)
284
+ .slerp(userData.currentQuaternion, simulationOffset.current);
285
+ }
286
+ // Rapier has no concept of scale, so we use the mesh's scale
287
+ mesh.getWorldScale(tempVector3);
288
+ tempObject.scale.copy(tempVector3);
289
+ tempObject.updateMatrix();
290
+ if (mesh.parent)
291
+ tempObject.applyMatrix4(mesh.parent.matrixWorld.clone().invert());
292
+ tempObject.updateMatrix();
293
+ mesh.position.setFromMatrixPosition(tempObject.matrix);
294
+ mesh.rotation.setFromRotationMatrix(tempObject.matrix);
295
+ });
296
+ }, {
297
+ stage: synchronizationStage
298
+ });
299
+ return {
300
+ simulationTask: simulation.task,
301
+ synchronizationTask: synchronization.task
302
+ };
234
303
  };
@@ -1,21 +1,15 @@
1
- /// <reference types="svelte" />
2
- import type { Collider, RigidBody } from '@dimforge/rapier3d-compat';
3
1
  import RAPIER from '@dimforge/rapier3d-compat';
4
- import type { Object3D } from 'three';
5
- import type { ColliderEvents, RigidBodyEvents } from '../types/types';
6
- export declare const createRapierContext: (gravity: RAPIER.Vector, rawIntegrationParameters?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawIntegrationParameters | undefined, rawIslands?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawIslandManager | undefined, rawBroadPhase?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawBroadPhase | undefined, rawNarrowPhase?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawNarrowPhase | undefined, rawBodies?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawRigidBodySet | undefined, rawColliders?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawColliderSet | undefined, rawImpulseJoints?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawImpulseJointSet | undefined, rawMultibodyJoints?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawMultibodyJointSet | undefined, rawCCDSolver?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawCCDSolver | undefined, rawQueryPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawQueryPipeline | undefined, rawPhysicsPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawPhysicsPipeline | undefined, rawSerializationPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawSerializationPipeline | undefined, rawDebugRenderPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawDebugRenderPipeline | undefined) => {
7
- rapier: typeof RAPIER;
8
- world: RAPIER.World;
9
- colliderObjects: Map<number, Object3D<import("three").Object3DEventMap>>;
10
- rigidBodyObjects: Map<number, Object3D<import("three").Object3DEventMap>>;
11
- rigidBodyEventDispatchers: Map<number, RigidBodyEvents>;
12
- colliderEventDispatchers: Map<number, ColliderEvents>;
13
- addColliderToContext: (collider: Collider, object: Object3D, props: ColliderEvents) => void;
14
- removeColliderFromContext: (collider: Collider) => void;
15
- addRigidBodyToContext: (rigidBody: RigidBody, object: Object3D, events: RigidBodyEvents) => void;
16
- removeRigidBodyFromContext: (rigidBody: RigidBody) => void;
17
- debug: import("svelte/store").Writable<boolean>;
18
- pause: () => void;
19
- resume: () => void;
20
- paused: import("svelte/store").Readable<boolean>;
21
- };
2
+ import { type Key, type Stage } from '@threlte/core';
3
+ import type { Framerate, RapierContext } from '../types/types';
4
+ export declare const createRapierContext: (worldArgs: [gravity: RAPIER.Vector, rawIntegrationParameters?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawIntegrationParameters | undefined, rawIslands?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawIslandManager | undefined, rawBroadPhase?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawBroadPhase | undefined, rawNarrowPhase?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawNarrowPhase | undefined, rawBodies?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawRigidBodySet | undefined, rawColliders?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawColliderSet | undefined, rawImpulseJoints?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawImpulseJointSet | undefined, rawMultibodyJoints?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawMultibodyJointSet | undefined, rawCCDSolver?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawCCDSolver | undefined, rawQueryPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawQueryPipeline | undefined, rawPhysicsPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawPhysicsPipeline | undefined, rawSerializationPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawSerializationPipeline | undefined, rawDebugRenderPipeline?: import("@dimforge/rapier3d-compat/rapier_wasm3d").RawDebugRenderPipeline | undefined], options: {
5
+ framerate?: Framerate;
6
+ autoStart?: boolean;
7
+ simulationStageOptions?: {
8
+ before?: (Key | Stage) | (Key | Stage)[];
9
+ after?: (Key | Stage) | (Key | Stage)[];
10
+ };
11
+ synchronizationStageOptions?: {
12
+ before?: (Key | Stage) | (Key | Stage)[];
13
+ after?: (Key | Stage) | (Key | Stage)[];
14
+ };
15
+ }) => RapierContext;
@@ -1,7 +1,10 @@
1
1
  import RAPIER from '@dimforge/rapier3d-compat';
2
- import { readable, writable } from 'svelte/store';
3
- export const createRapierContext = (...args) => {
4
- const world = new RAPIER.World(...args);
2
+ import { currentWritable } from '@threlte/core';
3
+ import { derived, writable } from 'svelte/store';
4
+ import { createPhysicsStages } from './createPhysicsStages';
5
+ import { createPhysicsTasks } from './createPhysicsTasks';
6
+ export const createRapierContext = (worldArgs, options) => {
7
+ const world = new RAPIER.World(...worldArgs);
5
8
  const colliderObjects = new Map();
6
9
  const rigidBodyObjects = new Map();
7
10
  const rigidBodyEventDispatchers = new Map();
@@ -42,11 +45,17 @@ export const createRapierContext = (...args) => {
42
45
  rigidBodyObjects.delete(rigidBody.handle);
43
46
  rigidBodyEventDispatchers.delete(rigidBody.handle);
44
47
  };
45
- // Dummy functions, will be replaced by useFrameHandler fn
46
- // eslint-disable-next-line @typescript-eslint/no-empty-function
47
- const pause = () => { };
48
- // eslint-disable-next-line @typescript-eslint/no-empty-function
49
- const resume = () => { };
48
+ const framerate = currentWritable(options.framerate ?? 'varying');
49
+ const simulationOffset = currentWritable(1);
50
+ const updateRigidBodySimulationData = currentWritable(false);
51
+ const { simulationStage, synchronizationStage } = createPhysicsStages(framerate, simulationOffset, updateRigidBodySimulationData, options);
52
+ const autostart = options.autoStart ?? true;
53
+ const paused = writable(!autostart);
54
+ if (!autostart) {
55
+ simulationStage.stop();
56
+ synchronizationStage.stop();
57
+ }
58
+ const { simulationTask, synchronizationTask } = createPhysicsTasks(world, framerate, simulationOffset, rigidBodyObjects, updateRigidBodySimulationData, colliderEventDispatchers, rigidBodyEventDispatchers, simulationStage, synchronizationStage);
50
59
  return {
51
60
  rapier: RAPIER,
52
61
  world,
@@ -59,8 +68,23 @@ export const createRapierContext = (...args) => {
59
68
  addRigidBodyToContext,
60
69
  removeRigidBodyFromContext,
61
70
  debug: writable(false),
62
- pause,
63
- resume,
64
- paused: readable(false)
71
+ pause: () => {
72
+ paused.set(true);
73
+ simulationStage.stop();
74
+ synchronizationStage.stop();
75
+ },
76
+ resume: () => {
77
+ paused.set(false);
78
+ simulationStage.start();
79
+ synchronizationStage.start();
80
+ },
81
+ paused: derived(paused, (a) => a),
82
+ framerate,
83
+ simulationOffset,
84
+ simulationStage,
85
+ synchronizationStage,
86
+ updateRigidBodySimulationData,
87
+ simulationTask,
88
+ synchronizationTask
65
89
  };
66
90
  };
@@ -0,0 +1 @@
1
+ export declare const initRapier: () => true | Promise<void>;
@@ -0,0 +1,16 @@
1
+ import RAPIER from '@dimforge/rapier3d-compat';
2
+ let initialized = false;
3
+ let promise;
4
+ export const initRapier = () => {
5
+ if (initialized)
6
+ return true;
7
+ if (!promise) {
8
+ promise = new Promise((resolve) => {
9
+ RAPIER.init().then(() => {
10
+ initialized = true;
11
+ resolve();
12
+ });
13
+ });
14
+ }
15
+ return promise;
16
+ };
@@ -0,0 +1,2 @@
1
+ export declare const simulationKey: unique symbol;
2
+ export declare const synchronizationKey: unique symbol;
@@ -0,0 +1,2 @@
1
+ export const simulationKey = Symbol('simulation');
2
+ export const synchronizationKey = Symbol('synchronization');
@@ -1,7 +1,9 @@
1
1
  /// <reference types="svelte" />
2
- import type { Collider, RigidBody, TempContactManifold, Vector } from '@dimforge/rapier3d-compat';
3
- import type { Writable } from 'svelte/store';
4
- import type { createRapierContext } from '../lib/createRapierContext';
2
+ import { World, type Collider, type RigidBody, type TempContactManifold, type Vector } from '@dimforge/rapier3d-compat';
3
+ import RAPIER from '@dimforge/rapier3d-compat';
4
+ import type { CurrentWritable, Stage, Task } from '@threlte/core';
5
+ import type { Readable, Writable } from 'svelte/store';
6
+ import type { Object3D } from 'three';
5
7
  export type ColliderShapes = 'ball' | 'capsule' | 'segment' | 'triangle' | 'roundTriangle' | 'polyline' | 'trimesh' | 'cuboid' | 'roundCuboid' | 'heightfield' | 'cylinder' | 'roundCylinder' | 'cone' | 'roundCone' | 'convexHull' | 'convexMesh' | 'roundConvexHull' | 'roundConvexMesh';
6
8
  export type AutoCollidersShapes = 'cuboid' | 'ball' | 'trimesh' | 'convexHull' | 'capsule';
7
9
  export type ColliderEvents = {
@@ -45,7 +47,6 @@ export type RigidBodyEvents = ColliderEvents & {
45
47
  onsleep?: () => void;
46
48
  onwake?: () => void;
47
49
  };
48
- export type RapierContext = ReturnType<typeof createRapierContext>;
49
50
  export type CollisionGroupsContext = Writable<number> | undefined;
50
51
  export type RigidBodyUserData = {
51
52
  events?: RigidBodyEvents;
@@ -59,3 +60,31 @@ export type CollisionGroupsBitMask = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
59
60
  * Used in the <Attractor> component
60
61
  */
61
62
  export type GravityType = 'static' | 'linear' | 'newtonian';
63
+ export type Framerate = number | 'varying';
64
+ export type RapierContext = {
65
+ rapier: typeof RAPIER;
66
+ world: World;
67
+ colliderObjects: Map<number, Object3D>;
68
+ rigidBodyObjects: Map<number, Object3D>;
69
+ rigidBodyEventDispatchers: Map<number, RigidBodyEvents>;
70
+ colliderEventDispatchers: Map<number, ColliderEvents>;
71
+ addColliderToContext: (collider: Collider, object: Object3D, props: ColliderEvents) => void;
72
+ removeColliderFromContext: (collider: Collider) => void;
73
+ addRigidBodyToContext: (rigidBody: RigidBody, object: Object3D, events: RigidBodyEvents) => void;
74
+ removeRigidBodyFromContext: (rigidBody: RigidBody) => void;
75
+ debug: Writable<boolean>;
76
+ pause: () => void;
77
+ resume: () => void;
78
+ paused: Readable<boolean>;
79
+ framerate: CurrentWritable<Framerate>;
80
+ simulationStage: Stage;
81
+ simulationTask: Task;
82
+ synchronizationStage: Stage;
83
+ synchronizationTask: Task;
84
+ /**
85
+ * This number tells us how far we're off in the simulation stage as opposed
86
+ * to the render stage
87
+ */
88
+ simulationOffset: CurrentWritable<number>;
89
+ updateRigidBodySimulationData: CurrentWritable<boolean>;
90
+ };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@threlte/rapier",
3
- "version": "3.0.0-next.3",
3
+ "version": "3.0.0-next.5",
4
4
  "author": "Grischa Erbe <hello@legrisch.com> (https://legrisch.com)",
5
5
  "license": "MIT",
6
+ "description": "Components and hooks to use the Rapier physics engine in Threlte",
6
7
  "devDependencies": {
7
- "@dimforge/rapier3d-compat": "^0.12.0",
8
+ "@dimforge/rapier3d-compat": "^0.13.1",
8
9
  "@sveltejs/adapter-auto": "^3.2.0",
9
10
  "@sveltejs/kit": "^2.5.5",
10
11
  "@sveltejs/package": "^2.3.1",
@@ -21,7 +22,7 @@
21
22
  "prettier-plugin-svelte": "^3.2.2",
22
23
  "publint": "^0.2.7",
23
24
  "rimraf": "^5.0.5",
24
- "svelte": "5.0.0-next.107",
25
+ "svelte": "5.0.0-next.133",
25
26
  "svelte-check": "^3.6.9",
26
27
  "svelte-preprocess": "^5.1.3",
27
28
  "svelte2tsx": "^0.7.6",
@@ -30,7 +31,7 @@
30
31
  "type-fest": "^4.15.0",
31
32
  "typescript": "^5.4.5",
32
33
  "vite": "^5.2.8",
33
- "@threlte/core": "8.0.0-next.7"
34
+ "@threlte/core": "8.0.0-next.9"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@dimforge/rapier3d-compat": ">=0.12",
@@ -38,6 +39,24 @@
38
39
  "three": ">=0.152"
39
40
  },
40
41
  "type": "module",
42
+ "keywords": [
43
+ "threlte",
44
+ "rapier",
45
+ "svelte",
46
+ "three",
47
+ "three.js",
48
+ "3d",
49
+ "physics"
50
+ ],
51
+ "homepage": "https://threlte.xyz",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/threlte/threlte.git",
55
+ "directory": "packages/rapier"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/threlte/threlte/issues"
59
+ },
41
60
  "exports": {
42
61
  ".": {
43
62
  "types": "./dist/index.d.ts",
@@ -54,8 +73,8 @@
54
73
  "package": "svelte-kit sync && svelte-package && node ./scripts/cleanupPackage.js && publint",
55
74
  "check": "svelte-check --tsconfig ./tsconfig.json",
56
75
  "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
57
- "lint": "prettier --check --plugin-search-dir=. . && eslint .",
58
- "format": "prettier --write --plugin-search-dir=. .",
76
+ "lint": "prettier --check .",
77
+ "format": "prettier --write .",
59
78
  "cleanup": "rimraf node_modules .svelte-kit dist"
60
79
  }
61
80
  }
@@ -1,3 +0,0 @@
1
- import { type Stage, type Key } from '@threlte/core';
2
- import type { RapierContext } from '../types/types';
3
- export declare const useFrameHandler: (ctx: RapierContext, stage?: Stage | Key) => void;