Skip to content

Interactive Viewer

MLX3D ships a browser-based viewer for Gaussian Splatting scenes and NeRFs. Frames are rendered server-side on the Apple GPU — splats go through the Metal rasterization kernels, so typical scenes orbit in real time — and streamed to a canvas page with orbit / pan / zoom controls. No extra dependencies, nothing to install.

Truck checkpoint rendered by the same Gaussian renderer used by the browser viewer.

Viewing a Gaussian Splatting checkpoint

Any 3DGS-format .ply (trained with MLX3D or elsewhere):

mlx3d-view point_cloud.ply
# or: python -m mlx3d.viewer point_cloud.ply --background 1 1 1 --port 8090

This opens http://127.0.0.1:8090 in your browser.

From Python:

from mlx3d.splatting import GaussianModel
from mlx3d.viewer import view_gaussians

model = GaussianModel.load_ply("point_cloud.ply")
view_gaussians(model, background=(0, 0, 0))   # blocking; Ctrl-C to stop

Controls

Input Action
drag orbit
shift + drag, right-drag pan
scroll / pinch zoom
R reset camera
U flip the up axis (handy for COLMAP scenes, which are often "upside down")
D toggle Gaussian RGB / expected-depth rendering
M toggle Gaussian RGB / mesh-style depth-contour rendering
H toggle the help panel
[ / ] render resolution down / up

The page adapts resolution automatically: while you drag it renders at reduced resolution for responsiveness, then refines to full resolution when the camera settles. When nothing changes, no frames are requested at all — the GPU idles.

Gaussian checkpoints expose two display modes in the browser: RGB and depth. Depth uses a forward-only Metal splatting pass that accumulates transmittance-weighted expected depth per pixel, then colorizes it on the GPU before JPEG encoding. It is meant for geometry inspection during training and does not add work to the differentiable RGB training path.

The mesh-style mode uses the same GPU depth pass and overlays screen-space depth/alpha contours in MLX. It is a fast inspection view for geometry and holes, not an exported triangle mesh.

Viewing a NeRF

from mlx3d.viewer import view_nerf

view_nerf(model, near=2.0, far=6.0)   # model: a trained mlx3d.nn.NeRF

NeRF rendering is orders of magnitude heavier than splatting; the viewer starts at half resolution and leans on adaptive degradation. Expect seconds per full-resolution frame for a full-size NeRF — fine for inspecting a training run, not a real-time experience.

Viewing anything else

Viewer works with any callback that maps a camera to an image, so you can inspect custom renderers too:

import mlx.core as mx
from mlx3d.viewer import Viewer
from mlx3d.renderer import render_points

points = ...   # (P, 3)
mx.eval(points)  # arrays captured by the callback must be evaluated

viewer = Viewer(lambda cam: render_points(cam, points)["image"])
viewer.serve(port=8090)

Threading note

Frames are rendered on HTTP handler threads, and MLX cannot evaluate lazy arrays that were created on a different thread. Call mx.eval on everything your callback captures before serve() — the view_gaussians / view_nerf helpers already do this.