# Vertra
[](#license)
[](https://www.rust-lang.org)
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
| **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:
```toml
[dependencies]
vertra = "0.3.0"
```
To enable the bundled font stack (sans-serif, serif, and monospace faces):
```toml
vertra = { version = "0.3.0", features = ["default-fonts"] }
```
---
## Quick Example — Solar System
```rust
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
| `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:
1. Register at least one font: `scene.text_overlay.add_font("sans", font_bytes)`.
With the `default-fonts` feature the engine bundles fonts automatically.
2. Create a label using the fluent builder:
```rust,ignore
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();
```
3. 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`, and `on_draw_request` are suppressed.
* Orbit (Alt+drag), pan (middle-drag), and zoom (scroll wheel) control the camera.
* `T` / `R` / `E` switch between translate, rotate, and scale gizmos.
* Left-click picks objects or text labels; Ctrl+click multi-selects; `G` selects a subtree.
* `F` focuses 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.
* `Escape` exits 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.