Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/viser/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ class GlbProps:
"""Whether to receive shadows. If True, receives shadows normally. If
False, no shadows. If a float (0-1), shadows are rendered with a fixed
opacity regardless of lighting conditions. """
opacity: Optional[float] = None
"""Opacity of the mesh. None means opaque."""
scale: Union[float, Tuple[float, float, float]] = 1.0
"""A scale for resizing the GLB asset. A single float for uniform scaling
or a tuple of (x, y, z) for per-axis scaling."""
Expand Down Expand Up @@ -934,6 +936,10 @@ class BatchedGlbProps(_BatchedMeshExtraProps):
"""Whether or not to cast shadows."""
receive_shadow: bool
"""Whether or not to receive shadows."""
opacity: Optional[float] = None
"""Opacity of the mesh. None means opaque."""
batched_opacities: Optional[npt.NDArray[np.float32]] = None
"""Per-instance opacity multipliers, shape (N,). Multiplied with global opacity."""
scale: Union[float, Tuple[float, float, float]] = 1.0
"""Scale of the batched GLB. A single float for uniform scaling or a
tuple of (x, y, z) for per-axis scaling."""
Expand Down
30 changes: 30 additions & 0 deletions src/viser/_scene_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ def add_glb(
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,
opacity: float | None = None,
cast_shadow: bool = True,
receive_shadow: bool | float = True,
) -> GlbHandle:
Expand All @@ -693,6 +694,7 @@ def add_glb(
wxyz: Quaternion rotation to parent frame from local frame (R_pl).
position: Translation to parent frame from local frame (t_pl).
visible: Whether or not this scene node is initially visible.
opacity: Opacity of the mesh. None means opaque.
cast_shadow: Whether this node should cast shadows.
receive_shadow: Whether this node should receive shadows. If True,
receives shadows normally. If False, no shadows. If a float
Expand All @@ -708,6 +710,7 @@ def add_glb(
glb_data=glb_data,
cast_shadow=cast_shadow,
receive_shadow=receive_shadow,
opacity=opacity,
scale=scale,
),
)
Expand Down Expand Up @@ -1660,6 +1663,7 @@ def add_mesh_trimesh(
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,
opacity: float | None = None,
cast_shadow: bool = True,
receive_shadow: bool | float = True,
) -> GlbHandle:
Expand All @@ -1674,6 +1678,7 @@ def add_mesh_trimesh(
wxyz: Quaternion rotation to parent frame from local frame (R_pl).
position: Translation to parent frame from local frame (t_pl).
visible: Whether or not this scene node is initially visible.
opacity: Opacity of the mesh. None means opaque.
cast_shadow: Whether this mesh should cast shadows.
receive_shadow: Whether this mesh should receive shadows. If True,
receives shadows normally. If False, no shadows. If a float
Expand All @@ -1694,6 +1699,7 @@ def add_mesh_trimesh(
wxyz=wxyz,
position=position,
visible=visible,
opacity=opacity,
cast_shadow=cast_shadow,
receive_shadow=receive_shadow,
)
Expand Down Expand Up @@ -1827,7 +1833,9 @@ def add_batched_meshes_trimesh(
batched_positions: tuple[tuple[float, float, float], ...] | np.ndarray,
*,
batched_scales: tuple[float, ...] | np.ndarray | None = None,
batched_opacities: tuple[float, ...] | np.ndarray | None = None,
lod: Literal["auto", "off"] | tuple[tuple[float, float], ...] = "auto",
opacity: float | None = None,
cast_shadow: bool = True,
receive_shadow: bool = True,
scale: float | tuple[float, float, float] = 1.0,
Expand All @@ -1851,7 +1859,11 @@ def add_batched_meshes_trimesh(
batched_wxyzs: Float array of shape (N, 4) for orientations.
batched_positions: Float array of shape (N, 3) for positions.
batched_scales: Float array of shape (N,) for uniform scales or (N,3) for per-axis (XYZ) scales. None means scale of 1.0.
batched_opacities: Per-instance opacity multipliers, shape (N,). Each value is
multiplied with the global opacity parameter. None means all instances use
the global opacity.
lod: LOD settings, either "off", "auto", or a tuple of (distance, ratio) pairs.
opacity: Opacity of the meshes. None means opaque.
cast_shadow: Whether these meshes should cast shadows.
receive_shadow: Whether these meshes should receive shadows.
wxyz: Quaternion rotation to parent frame from local frame (R_pl).
Expand All @@ -1872,6 +1884,10 @@ def add_batched_meshes_trimesh(
batched_scales = np.asarray(batched_scales).astype(np.float32)
assert batched_scales.shape in ((num_instances,), (num_instances, 3))

if batched_opacities is not None:
batched_opacities = np.asarray(batched_opacities).astype(np.float32)
assert batched_opacities.shape == (num_instances,)

with io.BytesIO() as data_buffer:
mesh.export(data_buffer, file_type="glb")
glb_data = data_buffer.getvalue()
Expand All @@ -1883,6 +1899,8 @@ def add_batched_meshes_trimesh(
batched_positions=batched_positions.astype(np.float32),
batched_scales=batched_scales,
lod=lod,
opacity=opacity,
batched_opacities=batched_opacities,
cast_shadow=cast_shadow,
receive_shadow=receive_shadow,
scale=scale,
Expand All @@ -1899,7 +1917,9 @@ def add_batched_glb(
batched_positions: tuple[tuple[float, float, float], ...] | np.ndarray,
*,
batched_scales: tuple[float, ...] | np.ndarray | None = None,
batched_opacities: tuple[float, ...] | np.ndarray | None = None,
lod: Literal["auto", "off"] | tuple[tuple[float, float], ...] = "auto",
opacity: float | None = None,
cast_shadow: bool = True,
receive_shadow: bool = True,
scale: float | tuple[float, float, float] = 1.0,
Expand All @@ -1923,7 +1943,11 @@ def add_batched_glb(
batched_wxyzs: Float array of shape (N, 4) for orientations.
batched_positions: Float array of shape (N, 3) for positions.
batched_scales: Float array of shape (N,) for uniform scales or (N,3) for per-axis (XYZ) scales. None means scale of 1.0.
batched_opacities: Per-instance opacity multipliers, shape (N,). Each value is
multiplied with the global opacity parameter. None means all instances use
the global opacity.
lod: LOD settings, either "off", "auto", or a tuple of (distance, ratio) pairs.
opacity: Opacity of the meshes. None means opaque.
cast_shadow: Whether these GLB assets should cast shadows.
receive_shadow: Whether these GLB assets should receive shadows.
scale: Scale of the batched GLB. A single float for uniform
Expand All @@ -1946,6 +1970,10 @@ def add_batched_glb(
batched_scales = np.asarray(batched_scales).astype(np.float32)
assert batched_scales.shape in ((num_instances,), (num_instances, 3))

if batched_opacities is not None:
batched_opacities = np.asarray(batched_opacities).astype(np.float32)
assert batched_opacities.shape == (num_instances,)

message = _messages.BatchedGlbMessage(
name=name,
props=_messages.BatchedGlbProps(
Expand All @@ -1954,6 +1982,8 @@ def add_batched_glb(
batched_positions=batched_positions.astype(np.float32),
batched_scales=batched_scales,
lod=lod,
opacity=opacity,
batched_opacities=batched_opacities,
cast_shadow=cast_shadow,
receive_shadow=receive_shadow,
scale=scale,
Expand Down
83 changes: 0 additions & 83 deletions src/viser/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/viser/client/src/WebsocketMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface GlbMessage {
glb_data: Uint8Array<ArrayBuffer>;
cast_shadow: boolean;
receive_shadow: boolean | number;
opacity: number | null;
scale: number | [number, number, number];
};
}
Expand Down Expand Up @@ -387,6 +388,8 @@ export interface BatchedGlbMessage {
glb_data: Uint8Array<ArrayBuffer>;
cast_shadow: boolean;
receive_shadow: boolean;
opacity: number | null;
batched_opacities: Float32Array | null;
scale: number | [number, number, number];
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/viser/client/src/mesh/BatchedGlbAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export const BatchedGlbAsset = React.forwardRef<
batched_wxyzs={message.props.batched_wxyzs}
batched_scales={message.props.batched_scales}
batched_colors={null}
opacity={null}
batched_opacities={null}
opacity={message.props.opacity}
batched_opacities={message.props.batched_opacities}
lod={message.props.lod}
cast_shadow={message.props.cast_shadow}
receive_shadow={message.props.receive_shadow}
Expand Down
37 changes: 35 additions & 2 deletions src/viser/client/src/mesh/BatchedMeshBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ function createLODs(
return { geometries, materials };
}

/** Collect all materials from a mesh and its LOD levels. */
function getAllMaterials(mesh: InstancedMesh2): THREE.Material[] {
const out: THREE.Material[] = [];
const push = (m: THREE.Material | THREE.Material[]) => {
if (Array.isArray(m)) out.push(...m);
else out.push(m);
};
push(mesh.material);
if (mesh.LODinfo && mesh.LODinfo.objects) {
mesh.LODinfo.objects.forEach((obj) => push(obj.material));
}
return out;
}

/**
* Shared base component for batched mesh rendering
*
Expand Down Expand Up @@ -354,9 +368,22 @@ export const BatchedMeshBase = React.forwardRef<
}
}, [props.batched_colors, mesh]);

// Apply global opacity to the material when no per-instance opacities are set.
React.useEffect(() => {
if (mesh === null || props.batched_opacities !== null) return;

const opacity = props.opacity ?? 1.0;
const transparent = opacity < 1.0;

for (const mat of getAllMaterials(mesh)) {
mat.opacity = opacity;
mat.transparent = transparent;
mat.depthWrite = !transparent;
mat.needsUpdate = true;
}
}, [props.opacity, props.batched_opacities, mesh]);

// Update per-instance opacity when batched_opacities is provided.
// When only global opacity is set (no batched_opacities), it's handled
// by the material's opacity property directly - more efficient.
React.useEffect(() => {
if (mesh === null || props.batched_opacities === null) return;

Expand All @@ -370,6 +397,12 @@ export const BatchedMeshBase = React.forwardRef<
return;
}

for (const mat of getAllMaterials(mesh)) {
mat.transparent = true;
mat.depthWrite = false;
mat.needsUpdate = true;
}

const opacityView = new DataView(
props.batched_opacities.buffer,
props.batched_opacities.byteOffset,
Expand Down
22 changes: 22 additions & 0 deletions src/viser/client/src/mesh/SingleGlbAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ export const SingleGlbAsset = React.forwardRef<
});
}, [gltf, message.props.cast_shadow, message.props.receive_shadow]);

// Apply opacity to all materials in the GLB.
React.useEffect(() => {
if (!gltf) return;

const opacity = message.props.opacity ?? 1.0;
const transparent = opacity < 1.0;

gltf.scene.traverse((obj) => {
if (obj instanceof THREE.Mesh) {
const materials = Array.isArray(obj.material)
? obj.material
: [obj.material];
for (const mat of materials) {
mat.opacity = opacity;
mat.transparent = transparent;
mat.depthWrite = !transparent;
mat.needsUpdate = true;
}
}
});
}, [gltf, message.props.opacity]);

// Update animations on each frame if mixer exists.
useFrame((_, delta: number) => {
mixerRef.current?.update(delta);
Expand Down
Loading