MotionLoom
MotionLoom is the DSL parser and renderer crate used by Anica for video effects, scene graphs, motion graphics, and world graphs.
It is designed to be used as a Rust library. Anica can expose MotionLoom through
application tools such as anica.motionloom/render_scene, while this crate
provides the lower-level API for parsing, rendering, runtime evaluation, process
catalog lookup, and GLB inspection.
Install
[]
= "0.1"
MotionLoom requires Rust 1.85 or newer. WebGPU/wgpu is part of the core
renderer path, so the crate intentionally depends on wgpu by default.
Video export requires an FFmpeg binary path supplied by the caller. MotionLoom does not bundle FFmpeg. Single-frame rendering and PNG sequence export do not require FFmpeg.
Public API
New Rust integrations should start with motionloom::api or
motionloom::prelude.
motionloom::apiis the recommended stable integration surface.motionloom::preludecontains a small convenience import set for common use.motionloom::experimentalexposes advanced editor, world, GLB, text layout, and timeline helpers. These APIs are public, but their stability is lower thanmotionloom::api.- The crate root keeps broader re-exports for short-term compatibility with
existing applications. Prefer
motionloom::apiin new code.
See PUBLIC_API.md for the curated main API map.
Release Notes
See CHANGELOG.md for crate release history.
Core Functions
DSL detection and parsing
is_graph_script(input: &str) -> bool
Checks whether a string looks like a MotionLoom graph script.
parse_graph_script(input: &str) -> Result<GraphScript, GraphParseError>
Parses a scene/effect/composition graph. Use this for <Scene>, <Tex>,
<Pass>, <Layer>, <Precompose>, <Mask>, and most motion graphics DSL.
is_world_graph_script(input: &str) -> bool
Checks whether a string should be handled by the world graph parser.
parse_world_graph_script(input: &str) -> Result<WorldGraph, GraphParseError>
Parses a <World> graph. Use this for GLB actors, camera/world
world, directional sprites, retarget maps, and skeletal actions.
Present Rule
Every MotionLoom graph must contain exactly one <Present ... /> node.
<Present ... /> must be a direct child of <Graph> and must be the final node
before </Graph>. It cannot be nested inside <Scene>, <World>, or
<Process>.
For process graphs, use root-level present from the process id:
Scene Rendering
render_scene_graph_frame(graph: &GraphScript, frame: u32, profile: SceneRenderProfile)
Renders one scene/composition graph frame to an image::RgbaImage.
SceneRenderer::new(profile: SceneRenderProfile)
Creates a reusable scene renderer. Prefer this when rendering many frames, because internal caches can be reused across frames.
render_scene_graph_to_video_with_progress(ffmpeg_bin, graph, output_path, profile, progress_every_frames, callback)
Renders a full scene/composition graph to a video file through ffmpeg and reports progress.
render_scene_graph_to_png_sequence_with_progress(graph, output_dir, progress_every_frames, callback)
Renders a scene/composition graph to a PNG image sequence. In this mode
output_dir is treated as an output directory and frames are written as
frame_000000.png, frame_000001.png, and so on. FFmpeg is not used for PNG
sequence export.
next_scene_output_path(output_dir) and next_scene_output_path_for_profile(output_dir, profile)
Build timestamped output paths for scene renders.
SceneRenderProfile
Selects the renderer/output path:
SceneRenderProfile::Cpu— CPU render, ProRes MOV export through FFmpeg.SceneRenderProfile::Gpu— GPU scene compositor, H.264/MP4 export through FFmpeg.SceneRenderProfile::GpuProRes— GPU scene compositor, ProRes MOV export through FFmpeg.SceneRenderProfile::GpuProRes4444— GPU scene compositor, ProRes 4444 MOV export through FFmpeg.SceneRenderProfile::GpuPngSequence— GPU scene compositor, PNG frame sequence export without FFmpeg.
Export support:
| Output | API | FFmpeg required |
|---|---|---|
| Single frame PNG/image | render_scene_graph_frame then save the returned RgbaImage |
No |
| PNG sequence | render_scene_graph_to_png_sequence_with_progress |
No |
| MP4/MOV video | render_scene_graph_to_video_with_progress with Cpu, Gpu, GpuProRes, or GpuProRes4444 |
Yes |
| Root document PNG sequence | render_motionloom_document_to_png_sequence_with_progress |
No |
| Root document video | render_motionloom_document_to_video_with_progress |
Yes |
Process graphs that use external timeline inputs such as
<Input id="clip0" from="input:clip0" /> are intended for host applications
such as Layer FX. Standalone MotionLoom export cannot render those process-only
graphs unless the process wraps a self-contained <Scene> or <World> source.
Scene zDepth uses camera-space depth: negative is closer, positive is farther.
Preview Surfaces
SceneRenderer::render_frame_to_preview_surface(graph, frame, options)
Renders a scene frame to the fastest preview surface available on the current platform. This is the intended integration point for host applications such as Anica that want to display live previews without choosing between CPU, GPU, and platform interop code themselves.
ScenePreviewSurfaceOptions::default() uses ScenePreviewBackend::Auto, which
picks a platform surface when available, otherwise falls back to a wgpu texture
or CPU BGRA bytes.
Supported backends:
ScenePreviewBackend::Auto— prefer platform surface, then wgpu texture, then CPU BGRA.ScenePreviewBackend::WgpuTexture— return aSceneGpuTexturewrapping anArc<wgpu::Texture>inRgba8Unorm.ScenePreviewBackend::PlatformSurface— return a platform display surface.ScenePreviewBackend::CpuBgra— return CPU BGRA bytes for compatibility.
Platform surfaces are host-consumable descriptors. MotionLoom produces the surface; it is the downstream application's responsibility to import and paint it (for example through GPUI, DirectComposition, Wayland, or Metal).
- macOS —
ScenePlatformPreviewSurface::MacOs { surface: CVPixelBuffer, ... }inBgra8Unorm. The pixel buffer is Metal-compatible. - Windows —
ScenePlatformPreviewSurface::WindowsD3D(WindowsD3DSharedSurface)inBgra8Unorm. The containedWindowsD3DSharedSurfacekeeps theID3D11Texture2Dalive so the legacy DXGI shared handle remains valid. The host can openshared_handleon another D3D10/11 device on the same adapter; the handle is owned by the OS and does not need to be closed by the caller. - Linux —
ScenePlatformPreviewSurface::LinuxDmabuf { ... }is the planned DMA-BUF BGRA descriptor. As long as real fd/export is not implemented,PlatformSurfacereturns a clear error andAutofalls back toCpuBgra.
use ;
let graph = parse_graph_script?;
let mut renderer = block_on?;
let surface = block_on?;
match surface
# Ok::
Standalone WGPU Live Preview Example
crates/motionloom/examples/wgpu_live_preview.rs is a native diagnostic viewer
for testing MotionLoom's direct wgpu preview path without Anica, ffmpeg, video
encoding, or CPU readback. Use it to measure scene/process GPU render cost,
surface format behavior, and preview quality tradeoffs.
Run the live preview:
Print copyable timing stats once per second with --print-stats or --stats:
Keyboard controls:
1— Full quality, 100% render target.2— Balanced quality, 50% render target.3— Speed quality, 25% render target.4— High Speed quality, 10% render target.5— Ultra Speed quality, 5% render target.Esc— close the preview window.
The window title reports frame index, render time, blit/present time, tick rate,
target size, surface format, quality mode, and script path. --print-stats
prints rows such as quality=... target=... render_ms=... for benchmark notes.
World Rendering
render_world_frame(graph: &WorldGraph, frame: u32, asset_root)
Renders one world frame using the compatibility world renderer.
WorldFrameRenderer::new()
Creates a reusable world renderer with image, GLB mesh, and GPU caches.
WorldFrameRenderer::render_frame(graph, frame, asset_root)
Renders an world frame on the CPU/debug path.
WorldFrameRenderer::render_frame_gpu(graph, frame, asset_root)
Renders an world frame using the GPU actor path where available.
WorldFrameRenderer::render_frame_gpu_with_ground_grid(...)
Renders GPU world with a ground grid overlay for viewport/debug use.
render_world_graph_to_video_with_progress(ffmpeg_bin, graph, asset_root, output_path, profile, progress_every_frames, callback)
Renders an world graph to video through ffmpeg and reports progress.
World graphs also support PNG sequence export through
render_world_graph_to_png_sequence_with_progress. In that mode output_dir
is an output directory and FFmpeg is not used.
Runtime Evaluation
compile_runtime_program(graph: GraphScript) -> Result<RuntimeProgram, RuntimeCompileError>
Compiles a graph into a timeline/effect runtime program.
RuntimeProgram::evaluate_frame(frame: u32) -> RuntimeFrameOutput
Evaluates effect parameters for a frame.
RuntimeProgram::evaluate_at_time_sec(time_norm: f32, time_sec: f32) -> RuntimeFrameOutput
Evaluates effect parameters at an explicit normalized time and second value.
eval_time_expr(value, time_norm, time_sec)
Evaluates MotionLoom time expressions such as $time.sec, curve(...), and
math expressions used in effect parameters.
GPU Compatibility Inspector
inspect_gpu_compatibility(script: &str) -> Result<GpuCompatibilityReport, GraphParseError>
Inspects a MotionLoom script and reports whether it is likely to run on the strict GPU preview paths or fall back to CPU. This is a static diagnostic tool: it parses and analyzes the DSL, but it does not render frames and does not allocate GPU resources.
Use this before choosing a preview/render path in host applications:
use ;
let report = inspect_gpu_compatibility?;
if report.likely_cpu_fallback
// `likely_preview_path` predicts what `ScenePreviewBackend::Auto` will pick
// on the current platform: `MacOsCVPixelBuffer`, `WindowsD3D`, `WgpuTexture`,
// or `CpuBgra`. (Linux DMA-BUF is planned but reports `CpuBgra` for now.)
match report.likely_preview_path
if report.can_use_wasm_scene_canvas else if report.can_use_wasm_process_webgpu else
# let _ = Blocking;
# Ok::
The report is intentionally conservative. It only flags known limitations, so a script that passes inspection can still fail at runtime because of the user's GPU, driver, browser, missing assets, or platform-specific surface integration.
Current report targets:
NativeScenePreview— Anica/native live scene preview.WasmSceneCanvas— browser direct WebGPU scene-to-canvas render.WasmProcessWebGpu— browser WebGPU process/effect render.WgpuTextureOutput—SceneRenderer::render_frame_to_wgpu_texture.
Common CPU-fallback reasons:
- Mixed
<Scene>+<Process>graphs need scene-to-process composition. Tex/Pass/Outputcomposition is not supported by the strict direct scene canvas path yet.Tex from="scene:..."requires scene output to become a process input.- Some process effects are not implemented in the WASM process WebGPU path yet.
Important distinction: Anica/native and WASM/browser do not have identical GPU paths. A script can be GPU-compatible in one target and CPU fallback in another. Use the per-target booleans and issue list instead of assuming one target's result applies to every platform.
Cinematic Light Process Effects
MotionLoom exposes cinematic lighting as process effects. They use the existing
<Pass effect="..."> surface, so host applications can use them by updating the
MotionLoom crate and rendering the script; no new scene node schema is required.
Supported effect ids:
glow_stack— multi-radius glow stack with threshold, intensity, radii, and tint controls.tone_map— exposure, contrast, filmic shoulder, gamma, and saturation.light_sweep— animated directional sweep highlight for text, logos, and energy reveals.
Copyable examples live in motionloom-example/core/process/:
cp-000008—glow_stackcp-000009—tone_mapcp-000010—light_sweep
Process Catalog
process_effects() -> &'static [ProcessEffectDefinition]
Returns the built-in effect/process catalog.
process_effect_for_id(id: &str)
Looks up one process effect by ID.
process_effects_for_category(category)
Lists process effects by category.
kernel_source_by_name(kernel: &str)
Returns embedded WGSL source for a known process kernel.
is_known_process_kernel(kernel: &str) -> bool
Checks whether a kernel name is built into the crate.
GLB Helpers
load_glb_metadata(path) and parse_glb_metadata(path, bytes)
Load or parse lightweight GLB metadata such as nodes, meshes, joints, and materials.
load_glb_mesh_data(path) and parse_glb_mesh_data(path, bytes)
Load or parse GLB mesh data for world rendering and diagnostics.
diagnose_world_glb_gpu_plan(mesh)
Builds a diagnostic report for whether a GLB mesh is suitable for the GPU world path.
diagnose_world_graph_actor_gpu_frame(graph, actor_id, frame, asset_root)
Diagnoses one actor in an world graph at a specific frame.
Minimal Scene Example
use ;
let script = r##"
<Graph fps={30} duration="1s" size={[640,360]}>
<Background color="#101827" />
<Scene id="example_scene">
<Circle x="320" y="180" radius="96" color="#4cc9f0" />
<Text x="320" y="306" value="MotionLoom" fontSize="34" color="#f7f7f7" />
</Scene>
<Present from="example_scene" />
</Graph>
"##;
let graph = parse_graph_script?;
let frame = block_on
.or_else?;
frame.save?;
# Ok::
Minimal World Example
use ;
let script = r##"
<Graph fps={30} duration="1s" size={[320,180]}>
<World id="world">
<Background color="#ffffff" />
<Camera yaw="0" pitch="0" zoom="1" />
</World>
<Present from="world" />
</Graph>
"##;
let graph = parse_world_graph_script?;
let mut renderer = new;
let frame = renderer.render_frame?;
frame.save?;
# Ok::
Notes
Scene graph APIs are for 2D scene/motion graphics/effect graphs. World graph APIs are for world/camera/actor/directional-character rendering.
GPU rendering requires a working wgpu backend on the host machine. For tools
that need robust fallback behavior, try SceneRenderProfile::Gpu first and fall
back to SceneRenderProfile::Cpu when GPU initialization fails.
Video rendering functions require an ffmpeg binary path supplied by the caller. PNG sequence and single-frame image rendering do not require FFmpeg.