Skip to content

Commit

Permalink
Screen space line width for camera frustums (#330)
Browse files Browse the repository at this point in the history
* Use screen space line width for camera frustums

* Fix color flip

* Sync typescript/python defs

* visual cleanup

* hover tweak
  • Loading branch information
brentyi authored Nov 12, 2024
1 parent 51964d2 commit e473b33
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 93 deletions.
4 changes: 2 additions & 2 deletions src/viser/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ class CameraFrustumProps:
"""Aspect ratio of the camera (width over height). Synchronized automatically when assigned."""
scale: float
"""Scale factor for the size of the frustum. Synchronized automatically when assigned."""
line_thickness: float
"""Thickness of the frustum lines. Synchronized automatically when assigned."""
line_width: float
"""Width of the frustum lines. Synchronized automatically when assigned."""
color: Tuple[int, int, int]
"""Color of the frustum as RGB integers. Synchronized automatically when assigned."""
image_media_type: Optional[Literal["image/jpeg", "image/png"]]
Expand Down
18 changes: 11 additions & 7 deletions src/viser/_scene_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,14 +739,15 @@ def add_camera_frustum(
fov: float,
aspect: float,
scale: float = 0.3,
line_thickness: float | None = None,
line_width: float = 2.0,
color: RgbTupleOrArray = (20, 20, 20),
image: np.ndarray | None = None,
format: Literal["png", "jpeg"] = "jpeg",
jpeg_quality: int | None = None,
wxyz: tuple[float, float, float, float] | np.ndarray = (1.0, 0.0, 0.0, 0.0),
position: tuple[float, float, float] | np.ndarray = (0.0, 0.0, 0.0),
visible: bool = True,
*_removed_kwargs,
) -> CameraFrustumHandle:
"""Add a camera frustum to the scene for visualization.
Expand All @@ -755,16 +756,15 @@ def add_camera_frustum(
and coverage of a camera in the 3D space.
Like all cameras in the viser Python API, frustums follow the OpenCV [+Z forward,
+X right, +Y down] convention. fov is vertical in radians; aspect is width over height
+X right, +Y down] convention. fov is vertical in radians; aspect is width over height.
Args:
name: A scene tree name. Names in the format of /parent/child can be used to
define a kinematic tree.
fov: Field of view of the camera (in radians).
aspect: Aspect ratio of the camera (width over height).
scale: Scale factor for the size of the frustum.
line_thickness: Thickness of the frustum lines. If not set,
defaults to `0.03 * scale`.
line_width: Width of the frustum lines, in screen space. Defaults to `2.0`.
color: Color of the frustum as an RGB tuple.
image: Optional image to be displayed on the frustum.
format: Format of the provided image ('png' or 'jpeg').
Expand All @@ -777,6 +777,12 @@ def add_camera_frustum(
Handle for manipulating scene node.
"""

if "line_thickness" in _removed_kwargs:
warnings.warn(
"The 'line_thickness' argument has been removed. Please use 'line_width' instead. Note that the units have been changed from world space to screen space.",
DeprecationWarning,
)

if image is not None:
media_type, binary = _encode_image_binary(
image, format, jpeg_quality=jpeg_quality
Expand All @@ -791,9 +797,7 @@ def add_camera_frustum(
fov=fov,
aspect=aspect,
scale=scale,
line_thickness=line_thickness
if line_thickness is not None
else 0.03 * scale,
line_width=line_width,
color=_encode_rgb(color),
image_media_type=media_type,
image_binary=binary,
Expand Down
2 changes: 1 addition & 1 deletion src/viser/client/src/SceneTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ function useObjectFactory(message: SceneNodeMessage | undefined): {
fov={message.props.fov}
aspect={message.props.aspect}
scale={message.props.scale}
lineThickness={message.props.line_thickness}
lineWidth={message.props.line_width}
color={rgbToInt(message.props.color)}
imageBinary={message.props.image_binary}
imageMediaType={message.props.image_media_type}
Expand Down
128 changes: 46 additions & 82 deletions src/viser/client/src/ThreeAssets.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Instance, Instances, shaderMaterial } from "@react-three/drei";
import { Instance, Instances, Line, shaderMaterial } from "@react-three/drei";
import { createPortal, useFrame, useThree } from "@react-three/fiber";
import { Outlines } from "./Outlines";
import React from "react";
Expand Down Expand Up @@ -699,7 +699,7 @@ export const CameraFrustum = React.forwardRef<
fov: number;
aspect: number;
scale: number;
lineThickness: number;
lineWidth: number;
color: number;
imageBinary: Uint8Array | null;
imageMediaType: string | null;
Expand All @@ -722,59 +722,53 @@ export const CameraFrustum = React.forwardRef<
let z = 1.0;

const volumeScale = Math.cbrt((x * y * z) / 3.0);
x /= volumeScale;
y /= volumeScale;
z /= volumeScale;

function scaledLineSegments(points: [number, number, number][]) {
points = points.map((xyz) => [xyz[0] * x, xyz[1] * y, xyz[2] * z]);
return [...Array(points.length - 1).keys()].map((i) => (
<LineSegmentInstance
key={i}
start={new THREE.Vector3()
.fromArray(points[i])
.multiplyScalar(props.scale)}
end={new THREE.Vector3()
.fromArray(points[i + 1])
.multiplyScalar(props.scale)}
color={props.color}
/>
));
}
x /= volumeScale * props.scale;
y /= volumeScale * props.scale;
z /= volumeScale * props.scale;

const hoveredRef = React.useContext(HoverableContext)!;
const [isHovered, setIsHovered] = React.useState(false);

useFrame(() => {
if (hoveredRef.current !== isHovered) {
setIsHovered(hoveredRef.current);
}
});

const frustumPoints: [number, number, number][] = [
// Rectangle.
[-1, -1, 1],
[1, -1, 1],
[1, -1, 1],
[1, 1, 1],
[1, 1, 1],
[-1, 1, 1],
[-1, 1, 1],
[-1, -1, 1],
// Lines to origin.
[-1, -1, 1],
[0, 0, 0],
[0, 0, 0],
[1, -1, 1],
// Lines to origin.
[-1, 1, 1],
[0, 0, 0],
[0, 0, 0],
[1, 1, 1],
// Up direction indicator.
// Don't overlap with the image if the image is present.
[0.0, -1.2, 1.0],
imageTexture === undefined ? [0.0, -0.9, 1.0] : [0.0, -1.0, 1.0],
].map((xyz) => [xyz[0] * x, xyz[1] * y, xyz[2] * z]);

return (
<group ref={ref}>
<Instances limit={9}>
<meshBasicMaterial color={props.color} side={THREE.DoubleSide} />
<cylinderGeometry
args={[props.lineThickness, props.lineThickness, 1.0, 3]}
/>
{scaledLineSegments([
// Rectangle.
[-1, -1, 1],
[1, -1, 1],
[1, 1, 1],
[-1, 1, 1],
[-1, -1, 1],
])}
{scaledLineSegments([
// Lines to origin.
[-1, -1, 1],
[0, 0, 0],
[1, -1, 1],
])}
{scaledLineSegments([
// Lines to origin.
[-1, 1, 1],
[0, 0, 0],
[1, 1, 1],
])}
{scaledLineSegments([
// Up direction.
[0.0, -1.2, 1.0],
[0.0, -0.9, 1.0],
])}
</Instances>
<Line
points={frustumPoints}
color={isHovered ? 0xfbff00 : props.color}
lineWidth={isHovered ? 1.5 * props.lineWidth : props.lineWidth}
segments
/>
{imageTexture && (
<mesh
position={[0.0, 0.0, props.scale * z]}
Expand All @@ -797,36 +791,6 @@ export const CameraFrustum = React.forwardRef<
);
});

function LineSegmentInstance(props: {
start: THREE.Vector3;
end: THREE.Vector3;
color: number;
}) {
const desiredDirection = new THREE.Vector3()
.subVectors(props.end, props.start)
.normalize();
const canonicalDirection = new THREE.Vector3(0.0, 1.0, 0.0);
const orientation = new THREE.Quaternion().setFromUnitVectors(
canonicalDirection,
desiredDirection,
);

const length = props.start.distanceTo(props.end);
const midpoint = new THREE.Vector3()
.addVectors(props.start, props.end)
.divideScalar(2.0);

return (
<Instance
position={midpoint}
quaternion={orientation}
scale={[1.0, length, 1.0]}
>
<OutlinesIfHovered creaseAngle={0.0} />
</Instance>
);
}

export const HoverableContext =
React.createContext<React.MutableRefObject<boolean> | null>(null);

Expand Down
2 changes: 1 addition & 1 deletion src/viser/client/src/WebsocketMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export interface CameraFrustumMessage {
fov: number;
aspect: number;
scale: number;
line_thickness: number;
line_width: number;
color: [number, number, number];
image_media_type: "image/jpeg" | "image/png" | null;
image_binary: Uint8Array | null;
Expand Down

0 comments on commit e473b33

Please sign in to comment.