diff --git a/readme.md b/readme.md index ea55c6ce..658c2d61 100644 --- a/readme.md +++ b/readme.md @@ -16,6 +16,7 @@ These minor changes and improvements were also made: - Add support for [Trigger bodies](https://pmndrs.github.io/cannon-es/examples/trigger). [#83](https://github.com/pmndrs/cannon-es/pull/83) - Deprecated properties and methods have been removed. - The [original cannon.js debugger](https://github.com/schteppe/cannon.js/blob/master/tools/threejs/CannonDebugRenderer.js), which shows the wireframes of each body, has been moved to its own repo [cannon-es-debugger](https://github.com/pmndrs/cannon-es-debugger). +- Added optional property `World.frictionGravity: Vec3` which can be set to customize the force used when computing the friction between two colliding bodies. If `undefined`, `World.gravity` will be used. This property is useful to enable friction in zero gravity. This addresses issue [#224](https://github.com/schteppe/cannon.js/issues/224) and follows the [pattern established for p2.js](https://github.com/schteppe/p2.js/blob/master/src/world/World.js#L88-L92). If instead you're using three.js in a **React** environment with [react-three-fiber](https://github.com/pmndrs/react-three-fiber), check out [use-cannon](https://github.com/pmndrs/use-cannon)! It's a wrapper around cannon-es. diff --git a/src/world/Narrowphase.test.ts b/src/world/Narrowphase.test.ts index 99c1125d..3b937c33 100644 --- a/src/world/Narrowphase.test.ts +++ b/src/world/Narrowphase.test.ts @@ -6,6 +6,7 @@ import { Sphere } from '../shapes/Sphere' import { Body } from '../objects/Body' import { World } from '../world/World' import { ContactEquation } from '../equations/ContactEquation' +import { FrictionEquation } from '../equations/FrictionEquation' describe('Narrowphase', () => { test('sphere + sphere contact', () => { @@ -63,6 +64,92 @@ describe('Narrowphase', () => { expect(result.length).toBe(1) }) + + test('should default to using global gravity to create friction equations', () => { + const gravity = new Vec3(0, -9.81, 0) + const world = new World({ gravity }) + // No frictionGravity override. + expect(world.frictionGravity).toBeUndefined() + + const narrowPhase = new Narrowphase(world) + const contacts: ContactEquation[] = [] + const sphereShape = new Sphere(1) + + const bodyA = new Body({ mass: 1 }) + const bodyB = new Body({ mass: 1 }) + bodyA.addShape(sphereShape) + bodyB.addShape(sphereShape) + + narrowPhase.result = contacts + narrowPhase.sphereSphere( + sphereShape, + sphereShape, + new Vec3(0.5, 0, 0), + new Vec3(-0.5, 0, 0), + new Quaternion(), + new Quaternion(), + bodyA, + bodyB + ) + + expect(contacts.length).toBe(1) + const [contact] = contacts + const result: FrictionEquation[] = [] + + // No frictionGravity, should use global gravity. + narrowPhase.createFrictionEquationsFromContact(contact, result) + expect(result.length).toBe(2) + result.forEach((result) => { + expect(result.maxForce > 0).toBe(true) + }) + }) + + test('if provided, should use frictionGravity to create friction equations', () => { + const gravity = new Vec3(0, 0, 0) + const frictionGravity = new Vec3(0, -9.81, 0) + const world = new World({ gravity, frictionGravity }) + + const narrowPhase = new Narrowphase(world) + const contacts: ContactEquation[] = [] + const sphereShape = new Sphere(1) + + const bodyA = new Body({ mass: 1 }) + const bodyB = new Body({ mass: 1 }) + bodyA.addShape(sphereShape) + bodyB.addShape(sphereShape) + + narrowPhase.result = contacts + narrowPhase.sphereSphere( + sphereShape, + sphereShape, + new Vec3(0.5, 0, 0), + new Vec3(-0.5, 0, 0), + new Quaternion(), + new Quaternion(), + bodyA, + bodyB + ) + + expect(contacts.length).toBe(1) + const [contact] = contacts + const result: FrictionEquation[] = [] + + // No gravity, frictionGravity defined, friction. + narrowPhase.createFrictionEquationsFromContact(contact, result) + expect(result.length).toBe(2) + result.forEach((result) => { + expect(result.maxForce > 0).toBe(true) + }) + + // No gravity, no frictionGravity, no friction. + result.length = 0 + world.frictionGravity = new Vec3(0, 0, 0) + narrowPhase.createFrictionEquationsFromContact(contact, result) + expect(result.length).toBe(2) + result.forEach((result) => { + expect(result.maxForce).toBe(0) + }) + }) }) function createHeightfield() { diff --git a/src/world/Narrowphase.ts b/src/world/Narrowphase.ts index ca6c2f2a..9c7c978e 100644 --- a/src/world/Narrowphase.ts +++ b/src/world/Narrowphase.ts @@ -224,7 +224,8 @@ export class Narrowphase { if (friction > 0) { // Create 2 tangent equations - const mug = friction * world.gravity.length() + // Users may provide a force different from global gravity to use when computing contact friction. + const mug = friction * (world.frictionGravity || world.gravity).length() let reducedMass = bodyA.invMass + bodyB.invMass if (reducedMass > 0) { reducedMass = 1 / reducedMass diff --git a/src/world/World.test.ts b/src/world/World.test.ts index cc5229c5..f657f410 100644 --- a/src/world/World.test.ts +++ b/src/world/World.test.ts @@ -259,4 +259,21 @@ describe('World', () => { test('using ObjectCollisionMatrix', () => { testCollisionMatrix(ObjectCollisionMatrix) }) + + test('frictionGravity: should be undefined by default', () => { + const gravity = new Vec3(0, -9.81, 0) + const world = new World({ gravity }) + + expect(world.gravity).toEqual(gravity) + expect(world.frictionGravity).toBeUndefined() + }) + + test('frictionGravity: should be configurable', () => { + const gravity = new Vec3(0, 0, 0) + const frictionGravity = new Vec3(0, -9.81, 0) + const world = new World({ gravity, frictionGravity }) + + expect(world.gravity).toEqual(gravity) + expect(world.frictionGravity).toEqual(frictionGravity) + }) }) diff --git a/src/world/World.ts b/src/world/World.ts index 9acd4e22..9d917cb8 100644 --- a/src/world/World.ts +++ b/src/world/World.ts @@ -79,6 +79,13 @@ export class World extends EventTarget { */ gravity: Vec3 + /** + * Gravity to use when approximating the friction max force (mu*mass*gravity). + * If undefined, global gravity will be used. + * Use to enable friction in a World with a null gravity vector (no gravity). + */ + frictionGravity?: Vec3 + /** * The broadphase algorithm to use. * @default NaiveBroadphase @@ -170,6 +177,11 @@ export class World extends EventTarget { * The gravity of the world. */ gravity?: Vec3 + /** + * Gravity to use when approximating the friction max force (mu*mass*gravity). + * If undefined, global gravity will be used. + */ + frictionGravity?: Vec3 /** * Makes bodies go to sleep when they've been inactive. * @default false @@ -215,6 +227,10 @@ export class World extends EventTarget { if (options.gravity) { this.gravity.copy(options.gravity) } + if (options.frictionGravity) { + this.frictionGravity = new Vec3() + this.frictionGravity.copy(options.frictionGravity) + } this.broadphase = options.broadphase !== undefined ? options.broadphase : new NaiveBroadphase() this.bodies = []