2 unstable releases
Uses new Rust 2024
| 0.3.0 | May 19, 2026 |
|---|---|
| 0.2.0 | Apr 26, 2026 |
#606 in #cross-platform
1MB
4.5K
SLoC
Vertra
Vertra is a lightweight, cross-platform 3D rendering engine for Rust, built on top of wgpu.
It provides a streamlined abstraction for hardware-accelerated graphics with a professional
perspective camera, a safe hierarchical scene graph, a built-in static scene editor,
a compact binary scene format (VTR), a screen-space text overlay, a per-object scripting
system, and a WASM/JavaScript binder layer.
Features
| Feature | Details |
|---|---|
| Scene Graph & Hierarchy | Parent-child relationships with inherited world transforms. Safe mutation via spawn, delete, reparent, and scene-graph change events. |
| Perspective Camera | Full view and projection matrix implementation (Y-up, left-handed, WGPU depth range). Builder-pattern construction with WASD + mouse-look helpers. |
| Procedural Geometry | Built-in Cube, Box, Plane, Pyramid, Sphere, and Capsule primitives. Geometry is generated on demand and batched into a single GPU draw call per texture group. |
| Texture Support | Load textures from RGBA data (or a file path on native) and bind them to objects by matching texture_path. |
| Built-in Editor | Static scene editor with orbit/pan/zoom camera, translate/rotate/scale gizmos, multi-select, group transform, object picker, text-label editing, and a skybox. Activated with scene.enable_editor_mode(). |
| Fixed-Update Loop | Separate on_fixed_update callback running at 60 Hz for physics-stable simulation. |
| Per-Object Scripting | Attach ObjectScript implementations to any scene object. Callbacks: on_start, on_update, on_fixed_update. Deferred world mutations keep script callbacks re-entrancy-safe. |
| Screen-Space Text Overlay | GPU-rasterised HUD labels via TextOverlay. Fluent TextLabelBuilder API with font management, z-index ordering, and horizontal/vertical alignment anchors. Full editor integration for interactive placement and resizing. |
| Per-Frame Metrics | FrameContext exposes smoothed fps, frame_time_ms, draw_calls, and triangle_count in every callback. |
| Play-Mode Snapshots | World state is automatically captured on editor entry and restored on exit, giving every play session a clean start. |
| VTR Binary Format | Compact, deterministic, little-endian binary format for saving and loading complete scenes. Roundtrips camera, hierarchy, transforms, colours, geometry, texture paths, and text labels. |
| Cross-Platform | wgpu backend supports Vulkan, Metal, DX12, WebGL, and WebGPU. |
| WASM / JS Binder | binder/ crate exposes the full API to JavaScript via wasm-bindgen, including deferred scene-graph events safe from JS re-entrancy. |
| Scene-Graph Events | World::on_scene_graph_modified callback fires after every structural mutation (add / delete / reparent). Events are queued and dispatched outside the mutation borrow in the binder. |
Getting Started
Add Vertra to your Cargo.toml dependencies:
[dependencies]
vertra = "0.3.0"
To enable the bundled font stack (sans-serif, serif, and monospace faces):
vertra = { version = "0.3.0", features = ["default-fonts"] }
Quick Example — Solar System
use std::collections::HashSet;
use winit::event::{DeviceEvent, ElementState, Event, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey};
use vertra::camera::Camera;
use vertra::window::Window;
use vertra::transform::Transform;
use vertra::geometry::Geometry;
use vertra::objects::Object;
struct AppState {
pressed_keys: HashSet<KeyCode>,
sun_id: usize,
planet_id: usize,
}
fn main() {
Window::new(AppState { pressed_keys: HashSet::new(), sun_id: 0, planet_id: 0 })
.with_title("Solar System")
.with_camera(
Camera::new()
.with_position([0.0, 8.0, -12.0])
.with_rotation(90.0, -30.0),
)
.with_event_handler(|state, scene, event, _| {
match event {
Event::WindowEvent {
event: WindowEvent::KeyboardInput { event: ke, .. }, ..
} => {
if let PhysicalKey::Code(code) = ke.physical_key {
match ke.state {
ElementState::Pressed => { state.pressed_keys.insert(code); }
ElementState::Released => { state.pressed_keys.remove(&code); }
}
}
}
Event::DeviceEvent {
event: DeviceEvent::MouseMotion { delta }, ..
} => {
scene.camera.rotate(delta.0 as f32 * 0.1, delta.1 as f32 * 0.1, false);
}
_ => {}
}
})
.on_startup(|state, scene, _| {
let sun = Object {
name: "Sun".to_string(),
geometry: Some(Geometry::Sphere { radius: 2.0, subdivisions: 32 }),
color: [1.0, 0.9, 0.2, 1.0],
..Default::default()
};
state.sun_id = scene.spawn(sun, None);
let planet = Object {
name: "Planet".to_string(),
transform: Transform::from_position(6.0, 0.0, 0.0),
geometry: Some(Geometry::Sphere { radius: 0.8, subdivisions: 24 }),
color: [0.2, 0.5, 1.0, 1.0],
..Default::default()
};
state.planet_id = scene.spawn(planet, Some(state.sun_id));
let moon = Object {
name: "Moon".to_string(),
transform: Transform::from_position(1.5, 0.0, 0.0),
geometry: Some(Geometry::Sphere { radius: 0.3, subdivisions: 16 }),
color: [0.7, 0.7, 0.7, 1.0],
..Default::default()
};
scene.spawn(moon, Some(state.planet_id));
})
.on_update(|state, scene, ctx| {
scene.camera.handle_default_input(&state.pressed_keys, 3.0, ctx);
if let Some(sun) = scene.world.get_mut(state.sun_id) {
sun.transform.rotation[1] += 30.0 * ctx.dt;
}
if let Some(planet) = scene.world.get_mut(state.planet_id) {
planet.transform.rotation[1] += 100.0 * ctx.dt;
}
})
.create();
}
Architecture Overview
Module Map
| Module | Purpose |
|---|---|
camera |
Perspective camera: eye/target/up, FOV, clip planes, builder setters, WASD helper |
scene |
Root scene container — spawn, texture, VTR save/load, editor integration |
world |
Scene-graph — object storage, hierarchy mutations, string/integer ID cache, change events |
objects |
Object struct — the fundamental scene-graph node (transform, geometry, colour, texture path) |
geometry |
Procedural mesh primitives — Cube, Box, Plane, Pyramid, Sphere, Capsule |
transform |
TRS transform — position/rotation/scale, matrix conversion, point transformation |
mesh |
CPU mesh builder (MeshData) and GPU baked mesh (BakedMesh) |
math |
Column-major Matrix4 — identity, perspective, look-at, point projection |
timer |
Simple countdown timer for use in game logic |
window |
Builder-pattern windowing and event-loop host with typed callbacks |
editor |
Static scene editor — orbit cam, gizmos, multi-select, inspector, label editing |
script |
Per-object scripting — ObjectScript trait and ScriptRegistry |
text_overlay |
Screen-space text overlay — font management, label storage, GPU rasterisation |
text_label |
TextLabel, TextLabelBuilder, TextLabelHandle, HorizontalAlignment, VerticalAlignment |
frame_stats |
Internal per-frame performance tracker backing FrameContext metrics |
vtr |
Binary .vtr scene format — read/write for camera + full object hierarchy + text labels |
constants |
Engine-wide default values |
event |
Re-exports of winit event types |
Scene Graph
Objects form a tree. Each Object stores its parent's and children's integer IDs.
During rendering the engine traverses the tree recursively, combining parent and child
Transform matrices so that children automatically inherit position, rotation, and scale.
The World type manages the graph and exposes safe mutation methods:
spawn_object(object, parent_id)— insert; unknown parent falls back to root.delete(id)— remove an object and all its descendants.reparent(id, new_parent)— move an object in the hierarchy with cycle detection.get_id(str_id)— resolve a stable string handle to an integer ID (call once, cache the result).on_scene_graph_modified— optional callback fired after every structural mutation.
Coordinate System
Y-up, left-handed. The default camera looks along +Z. All rotation angles are in degrees (Euler, Y → X → Z order).
Rendering Pipeline
Geometry is baked each frame: the scene tree is walked, all object meshes are assembled
into MeshData builders grouped by texture_path, then uploaded to the GPU as a small
number of batched draw calls. The editor gizmo overlay is rendered as a separate pass.
Per-Object Scripting
Implement ObjectScript and attach it to any scene object via scene.scripts.attach(id, script).
The runtime invokes on_start once on the first update after attachment, then on_update every
frame and on_fixed_update at 60 Hz—both only while editor mode is inactive.
World mutations (spawn, delete, reparent) that occur inside a script callback are safely
deferred and applied after the full update iteration completes, preventing borrow conflicts in
both native and WASM environments. Stale entries whose object has been deleted are pruned
automatically with an O(1) swap-remove.
Screen-Space Text Overlay
scene.text_overlay is a TextOverlay that manages fonts and screen-space labels:
- Register at least one font:
scene.text_overlay.add_font("sans", font_bytes). With thedefault-fontsfeature the engine bundles fonts automatically. - Create a label using the fluent builder:
let fps_label = scene.text_overlay .add_label("FPS: 0") .at(10.0, 10.0) .with_font_size(20.0) .with_color([1.0, 1.0, 0.0, 1.0]) .with_horizontal_alignment(HorizontalAlignment::Left) .build(); - Update later:
fps_label.set_text(&mut scene.text_overlay, &format!("FPS: {:.0}", ctx.fps));
Labels support HorizontalAlignment (Left, Center, Right, Free) and
VerticalAlignment (Top, Middle, Bottom, Free) anchors that control repositioning on
window resize. Z-index controls depth ordering when labels overlap.
Built-in Editor
Enable with scene.enable_editor_mode() in on_startup. While active:
on_update,on_fixed_update, andon_draw_requestare suppressed.- Orbit (Alt+drag), pan (middle-drag), and zoom (scroll wheel) control the camera.
T/R/Eswitch between translate, rotate, and scale gizmos.- Left-click picks objects or text labels; Ctrl+click multi-selects;
Gselects a subtree. Ffocuses the camera on the selection.- Text labels can be dragged to reposition and edge-dragged to resize font size. A draft mode previews the resize in real-time without re-rasterising until drag-end.
Escapeexits editor mode, restores the play-mode snapshot, and returns to play mode.
Use Window::on_editor_event to react to gizmo-mode changes, drag start/end, selection
changes, and label interactions (LabelSelected, LabelDragStart, LabelResizeEnd, …).
VTR Binary Format
.vtr files store the full camera state, scene hierarchy, and text labels in a compact
little-endian binary layout. Use scene.save_vtr_file / scene.load_vtr_file
on native, or vtr::write / vtr::read directly on any Write/Read impl.
License
Copyright 2026 xCirno Labs.
Licensed under the Apache License, Version 2.0.
http://www.apache.org/licenses/LICENSE-2.0
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be licensed as above, without any additional terms or conditions.
Dependencies
~22–55MB
~897K SLoC