Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## Unreleased

### Added

- Add zero-allocation scalar component getters and setters to `RigidBody`, `Collider`,
and `KinematicCharacterController`. Methods like `translationX()`, `translationY()`,
`rotationAngle()` (2D), `rotationX/Y/Z/W()` (3D), `linvelX/Y(/Z)()` return individual
`number` values without creating intermediate JS objects or WASM heap allocations.
Scalar setters `setLinvelXY/XYZ()`, `addForceXY/XYZ()`, `applyImpulseXY/XYZ()` avoid
the `VectorOps.intoRaw()` allocation on the input path. These are performance alternatives
to the existing struct-returning methods, which remain unchanged.

## 0.19.3 (05 Nov. 2025)

- Significantly improve performances of `combineVoxelStates`.
Expand Down
110 changes: 110 additions & 0 deletions rapier-compat/tests/ScalarGetters2d.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
init,
Vector2,
World,
RigidBodyDesc,
ColliderDesc,
} from "../builds/2d-deterministic/pkg";

describe("2d/ScalarGetters", () => {
let world: World;

beforeAll(init);

afterAll(async () => {
await Promise.resolve();
});

beforeEach(() => {
world = new World(new Vector2(0, -9.81));
});

afterEach(() => {
world.free();
});

test("rbTranslationX/Y match translation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setTranslation(3.0, 7.0);
const body = world.createRigidBody(bodyDesc);
expect(body.translationX()).toBeCloseTo(3.0);
expect(body.translationY()).toBeCloseTo(7.0);
const full = body.translation();
expect(body.translationX()).toBeCloseTo(full.x);
expect(body.translationY()).toBeCloseTo(full.y);
});

test("rbRotationAngle matches rotation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setRotation(1.5);
const body = world.createRigidBody(bodyDesc);
expect(body.rotationAngle()).toBeCloseTo(1.5);
expect(body.rotationAngle()).toBeCloseTo(body.rotation());
});

test("linvelX/Y match linvel()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setLinvel(2.0, -3.0);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
expect(body.linvelX()).toBeCloseTo(2.0);
expect(body.linvelY()).toBeCloseTo(-3.0);
});

test("setLinvelXY sets velocity correctly", () => {
const bodyDesc = RigidBodyDesc.dynamic();
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
body.setLinvelXY(5.0, -2.0, true);
expect(body.linvelX()).toBeCloseTo(5.0);
expect(body.linvelY()).toBeCloseTo(-2.0);
});

test("addForceXY adds force correctly", () => {
const bodyDesc = RigidBodyDesc.dynamic();
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
body.addForceXY(10.0, 20.0, true);
const force = body.userForce();
expect(force.x).toBeCloseTo(10.0);
expect(force.y).toBeCloseTo(20.0);
});

test("applyImpulseXY changes velocity", () => {
const bodyDesc = RigidBodyDesc.dynamic();
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
body.applyImpulseXY(1.0, 0.0, true);
expect(body.linvelX()).not.toBeCloseTo(0.0);
});

test("scalar getters work after simulation step", () => {
const bodyDesc = RigidBodyDesc.dynamic().setTranslation(0, 10);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
world.step();
const full = body.translation();
expect(body.translationX()).toBeCloseTo(full.x);
expect(body.translationY()).toBeCloseTo(full.y);
});

test("collider scalar translation getters match translation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setTranslation(5.0, 3.0);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
const collider = world.createCollider(colliderDesc, body);
const full = collider.translation();
expect(collider.translationX()).toBeCloseTo(full.x);
expect(collider.translationY()).toBeCloseTo(full.y);
});

test("collider scalar rotation getter matches rotation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setRotation(0.7);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
const collider = world.createCollider(colliderDesc, body);
expect(collider.rotationAngle()).toBeCloseTo(collider.rotation());
});
});
129 changes: 129 additions & 0 deletions rapier-compat/tests/ScalarGetters3d.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
init,
Vector3,
Quaternion,
World,
RigidBodyDesc,
ColliderDesc,
} from "../builds/3d-deterministic/pkg";

describe("3d/ScalarGetters", () => {
let world: World;

beforeAll(init);

afterAll(async () => {
await Promise.resolve();
});

beforeEach(() => {
world = new World(new Vector3(0, -9.81, 0));
});

afterEach(() => {
world.free();
});

test("rbTranslationX/Y/Z match translation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setTranslation(3.0, 7.0, -1.0);
const body = world.createRigidBody(bodyDesc);
expect(body.translationX()).toBeCloseTo(3.0);
expect(body.translationY()).toBeCloseTo(7.0);
expect(body.translationZ()).toBeCloseTo(-1.0);
const full = body.translation();
expect(body.translationX()).toBeCloseTo(full.x);
expect(body.translationY()).toBeCloseTo(full.y);
expect(body.translationZ()).toBeCloseTo(full.z);
});

test("rbRotationX/Y/Z/W match rotation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setRotation(
new Quaternion(0.0, 0.3826834, 0.0, 0.9238795),
);
const body = world.createRigidBody(bodyDesc);
const full = body.rotation();
expect(body.rotationX()).toBeCloseTo(full.x);
expect(body.rotationY()).toBeCloseTo(full.y);
expect(body.rotationZ()).toBeCloseTo(full.z);
expect(body.rotationW()).toBeCloseTo(full.w);
});

test("linvelX/Y/Z match linvel()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setLinvel(2.0, -3.0, 1.0);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
expect(body.linvelX()).toBeCloseTo(2.0);
expect(body.linvelY()).toBeCloseTo(-3.0);
expect(body.linvelZ()).toBeCloseTo(1.0);
});

test("setLinvelXYZ sets velocity correctly", () => {
const bodyDesc = RigidBodyDesc.dynamic();
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
body.setLinvelXYZ(5.0, -2.0, 3.0, true);
expect(body.linvelX()).toBeCloseTo(5.0);
expect(body.linvelY()).toBeCloseTo(-2.0);
expect(body.linvelZ()).toBeCloseTo(3.0);
});

test("addForceXYZ adds force correctly", () => {
const bodyDesc = RigidBodyDesc.dynamic();
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
body.addForceXYZ(10.0, 20.0, -5.0, true);
const force = body.userForce();
expect(force.x).toBeCloseTo(10.0);
expect(force.y).toBeCloseTo(20.0);
expect(force.z).toBeCloseTo(-5.0);
});

test("applyImpulseXYZ changes velocity", () => {
const bodyDesc = RigidBodyDesc.dynamic();
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
body.applyImpulseXYZ(1.0, 0.0, 0.0, true);
expect(body.linvelX()).not.toBeCloseTo(0.0);
});

test("scalar getters work after simulation step", () => {
const bodyDesc = RigidBodyDesc.dynamic().setTranslation(0, 10, 0);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
world.createCollider(colliderDesc, body);
world.step();
const full = body.translation();
expect(body.translationX()).toBeCloseTo(full.x);
expect(body.translationY()).toBeCloseTo(full.y);
expect(body.translationZ()).toBeCloseTo(full.z);
});

test("collider scalar translation getters match translation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setTranslation(5.0, 3.0, -2.0);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
const collider = world.createCollider(colliderDesc, body);
const full = collider.translation();
expect(collider.translationX()).toBeCloseTo(full.x);
expect(collider.translationY()).toBeCloseTo(full.y);
expect(collider.translationZ()).toBeCloseTo(full.z);
});

test("collider scalar rotation getters match rotation()", () => {
const bodyDesc = RigidBodyDesc.dynamic().setRotation(
new Quaternion(0.0, 0.3826834, 0.0, 0.9238795),
);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = ColliderDesc.ball(0.5);
const collider = world.createCollider(colliderDesc, body);
const full = collider.rotation();
expect(collider.rotationX()).toBeCloseTo(full.x);
expect(collider.rotationY()).toBeCloseTo(full.y);
expect(collider.rotationZ()).toBeCloseTo(full.z);
expect(collider.rotationW()).toBeCloseTo(full.w);
});
});
29 changes: 29 additions & 0 deletions src.ts/control/character_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,35 @@ export class KinematicCharacterController {
return VectorOps.fromRaw(this.raw.computedMovement());
}

/**
* The `x` component of the movement computed by the last call to `this.computeColliderMovement`.
*
* This is a zero-allocation alternative to `computedMovement().x`.
*/
public computedMovementX(): number {
return this.raw.computedMovementX();
}

/**
* The `y` component of the movement computed by the last call to `this.computeColliderMovement`.
*
* This is a zero-allocation alternative to `computedMovement().y`.
*/
public computedMovementY(): number {
return this.raw.computedMovementY();
}

// #if DIM3
/**
* The `z` component of the movement computed by the last call to `this.computeColliderMovement`.
*
* This is a zero-allocation alternative to `computedMovement().z`.
*/
public computedMovementZ(): number {
return this.raw.computedMovementZ();
}
// #endif

/**
* The result of ground detection computed by the last call to `this.computeColliderMovement`.
*/
Expand Down
Loading