Skip to content

Transparent batched mesh instances are not depth-sorted #752

Description

@julianmueller

When rendering a voxel grid as batched cubes via add_batched_meshes_simple with opacity < 1.0, the result shows incorrect transparency compositing: backfaces and geometry that is further from the camera are blended over geometry that is closer, and the artifact pattern changes discontinuously as the camera orbits.

I believe the cause is that transparency sorting in three.js happens at the object level, not the instance level. All cubes live in a single InstancedMesh / BatchedMesh, so the renderer performs one depth sort for the whole batch and then draws the instances in buffer order. For a correct back-to-front alpha blend, the instances themselves would need to be sorted per frame against the current camera position.

opacity: 1.0

Image

opacity: 0.5

Image

For Both: flat shading, no cast shadow, no receive shadow, side "front". Note the interior faces of the far side of the beam reading as if they were in front of the near side, and the stair-step regions that invert between the two views.

Reproduction

The effect is subtler here than in my real scene, but visible when orbiting — watch the boundary where the shell's far wall passes behind the near wall, and compare against the opaque control on the right.

import numpy as np
import viser

VOXEL_SIZE = 0.1

# Unit cube, counter-clockwise winding (all face normals point outward).
_h = VOXEL_SIZE / 2.0
CUBE_VERTICES = np.array(
    [
        [-_h, -_h, -_h],
        [+_h, -_h, -_h],
        [+_h, +_h, -_h],
        [-_h, +_h, -_h],
        [-_h, -_h, +_h],
        [+_h, -_h, +_h],
        [+_h, +_h, +_h],
        [-_h, +_h, +_h],
    ],
    dtype=np.float32,
)
CUBE_FACES = np.array(
    [
        [0, 2, 1],
        [0, 3, 2],  # -Z
        [4, 5, 6],
        [4, 6, 7],  # +Z
        [0, 1, 5],
        [0, 5, 4],  # -Y
        [3, 7, 6],
        [3, 6, 2],  # +Y
        [0, 4, 7],
        [0, 7, 3],  # -X
        [1, 2, 6],
        [1, 6, 5],  # +X
    ],
    dtype=np.uint32,
)


def hollow_sphere_voxels(res: int = 24, r_inner: float = 6.0, r_outer: float = 11.0):
    """Voxelized spherical shell. The hollow core exposes interior faces."""
    idx = np.stack(np.meshgrid(*[np.arange(res)] * 3, indexing="ij"), axis=-1)
    center = (res - 1) / 2.0
    radius = np.linalg.norm(idx - center, axis=-1)
    mask = (radius > r_inner) & (radius < r_outer)

    positions = (idx[mask] - center).astype(np.float32) * VOXEL_SIZE
    colors = ((idx[mask] / (res - 1)) * 255).astype(np.uint8)
    return positions, colors


server = viser.ViserServer()

positions, colors = hollow_sphere_voxels()  # 4704 voxels
wxyzs = np.tile(np.array([1.0, 0.0, 0.0, 0.0], np.float32), (len(positions), 1))

shared = dict(
    vertices=CUBE_VERTICES,
    faces=CUBE_FACES,
    batched_wxyzs=wxyzs,
    batched_colors=colors,
    flat_shading=True,
    cast_shadow=False,
    receive_shadow=False,
    side="front",
)

# Left: transparent. Artifacts appear here.
server.scene.add_batched_meshes_simple(
    "/shell_transparent",
    batched_positions=positions + np.array([-0.5, 0.0, 0.0], np.float32),
    opacity=0.5,
    **shared,
)

# Right: identical geometry, fully opaque. Renders correctly -- control case.
server.scene.add_batched_meshes_simple(
    "/shell_solid",
    batched_positions=positions + np.array([+0.5, 0.0, 0.0], np.float32),
    opacity=1.0,
    **shared,
)

server.sleep_forever()

Environment

viser version: 1.0.30
Python version: 3.11.15
OS: Ubuntu 24.04
Browser and version: Firefox 152.0.4 (on Windows)
GPU: NVIDIA RTX 4080

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions