motionloom 0.1.0

MotionLoom DSL parser and renderer for video effects, scene graphs, motion graphics, and world graphs.
Documentation

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

[dependencies]
motionloom = "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::api is the recommended stable integration surface.
  • motionloom::prelude contains a small convenience import set for common use.
  • motionloom::experimental exposes advanced editor, world, GLB, text layout, and timeline helpers. These APIs are public, but their stability is lower than motionloom::api.
  • The crate root keeps broader re-exports for short-term compatibility with existing applications. Prefer motionloom::api in 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:

<Process id="FinalProcess">
  <Tex id="out" fmt="rgba16f" size={[1920,1080]} />
</Process>

<Present from="FinalProcess" />

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 a SceneGpuTexture wrapping an Arc<wgpu::Texture> in Rgba8Unorm.
  • 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).

  • macOSScenePlatformPreviewSurface::MacOs { surface: CVPixelBuffer, ... } in Bgra8Unorm. The pixel buffer is Metal-compatible.
  • WindowsScenePlatformPreviewSurface::WindowsD3D(WindowsD3DSharedSurface) in Bgra8Unorm. The contained WindowsD3DSharedSurface keeps the ID3D11Texture2D alive so the legacy DXGI shared handle remains valid. The host can open shared_handle on 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.
  • LinuxScenePlatformPreviewSurface::LinuxDmabuf { ... } is the planned DMA-BUF BGRA descriptor. As long as real fd/export is not implemented, PlatformSurface returns a clear error and Auto falls back to CpuBgra.
use motionloom::{
    ScenePreviewBackend, ScenePreviewSurface, ScenePreviewSurfaceOptions, SceneRenderer,
    SceneRenderProfile, parse_graph_script,
};

let graph = parse_graph_script(script)?;
let mut renderer = pollster::block_on(SceneRenderer::new(SceneRenderProfile::Gpu))?;
let surface = pollster::block_on(renderer.render_frame_to_preview_surface(
    &graph,
    0,
    ScenePreviewSurfaceOptions::default(),
))?;

match surface {
    ScenePreviewSurface::PlatformSurface(platform) => {
        // Hand the platform surface off to the host compositor/GPUI.
    }
    ScenePreviewSurface::WgpuTexture(tex) => {
        // Consume tex.texture as a wgpu texture.
    }
    ScenePreviewSurface::CpuBgra { width, height, data, .. } => {
        // Upload data (width x height BGRA bytes) to the UI.
    }
}
# Ok::<(), Box<dyn std::error::Error>>(())

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:

cargo run --release -p motionloom --example wgpu_live_preview -- ../motionloom-example/showcase/s-000005/main.motionloom

Print copyable timing stats once per second with --print-stats or --stats:

cargo run --release -p motionloom --example wgpu_live_preview -- --print-stats ../motionloom-example/showcase/s-000005/main.motionloom

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 motionloom::{GpuCompatibilitySeverity, inspect_gpu_compatibility};

let report = inspect_gpu_compatibility(script)?;

if report.likely_cpu_fallback {
    for issue in report.blocking_issues() {
        eprintln!("[{:?}] {}: {}", issue.target, issue.code, issue.message);
    }
}

// `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 {
    // Browser/WASM can try the direct WebGPU scene canvas path.
} else if report.can_use_wasm_process_webgpu {
    // Browser/WASM can try the process WebGPU path for supported process effects.
} else {
    // Use CPU/WASM fallback or a compatibility renderer.
}

# let _ = GpuCompatibilitySeverity::Blocking;
# Ok::<(), Box<dyn std::error::Error>>(())

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.
  • WgpuTextureOutputSceneRenderer::render_frame_to_wgpu_texture.

Common CPU-fallback reasons:

  • Mixed <Scene> + <Process> graphs need scene-to-process composition.
  • Tex / Pass / Output composition 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-000008glow_stack
  • cp-000009tone_map
  • cp-000010light_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 motionloom::{SceneRenderProfile, parse_graph_script, render_scene_graph_frame};

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(script)?;
let frame = pollster::block_on(render_scene_graph_frame(&graph, 0, SceneRenderProfile::Gpu))
    .or_else(|_| pollster::block_on(render_scene_graph_frame(&graph, 0, SceneRenderProfile::Cpu)))?;
frame.save("motionloom_frame.png")?;
# Ok::<(), Box<dyn std::error::Error>>(())

Minimal World Example

use motionloom::{WorldFrameRenderer, parse_world_graph_script};

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(script)?;
let mut renderer = WorldFrameRenderer::new();
let frame = renderer.render_frame(&graph, 0, ".")?;
frame.save("motionloom_world_frame.png")?;
# Ok::<(), Box<dyn std::error::Error>>(())

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.