Skip to content

Instantly share code, notes, and snippets.

@micmania1
Created July 27, 2022 11:07
Show Gist options
  • Save micmania1/6a189a51691b8239826a0a3ecad7f0d5 to your computer and use it in GitHub Desktop.
Save micmania1/6a189a51691b8239826a0a3ecad7f0d5 to your computer and use it in GitHub Desktop.
import { Physics, SphereProps, useSphere } from '@react-three/cannon';
import { SphereGeometryProps, useThree } from '@react-three/fiber';
import { useXR } from '@react-three/xr';
import { useCallback, useEffect, useRef } from 'react';
import { Group, Mesh, Vector3 } from 'three';
import { GrabEvent, XRInteraction } from '../grabbable/XRInteraction';
import PhysicalBox from '../physical-box/physical-box';
import { states, useAppStore } from '../store/useAppStore';
import { useUserPreferences } from '../store/useUserPreferences';
import { useBouncyBallMaterial } from '../use-physics-material/use-physics-material';
import { useXRMotionControllers } from '../use-xrmotion-controller/use-xrmotion-controller';
type BallProps = SphereProps &
Pick<SphereGeometryProps, 'args'> & { visible?: boolean };
const Ball = ({ args, visible = true, ...props }: BallProps) => {
const groupRef = useRef<Group>();
const [ref, api] = useSphere<Mesh>(() => ({
args,
mass: 1,
...props,
}));
const grabStartHandler = (e: unknown) => {
const event = e as GrabEvent;
const group = groupRef.current;
const ball = ref.current;
const controller = event.controller;
if (controller && group && ball) {
api.sleep();
ball.visible = false;
controller.grip.add(ball);
api.position.set(0, 0, 0);
ball.visible = true;
}
};
const grabEndHandler = (e: unknown) => {
const event = e as GrabEvent;
const group = groupRef.current;
const ball = ref.current;
const controller = event.controller;
if (controller && group && ball) {
const worldPosition = new Vector3();
controller.grip.getWorldPosition(worldPosition);
ball.visible = false;
group.add(ball);
api.position.set(worldPosition.x, worldPosition.y, worldPosition.z);
ball.visible = true;
api.wakeUp();
}
};
useEffect(() => {
const test = groupRef.current;
if (test) {
test.addEventListener('grabstart', grabStartHandler);
test.addEventListener('grabend', grabEndHandler);
}
return () => {
if (test) {
test.removeEventListener('grabstart', grabStartHandler);
test.removeEventListener('grabend', grabEndHandler);
}
};
});
return (
<XRInteraction ref={groupRef}>
<mesh ref={ref} visible={visible}>
<sphereGeometry args={args} />
<meshStandardMaterial color={0xff0000} roughness={0.2} />
</mesh>
</XRInteraction>
);
};
function Level1WithPhysics(props: Level1Props) {
const state = useAppStore((state) => state.state);
const camera = useThree((three) => three.camera);
const { player } = useXR();
const playerHeight = useUserPreferences((user) => user.playerHeight);
const transitionToStartMenu = useAppStore((app) => app.transitionToStartMenu);
const { left, right } = useXRMotionControllers();
const pauseHandler = useCallback(
() => transitionToStartMenu({ time: 2000, player, playerHeight, camera }),
[transitionToStartMenu, player, playerHeight, camera]
);
useEffect(() => {
const buttons = { ...left?.buttons, ...right?.buttons };
const pauseButtons = [buttons['x-button'], buttons['a-button']];
pauseButtons.forEach((button) => {
if (button) {
button.addEventListener('buttonpress', pauseHandler);
}
});
return () => {
pauseButtons.forEach((button) => {
if (button) {
button.removeEventListener('buttonpress', pauseHandler);
}
});
};
}, [left?.buttons, pauseHandler, right?.buttons]);
useEffect(() => {
switch (state) {
case states.LEVEL_1:
player.position.set(0, 0, -0.3);
player.rotation.set(0, Math.PI, 0);
camera.position.set(0, 0, 0);
camera.rotation.set(0, 0, 0);
break;
}
}, [
camera.position,
camera.rotation,
player.position,
player.rotation,
playerHeight,
state,
]);
const bouncyBallMaterial = useBouncyBallMaterial();
return (
<>
<PhysicalBox />
<Ball
args={[0.05]}
position={[0, 3.8, 0.5]}
material={bouncyBallMaterial}
/>
</>
);
}
import { useXR, useXREvent, XRController } from '@react-three/xr';
import { forwardRef, ReactNode, useImperativeHandle, useRef } from 'react';
import * as THREE from 'three';
import { Object3D } from 'three';
import create from 'zustand';
/*-------------------------------------------------------------------------------------
* Utility functions
*------------------------------------------------------------------------------------*/
const intersects = (controller: THREE.Box3, group: THREE.Group) => {
const box = new THREE.Box3();
box.setFromObject(group);
return controller.intersectsBox(box);
};
/*-------------------------------------------------------------------------------------
* XR Interaction Manager / State store
*------------------------------------------------------------------------------------*/
export type GrabEvent = {
type: 'grabstart' | 'grabend';
target: Object3D;
controller: XRController;
};
type XRInteractionManager = {
grabbing: Partial<Record<THREE.XRHandedness, THREE.Object3D | undefined>>;
startGrab(object: THREE.Object3D, controller: XRController): void;
endGrab(object: THREE.Object3D, controller: XRController): void;
};
const useXRInteractionManager = create<XRInteractionManager>((set, get) => ({
grabbing: {},
startGrab(object: THREE.Object3D, controller: XRController) {
const grabbing = get().grabbing;
const handedness = controller.inputSource.handedness;
set({
grabbing: {
...grabbing,
[handedness]: object,
},
});
object.dispatchEvent({ type: 'grabstart', controller });
},
endGrab(object: THREE.Object3D, controller: XRController) {
const grabbing = get().grabbing;
const handedness = controller.inputSource.handedness;
if (object.id === grabbing[handedness]?.id) {
grabbing[handedness]?.dispatchEvent({
type: 'grabend',
controller,
});
set({
grabbing: {
...grabbing,
[handedness]: undefined,
},
});
}
},
}));
/*-------------------------------------------------------------------------------------
* Interactive Component
*------------------------------------------------------------------------------------*/
// eslint-disable-next-line @typescript-eslint/ban-types
type XRInteractionProps = {
children: ReactNode;
};
export const XRInteraction = forwardRef<unknown, XRInteractionProps>(
({ children }, ref) => {
const groupRef = useRef<THREE.Group>(null);
useImperativeHandle(ref, () => groupRef.current);
const { controllers } = useXR();
const startGrab = useXRInteractionManager((im) => im.startGrab);
const endGrab = useXRInteractionManager((im) => im.endGrab);
useXREvent('squeezestart', (e) => {
if (groupRef.current) {
const controller = controllers.find(
(controller) =>
controller.inputSource.handedness ===
e.controller.inputSource.handedness
);
if (controller) {
const controllerBoundingBox = new THREE.Box3();
controllerBoundingBox.setFromObject(controller.grip);
if (
controllerBoundingBox &&
intersects(controllerBoundingBox, groupRef.current)
) {
startGrab(groupRef.current, e.controller);
}
}
}
});
useXREvent('squeezeend', (e) => {
if (groupRef.current) {
endGrab(groupRef.current, e.controller);
}
});
return (
<group name="XRInteraction" ref={groupRef}>
{children}
</group>
);
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment