Skip to content

Latest commit

Β 

History

730 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Martin β€” a deFEEST demoscene engine

build

A music-synced Gaussian-splat demoscene engine (Bevy + Vulkan) that flies a camera around 3D splats while they morph into one another β€” a title, two faces, a dog, a whole city β€” entirely without CUDA or ROCm. The domain model + vocabulary live in DOMAIN.md. Developed and previewed on a modest AMD Ryzen AI 7 PRO 350 / Radeon 860M iGPU (CPU + Vulkan / Mesa RADV, openSUSE Tumbleweed); the released demo targets bigger metal at showtime, so it can afford heavier clouds and the sh3 view-dependent profile (see build profiles). Demoscene spirit, all-AMD dev box. πŸͺ©

cargo +nightly run --release                     # the default demo β€” a splat assembles out of a ball cloud
cargo run --release -- <show.show>               # run a .show config (or --production <name>)
cargo run --release -- <show.show> --validate    # dry-run: print the resolved timeline + exit
#   free-orbit: ←/β†’ yaw Β· ↑/↓ pitch Β· W/S zoom Β· A/D & Q/E pan Β· M mark waypoint Β· Space restart Β· F11/F fs
./record.sh out.mp4                              # render the whole timeline to ./out.mp4

It's one sequence engine: every run is a timeline of parts (splat-text or splats) that each assemble out of a ball cloud, then morph into the next (per-Gaussian, on the GPU), with HDR bloom on black. A show is a .show file β€” the config β€” that you run with martin <show> (or --production <name>); it expands into MARTIN_* env vars (the internal IR), which a CLI layers run-mode flags on top of (--record/--shot/--validate/…). Precedence: **CLI flag > env

.show [settings] > default**. Built on Bevy 0.19 + bevy_gaussian_splatting 8.0.1 (our fork β€” the martin-tightcut branch of annejan/bevy_gaussian_splatting, a git dep), wgpu β†’ Vulkan, nightly toolchain (a dated pin, nightly-2026-08-28, not rolling).

  • USAGE.md β€” the CLI + .show file format + the MARTIN_* env reference.
  • ART-DIRECTION.md β€” how to shoot and prep good splats for the demo (capture recipe, lighting, the two splat "flavours", cleanup).
  • productions/ β€” one folder per demo (showbook, show, bundle, captures); the engine stays theme-agnostic. Current: intro (the bundled showcase), camping (in design β€” see its SHOWBOOK), and the aerial-city demos austin / nyc / cities (local Google-imagery captures, not shippable β€” see pipeline/AERIAL-CITIES.md). Shared building blocks: parts/.

Make your own splats

The repo ships a CUDA-free splat-creation pipeline (pipeline/) that turns a phone video or a folder of photos into the .ply assets the demo renders:

./pipeline/splat-setup.sh                 # once: builds COLMAP (CPU) + Brush (Vulkan)
./pipeline/splat.sh my_video.mp4          # or:  ./pipeline/splat.sh ./photos/
VIEWER=1 ./pipeline/splat.sh ./photos/    # watch training live in Brush's window
Script What it does
pipeline/splat-setup.sh One-time: installs COLMAP build deps via zypper, builds COLMAP (CUDA off) and Brush (wgpu/Vulkan), symlinks ~/.local/bin/brush.
pipeline/splat.sh video | image-dir β†’ ffmpeg frames β†’ COLMAP CPU SfM + undistort β†’ Brush training β†’ .ply.
pipeline/mesh-splat.sh A mesh (.obj/.dae/.stl/.ply/.glb) β†’ Blender (EEVEE) orbital renders with known poses (no COLMAP) β†’ Brush training β†’ a "proper" .ply. The offline bake for when a mesh matters (vs the in-engine mesh: sampler). Needs Blender (BLENDER=blender-5.0).

Capture quality is 90% of the result β€” see ART-DIRECTION.md for the full recipe (and the single-image TRELLIS shortcut). View / clean / compress any .ply at https://superspl.at/editor.

Ready-made demo splats

Two reference clouds from Mitchell Mosure (the upstream author) β€” fetched on demand (they're >100 MB so they're gitignored, not committed):

./pipeline/fetch-demo-assets.sh    # β†’ assets/go_trimmed.ply + assets/trellis.glb
MARTIN_PLY=assets/go_trimmed.ply cargo r-sh3                     # multi-view photogrammetry (SH3 glints)
MARTIN_GLB=assets/trellis.glb cargo r-sh0    # a KHR_gaussian_splatting glTF scene (camera auto-frames it)

The go-board is a real multi-view capture (its sh3 glint is visible β€” cargo r-sh3); the trellis is a single-image TRELLIS export (flat SH, so sh0 loses nothing). See build profiles.

Procedural shapes (splatgen)

No capture, no python β€” synthesize an abstract cloud straight to a .ply with the splatgen tool (it shares the exact generator build.rs uses to auto-make the demo splats, so the clouds morph cleanly with everything else):

cargo run --release --bin splatgen -- list                  # sphere cube torus ring helix galaxy star wave knot mobius supershape lsystem fern menger shell tent pine flame rocket saturn ufo terrain moon mountains
cargo run --release --bin splatgen -- lsystem assets/tree.ply   # a 3D L-system plant
cargo run --release --bin splatgen -- torus                 # β†’ torus.ply
MARTIN_PLY=assets/tree.ply cargo run --release              # then view / use it in a show (splat:tree.ply)

The fractal/organic shapes: lsystem (3D branching tree, brown trunk β†’ green canopy), fern (an arching fractal-plant frond), menger (a depth-3 Menger sponge), shell (a log-spiral nautilus). The camping set: tent (ridge tent), pine (a tiered conifer), flame (a campfire tongue β€” pairs with MARTIN_PARTICLES=embers), terrain (rolling ground/hills so scenes aren't floating), moon (a grey cratered moon). The space set: rocket (a cartoon rocket β€” white hull, red nose+fins, a hot exhaust plume; nose points +Y so a travel: toward +Y reads as a launch), saturn (a banded ringed planet), ufo (a flying saucer β€” metal disc, cyan glass dome, alternating rim lights). The rest are parametric solids (sphere/cube/torus/ring/helix/galaxy/star/wave/knot/ mobius/supershape). All ~140k splats, deterministic (fixed seed β†’ reproducible .ply).

Running the demo

With nothing set, cargo +nightly run --release plays the intro production (productions/intro/intro.show) β€” the very same showcase CI bakes into the downloadable single binary, so a fresh git clone runs exactly what the download shows. Its procedural splats are synthesized by build.rs on first build (no python/numpy), so the clone needs no extra step. (The older effect-catalogue demo still lives at assets/demo.show, built only from shipped assets β€” MARTIN_SHOW=assets/demo.show cargo run --release.) Point it at your own splat with MARTIN_PLY=assets/your.ply cargo run --release, then chain it into a MARTIN_SEQ to morph between splats β€” e.g. splat:a.ply; splat:b.ply β€” a per-Gaussian GaussianInterpolate blend where each source is paired to the target by Morton (Z-order) spatial sort, so particles flow into their nearest part of the target (no teleporting) and colours/positions lerp together (e.g. two Martins β†’ one dog: each becomes a half of the dog). A front-facing camera sway keeps the hollow back of single-image splats out of frame (MARTIN_YAW=<rad> pins the angle for inspection). Export uncompressed/standard PLY from SuperSplat β€” the loader rejects the compressed format (missing required properties). Linux build deps: systemd-devel (libudev) + alsa (and a Vulkan/RADV driver).

Prebuilt binaries: GitHub Actions builds release binaries for Linux, Windows, and macOS on every push β€” grab them from the artifacts of the latest build run. The release binary is ~75 MB (strip + thin LTO); use cargo run --release to show it off (the 1.8 GiB build is debug-only, for fast iteration).

Effects & env vars β€” mix and match

A .show file sets these (its [settings] block + sections); the same knobs are also MARTIN_* env vars you can combine ad-hoc for a quick experiment. All splat positions/scales are particles in the same system, so any of these morphs into any other. Full reference in USAGE.md.

Env var Effect
MARTIN_SHOW=show.show Unified scene file β€” one file with settings + a [reel] ([seq]) + a [compose] stage + a music-timed [camera] track (keyframes can anchor to a music section, t=@@drop). A kind = intro|demo setting declares the production kind, and a [scenes] block can author the show as an arc of named scenes that flattens to [reel] (see DOMAIN.md). Expands into the env vars below (which still override it). The recommended way to author a whole show; see assets/example.show.
MARTIN_VALIDATE=1 Dry-run β€” parse the show, print the resolved timeline (part cue times, effects, compose, camera) and exit, no render. A fast authoring check.
MARTIN_PLY=/abs/x.ply Load a splat (sets the asset folder β€” its parent becomes the asset root).
MARTIN_SEQ="…" Timeline β€” a chain of parts that morph into one another (see below). Use splat:/text: parts to load splats and titles.
MARTIN_COMPOSE=stage.compose Composition β€” many objects on one stage at once, placed + spinning/bobbing/drifting, fading in on the music, camera auto-orbiting (vs the morph timeline). Example: assets/examples/stage.show.
MARTIN_FPS=1 / I key Log FPS + splat count (the I key toggles it live + logs a snapshot).
~name (per-part token) How each part arrives: morph/swarm/ball/fade/explode/implode/drop/swirl, or the shader ones typewriter/wipe/sparkle/slither/vortex/outline/pen-write. swarm = like morph but the splats flock along curled paths between the two scenes (the @_,_,N value tunes the swarm strength).
MARTIN_DEFORM=wave A scene-wide persistent deform field held the whole part (wave/cloth/ripple/twist/wind/turbulence/pulse/jitter/spiral) β€” great on a wall: of text, or to gently wobble a whole splat scene while you fly around it; applies to compose objects too. Per-part ^name wins.
^name:amp (per-part token) Scale a part's deform amplitude (^wave:0.3 β‰ˆ a gentle wobble on a big scene; 1 = default).
mesh:model.dae (part) A mesh: part (.dae/.obj/.stl/.ply) is surface-sampled into flat, normal-aligned gaussians, coloured from the diffuse texture (sampled at the UV), else vertex/material colour. The per-part disk:<f> token tunes the splat-disk overlap.
MARTIN_MORPH_COUNT=250000 Gaussian budget (0=max ~1.15M β‰ˆ 20 fps; 250k β‰ˆ 60 fps on the iGPU).
MARTIN_ZOOM=1.5 Camera closeness (>1 = closer / more zoomed in, <1 = pull back).
MARTIN_ROT=rx,ry,rz Orient the cloud (euler degrees) β€” e.g. stand a COLMAP scene upright.
MARTIN_YAW=1.4 MARTIN_PITCH=0.1 Seed the free-orbit camera angle (radians); MARTIN_YAW also holds it (no sway) when recording.
MARTIN_WAYPOINTS=path.json Where the M-key camera waypoints are logged / read (default waypoints.json) β€” fly + mark to author a camera path.
MARTIN_FLY=2 Fly the camera through the marked waypoints. Recording: the path fills each part (longer hold = slower flyby), flowing through the morph. Live: <secs> = pace.
MARTIN_FPS=1 Log frame time / FPS.
MARTIN_SERVE=1 Live control bridge (default port 7878): boot the show windowed, render offscreen, and drive the camera + clock live over a line-JSON TCP protocol (camera/seek/pause/play/step/screenshot/dump_camera/state) β€” author + inspect without reloading. A stdio MCP server (martin mcp) proxies these as native tools for an MCP client (e.g. Claude Code). See USAGE.
MARTIN_RECORD=/dir Dump one PNG per frame (used by record.sh).
MARTIN_PREVIEW_FPS=8 Render the timeline at N fps instead of 60 β€” far fewer frames for a fast preview (rendering frames is the slow part). Timing + audio sync stay correct; record.sh muxes at the same fps.
MARTIN_RASTER=position Debug-shading view for the whole show (color/depth/normal/position/classification/flow/velocity) β€” the fork's RasterizeMode. Per-part raster:<mode> token overrides it. position colours by XYZ (rainbow).
MARTIN_SHOT=/x.png MARTIN_SHOT_AT=<s> Headless screenshot at time s, then exit.
MARTIN_FULLSCREEN=1 Start borderless-fullscreen; toggle live with F11 / F.
MARTIN_FLASH=0.6 Over-bright bloom flash on each part cut (0 = off).
MARTIN_SYNTH_WAV=/x.wav Render the bundled deFEEST synth to a WAV and exit (mux onto a recording).
MARTIN_MUTE=1 Silence the live synth (it plays in the window by default; recordings still mux the WAV).
MARTIN_SCORE=score.txt Load a tracker-DSL score file (tempo / sections / drum patterns / dynamics) β€” drives the synth and the @@anchors. Editable default ships at assets/score.txt. A score can also pick each voice's CHARACTER via the instrument-palette knobs (leadsw/basssw/padsw/drumsw, even per-section) β€” see DOMAIN.md.
MARTIN_SCORE_DUMP=score.txt Export the built-in score as an editable file and exit.

MARTIN_SEQ is a ;-separated list of parts (or a path to a file of them, one per line; # comments allowed). Each part morphs into the next, through a ball cloud:

text:STRING               # splat-text (glowing)
image:logo.png            # a PNG (in the MARTIN_PLY folder), rasterized to gaussians
svg:logo.svg              # an SVG, rasterized (vector β†’ pixels) into gaussians β€” any vector art
mesh:model.dae            # a 3D mesh (.dae/.obj/.stl/.ply), surface-sampled into gaussians
glb:badge.glb             # a real glTF mesh: rendered crisp, then DISSOLVES into its own splats
shader:warp               # a fullscreen-effect INTERLUDE (warp/plasma/tunnel/stars/rings/grid/kaleido/bolt); splats clear
splat:a.ply               # a splat (filename in the MARTIN_PLY folder)
splat:a.ply+b.ply         # several splats, auto-arranged side by side
…any part… @hold,morph,bulge ~transition ^deform out:departure @@anchor   # timing Β· arrival Β· deform Β· departure Β· cue

The trailing ~transition picks how a part arrives β€” data-only ball (default), fade, explode, implode, drop, swirl, extrude, helix, fold, zoom, morph, or the per-particle shader transitions typewriter, wipe, sparkle, slither, vortex, outline, pen-write (great for text). The ball is just one of many; the design + the shader fork are in DESIGN.md / SHADER-BLUEPRINT.md.

The optional @@anchor pins a part's start to the music clock (Cinder's ported synth/score, src/{audio,score}.rs): @@drop (a section), @@bar32, @@beat:64, or @@12.5 seconds β€” so the visuals lock to the track. MARTIN_SYNTH_WAV renders that synth to a WAV; mux it onto a recording with ffmpeg for a video-with-sound (see USAGE.md β†’ Music).

Example β€” the full show (title β†’ dog β†’ greetings β†’ credits):

MARTIN_PLY=assets/doggo.ply \
MARTIN_SEQ="text:MARTIN GAUS @2,3,0; splat:doggo.ply @2,3,0.9; text:GREETINGS @1.5,3,0.9; text:CODE ANNEJAN @2,3,0.6" \
cargo +nightly run --release
#   ./record.sh out.mp4   renders the whole timeline to video

Composing scenes in Blender (the bridge)

Pose a show's [stage]/[compose] props and the camera in Blender's viewport, then read them straight back into the .show β€” full transform (position Β· rotation Β· scale Β· camera) round-trips 1:1 with martin. It's pipeline/blender_bridge.py, driven over the blender-mcp MCP server. In Blender:

exec(open('/path/to/martin/pipeline/blender_bridge.py').read())
bridge_import('productions/camping/campsite-max.show', cam_anchor='climax')  # spawn [stage] + set camera
# …drag / rotate / scale props, orbit the camera (camera-lock is ON); scale the PARTICLE.embers
#   cube to dial the campfire ember volume…
bridge_export('productions/camping/campsite-max.show')   # patch @pos/*scale/rot + particle volume back
bridge_shot('/tmp/x.png')                            # save a shot (DISPLAY=:0 xdg-open to view)

Export patches only @pos/*scale/rot (+ particle_origin/particle_spread) on each prop's source line, so every other show token is preserved. Poseable = [stage]/[compose] props; programmatic [reel]/path:/travel: content is temporal (load one reel frame as a backdrop= to pose over it). Full reference: the script docstring + the "Blender ↔ martin bridge" section of AGENTS.md.

Build profiles

The spherical-harmonic degree is a compile-time choice (one-hot in the splat crate), exposed as two build profiles. Synthetic content (text/morph) looks identical in both β€” only real captures differ:

profile what when
sh0 (default) one flat colour per splat β€” lean, fast the synthetic demo; the AMD iGPU dev box
sh3 full degree-3 view-dependent colour (specular glint) real captures (camping footage β†’ Brush); the showtime machine
cargo b-sh0      # = cargo build --release            β†’ target/release/martin    (sh0, default)
cargo b-sh3      # sh3 build into target/sh3/release/martin (both binaries coexist; r-sh0 / r-sh3 to run)

sh3 costs ~16Γ— the per-splat colour data in VRAM for captures (synthetic clouds compress that away), so the lean sh0 stays the default; flip to sh3 once camping captures land. The aliases live in .cargo/config.toml.

Single-binary bundle

Ship a whole show as one self-contained executable β€” assets baked in, no files, no env vars:

./pipeline/bundle.sh            # or: cargo build --release --features bundle
./target/release/martin         # runs the baked-in show anywhere

cargo build --release --features bundle is the pipeline: build.rs reads bundle.toml (the show β€” a seq/compose + optional score, logo, morph_count), auto-collects every .ply/PNG the show references, lz4-compresses them into the binary, and bakes the show string in. At startup the binary self-extracts the assets to a temp dir (reused across relaunches) and plays the show, with a loader screen (logo + progress bar) while the splats decompress. Env vars still override the baked-in defaults (e.g. MARTIN_LOOP=1). Fonts and the default score are already compiled into martin, so only splats (and any logo PNG) ship. Edit bundle.toml to pick the show; its .ply must be present locally at build time (they're git-ignored).

Releasing

./pipeline/release.sh builds a single self-contained binary (the show + all its assets baked in) from bundle.toml, then verifies it self-extracts and plays:

./pipeline/release.sh                 # β†’ target/release/martin (one file, no assets, no env)
./target/release/martin               # plays the baked-in show anywhere

CI bakes the intro production into the download (productions/intro/bundle.toml β†’ intro.show); the root bundle.toml defaults to assets/demo.show. Either stays self-contained by using only light assets β€” procedural demo shapes + a few tracked meshes + text, no 500 MB photogrammetry scenes. A bundle lands around ~180 MB (Bevy base ~75 MB + lz4-compressed splats). To shrink it: ship fewer / downsampled .ply (the real lever β€” splat floats dominate), or trim the show.

Portability (run on other distros). A binary linked against this dev box's glibc (openSUSE Tumbleweed = bleeding-edge) fails on older distros with version 'GLIBC_2.xx' not found. So release.sh links against an old glibc via cargo-zigbuild when zig + cargo-zigbuild are on PATH β€” built here, runs on Ubuntu 20.04+/Debian 11+/Mint 20+ (glibc β‰₯ TARGET_GLIBC, default 2.31; the GPU/audio/window libs are dlopen'd at runtime and present on any desktop). One-time setup: cargo install cargo-zigbuild and put zig on PATH. Cross-OS (Windows/macOS): run release.sh on each, or use GitHub Actions, then gh release upload.

Note on git

Splats, captures, run outputs, and the external COLMAP/Brush checkouts are git-ignored (multi-GB binaries). Only source/tools are tracked.

About

A music-synced Gaussian-splat demoscene engine (Bevy + Vulkan) that flies a camera around 3D splats while they morph into one another, a title, two faces, a dog, a whole city .. entirely without CUDA or ROCm.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages