Skip to content

Quickstart

Install

pip install mlx3d

For development (with uv):

git clone https://github.com/amirhossein-razlighi/mlx3D
cd mlx3D
uv sync              # creates .venv with all dev dependencies
uv run pytest tests/

Meshes and point clouds

import mlx.core as mx
from mlx3d.structures import Meshes, Pointclouds
from mlx3d.utils import ico_sphere, torus

sphere = ico_sphere(level=3)          # a Meshes batch with one mesh
print(sphere.verts_packed().shape)    # (642, 3)
print(sphere.faces_packed().shape)    # (1280, 3)

normals = sphere.verts_normals_packed()   # area-weighted vertex normals
areas = sphere.faces_areas_packed()

# Batches can mix meshes of different sizes:
batch = Meshes(
    verts=[sphere.verts_packed(), torus().verts_packed()],
    faces=[sphere.faces_packed(), torus().faces_packed()],
)
padded = batch.verts_padded()   # (2, max_V, 3), zero padded

Everything that depends on vertex positions is differentiable — you can rebuild a Meshes from an optimized vertex array every iteration and gradients flow through normals, areas and losses.

Loading and saving 3D files

from mlx3d.io import load_obj, load_ply, save_obj, save_ply

data = load_obj("bunny.obj")        # verts, faces, normals, texcoords, colors
save_ply("bunny.ply", data.verts, faces=data.faces)

Cameras and rendering points

from mlx3d.cameras import Camera
from mlx3d.renderer import render_points
from mlx3d.ops import sample_points_from_meshes

camera = Camera.look_at(eye=(2, 1, -3), at=(0, 0, 0), fov=60.0,
                        width=512, height=512)

points = sample_points_from_meshes(ico_sphere(3), 20_000)[0]
out = render_points(camera, points, radius=1.5)
image = out["image"]   # (512, 512, 3), differentiable w.r.t. points & colors

Computing losses

from mlx3d.losses import chamfer_distance, mesh_laplacian_smoothing

p1 = mx.random.normal((1, 1000, 3))
p2 = mx.random.normal((1, 1000, 3))
loss, _ = chamfer_distance(p1, p2)

smooth = mesh_laplacian_smoothing(sphere)

Optimization loops in MLX

MLX is functional: compute gradients with mx.value_and_grad and apply them with an optimizer. A minimal template used throughout the tutorials:

import mlx.optimizers as optim

params = {"points": mx.random.normal((1000, 3))}
optimizer = optim.Adam(learning_rate=1e-2)
target = sample_points_from_meshes(torus(), 1000)[0]

def loss_fn(points):
    loss, _ = chamfer_distance(points, target)
    return loss

for step in range(200):
    loss, grads = mx.value_and_grad(loss_fn)(params["points"])
    params = optimizer.apply_gradients({"points": grads}, params)
    mx.eval(params["points"])   # evaluate the lazy graph once per step

MLX is lazy

Operations build a graph; nothing runs until mx.eval (or a value is inspected). Evaluate once per iteration — more often adds overhead, much less often grows the graph too large.

Next: read the Conventions page (camera axes, quaternion order) — it will save you debugging time with real datasets.