vertra 0.3.0

A cross-platform graphics editor built with Rust and WebAssembly.
Documentation

Vertra

License Rust

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:

  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:
    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.