UNPKG

@react-three/p2

Version:

2D physics based hooks for react-three-fiber

863 lines (769 loc) 25.9 kB
import type { ContactMaterialOptions, MaterialOptions } from 'p2-es' import type { DependencyList, MutableRefObject, Ref, RefObject } from 'react' import { useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { DynamicDrawUsage, InstancedMesh, MathUtils, Object3D } from 'three' import type { AtomicName, CollideBeginEvent, CollideEndEvent, CollideEvent, ConstraintOptns, ConstraintTypes, DistanceConstraintOpts, Duplet, GearConstraintOpts, LockConstraintOpts, PrismaticConstraintOpts, PropValue, ProviderContext, RayhitEvent, RayMode, RayOptions, RevoluteConstraintOpts, SetOpName, SpringOptns, SubscriptionName, SubscriptionTarget, VectorName, WheelInfoOptions, } from './setup' import { context, debugContext } from './setup' import type { CannonWorkerAPI } from './cannon-worker-api' export type AtomicProps = { allowSleep: boolean angle: number angularDamping: number angularVelocity: number collisionFilterGroup: number collisionFilterMask: number collisionResponse: boolean fixedRotation: boolean isTrigger: boolean linearDamping: number mass: number material: MaterialOptions sleepSpeedLimit: number sleepTimeLimit: number userData: Record<PropertyKey, any> } export type VectorProps = Record<VectorName, Duplet> export type BodyProps<T extends any[] = unknown[]> = Partial<AtomicProps> & Partial<VectorProps> & { args?: T onCollide?: (e: CollideEvent) => void onCollideBegin?: (e: CollideBeginEvent) => void onCollideEnd?: (e: CollideEndEvent) => void type?: 'Dynamic' | 'Static' | 'Kinematic' } export type BodyPropsArgsRequired<T extends any[] = unknown[]> = BodyProps<T> & { args: T } export type ShapeType = | 'Box' | 'Circle' | 'Capsule' | 'Particle' | 'Plane' | 'Convex' | 'Line' | 'Heightfield' export type BodyShapeType = ShapeType | 'Compound' export type BoxArgs = [width?: number, height?: number] export type CapsuleArgs = [length?: number, radius?: number] export type CircleArgs = [radius?: number] export type ConvexArgs = [vertices: number[][], axes?: number[][]] export type LineArgs = [length?: number] export type HeightfieldArgs = [ heights: number[], options?: { elementWidth?: number; maxValue?: number; minValue?: number }, ] export type BoxProps = BodyProps<BoxArgs> export type CapsuleProps = BodyProps<CapsuleArgs> export type CircleProps = BodyProps<CircleArgs> export type ConvexProps = BodyProps<ConvexArgs> export type LineProps = BodyProps<LineArgs> export type HeightfieldProps = BodyPropsArgsRequired<HeightfieldArgs> export interface CompoundBodyProps extends BodyProps { shapes: BodyProps & { type: ShapeType }[] } export type AtomicApi<K extends AtomicName> = { set: (value: AtomicProps[K]) => void subscribe: (callback: (value: AtomicProps[K]) => void) => () => void } export type VectorApi = { copy: (array: Duplet) => void set: (x: number, y: number) => void subscribe: (callback: (value: Duplet) => void) => () => void } export type WorkerApi = { [K in AtomicName]: AtomicApi<K> } & { [K in VectorName]: VectorApi } & { applyForce: (force: Duplet, worldPoint: Duplet) => void applyImpulse: (impulse: Duplet, worldPoint: Duplet) => void applyLocalForce: (force: Duplet, localPoint: Duplet) => void applyLocalImpulse: (impulse: Duplet, localPoint: Duplet) => void applyTorque: (torque: Duplet) => void sleep: () => void wakeUp: () => void } export interface PublicApi extends WorkerApi { at: (index: number) => WorkerApi } export type Api = [RefObject<Object3D>, PublicApi] const temp = new Object3D() function useForwardedRef<T>(ref: Ref<T>): MutableRefObject<T | null> { const nullRef = useRef<T>(null) return ref && typeof ref !== 'function' ? ref : nullRef } function capitalize<T extends string>(str: T): Capitalize<T> { return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize<T> } function getUUID(ref: Ref<Object3D>, index?: number): string | null { const suffix = index === undefined ? '' : `/${index}` if (typeof ref === 'function') return null return ref && ref.current && `${ref.current.uuid}${suffix}` } let incrementingId = 0 function subscribe<T extends SubscriptionName>( ref: RefObject<Object3D>, worker: CannonWorkerAPI, subscriptions: ProviderContext['subscriptions'], type: T, index?: number, target: SubscriptionTarget = 'bodies', ) { return (callback: (value: PropValue<T>) => void) => { const id = incrementingId++ subscriptions[id] = { [type]: callback } const uuid = getUUID(ref, index) uuid && worker.subscribe({ props: { id, target, type }, uuid }) return () => { delete subscriptions[id] worker.unsubscribe({ props: id }) } } } function prepare(object: Object3D, props: BodyProps) { object.userData = props.userData || {} // @todo match normal // object.position.set(...(props.position || [0, 0, 0])) // object.rotation.set(...(props.rotation || [0, 0, 0])) object.updateMatrix() } function setupCollision( events: ProviderContext['events'], { onCollide, onCollideBegin, onCollideEnd }: Partial<BodyProps>, uuid: string, ) { events[uuid] = { collide: onCollide, collideBegin: onCollideBegin, collideEnd: onCollideEnd, } } type GetByIndex<T extends BodyProps> = (index: number) => T type ArgFn<T> = (args: T) => unknown[] function useBody<B extends BodyProps<unknown[]>>( type: BodyShapeType, fn: GetByIndex<B>, argsFn: ArgFn<B['args']>, fwdRef: Ref<Object3D>, deps: DependencyList = [], ): Api { const ref = useForwardedRef(fwdRef) const { worker, refs, events, subscriptions } = useContext(context) const debugApi = useContext(debugContext) useLayoutEffect(() => { if (!ref.current) { // When the reference isn't used we create a stub // The body doesn't have a visual representation but can still be constrained ref.current = new Object3D() } const object = ref.current const currentWorker = worker const objectCount = object instanceof InstancedMesh ? (object.instanceMatrix.setUsage(DynamicDrawUsage), object.count) : 1 const uuid = object instanceof InstancedMesh ? new Array(objectCount).fill(0).map((_, i) => `${object.uuid}/${i}`) : [object.uuid] const props: (B & { args: unknown })[] = object instanceof InstancedMesh ? uuid.map((id, i) => { const props = fn(i) prepare(temp, props) object.setMatrixAt(i, temp.matrix) object.instanceMatrix.needsUpdate = true refs[id] = object if (debugApi) debugApi.add(id, props, type) setupCollision(events, props, id) return { ...props, args: argsFn(props.args) } }) : uuid.map((id, i) => { const props = fn(i) prepare(object, props) refs[id] = object if (debugApi) debugApi.add(id, props, type) setupCollision(events, props, id) return { ...props, args: argsFn(props.args) } }) // Register on mount, unregister on unmount currentWorker.addBodies({ props: props.map(({ onCollide, onCollideBegin, onCollideEnd, ...serializableProps }) => { return { onCollide: Boolean(onCollide), ...serializableProps } }), type, uuid, }) return () => { uuid.forEach((id) => { delete refs[id] if (debugApi) debugApi.remove(id) delete events[id] }) currentWorker.removeBodies({ uuid }) } }, deps) const api = useMemo(() => { const makeAtomic = <T extends AtomicName>(type: T, index?: number) => { const op: SetOpName<T> = `set${capitalize(type)}` return { set: (value: PropValue<T>) => { const uuid = getUUID(ref, index) uuid && worker[op]({ props: value, uuid, } as never) }, subscribe: subscribe(ref, worker, subscriptions, type, index), } } const makeVec = (type: VectorName, index?: number) => { const op: SetOpName<VectorName> = `set${capitalize(type)}` return { copy: (vec: number[]) => { const uuid = getUUID(ref, index) uuid && worker[op]({ props: [vec[0], vec[1]], uuid }) }, set: (x: number, y: number) => { const uuid = getUUID(ref, index) uuid && worker[op]({ props: [x, y], uuid }) }, subscribe: subscribe(ref, worker, subscriptions, type, index), } } function makeApi(index?: number): WorkerApi { return { allowSleep: makeAtomic('allowSleep', index), angle: makeAtomic('angle', index), angularDamping: makeAtomic('angularDamping', index), angularVelocity: makeAtomic('angularVelocity', index), applyForce(force: Duplet, worldPoint: Duplet) { const uuid = getUUID(ref, index) uuid && worker.applyForce({ props: [force, worldPoint], uuid }) }, applyImpulse(impulse: Duplet, worldPoint: Duplet) { const uuid = getUUID(ref, index) uuid && worker.applyImpulse({ props: [impulse, worldPoint], uuid }) }, applyLocalForce(force: Duplet, localPoint: Duplet) { const uuid = getUUID(ref, index) uuid && worker.applyLocalForce({ props: [force, localPoint], uuid }) }, applyLocalImpulse(impulse: Duplet, localPoint: Duplet) { const uuid = getUUID(ref, index) uuid && worker.applyLocalImpulse({ props: [impulse, localPoint], uuid }) }, applyTorque(torque: Duplet) { const uuid = getUUID(ref, index) uuid && worker.applyTorque({ props: [torque], uuid }) }, collisionFilterGroup: makeAtomic('collisionFilterGroup', index), collisionFilterMask: makeAtomic('collisionFilterMask', index), collisionResponse: makeAtomic('collisionResponse', index), fixedRotation: makeAtomic('fixedRotation', index), isTrigger: makeAtomic('isTrigger', index), linearDamping: makeAtomic('linearDamping', index), mass: makeAtomic('mass', index), material: makeAtomic('material', index), position: makeVec('position', index), sleep() { const uuid = getUUID(ref, index) uuid && worker.sleep({ uuid }) }, sleepSpeedLimit: makeAtomic('sleepSpeedLimit', index), sleepTimeLimit: makeAtomic('sleepTimeLimit', index), userData: makeAtomic('userData', index), velocity: makeVec('velocity', index), wakeUp() { const uuid = getUUID(ref, index) uuid && worker.wakeUp({ uuid }) }, } } const cache: { [index: number]: WorkerApi } = {} return { ...makeApi(undefined), at: (index: number) => cache[index] || (cache[index] = makeApi(index)), } }, []) return [ref, api] } export function usePlane(fn: GetByIndex<BodyProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList) { return useBody('Plane', fn, () => [], fwdRef, deps) } export function useBox(fn: GetByIndex<BoxProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList) { return useBody('Box', fn, (args = [] as BoxArgs) => args, fwdRef, deps) } export function useCapsule( fn: GetByIndex<CapsuleProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList, ) { return useBody('Capsule', fn, (args = [] as CapsuleArgs) => args, fwdRef, deps) } export function useCircle(fn: GetByIndex<CircleProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList) { return useBody('Circle', fn, (args = [] as CircleArgs) => args, fwdRef, deps) } export function useConvex(fn: GetByIndex<ConvexProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList) { return useBody('Convex', fn, (args = [[], []] as ConvexArgs) => args, fwdRef, deps) } export function useHeightfield( fn: GetByIndex<HeightfieldProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList, ) { return useBody('Heightfield', fn, (args = [[], {}] as HeightfieldArgs) => args, fwdRef, deps) } export function useLine(fn: GetByIndex<LineProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList) { return useBody('Line', fn, (args = [] as LineArgs) => args, fwdRef, deps) } export function useParticle(fn: GetByIndex<BodyProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList) { return useBody('Particle', fn, () => [], fwdRef, deps) } export function useCompoundBody( fn: GetByIndex<CompoundBodyProps>, fwdRef: Ref<Object3D> = null, deps?: DependencyList, ) { return useBody('Compound', fn, (args) => args as unknown[], fwdRef, deps) } type ConstraintApi = [ RefObject<Object3D>, RefObject<Object3D>, { disable: () => void enable: () => void }, ] type MotorConstraintApi = [ RefObject<Object3D>, RefObject<Object3D>, { disable: () => void disableMotor: () => void enable: () => void enableMotor: () => void setMotorMaxForce: (value: number) => void setMotorSpeed: (value: number) => void }, ] type SpringApi = [ RefObject<Object3D>, RefObject<Object3D>, { setDamping: (value: number) => void setRestLength: (value: number) => void setStiffness: (value: number) => void }, ] type ConstraintORHingeApi<T extends 'Prismatic' | 'Revolute' | ConstraintTypes> = T extends ConstraintTypes ? ConstraintApi : MotorConstraintApi function useConstraint<T extends 'Prismatic' | 'Revolute' | ConstraintTypes>( type: T, bodyA: Ref<Object3D>, bodyB: Ref<Object3D>, optns: ConstraintOptns | PrismaticConstraintOpts | RevoluteConstraintOpts = {}, deps: DependencyList = [], ): ConstraintORHingeApi<T> { const { worker } = useContext(context) const uuid = MathUtils.generateUUID() const refA = useForwardedRef(bodyA) const refB = useForwardedRef(bodyB) useEffect(() => { if (refA.current && refB.current) { worker.addConstraint({ props: [refA.current.uuid, refB.current.uuid, optns], type, uuid, }) return () => worker.removeConstraint({ uuid }) } }, deps) const api = useMemo(() => { if (type === 'Prismatic' || type === 'Revolute') { return { disableMotor: () => worker.disableConstraintMotor({ uuid }), enableMotor: () => worker.enableConstraintMotor({ uuid }), setMotorSpeed: (value: number) => worker.setConstraintMotorSpeed({ props: value, uuid }), } } return {} }, deps) return [refA, refB, api] as ConstraintORHingeApi<T> } export function useDistanceConstraint( bodyA: Ref<Object3D> = null, bodyB: Ref<Object3D> = null, optns: DistanceConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Distance', bodyA, bodyB, optns, deps) } export function useGearConstraint( bodyA: Ref<Object3D> = null, bodyB: Ref<Object3D> = null, optns: GearConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Gear', bodyA, bodyB, optns, deps) } export function useLockConstraint( bodyA: Ref<Object3D> = null, bodyB: Ref<Object3D> = null, optns: LockConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Lock', bodyA, bodyB, optns, deps) } export function usePrismaticConstraint( bodyA: Ref<Object3D> = null, bodyB: Ref<Object3D> = null, optns: PrismaticConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Prismatic', bodyA, bodyB, optns, deps) } export function useRevoluteConstraint( bodyA: Ref<Object3D> = null, bodyB: Ref<Object3D> = null, optns: RevoluteConstraintOpts, deps: DependencyList = [], ) { return useConstraint('Revolute', bodyA, bodyB, optns, deps) } export function useSpring( bodyA: Ref<Object3D> = null, bodyB: Ref<Object3D> = null, optns: SpringOptns, deps: DependencyList = [], ): SpringApi { const { worker } = useContext(context) const [uuid] = useState(() => MathUtils.generateUUID()) const refA = useForwardedRef(bodyA) const refB = useForwardedRef(bodyB) useEffect(() => { if (refA.current && refB.current) { worker.addSpring({ props: [refA.current.uuid, refB.current.uuid, optns], uuid, }) return () => { worker.removeSpring({ uuid }) } } }, deps) const api = useMemo( () => ({ setDamping: (value: number) => worker.setSpringDamping({ props: value, uuid }), setRestLength: (value: number) => worker.setSpringRestLength({ props: value, uuid }), setStiffness: (value: number) => worker.setSpringStiffness({ props: value, uuid }), }), deps, ) return [refA, refB, api] } function useRay( mode: RayMode, options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { const { worker, events } = useContext(context) const [uuid] = useState(() => MathUtils.generateUUID()) useEffect(() => { events[uuid] = { rayhit: callback } worker.addRay({ props: { ...options, mode }, uuid }) return () => { worker.removeRay({ uuid }) delete events[uuid] } }, deps) } export function useRaycastClosest( options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { useRay('Closest', options, callback, deps) } export function useRaycastAny( options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { useRay('Any', options, callback, deps) } export function useRaycastAll( options: RayOptions, callback: (e: RayhitEvent) => void, deps: DependencyList = [], ) { useRay('All', options, callback, deps) } export interface TopDownVehiclePublicApi { applyEngineForce: (value: number, wheelIndex: number) => void setBrake: (brake: number, wheelIndex: number) => void setSteeringValue: (value: number, wheelIndex: number) => void } export interface TopDownVehicleProps { chassisBody: Ref<Object3D> wheels: WheelInfoOptions[] } export function useTopDownVehicle( fn: () => TopDownVehicleProps, fwdRef: Ref<Object3D> = null, deps: DependencyList = [], ): [RefObject<Object3D>, TopDownVehiclePublicApi] { const ref = useForwardedRef(fwdRef) const { worker } = useContext(context) useLayoutEffect(() => { if (!ref.current) { // When the reference isn't used we create a stub // The body doesn't have a visual representation but can still be constrained ref.current = new Object3D() } const currentWorker = worker const uuid: string = ref.current.uuid const { chassisBody, wheels } = fn() const chassisBodyUUID = getUUID(chassisBody) if (!chassisBodyUUID) return currentWorker.addTopDownVehicle({ props: [chassisBodyUUID, wheels], uuid, }) return () => { currentWorker.removeTopDownVehicle({ uuid }) } }, deps) const api = useMemo<TopDownVehiclePublicApi>(() => { return { applyEngineForce(value: number, wheelIndex: number) { const uuid = getUUID(ref) uuid && worker.applyTopDownVehicleEngineForce({ props: [value, wheelIndex], uuid }) }, setBrake(brake: number, wheelIndex: number) { const uuid = getUUID(ref) uuid && worker.setTopDownVehicleBrake({ props: [brake, wheelIndex], uuid }) }, setSteeringValue(value: number, wheelIndex: number) { const uuid = getUUID(ref) uuid && worker.setTopDownVehicleSteeringValue({ props: [value, wheelIndex], uuid }) }, } }, deps) return [ref, api] } export function useContactMaterial( materialA: MaterialOptions, materialB: MaterialOptions, options: ContactMaterialOptions, deps: DependencyList = [], ): void { const { worker } = useContext(context) const [uuid] = useState(() => MathUtils.generateUUID()) useEffect(() => { worker.addContactMaterial({ props: [materialA, materialB, options], uuid, }) return () => { worker.removeContactMaterial({ uuid }) } }, deps) } type KinematicCharacterControllerCollisions = { above: boolean below: boolean climbingSlope: boolean descendingSlope: boolean faceDir: number fallingThroughPlatform: boolean left: boolean right: boolean slopeAngle: number slopeAngleOld: number velocityOld: Duplet } export interface KinematicCharacterControllerPublicApi { collisions: { subscribe: (callback: (collisions: KinematicCharacterControllerCollisions) => void) => void } raysData: { subscribe: (callback: (raysData: []) => void) => void } setInput: (input: [x: number, y: number]) => void setJump: (isDown: boolean) => void } export interface KinematicCharacterControllerProps { accelerationTimeAirborne?: number accelerationTimeGrounded?: number body: Ref<Object3D> collisionMask: number dstBetweenRays?: number maxClimbAngle?: number maxDescendAngle?: number maxJumpHeight?: number minJumpHeight?: number moveSpeed?: number skinWidth?: number timeToJumpApex?: number velocityXMin?: number velocityXSmoothing?: number wallJumpClimb?: Duplet wallJumpOff?: Duplet wallLeap?: Duplet wallSlideSpeedMax?: number wallStickTime?: number } export function useKinematicCharacterController( fn: () => KinematicCharacterControllerProps, fwdRef: Ref<Object3D> = null, deps: DependencyList = [], ): [RefObject<Object3D>, KinematicCharacterControllerPublicApi] { const ref = useForwardedRef(fwdRef) const { worker, subscriptions } = useContext(context) useLayoutEffect(() => { if (!ref.current) { // When the reference isn't used we create a stub // The body doesn't have a visual representation but can still be constrained ref.current = new Object3D() } const currentWorker = worker const uuid: string = ref.current.uuid const kinematicCharacterControllerProps = fn() const bodyUUID = getUUID(kinematicCharacterControllerProps.body) if (!bodyUUID) return currentWorker.addKinematicCharacterController({ props: [ bodyUUID, kinematicCharacterControllerProps.collisionMask, kinematicCharacterControllerProps.accelerationTimeAirborne, kinematicCharacterControllerProps.accelerationTimeGrounded, kinematicCharacterControllerProps.moveSpeed, kinematicCharacterControllerProps.wallSlideSpeedMax, kinematicCharacterControllerProps.wallStickTime, kinematicCharacterControllerProps.wallJumpClimb, kinematicCharacterControllerProps.wallJumpOff, kinematicCharacterControllerProps.wallLeap, kinematicCharacterControllerProps.timeToJumpApex, kinematicCharacterControllerProps.maxJumpHeight, kinematicCharacterControllerProps.minJumpHeight, kinematicCharacterControllerProps.velocityXSmoothing, kinematicCharacterControllerProps.velocityXMin, kinematicCharacterControllerProps.maxClimbAngle, kinematicCharacterControllerProps.maxDescendAngle, kinematicCharacterControllerProps.skinWidth, kinematicCharacterControllerProps.dstBetweenRays, ], uuid, }) return () => { currentWorker.removeKinematicCharacterController({ uuid }) } }, deps) const api = useMemo<KinematicCharacterControllerPublicApi>(() => { return { collisions: { subscribe: subscribe(ref, worker, subscriptions, 'collisions', undefined, 'controllers'), }, raysData: { subscribe: subscribe(ref, worker, subscriptions, 'raysData', undefined, 'controllers'), }, setInput(input: [x: number, y: number]) { const uuid = getUUID(ref) uuid && worker.setKinematicCharacterControllerInput({ props: input, uuid }) }, setJump(isDown: boolean) { const uuid = getUUID(ref) uuid && worker.setKinematicCharacterControllerJump({ props: isDown, uuid }) }, } }, deps) return [ref, api] } export interface PlatformControllerPublicApi { collisions: { subscribe: (callback: (collisions: {}) => void) => void } raysData: { subscribe: (callback: (raysData: []) => void) => void } } export interface PlatformControllerProps { body: Ref<Object3D> dstBetweenRays?: number localWaypoints: Duplet[] passengerMask: number skinWidth?: number speed?: number } export function usePlatformController( fn: () => PlatformControllerProps, fwdRef: Ref<Object3D> = null, deps: DependencyList = [], ): [RefObject<Object3D>, PlatformControllerPublicApi] { const ref = useForwardedRef(fwdRef) const { worker, subscriptions } = useContext(context) useLayoutEffect(() => { if (!ref.current) { // When the reference isn't used we create a stub // The body doesn't have a visual representation but can still be constrained ref.current = new Object3D() } const currentWorker = worker const uuid: string = ref.current.uuid const platformControllerProps = fn() const bodyUUID = getUUID(platformControllerProps.body) if (!bodyUUID) return currentWorker.addPlatformController({ props: [ bodyUUID, platformControllerProps.passengerMask, platformControllerProps.localWaypoints, platformControllerProps.speed, platformControllerProps.skinWidth, platformControllerProps.dstBetweenRays, ], uuid, }) return () => { currentWorker.removePlatformController({ uuid }) } }, deps) const api = useMemo<PlatformControllerPublicApi>(() => { return { collisions: { subscribe: subscribe(ref, worker, subscriptions, 'collisions', undefined, 'controllers'), }, raysData: { subscribe: subscribe(ref, worker, subscriptions, 'raysData', undefined, 'controllers'), }, } }, deps) return [ref, api] }