Creature Flow: skeleton-conditioned Point-DiT

A flow matching model that generates a 2048-point surface of a quadruped from its kinematic skeleton alone (35 joints, SMAL-like topology). Because it only reads the skeleton, it can also grow bodies for skeletons it was never trained on, such as a chimera grafted from a lion body and a horse neck.

Try it: creature-flow-viewer plays the whole flow from Gaussian noise (t = 0) to the finished specimen (t = 1).

Generated creatures, side and top views

2048-point clouds generated in 10 Euler steps from the skeleton alone (red). The last column is a chimera: lion body, horse neck and head.

Model

Architecture Point-DiT, 6 blocks, width 256, 8 heads, AdaLN-Zero time modulation
Conditioning Transformer skeleton encoder, then point-to-joint cross-attention with a multi-scale distance bias
Parameters 10.4 M
Objective Flow matching on linear paths, $x_t = (1-t),x_0 + t,x_1$, target $x_1 - x_0$, with per-shape OT coupling: the 2048 noise points are matched to the 2048 surface points by exact assignment
Sampling Euler ODE solver, 10 steps
Weights Exponential moving average (decay 0.995) after 180 epochs
Data scale Points and joints are multiplied by 4 inside the flow (data_scale), so the data matches the unit-variance noise
Training data procedural-quadrupeds, 500 shapes
Training cost About 32 min on an RTX 4060 Laptop GPU in bf16, plus about 30 min of CPU once to precompute the OT-matched noise

Each attention head $h$ subtracts $\lambda_h \lVert \hat{x}_{1,i} - J_k \rVert^2$ from the point-to-joint score, with $\lambda_h = 2 \cdot 2^h$, so the 8 heads range from near-global (2) to very local (256) attention. The distance uses an estimate of the final position $\hat{x}_1 = x_t + (1-t),\hat{v}$ rather than $x_t$, which is pure noise early in the trajectory.

Results

Evaluated on 40 skeletons never seen in training (8 per family). Each generated shape is assigned to the family whose 8 reference shapes are closest on average in Chamfer distance.

This checkpoint
Family recognised 40/40 (100 %)
Chamfer distance to its family 0.0009
MMD-CD (lower is better) 0.00084
COV-CD (higher is better) 57 %
1-NNA-CD (50 % is ideal) 51.0 % (generated 2 %, real 100 %)
Median distance to the true surface 0.0029 (real shapes: 0.0004)

For scale, two real shapes of the same family are 0.0007 apart on average, and an untrained model scores about 1.9. Two training runs of this recipe both recognised 40/40, with a Chamfer distance of 0.0009 and a median surface distance of 0.0029 and 0.0030. MMD, COV and 1-NNA are the standard point-cloud generation metrics of PointFlow, computed between 200 generated shapes and 200 real shapes drawn separately (scripts/evaluate_generation.py). The surface distance is the signed distance of each point to the nearest bone cylinder, computed from the skeleton.

In the ablation, the multi-scale distance bias accounts for most of the gain: a single fixed bias reached only 40 %.

The last step came from the data, not the model. The model only reads the skeleton, and bovids differed from horses mainly by body thickness, which no joint encoded: about half of the generated bovids were taken for horses (90 % overall). Generator version 4 makes that difference visible in the skeleton. Legs stand wider under a thicker body, and bovids carry the neck at 20° instead of 49°. With the same model and training, bovids went from 13/24 to 24/24 over three seeds, with no loss on the other families.

The surfaces were then too thick. Normalised shapes have a standard deviation of about 0.25, while the starting noise has a standard deviation of 1. Multiplying the coordinates by 4 inside the flow (data_scale: 4, in the spirit of EDM's $\sigma_{data}$) improves every measure over three seeds each:

Mean over 3 seeds data_scale: 1 data_scale: 4
Median distance to the true surface 0.0129 0.0092
Points more than 30 % of the radius outside the surface 15.9 % 8.8 %
MMD-CD 0.00224 0.00154
COV-CD 48 % 51 %
1-NNA-CD 98.4 % 96.3 %

A scale of 8 (one run) was worse than 4.

The remaining blur came from undertraining. More ODE steps changed nothing, and Fourier input features were slightly worse, but shapes kept improving with every training stage while the loss barely moved (1.30 to 1.29). Training 90 epochs instead of 30 fixed most of it, over three runs each:

Mean over 3 seeds (data_scale: 4) 30 epochs 90 epochs
Median distance to the true surface 0.0090 0.0051
Points more than 30 % of the radius outside the surface 8.8 % 2.2 %
MMD-CD 0.00150 0.00098
COV-CD 50 % 56 %
1-NNA-CD 99.6 % 53.6 %
of which generated shapes correctly classified 99 % 7 %
of which real shapes correctly classified 100 % 100 %

A 1-NNA near 50 % does not mean the generated shapes are indistinguishable here. It averages two opposite errors. A real shape always has another real shape as its nearest neighbour (100 %), because the more diffuse generated shapes stay farther from it than its real neighbours. A generated shape, in turn, almost always has a real shape as its nearest neighbour (7 %): it sits closer to the real shapes than to other generated ones. The gap that remains is visible in the split, which evaluate_generation.py prints.

Per-shape OT coupling

After 90 epochs the surface plateaued at a median distance of about 0.005. A cosine learning-rate schedule, a wider model (width 384, 23 M parameters), scaling t by 1000 before the sinusoidal embedding, and a k-nearest-neighbour local attention in every block all stayed between 0.0049 and 0.0054. The gap was the same on training and unseen skeletons, and the same width (0.005 to 0.008) for bone radii from 0.04 to 0.32, which rules out memorisation and scale ambiguity.

What was left was the training target. With independent coupling, each noise point is paired with a random surface point, so $x_1 - x_0$ has a huge irreducible variance and the surface detail is a tiny part of it. Matching noise to points by optimal transport inside each shape makes the target nearly deterministic:

90 epochs, bf16 Independent coupling OT + 20 % independent noise Pure OT (2 runs)
Final loss 1.25 0.29 0.013
Median distance to the true surface 0.0052 0.0094 0.0040 / 0.0042
Mean signed distance (outward bias) +0.0019 +0.0033 +0.0003 / +0.0002
MMD-CD 0.00097 0.00162 0.00092 / 0.00095
Family recognised 100 % 100 % 100 % / 100 %

Any share of independent noise brings the variance back. Paths also become nearly straight: with this checkpoint, Euler with 5 steps (5 network evaluations, surface distance 0.0039) matches Heun with 20 steps (40 evaluations, 0.0040), and Euler 10 is the default. Heun 2 still gives usable shapes (MMD 0.00124, surface distance 0.0071). These step counts were measured on a 90-epoch checkpoint.

With a nearly deterministic target, the loss keeps falling well after 90 epochs, and the surface sharpens with it (200 vs 200 shapes, Euler 10):

90 epochs 180 epochs (2 runs) 360 epochs
Median distance to the true surface 0.0038 0.0029 / 0.0030 0.0026
MMD-CD 0.00092 0.00084 / 0.00085 0.00082
COV-CD 58 % 57 % / 56 % 56 %

A constant learning rate does as well as a cosine decay (0.0030). The gain keeps going but slows down after 180 epochs, which is the default. Two OT noise draws per shape instead of eight give the same result.

Use

git clone https://huggingface.co/MissawB/creature-flow-point-dit
cd creature-flow-point-dit
pip install -r requirements.txt safetensors
import torch
from safetensors.torch import load_file
from data.smal_quadrupeds import SMALQuadrupedGenerator
from models.point_dit import PointDiT
from models.flow_matching import OptimalTransportFlowMatching

model = PointDiT(point_dim=3, hidden_dim=256, num_layers=6, num_heads=8,
                 num_joints=35, spatial_distance_scale=2.0 / 4**2, data_scale=4.0)
model.load_state_dict(load_file("model.safetensors"))
model.eval()

joints = SMALQuadrupedGenerator(seed=0).generate_instance("felidae")["joints"]
fm = OptimalTransportFlowMatching(model)
_, points = fm.sample(torch.from_numpy(joints).unsqueeze(0), num_points=2048,
                      num_steps=10, solver="euler")
print(points.shape)  # (1, 2048, 3)

To build a chimera, graft a donor subtree onto a host skeleton with pipeline.hybrid_splicer.KinematicHybridSplicer().splice(host, donor, junction_joint="neck_base") and sample from the result.

The repository also holds the training (scripts/train.py), evaluation (scripts/evaluate_families.py, scripts/evaluate_generation.py) and viewer export (scripts/sample_and_export.py) scripts. Those scripts read .pt checkpoints produced by training. This repo ships only the EMA weights as model.safetensors.

Limitations

  • Synthetic data. Shapes are cylinders sampled around a parametric SMAL-like skeleton, not real animal scans or the SMAL model itself. Within-family variation is small (about ±3 % on proportions).
  • An easy test. The five families are separated by construction (proportions, stance, head carriage) and vary little inside a family, so 100 % shows that the model follows the skeleton, not that it generalises to real animals.
  • Surface precision. Generated surfaces are still more diffuse than real ones (median distance 0.0029 against 0.0004), and shapes about 1.3 times as far from their family as two real shapes are from each other (0.0009 against 0.0007). A nearest-neighbour classifier still recognises every real shape (1-NNA on the real side: 100 %).
  • Grafting aligns the donor subtree by translation only, with no scaling or rotation.

Collection

Source code, experiments and full results: GitHub.

Part of the Skeleton-conditioned creature flow collection with the viewer Space and the training dataset.

Downloads last month

-

Downloads are not tracked for this model. How to track
Safetensors
Model size
10.4M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Dataset used to train MissawB/creature-flow-point-dit

Space using MissawB/creature-flow-point-dit 1

Collection including MissawB/creature-flow-point-dit