diff --git a/src/viser/_messages.py b/src/viser/_messages.py index f050f572..8ff0faf9 100644 --- a/src/viser/_messages.py +++ b/src/viser/_messages.py @@ -495,9 +495,9 @@ class EnvironmentMapMessage(Message): background: bool background_blurriness: float background_intensity: float - background_rotation: tuple[float, float, float] + background_wxyz: Tuple[float, float, float, float] environment_intensity: float - environment_rotation: tuple[float, float, float] + environment_wxyz: Tuple[float, float, float, float] @dataclasses.dataclass diff --git a/src/viser/_scene_api.py b/src/viser/_scene_api.py index c2591327..a16ca400 100644 --- a/src/viser/_scene_api.py +++ b/src/viser/_scene_api.py @@ -181,6 +181,11 @@ def set_up_direction( (similar to Blender, 3DS Max, ROS, etc), the most common alternative is +Y (OpenGL, Maya, etc). + In practice, the impact of this can improve (1) the ergonomics of + camera controls, which will default to the same up direction as the + scene, and (2) lighting, because the default lights and environment map + are oriented to match the scene's up direction. + Args: direction: New up direction. Can either be a string (one of +x, +y, +z, -x, -y, -z) or a length-3 direction vector. @@ -491,9 +496,19 @@ def set_environment_map( background: bool = False, background_blurriness: float = 0.0, background_intensity: float = 1.0, - background_rotation: tuple[float, float, float] = (0.0, 0.0, 0.0), + background_wxyz: tuple[float, float, float, float] | np.ndarray = ( + 1.0, + 0.0, + 0.0, + 0.0, + ), environment_intensity: float = 1.0, - environment_rotation: tuple[float, float, float] = (0.0, 0.0, 0.0), + environment_wxyz: tuple[float, float, float, float] | np.ndarray = ( + 1.0, + 0.0, + 0.0, + 0.0, + ), ) -> None: """Set the environment map for the scene. This will set some lights and background. @@ -502,9 +517,9 @@ def set_environment_map( background: Show or hide the environment map in the background. background_blurriness: Blur factor of the environment map background (0-1). background_intensity: Intensity of the background. - background_rotation: Rotation of the background in radians. + background_wxyz: Orientation of the background. environment_intensity: Intensity of the environment lighting. - environment_rotation: Rotation of the environment lighting in radians. + environment_wxyz: Orientation of the environment lighting. """ self._websock_interface.queue_message( _messages.EnvironmentMapMessage( @@ -512,9 +527,9 @@ def set_environment_map( background=background, background_blurriness=background_blurriness, background_intensity=background_intensity, - background_rotation=background_rotation, + background_wxyz=cast_vector(background_wxyz, 4), environment_intensity=environment_intensity, - environment_rotation=environment_rotation, + environment_wxyz=cast_vector(environment_wxyz, 4), ) ) @@ -526,7 +541,7 @@ def enable_default_lights(self, enabled: bool = True) -> None: see :meth:`SceneApi.set_environment_map()`. Args: - enabled: True if user wants default lighting. False is user does + enabled: True if user wants default lighting. False if user does not want default lighting. """ self._websock_interface.queue_message(_messages.EnableLightsMessage(enabled)) diff --git a/src/viser/client/src/App.tsx b/src/viser/client/src/App.tsx index 3ac42adc..61719689 100644 --- a/src/viser/client/src/App.tsx +++ b/src/viser/client/src/App.tsx @@ -487,6 +487,52 @@ function DefaultLights() { ); const environmentMap = viewer.useSceneTree((state) => state.environmentMap); + // Environment map frames: + // - We want the `background_wxyz` and `environment_wxyz` to be in the Viser + // world frame. This is different from the threejs world frame, which should + // not be exposed to the user. + // - `backgroundRotation` and `environmentRotation` for the `Environment` component + // are in the threejs world frame. + const [R_threeworld_world, setR_threeworld_world] = React.useState( + // In Python, this will be set by `set_up_direction()`. + viewer.nodeAttributesFromName.current![""]!.wxyz!, + ); + useFrame(() => { + const currentR_threeworld_world = + viewer.nodeAttributesFromName.current![""]!.wxyz!; + if (currentR_threeworld_world !== R_threeworld_world) { + setR_threeworld_world(currentR_threeworld_world); + } + }); + + const Rquat_threeworld_world = new THREE.Quaternion( + R_threeworld_world[1], + R_threeworld_world[2], + R_threeworld_world[3], + R_threeworld_world[0], + ); + const Rquat_world_threeworld = Rquat_threeworld_world.clone().invert(); + const backgroundRotation = new THREE.Euler().setFromQuaternion( + new THREE.Quaternion( + environmentMap.background_wxyz[1], + environmentMap.background_wxyz[2], + environmentMap.background_wxyz[3], + environmentMap.background_wxyz[0], + ) + .premultiply(Rquat_threeworld_world) + .multiply(Rquat_world_threeworld), + ); + const environmentRotation = new THREE.Euler().setFromQuaternion( + new THREE.Quaternion( + environmentMap.environment_wxyz[1], + environmentMap.environment_wxyz[2], + environmentMap.environment_wxyz[3], + environmentMap.environment_wxyz[0], + ) + .premultiply(Rquat_threeworld_world) + .multiply(Rquat_world_threeworld), + ); + let envMapNode; if (environmentMap.hdri === null) { envMapNode = null; @@ -510,9 +556,9 @@ function DefaultLights() { background={environmentMap.background} backgroundBlurriness={environmentMap.background_blurriness} backgroundIntensity={environmentMap.background_intensity} - backgroundRotation={environmentMap.background_rotation} + backgroundRotation={backgroundRotation} environmentIntensity={environmentMap.environment_intensity} - environmentRotation={environmentMap.environment_rotation} + environmentRotation={environmentRotation} /> ); } diff --git a/src/viser/client/src/SceneTreeState.tsx b/src/viser/client/src/SceneTreeState.tsx index cddbd070..dc0aac5a 100644 --- a/src/viser/client/src/SceneTreeState.tsx +++ b/src/viser/client/src/SceneTreeState.tsx @@ -81,9 +81,9 @@ export function useSceneTreeState( background: false, background_blurriness: 0, background_intensity: 1, - background_rotation: [0, 0, 0], + background_wxyz: [1, 0, 0, 0], environment_intensity: 1, - environment_rotation: [0, 0, 0], + environment_wxyz: [1, 0, 0, 0], }, setClickable: (name, clickable) => set((state) => { diff --git a/src/viser/client/src/WebsocketMessages.ts b/src/viser/client/src/WebsocketMessages.ts index edd034e1..443b11df 100644 --- a/src/viser/client/src/WebsocketMessages.ts +++ b/src/viser/client/src/WebsocketMessages.ts @@ -301,9 +301,9 @@ export interface EnvironmentMapMessage { background: boolean; background_blurriness: number; background_intensity: number; - background_rotation: [number, number, number]; + background_wxyz: [number, number, number, number]; environment_intensity: number; - environment_rotation: [number, number, number]; + environment_wxyz: [number, number, number, number]; } /** Spot light message. * diff --git a/src/viser/transforms/_so3.py b/src/viser/transforms/_so3.py index c8a7deb6..7dc3b382 100644 --- a/src/viser/transforms/_so3.py +++ b/src/viser/transforms/_so3.py @@ -1,7 +1,7 @@ from __future__ import annotations import dataclasses -from typing import Tuple +from typing import NamedTuple, Tuple import numpy as onp import numpy.typing as onpt @@ -11,8 +11,7 @@ from .utils import broadcast_leading_axes, get_epsilon -@dataclasses.dataclass(frozen=True) -class RollPitchYaw: +class RollPitchYaw(NamedTuple): """Struct containing roll, pitch, and yaw Euler angles.""" roll: onpt.NDArray[onp.floating] @@ -131,7 +130,7 @@ def as_rpy_radians(self) -> RollPitchYaw: """Computes roll, pitch, and yaw angles. Uses the ZYX mobile robot convention. Returns: - Named tuple containing Euler angles in radians. + NamedTuple containing Euler angles in radians. """ return RollPitchYaw( roll=self.compute_roll_radians(),