Skip to content

Commit

Permalink
Improved camera controls (#101)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
AdamRashid96 and brentyi authored Sep 25, 2023
1 parent c6b8233 commit 6123ac9
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 19 deletions.
2 changes: 2 additions & 0 deletions src/viser/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type ViewerContextContents = {
cameraRef: React.MutableRefObject<THREE.PerspectiveCamera | null>;
backgroundMaterialRef: React.MutableRefObject<THREE.ShaderMaterial | null>;
cameraControlRef: React.MutableRefObject<CameraControls | null>;
resetCameraViewRef: React.MutableRefObject<(() => void) | null>;
// Scene node attributes.
// This is intentionally placed outside of the Zustand state to reduce overhead.
nodeAttributesFromName: React.MutableRefObject<{
Expand Down Expand Up @@ -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([]),
Expand Down
81 changes: 64 additions & 17 deletions src/viser/client/src/CameraControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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: [
Expand All @@ -61,6 +93,9 @@ export function SynchronizedCameraControls() {
});
}, [camera, sendCameraThrottled]);

//Camera Animation code
const animationId = useRef<number | null>(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);
Expand All @@ -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,
Expand All @@ -94,24 +122,38 @@ export function SynchronizedCameraControls() {
ARROW_UP: 38,
ARROW_RIGHT: 39,
ARROW_DOWN: 40,
SPACE: " ",
Q: 81,
E: 69,
};
const cameraControls = viewer.cameraControlRef.current!;

const wKey = new holdEvent.KeyboardKeyHold(KEYCODE.W, 20);
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);
Expand All @@ -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,
);
Expand All @@ -150,15 +192,20 @@ 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 (
<CameraControls
ref={viewer.cameraControlRef}
minDistance={0.1}
maxDistance={200.0}
dollySpeed={0.3}
smoothTime={0.0}
smoothTime={0.05}
draggingSmoothTime={0.0}
onChange={sendCamera}
makeDefault
Expand Down
11 changes: 10 additions & 1 deletion src/viser/client/src/ControlPanel/ServerControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
Switch,
TextInput,
} from "@mantine/core";
import { IconBrandGithub, IconHomeMove, IconPhoto } from "@tabler/icons-react";
import { Box, Stats } from "@react-three/drei";
import { IconBrandGithub, IconPhoto } from "@tabler/icons-react";
import React from "react";
import SceneTreeTable from "./SceneTreeTable";

Expand Down Expand Up @@ -102,6 +102,15 @@ export default function ServerControls() {
>
Export Canvas
</Button>
<Button
onClick={() => {
viewer.resetCameraViewRef.current!();
}}
fullWidth
leftIcon={<IconHomeMove size="1rem" />}
>
Reset View
</Button>
<Switch
label="WebGL Statistics"
onChange={(event) => {
Expand Down
3 changes: 2 additions & 1 deletion src/viser/client/src/WebsocketInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -353,6 +353,7 @@ function useMessageHandler() {
prevPosition.x,
prevPosition.y,
prevPosition.z,
false,
);
return;
}
Expand Down

0 comments on commit 6123ac9

Please sign in to comment.