From 6123ac92cb838b1320a19427a9f08845c2bd3e7c Mon Sep 17 00:00:00 2001 From: AdamRashid96 <71362382+AdamRashid96@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:14:44 -0700 Subject: [PATCH] Improved camera controls (#101) * q and e and space support * space support * Animate camera going home * clean up code ' * Run prettier * Run prettier, add missing docs page * Remove extra file * Make view reset a button * Fix typescript * Run prettier * Remove unused imports --------- Co-authored-by: Brent Yi --- src/viser/client/src/App.tsx | 2 + src/viser/client/src/CameraControls.tsx | 81 +++++++++++++++---- .../src/ControlPanel/ServerControls.tsx | 11 ++- src/viser/client/src/WebsocketInterface.tsx | 3 +- 4 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/viser/client/src/App.tsx b/src/viser/client/src/App.tsx index 0653459c5..f5f5505d0 100644 --- a/src/viser/client/src/App.tsx +++ b/src/viser/client/src/App.tsx @@ -49,6 +49,7 @@ export type ViewerContextContents = { cameraRef: React.MutableRefObject; backgroundMaterialRef: React.MutableRefObject; cameraControlRef: React.MutableRefObject; + resetCameraViewRef: React.MutableRefObject<(() => void) | null>; // Scene node attributes. // This is intentionally placed outside of the Zustand state to reduce overhead. nodeAttributesFromName: React.MutableRefObject<{ @@ -101,6 +102,7 @@ function ViewerRoot() { cameraRef: React.useRef(null), backgroundMaterialRef: React.useRef(null), cameraControlRef: React.useRef(null), + resetCameraViewRef: React.useRef(null), // Scene node attributes that aren't placed in the zustand state for performance reasons. nodeAttributesFromName: React.useRef({}), messageQueueRef: React.useRef([]), diff --git a/src/viser/client/src/CameraControls.tsx b/src/viser/client/src/CameraControls.tsx index 58f4f9336..503ed7840 100644 --- a/src/viser/client/src/CameraControls.tsx +++ b/src/viser/client/src/CameraControls.tsx @@ -3,11 +3,10 @@ import { makeThrottledMessageSender } from "./WebsocketFunctions"; import { CameraControls } from "@react-three/drei"; import { useThree } from "@react-three/fiber"; import * as holdEvent from "hold-event"; -import React, { useContext } from "react"; +import React, { useContext, useRef } from "react"; import { PerspectiveCamera } from "three"; import * as THREE from "three"; -/** OrbitControls, but synchronized with the server and other panels. */ export function SynchronizedCameraControls() { const viewer = useContext(ViewerContext)!; const camera = useThree((state) => state.camera as PerspectiveCamera); @@ -17,6 +16,30 @@ export function SynchronizedCameraControls() { 20, ); + // Helper for resetting camera poses. + const initialCameraRef = useRef<{ + camera: PerspectiveCamera; + lookAt: THREE.Vector3; + } | null>(null); + + viewer.resetCameraViewRef.current = () => { + viewer.cameraControlRef.current!.setLookAt( + initialCameraRef.current!.camera.position.x, + initialCameraRef.current!.camera.position.y, + initialCameraRef.current!.camera.position.z, + initialCameraRef.current!.lookAt.x, + initialCameraRef.current!.lookAt.y, + initialCameraRef.current!.lookAt.z, + true, + ); + viewer.cameraRef.current!.up.set( + initialCameraRef.current!.camera.up.x, + initialCameraRef.current!.camera.up.y, + initialCameraRef.current!.camera.up.z, + ); + viewer.cameraControlRef.current!.updateCameraUp(); + }; + // Callback for sending cameras. const sendCamera = React.useCallback(() => { const three_camera = camera; @@ -42,6 +65,15 @@ export function SynchronizedCameraControls() { .getTarget(new THREE.Vector3()) .applyQuaternion(R_world_threeworld); const up = three_camera.up.clone().applyQuaternion(R_world_threeworld); + + //Store initial camera values + if (initialCameraRef.current === null) { + initialCameraRef.current = { + camera: three_camera.clone(), + lookAt: camera_control.getTarget(new THREE.Vector3()), + }; + } + sendCameraThrottled({ type: "ViewerCameraMessage", wxyz: [ @@ -61,6 +93,9 @@ export function SynchronizedCameraControls() { }); }, [camera, sendCameraThrottled]); + //Camera Animation code + const animationId = useRef(null); + // Send camera for new connections. // We add a small delay to give the server time to add a callback. const connected = viewer.useGui((state) => state.websocketConnected); @@ -77,13 +112,6 @@ export function SynchronizedCameraControls() { }, [camera]); // Keyboard controls. - // - // TODO: (critical) we should move this to the root component. Currently if - // we add 100 panes and remove 99 of them, we'll still have 100 event - // listeners. This should also be combined with some notion notion of the - // currently active pane, and only apply keyboard controls to that pane. - // - // Currently all panes listen to events all the time. React.useEffect(() => { const KEYCODE = { W: 87, @@ -94,6 +122,9 @@ export function SynchronizedCameraControls() { ARROW_UP: 38, ARROW_RIGHT: 39, ARROW_DOWN: 40, + SPACE: " ", + Q: 81, + E: 69, }; const cameraControls = viewer.cameraControlRef.current!; @@ -101,17 +132,28 @@ export function SynchronizedCameraControls() { const aKey = new holdEvent.KeyboardKeyHold(KEYCODE.A, 20); const sKey = new holdEvent.KeyboardKeyHold(KEYCODE.S, 20); const dKey = new holdEvent.KeyboardKeyHold(KEYCODE.D, 20); + const qKey = new holdEvent.KeyboardKeyHold(KEYCODE.Q, 20); + const eKey = new holdEvent.KeyboardKeyHold(KEYCODE.E, 20); + + // TODO: these event listeners are currently never removed, even if this + // component gets unmounted. aKey.addEventListener("holding", (event) => { - cameraControls.truck(-0.002 * event?.deltaTime, 0, false); + cameraControls.truck(-0.002 * event?.deltaTime, 0, true); }); dKey.addEventListener("holding", (event) => { - cameraControls.truck(0.002 * event?.deltaTime, 0, false); + cameraControls.truck(0.002 * event?.deltaTime, 0, true); }); wKey.addEventListener("holding", (event) => { - cameraControls.forward(0.002 * event?.deltaTime, false); + cameraControls.forward(0.002 * event?.deltaTime, true); }); sKey.addEventListener("holding", (event) => { - cameraControls.forward(-0.002 * event?.deltaTime, false); + cameraControls.forward(-0.002 * event?.deltaTime, true); + }); + qKey.addEventListener("holding", (event) => { + cameraControls.elevate(0.002 * event?.deltaTime, true); + }); + eKey.addEventListener("holding", (event) => { + cameraControls.elevate(-0.002 * event?.deltaTime, true); }); const leftKey = new holdEvent.KeyboardKeyHold(KEYCODE.ARROW_LEFT, 20); @@ -120,14 +162,14 @@ export function SynchronizedCameraControls() { const downKey = new holdEvent.KeyboardKeyHold(KEYCODE.ARROW_DOWN, 20); leftKey.addEventListener("holding", (event) => { cameraControls.rotate( - -0.1 * THREE.MathUtils.DEG2RAD * event?.deltaTime, + -0.05 * THREE.MathUtils.DEG2RAD * event?.deltaTime, 0, true, ); }); rightKey.addEventListener("holding", (event) => { cameraControls.rotate( - 0.1 * THREE.MathUtils.DEG2RAD * event?.deltaTime, + 0.05 * THREE.MathUtils.DEG2RAD * event?.deltaTime, 0, true, ); @@ -150,7 +192,12 @@ export function SynchronizedCameraControls() { // TODO: we currently don't remove any event listeners. This is a bit messy // because KeyboardKeyHold attaches listeners directly to the // document/window; it's unclear if we can remove these. - }); + return () => { + if (animationId.current !== null) { + cancelAnimationFrame(animationId.current); + } + }; + }, [CameraControls]); return ( Export Canvas + { diff --git a/src/viser/client/src/WebsocketInterface.tsx b/src/viser/client/src/WebsocketInterface.tsx index 06ecad1f3..0e78e2e2f 100644 --- a/src/viser/client/src/WebsocketInterface.tsx +++ b/src/viser/client/src/WebsocketInterface.tsx @@ -325,7 +325,7 @@ function useMessageHandler() { message.look_at[2], ); target.applyQuaternion(R_threeworld_world); - cameraControls.setTarget(target.x, target.y, target.z); + cameraControls.setTarget(target.x, target.y, target.z, false); return; } case "SetCameraUpDirectionMessage": { @@ -353,6 +353,7 @@ function useMessageHandler() { prevPosition.x, prevPosition.y, prevPosition.z, + false, ); return; }