vertra 0.2.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), 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, 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.
VTR Binary Format Compact, deterministic, little-endian binary format for saving and loading complete scenes. Roundtrips camera, hierarchy, transforms, colours, geometry, and texture paths.
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

Vertra is not yet published to crates.io. Clone the repository and reference it via a path dependency:

[dependencies]

vertra = { path = "../path/to/vertra" }


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
vtr Binary .vtr scene format — read/write for camera + full object hierarchy
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.

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; Ctrl+click multi-selects; G selects a subtree.
  • F focuses the camera on the selection.
  • Escape exits editor mode and returns to play mode.

Use Window::on_editor_event to react to gizmo-mode changes, drag start/end, and selection changes while the editor is active.

VTR Binary Format

.vtr files store the full camera state and scene hierarchy in a compact little-endian binary layout (~84 bytes minimum for an empty scene). 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.