Rendering & preview
avatar render (the avatar-render crate) turns an avatar — and a Unity
world scene — into a PNG, headless, with no Unity in the loop. It builds on the runtime rig layer
(FBX/glTF → RawMesh) and the Unity-YAML reader; avatar view opens the same
scene in an interactive window. Full text: docs/reference/render.md.
The offscreen wgpu pipeline
A renderer-agnostic wgpu pipeline: input is a Scene of RenderMeshes (world-space
transform per mesh), a look-at camera, a directional+ambient light; output is RGBA8 pixels → PNG.
There is no window or surface — it renders into a texture and reads it back, so it works over SSH and
in CI wherever a GPU adapter (Vulkan/GL/Metal/DX) exists. render_to_rgba returns
Err when no adapter is available, and the crate's smoke test skips.
- One merged vertex buffer (transforms baked CPU-side), one uniform; the index buffer is grouped into per-texture batches — one draw per distinct texture.
- A
Scenecarries a texture pool; untextured batches bind a 1×1 white texel so the shader path is uniform. UVs are V-flipped (FBX/glTF are bottom-left origin, wgpu samples top-left), and an alpha cutout at 0.5 keeps foliage/hair/decal cards from rendering as opaque squares. - 4× MSAA + depth buffer; two-sided Lambert + ambient, because imported meshes have inconsistent triangle winding.
glam enters the CLI dependency graph for the first time — confined to the
renderer and the CLI's render_scene/world modules, per the workspace invariant.
Rendering an avatar
avatar render --avatar model.fbx -o out.png
The avatar loader imports the mesh(es) and renders the rest/bind pose from the raw control
points — it deliberately does not apply the FBX skin-bind matrices. Ripped and
converted avatars (notably MMD→FBX) ship inconsistent per-cluster Transform matrices,
so linear-blend skinning through them blends opposing rotations and the mesh collapses into spikes; the
control points are always a clean, undeformed bind.
The model is auto-uprighted by aligning its hips→head axis to +Y, measured from
cluster centroids in control-point space (weights + control points are reliable; the bind matrices are
not) — a model authored lying down or sideways comes out standing. Non-humanoid rigs fall back to
the file's declared UpAxis. The avatar is textured from its FBX-embedded materials (per
material slot, embedded blobs or files resolved relative to the FBX), and the camera auto-frames the
bounds (--yaw/--pitch orbit).
Pose-adjacent previews
--stretch HINGE:FACTOR(repeatable,*wildcards, FBX only) previews the chain-length editavatar physbone stretchmakes: bones below the hinge have their parent offsets scaled and the mesh is CPU-skinned through the resulting pose. The one place the renderer poses — it uses the pose delta only, identity everywhere the pose is untouched, never the untrusted per-cluster binds.--material-texture NAME=IMAGEdraws a material with a different image — preview a texture edit, or draw a hair material with its emission map to see where a glow lands.--pose PREFAB(FBX only) takes every bone's local transform from the prefab's same-named GameObject — the render shows what Unity will show for that prefab (stretched and re-angled chains, hand-posed bones), undoing Unity's import mirroring and import scale. Verified: posing from an untouched migration prefab (152 bones) reproduces the rest render to the pixel.
Rendering a world
avatar render --world scene.unity -o out.png
avatar render --world path/to/project -o out.png
The world loader parses a Unity .unity scene and emulates enough of Unity's FBX import
pipeline to place geometry correctly: Transform/MeshFilter/MeshRenderer/PrefabInstance objects are read
with a lossy YAML parse, world matrices composed up the m_Father chain, each FBX mesh placed
by its Model node's transform up the FBX parent chain, and Unity's import
scale applied — fixing cm-unit props that would otherwise render 100× too big.
Prefab-instanced models (whose meshes are never serialized into the scene) are re-instantiated from
m_SourcePrefab; materials resolve through scene .mats or the model importer's
material remap, falling back to FBX-embedded materials and then neutral grey. Unity's left-handed Y-up
space is converted to the renderer's right-handed space.
Validated against the Cozy Cabin world (PC export): the cabin assembles at correct scale (~6 m), props at real-world sizes, everything textured — alpha-cut foliage as proper pine.
Dropping an avatar into a world
avatar render --avatar model.fbx --world scene.unity -o out.png
With both, the avatar is placed where VRChat would materialise a player: the scene's spawn is resolved
from the VRC_SceneDescriptor's first spawns[] transform (falling back to the
VRCWorld GameObject), the avatar is normalised to human height
(1.6 m — ripped/MMD FBX units vary wildly), and its feet stand on the spawn point. The camera
defaults to framing on the avatar with the map around it; --frame world frames the whole
scene. Validated: the SDK2 avatar stands at the Cozy Cabin's real spawn point, on the porch, at correct
scale.
Interactive viewer — avatar view
avatar view --avatar X --world Y opens a native window onto the same assembled scene instead
of writing a PNG: drag to orbit, wheel to zoom,
WASD (+Space/Shift) to walk the focus point, R to reset,
Esc to quit. It reuses the offscreen geometry/shader pipeline but draws to a live
swapchain, opening at the same framing the PNG would produce. The CLI's viewer feature is on
by default (winit builds headlessly and only needs a display at runtime);
--no-default-features gives a pure-offscreen binary.
Limits
image handles (PNG/JPEG/TGA/BMP/GIF —
no DDS/PSD/EXR); undecodable textures fall back to the material's flat colour.
- FBX transform fidelity — only
Lcltranslation/rotation/scaling is composed: no per-nodeRotationOrder, pre/post-rotation, pivots, geometric transforms, or scale-inheritance modes. Rare on static world props; edge cases can be mis-oriented. - No prefab nesting or per-platform import overrides; only the first material per renderer is read for colour.
- The standalone avatar render is the rest pose — full posed skinning would need the
bind matrices this class of asset can't be trusted to provide (the
--stretch/--posedelta trick sidesteps that).