Skip to content

Commit

Permalink
Orbit origin tool (#361)
Browse files Browse the repository at this point in the history
* Camera orientation tool

* colors

* animate + polish

* comment

* downgrade ubuntu for Python 3.8

* Rename + show if logging camera

* Control panel nits
  • Loading branch information
brentyi authored Jan 4, 2025
1 parent e065643 commit 372da64
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 17 deletions.
260 changes: 244 additions & 16 deletions src/viser/client/src/CameraControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,83 @@ import { ViewerContext } from "./App";
import { CameraControls } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import * as holdEvent from "hold-event";
import React, { useContext, useRef } from "react";
import React, { useContext, useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
import { PerspectiveCamera } from "three";
import * as THREE from "three";
import { computeT_threeworld_world } from "./WorldTransformUtils";
import { useThrottledMessageSender } from "./WebsocketFunctions";
import { Grid, PivotControls } from "@react-three/drei";

function OrbitOriginTool({
forceShow,
pivotRef,
onPivotChange,
update,
}: {
forceShow: boolean;
pivotRef: React.RefObject<THREE.Group>;
onPivotChange: (matrix: THREE.Matrix4) => void;
update: () => void;
}) {
const viewer = useContext(ViewerContext)!;
const showCameraControls = viewer.useGui(
(state) => state.showOrbitOriginTool,
);
React.useEffect(update, [showCameraControls]);

if (!showCameraControls && !forceShow) return null;

return (
<PivotControls
ref={pivotRef}
scale={200}
lineWidth={4}
fixed={true}
axisColors={["#ffaaff", "#ff33ff", "#ffaaff"]}
disableScaling={true}
onDragEnd={() => {
onPivotChange(pivotRef.current!.matrix);
}}
>
<mesh>
<sphereGeometry args={[0.1, 32, 32]} />
<shaderMaterial
transparent
uniforms={{
color: { value: new THREE.Color("#ff33ff") },
size: { value: 200.0 },
}}
vertexShader={`
// Custom shader for defining sphere size in screen space.
uniform float size;
void main() {
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
vec4 clipPosOffset = projectionMatrix * modelViewMatrix * vec4(position * size / 1000.0, 1.0);
gl_Position = clipPos + (clipPosOffset - clipPos) * clipPos.w;
}
`}
fragmentShader={`
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 0.8);
}
`}
/>
</mesh>
<Grid
args={[10, 10, 10, 10]}
infiniteGrid
fadeStrength={0}
fadeFrom={0}
fadeDistance={1000}
sectionColor={"#ffaaff"}
cellColor={"#ffccff"}
side={THREE.DoubleSide}
/>
</PivotControls>
);
}

export function SynchronizedCameraControls() {
const viewer = useContext(ViewerContext)!;
Expand All @@ -20,7 +92,157 @@ export function SynchronizedCameraControls() {
lookAt: THREE.Vector3;
} | null>(null);

const pivotRef = useRef<THREE.Group>(null);

const cameraControlRef = viewer.cameraControlRef;

// Animation state interface
interface CameraAnimation {
startUp: THREE.Vector3;
targetUp: THREE.Vector3;
startLookAt: THREE.Vector3;
targetLookAt: THREE.Vector3;
startTime: number;
duration: number;
}

const [cameraAnimation, setCameraAnimation] =
useState<CameraAnimation | null>(null);

// Animation parameters
const ANIMATION_DURATION = 0.5; // seconds

useFrame((state) => {
if (cameraAnimation && cameraControlRef.current) {
const cameraControls = cameraControlRef.current;
const camera = cameraControls.camera;

const elapsed = state.clock.getElapsedTime() - cameraAnimation.startTime;
const progress = Math.min(elapsed / cameraAnimation.duration, 1);

// Smooth step easing
const t = progress * progress * (3 - 2 * progress);

// Interpolate up vector
const newUp = new THREE.Vector3()
.copy(cameraAnimation.startUp)
.lerp(cameraAnimation.targetUp, t)
.normalize();

// Interpolate look-at position
const newLookAt = new THREE.Vector3()
.copy(cameraAnimation.startLookAt)
.lerp(cameraAnimation.targetLookAt, t);

camera.up.copy(newUp);

// Back up position
const prevPosition = new THREE.Vector3();
cameraControls.getPosition(prevPosition);

cameraControls.updateCameraUp();

// Restore position and set new look-at
cameraControls.setPosition(
prevPosition.x,
prevPosition.y,
prevPosition.z,
false,
);

cameraControls.setLookAt(
prevPosition.x,
prevPosition.y,
prevPosition.z,
newLookAt.x,
newLookAt.y,
newLookAt.z,
false,
);

// Clear animation when complete
if (progress >= 1) {
setCameraAnimation(null);
}
}
});

const { clock } = useThree();

const updateCameraLookAtAndUpFromPivotControl = (matrix: THREE.Matrix4) => {
if (!cameraControlRef.current) return;

const targetPosition = new THREE.Vector3();
targetPosition.setFromMatrixPosition(matrix);

const cameraControls = cameraControlRef.current;
const camera = cameraControlRef.current.camera;

// Get target up vector from matrix
const targetUp = new THREE.Vector3().setFromMatrixColumn(matrix, 1);

// Get current look-at position
const currentLookAt = cameraControls.getTarget(new THREE.Vector3());

// Start new animation
setCameraAnimation({
startUp: camera.up.clone(),
targetUp: targetUp,
startLookAt: currentLookAt,
targetLookAt: targetPosition,
startTime: clock.getElapsedTime(),
duration: ANIMATION_DURATION,
});
};

const updatePivotControlFromCameraLookAtAndup = () => {
if (cameraAnimation !== null) return;
if (!cameraControlRef.current) return;
if (!pivotRef.current) return;

const cameraControls = cameraControlRef.current;
const lookAt = cameraControls.getTarget(new THREE.Vector3());

// Rotate matrix s.t. it's y-axis aligns with the camera's up vector.
// We'll do this with math.
const origRotation = new THREE.Matrix4().extractRotation(
pivotRef.current.matrix,
);

const cameraUp = camera.up.clone().normalize();
const pivotUp = new THREE.Vector3(0, 1, 0)
.applyMatrix4(origRotation)
.normalize();
const axis = new THREE.Vector3()
.crossVectors(pivotUp, cameraUp)
.normalize();
const angle = Math.acos(Math.min(1, Math.max(-1, cameraUp.dot(pivotUp))));

// Create rotation matrix
const rotationMatrix = new THREE.Matrix4();
if (axis.lengthSq() > 0.0001) {
// Check if cross product is valid
rotationMatrix.makeRotationAxis(axis, angle);
}
// rotationMatrix.premultiply(origRotation);

// Combine rotation with position
const matrix = new THREE.Matrix4();
matrix.multiply(rotationMatrix);
matrix.multiply(origRotation);
matrix.setPosition(lookAt);

pivotRef.current.matrix.copy(matrix);
pivotRef.current.updateMatrixWorld(true);
};

viewer.resetCameraViewRef.current = () => {
viewer.cameraRef.current!.up.set(
initialCameraRef.current!.camera.up.x,
initialCameraRef.current!.camera.up.y,
initialCameraRef.current!.camera.up.z,
);
viewer.cameraControlRef.current!.updateCameraUp();
viewer.cameraControlRef.current!.setLookAt(
initialCameraRef.current!.camera.position.x,
initialCameraRef.current!.camera.position.y,
Expand All @@ -30,12 +252,6 @@ export function SynchronizedCameraControls() {
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.
Expand All @@ -51,6 +267,8 @@ export function SynchronizedCameraControls() {
const t_world_camera = new THREE.Vector3();
const scale = new THREE.Vector3();
const sendCamera = React.useCallback(() => {
updatePivotControlFromCameraLookAtAndup();

const three_camera = camera;
const camera_control = viewer.cameraControlRef.current;

Expand Down Expand Up @@ -270,14 +488,24 @@ export function SynchronizedCameraControls() {
}, [CameraControls]);

return (
<CameraControls
ref={viewer.cameraControlRef}
minDistance={0.01}
dollySpeed={0.3}
smoothTime={0.05}
draggingSmoothTime={0.0}
onChange={sendCamera}
makeDefault
/>
<>
<CameraControls
ref={viewer.cameraControlRef}
minDistance={0.01}
dollySpeed={0.3}
smoothTime={0.05}
draggingSmoothTime={0.0}
onChange={sendCamera}
makeDefault
/>
<OrbitOriginTool
forceShow={logCamera !== null /* Always show if logging camera */}
pivotRef={pivotRef}
onPivotChange={(matrix) => {
updateCameraLookAtAndUpFromPivotControl(matrix);
}}
update={updatePivotControlFromCameraLookAtAndup}
/>
</>
);
}
2 changes: 1 addition & 1 deletion src/viser/client/src/ControlPanel/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function ControlPanel(props: {
>
<Tooltip
zIndex={100}
label={showSettings ? "Return to GUI" : "Connection & diagnostics"}
label={showSettings ? "Return to GUI" : "Configuration & diagnostics"}
withinPortal
>
{showSettings ? (
Expand Down
2 changes: 2 additions & 0 deletions src/viser/client/src/ControlPanel/GuiState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface GuiState {
shareUrl: string | null;
websocketConnected: boolean;
backgroundAvailable: boolean;
showOrbitOriginTool: boolean;
guiUuidSetFromContainerUuid: {
[containerUuid: string]: { [uuid: string]: true } | undefined;
};
Expand Down Expand Up @@ -65,6 +66,7 @@ const cleanGuiState: GuiState = {
shareUrl: null,
websocketConnected: false,
backgroundAvailable: false,
showOrbitOriginTool: false,
guiUuidSetFromContainerUuid: {},
modals: [],
guiOrderFromUuid: {},
Expand Down
28 changes: 28 additions & 0 deletions src/viser/client/src/ControlPanel/ServerControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Switch,
Text,
TextInput,
Tooltip,
} from "@mantine/core";
import { IconHomeMove, IconPhoto } from "@tabler/icons-react";
import { Stats } from "@react-three/drei";
Expand Down Expand Up @@ -130,6 +131,33 @@ export default function ServerControls() {
>
Reset View
</Button>
<Tooltip
label={
<>
Show tool for setting the look-at point and
<br />
up direction of the camera.
<br />
<br />
These can be used to set the origin of the
<br />
camera&apos;s orbit controls.
</>
}
refProp="rootRef"
position="top-start"
>
<Switch
radius="sm"
label="Orbit Origin Tool"
onChange={(event) => {
viewer.useGui.setState({
showOrbitOriginTool: event.currentTarget.checked,
});
}}
size="sm"
/>
</Tooltip>
<Switch
radius="sm"
label="WebGL Statistics"
Expand Down

0 comments on commit 372da64

Please sign in to comment.