Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adam/camera controls #101

Merged
merged 15 commits into from
Sep 25, 2023
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
14 changes: 13 additions & 1 deletion src/viser/client/src/ControlPanel/ServerControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ViewerContext } from "../App";
import { Button, Divider, Stack, Switch, TextInput } from "@mantine/core";
import { Stats } from "@react-three/drei";
import { IconPhoto } from "@tabler/icons-react";
import {
IconHomeMove,
IconPhoto,
} from "@tabler/icons-react";
import React from "react";
import SceneTreeTable from "./SceneTreeTable";

Expand Down Expand Up @@ -95,6 +98,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
Loading