GPU-accelerated ASCII 3D rendering engine written in Rust.
Renders 3D models (OBJ, STL) as real-time ASCII art in your terminal, using wgpu compute shaders or a CPU rasterizer and glam for vector math.
Inspired by voxcii, rebuilt from scratch in Rust for memory safety, performance, and extensibility.
Recorded with VHS from
assets/demo.tape—a3d models/dog.stl --color --fg 00ddff.
- Real-time 3D rendering with ASCII shading
- Z-buffered scanline triangle rasterization with plane-equation depth interpolation
- Orthographic projection with terminal aspect-ratio correction
- OBJ format support (with MTL material colors)
- STL format support (binary and ASCII)
- Auto-rotation with golden ratio oscillation
- Interactive mode (arrow keys + zoom)
- Automatic terminal resize handling
- Aspect ratio correction for non-square terminal characters
- wgpu GPU rendering with automatic CPU fallback
- Rust 1.85+ (2024 edition)
- For GPU rendering, a wgpu adapter with 64-bit atomic min/max; without one, a3d transparently falls back to the CPU renderer
- A terminal emulator
sudo pacman -S vulkan-icd-loader vulkan-headers
# For NVIDIA:
sudo pacman -S nvidia nvidia-utils
# For AMD:
sudo pacman -S vulkan-radeongit clone https://github.com/heiervang-technologies/a3d.rs.git
cd a3d.rs
cargo build --releaseThe binary will be at target/release/a3d.
# Auto-rotating model (default)
a3d model.obj
# Interactive mode
a3d model.obj --interactive
# With ANSI true color
a3d model.obj --color
# Custom FPS and zoom
a3d model.obj --fps 60 --zoom 2.5| Option | Short | Default | Description |
|---|---|---|---|
<MODEL> |
(required) | Path to OBJ or STL file | |
--fps |
-f |
30 | Target frames per second |
--interactive |
-i |
off | Manual rotation with arrow keys |
--zoom |
-z |
1.0 | Initial zoom level, from 0.1 to 10 |
--color |
-c |
off | Enable ANSI 24-bit true color output |
--gpu |
auto | Force GPU rendering (error if no adapter) | |
--cpu |
auto | Force CPU rendering | |
--fg |
model | Foreground color as hex, e.g. ff6600 (implies --color) |
|
--bg |
none | Background color as hex, e.g. 1a1a2e (implies --color) |
By default a3d briefly benchmarks both available renderers against the loaded
model and current terminal size, then uses the faster one. --cpu and --gpu
bypass the benchmark and force a backend.
| Key | Action |
|---|---|
Arrow keys / hjkl |
Rotate model |
| Scroll wheel | Zoom in/out |
+ / = |
Zoom in |
- |
Zoom out |
c |
Toggle color on/off |
q / Esc / Ctrl-C |
Quit |
RUST_LOG=info a3d model.objcargo test --all-features -- --test-threads=1
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-featuresgpu_comparerenders the same scene on the CPU and GPU and asserts they match; it skips automatically when no GPU adapter is present, so the suite is green on GPU-less machines.- Regenerate the CPU snapshots after an intentional rendering change:
cargo test --test gen_snapshots -- --ignored - Re-record the demo GIF (requires VHS
- ffmpeg; see the optimization note in
assets/demo.tape):
vhs assets/demo.tape
- ffmpeg; see the optimization note in
CI runs fmt --check, clippy -D warnings, rustdoc, the full test suite, and a
separate Rust 1.85 MSRV check on every pull request.
src/
├── main.rs # CLI and interactive render loop
├── lib.rs # Shared CPU/GPU entry points + CPU rasterizer
├── gpu/
│ ├── context.rs # wgpu device/queue/adapter initialization
│ ├── pipeline.rs # GPU pipeline + synchronous framebuffer readback
│ └── raster.wgsl # Compute shader entry points
├── model/
│ ├── loader.rs # OBJ and STL file parsing
│ └── mesh.rs # Vertex/Mesh types, normalization
├── render/
│ ├── ascii.rs # ASCII luminance ramp mapping
│ ├── camera.rs # Orbital camera with perspective projection
│ └── framebuffer.rs # Depth buffer + character grid
└── terminal/
└── display.rs # crossterm terminal rendering + input
Model file (OBJ/STL)
│
▼
Load & parse ──▶ Center + fit to unit sphere
│
▼
┌────────────── Render Loop ──────────────────┐
│ CPU: transform/light/rasterize triangles │
│ or │
│ GPU: transform → atomic depth → shade │
│ → synchronous framebuffer readback │
│ │ │
│ ▼ │
│ ANSI terminal output │
│ │ │
│ ▼ │
│ input + frame-rate limit │
└──────────────────────────────────────────────┘
Surface brightness maps to characters from dark to bright:
. , ' : ; ! + * = # $ @
◄─── dark bright ───►
Luminance is computed as dot(-face_normal, light_direction) * 0.5 + 0.5, giving a [0, 1] range that indexes into this 12-character ramp.
| Concern | Crate | Purpose |
|---|---|---|
| GPU compute | wgpu |
Portable compute shaders and device access |
| CPU math | glam |
SIMD-accelerated vectors and matrices |
| Terminal | crossterm |
Raw mode, cursor control, input events |
| OBJ loading | tobj |
Wavefront OBJ + MTL parsing |
| STL loading | stl_io |
Binary and ASCII STL parsing |
| CLI | clap |
Argument parsing with derive macros |
| GPU data | bytemuck |
Safe transmutes for GPU buffer data |
| Logging | log + env_logger |
Debug and info logging |
See Issue #1 for the full roadmap including:
- M1: Core rendering MVP ✅
- M2: GPU compute pipeline (wgpu shaders) ✅
- M3: Advanced rendering (Phong lighting, shadows, color) — current
- M4: Scene graph and animation
- M5: Performance and polish
- M6: Extensions (WASM, export, plugins)
- Inspired by voxcii by ashish0kumar.
- The bundled sample mesh
models/dog.stlis the author's own model. Seemodels/README.mdfor details.
Contributions are welcome — see CONTRIBUTING.md and our Code of Conduct. Security issues: please follow SECURITY.md.
a3d is licensed under the MIT License. This covers both the source
code and the bundled sample model (models/dog.stl), which is the author's own
work — see models/README.md.
