Skip to content

Commit

Permalink
Basic Snapshots (pmndrs#568)
Browse files Browse the repository at this point in the history
Co-authored-by: K1940 <[email protected]>
  • Loading branch information
wiledal and kayden1940 authored Dec 5, 2023
1 parent 0c88645 commit a71ede9
Show file tree
Hide file tree
Showing 21 changed files with 433 additions and 165 deletions.
7 changes: 5 additions & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["demo"]
"ignore": ["demo"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
5 changes: 5 additions & 0 deletions .changeset/soft-eels-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-three/rapier-addons": patch
---

Update changeset settings
5 changes: 5 additions & 0 deletions .changeset/tame-fireants-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-three/rapier": minor
---

Adds basic snapshot capabilities by adding `setWorld`
19 changes: 19 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Description
<!-- A summary of the changes. Add a video or screenshot if it makes sense! -->

### Type of change
<!-- Remove unrelated items -->

- 🐛 Bug fix
- ✨ New feature
- 📦 Other (tests, refactoring, docs, etc.)

### Checklist:
<!-- Check all that apply, remove any that don't -->

- [ ] 🔍 I have performed a self-review of my code
- [ ] 💬 I have commented my code, particularly in hard-to-understand areas
- [ ] 📗 I have made corresponding changes to the documentation
- [ ] ⭐️ My changes generate no new warnings
- [ ] 🧪 I have added tests
- [ ] 🟢 All new and existing unit tests pass
6 changes: 4 additions & 2 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { PerformanceExample } from "./examples/performance/PeformanceExample";
import { DynamicTypeChangeExample } from "./examples/dynamic-type-change/DynamicTypeChangeExample";
import { StutteringExample } from "./examples/stuttering/StutteringExample";
import { ImmutablePropsExample } from "./examples/immutable-props/ImmutablePropsExample";
import { SnapshotExample } from './examples/snapshot/SnapshotExample';

const demoContext = createContext<{
setDebug?(f: boolean): void;
Expand Down Expand Up @@ -114,7 +115,8 @@ const routes: Record<string, ReactNode> = {
performance: <PerformanceExample />,
"dynamic-type-changes": <DynamicTypeChangeExample />,
stuttering: <StutteringExample />,
"immutable-props": <ImmutablePropsExample />
"immutable-props": <ImmutablePropsExample />,
snapshot: <SnapshotExample />
};

export const App = () => {
Expand Down Expand Up @@ -147,7 +149,7 @@ export const App = () => {
interpolate={interpolate}
debug={debug}
timeStep={1 / 60}
// erp={0.2}
// erp={0.2}
>
<directionalLight
castShadow
Expand Down
71 changes: 71 additions & 0 deletions demo/src/examples/snapshot/SnapshotExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Box, Sphere, useTexture } from "@react-three/drei";
import { RigidBody, useRapier } from "@react-three/rapier";
import { RepeatWrapping } from "three";
import { Demo } from "../../App";

import React, { useRef } from "react";

import { useControls, button } from "leva";

export const SnapshotExample: Demo = () => {
const floor = useTexture(
new URL("../damping/white.png", import.meta.url).toString()
);
const ramp = useTexture(
new URL("../damping/red.png", import.meta.url).toString()
);
const ball = useTexture(
new URL("../damping/green.png", import.meta.url).toString()
);

floor.wrapS = floor.wrapT = RepeatWrapping;

const balls = Array.from(Array(10).keys());

const { world, setWorld, rapier } = useRapier();
const worldSnapshot = useRef<Uint8Array>();

useControls({
takeSnapshot: button(() => (worldSnapshot.current = world.takeSnapshot())),
restoreSnapshot: button(
() =>
!!worldSnapshot.current &&
setWorld(rapier.World.restoreSnapshot(worldSnapshot.current))
)
});

return (
<>
<group>
<RigidBody type="fixed" colliders="cuboid">
<Box args={[40, 1, 100]} position={[18, -1, 25]}>
<meshStandardMaterial map={floor} />
</Box>
</RigidBody>

<RigidBody type="fixed" colliders="cuboid">
<Box
args={[40, 0.5, 14]}
position={[18, 2, -5]}
rotation={[Math.PI / 8, 0, 0]}
>
<meshStandardMaterial map={ramp} />
</Box>
</RigidBody>

{balls.map((i) => (
<RigidBody
key={i}
colliders="ball"
position={[i * 3, 10, -10]}
angularDamping={i / 10}
>
<Sphere>
<meshStandardMaterial map={ball} />
</Sphere>
</RigidBody>
))}
</group>
</>
);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@changesets/cli": "^2.22.0",
"@changesets/cli": "^2.27.1",
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@manypkg/cli": "^0.19.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/react-three-rapier-addons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"sideEffects": false,
"peerDependencies": {
"@react-three/fiber": "*",
"@react-three/rapier": "1.1.2",
"@react-three/rapier": "1.x",
"react": ">=18.0.0",
"three": "*"
},
Expand All @@ -16,7 +16,7 @@
},
"devDependencies": {
"@react-three/fiber": "8.9.1",
"@react-three/rapier": "1.1.2",
"@react-three/rapier": "1.x",
"react": "18.2.0",
"react-dom": "18.2.0",
"three": "0.146.0"
Expand Down
36 changes: 36 additions & 0 deletions packages/react-three-rapier/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,3 +812,39 @@ Setting `<Physics updateLoop="independent" />` will make the physics simulation
<Physics updateLoop="independent">...</Physics>
</Canvas>
```

### Snapshots
The `world` can be serialized as a `Uint8Array` using `world.takeSnapshot()`, see Rapier's docs on [Serialization](https://rapier.rs/docs/user_guides/javascript/serialization/) for more info.

The snapshot can be used to construct a new world. In `r3/rapier`, you need to replace the world with this snapshot.

> [!NOTE]
> This only works if the snapshotted world is identical to the restored one. If objects, or the order of creation of objects vary, expect RigidBodies to scramble.

```tsx
import { useRapier } from '@react-three/rapier';

const SnapshottingComponent = () => {
const { world, setWorld, rapier } = useRapier();
const worldSnapshot = useRef<Uint8Array>();

// Store the snapshot
const takeSnapshot = () => {
const snapshot = world.takeSnapshot()
worldSnapshot.current = snapshot
}

// Create a new World from the snapshot, and replace the current one
const restoreSnapshot = () => {
setWorld(rapier.World.restoreSnapshot(worldSnapshot.current))
}

return <>
<Rigidbody>...</RigidBody>
<Rigidbody>...</RigidBody>
<Rigidbody>...</RigidBody>
<Rigidbody>...</RigidBody>
<Rigidbody>...</RigidBody>
</>
}
```
8 changes: 7 additions & 1 deletion packages/react-three-rapier/src/components/Physics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ export interface RapierContext {
*/
world: World;

/**
* overwriting the Rapier physics world with a restored snapshot
*/
setWorld: (world: World) => void;

/**
* If the physics simulation is paused
*/
Expand Down Expand Up @@ -368,7 +373,7 @@ export const Physics: FC<PhysicsProps> = (props) => {
* This creates a singleton proxy, so that the world is only created when
* something within it is accessed.
*/
const { proxy: worldProxy, reset: resetWorldProxy } = useConst(() =>
const { proxy: worldProxy, reset: resetWorldProxy, set: setWorldProxy } = useConst(() =>
createSingletonProxy<World>(
() => new rapier.World(vectorArrayToVector3(gravity))
)
Expand Down Expand Up @@ -710,6 +715,7 @@ export const Physics: FC<PhysicsProps> = (props) => {
() => ({
rapier,
world: worldProxy,
setWorld: (world: World) => { setWorldProxy(world) },
physicsOptions: {
colliders,
gravity
Expand Down
10 changes: 7 additions & 3 deletions packages/react-three-rapier/src/utils/singleton-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
*/
export const createSingletonProxy = <
SingletonClass extends object,
CreationFn extends () => SingletonClass = () => SingletonClass
CreationFn extends () => SingletonClass = () => SingletonClass,
>(
/**
* A function that returns a new instance of the class
*/
createInstance: CreationFn
): { proxy: SingletonClass; reset: () => void } => {
): { proxy: SingletonClass; reset: () => void, set: (newInstance: SingletonClass) => void } => {
let instance: SingletonClass | undefined;

const handler: ProxyHandler<SingletonClass> = {
Expand All @@ -36,8 +36,12 @@ export const createSingletonProxy = <
instance = undefined;
};

const set = (newInstance: SingletonClass) => {
instance = newInstance;
};

/**
* Return the proxy and a reset function
*/
return { proxy, reset };
return { proxy, reset, set };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`physics > snapshots > restores snapshots correctly 1`] = `
[
Vector3 {
"x": 0,
"y": -13.761244773864746,
"z": 0,
},
Vector3 {
"x": 2,
"y": -11.761244773864746,
"z": 2,
},
Vector3 {
"x": -2,
"y": -15.761244773864746,
"z": -2,
},
]
`;

exports[`physics > snapshots > restores snapshots correctly 2`] = `
[
Vector3 {
"x": 0,
"y": -13.761244773864746,
"z": 0,
},
Vector3 {
"x": 2,
"y": -11.761244773864746,
"z": 2,
},
Vector3 {
"x": -2,
"y": -15.761244773864746,
"z": -2,
},
]
`;
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ import {
import React, { useEffect } from "react";
import ReactThreeTestRenderer from "@react-three/test-renderer";
import { Box } from "@react-three/drei";
import { pause, renderHookWithErrors, UseRapierMounter } from "./test-utils";
import {
getScenePositions,
pause,
RapierContextCatcher,
renderHookWithErrors,
UseRapierMounter
} from "./test-utils";
import { useFrame } from "@react-three/fiber";
import { renderHook } from "@testing-library/react";

describe("physics", () => {
describe("useRapier exposed things", () => {
Expand Down Expand Up @@ -139,10 +146,8 @@ describe("physics", () => {
const beforeStepCallback = vi.fn();
const frameCallback = vi.fn();

let renderer: Awaited<ReturnType<typeof ReactThreeTestRenderer.create>>;

await new Promise(async (resolve) => {
renderer = await ReactThreeTestRenderer.create(
await ReactThreeTestRenderer.create(
<Physics updateLoop="independent">
<RigidBody colliders="cuboid" restitution={2}>
<CuboidCollider args={[1, 1, 1]} />
Expand Down Expand Up @@ -209,4 +214,52 @@ describe("physics", () => {
}).rejects.toEqual(error);
});
});

describe("snapshots", () => {
it("restores snapshots correctly", async () => {
let rapierContext: ReturnType<typeof useRapier>;

const renderer = await ReactThreeTestRenderer.create(
<Physics>
<RapierContextCatcher callback={(ctx) => (rapierContext = ctx)} />
<RigidBody colliders="cuboid">
<CuboidCollider args={[1, 1, 1]} />
</RigidBody>
<RigidBody colliders="cuboid" position={[2, 2, 2]}>
<CuboidCollider args={[1, 1, 1]} />
</RigidBody>
<RigidBody colliders="cuboid" position={[-2, -2, -2]}>
<CuboidCollider args={[1, 1, 1]} />
</RigidBody>
</Physics>
);

// Advance 100 frames to move the boxes
renderer.advanceFrames(100, 1 / 60);

// Advance make snapshot
const snap = rapierContext!.world.takeSnapshot();

// Advance 1 more frame
renderer.advanceFrames(1, 1 / 60);

// Save positions at this frame
const positions = getScenePositions(renderer);
expect(getScenePositions(renderer)).toMatchSnapshot();

renderer.advanceFrames(100, 1 / 60);

// Restore snapshot
rapierContext!.setWorld(
rapierContext!.rapier.World.restoreSnapshot(snap)
);

// Advance 1 more frame to move boxes again
renderer.advanceFrames(1, 1 / 60);

// Check for match
expect(positions).toEqual(getScenePositions(renderer));
expect(getScenePositions(renderer)).toMatchSnapshot();
});
});
});
File renamed without changes.
Loading

0 comments on commit a71ede9

Please sign in to comment.