Modular Rust-native 3D scenes for native and WASM apps.
scenix 1.5.0 is the current stable release. It adds cross-platform controls,
touch/gamepad/pointer-lock input, selection and transform primitives, reusable
gizmos, inspector snapshots, optional egui rendering, and on-demand GPU picking.
CPU authoring stays lightweight by default while GPU, browser, and UI paths
remain opt-in.
Most applications start with the facade crate:
[dependencies]
scenix = "1"Enable optional systems only when needed:
[dependencies]
scenix = { version = "1", features = ["loader"] }
scenix = { version = "1", features = ["renderer", "post"] }
scenix = { version = "1", features = ["animato"] }
scenix = { version = "1", features = ["wasm"] }
scenix = { version = "1", features = ["interaction"] }
scenix = { version = "1", features = ["editor", "egui"] }Focused crates can be used directly:
[dependencies]
scenix-math = "1"
scenix-core = "1"
scenix-input = "1"
scenix-scene = "1"
scenix-camera = "1"
scenix-mesh = "1"
scenix-material = "1"
scenix-light = "1"
scenix-texture = "1"
scenix-loader = "1"
scenix-renderer = "1"
scenix-post = "1"
scenix-raycaster = "1"
scenix-helpers = "1"
scenix-animato = "1"
scenix-wasm = "1"For no_std CPU authoring:
[dependencies]
scenix-math = { version = "1", default-features = false, features = ["libm"] }
scenix-core = { version = "1", default-features = false }
scenix-input = { version = "1", default-features = false }
scenix-scene = { version = "1", default-features = false }
scenix-camera = { version = "1", default-features = false }
scenix-mesh = { version = "1", default-features = false }
scenix-material = { version = "1", default-features = false }
scenix-light = { version = "1", default-features = false }
scenix-texture = { version = "1", default-features = false }
scenix-raycaster = { version = "1", default-features = false }
scenix-helpers = { version = "1", default-features = false }scenix-loader, scenix-renderer, scenix-post, scenix-animato, and
scenix-wasm are optional std paths. scenix-animato continues to target
Animato 1.7.0.
| Feature | Default | Description |
|---|---|---|
std |
yes | Standard-library support for CPU crates. |
scene, camera, mesh, material, light, texture |
yes | CPU authoring crates. |
raycaster, helpers |
yes | BVH picking and debug line helper data. |
interaction |
no | Convenience bundle for scene, camera, raycaster, and helpers. |
editor |
no | Inspector snapshots and editor-facing selection/transform data. |
egui |
no | Read-only egui inspector adapter; the host owns its window/render loop. |
loader |
no | Asset packages, asset manager, glTF/GLB extension metadata, OBJ/MTL, STL, image, KTX2, HDR/EXR loading, and exporters. |
renderer |
no | wgpu renderer with surface/headless targets. |
post |
no | Full-screen post-processing stack; use with renderer. |
animato |
no | Animato bridge: procedural tween/spring tracks and the clip-based Animation Runtime (clips, actions, mixer, loop modes, crossfade, additive blending, markers/events, light/morph targets, retargeting, CPU skinning). Targets Animato 1.7.0. |
wasm |
no | Browser canvas wrapper with WebGPU first, WebGL2 full fallback, WebGL1 reduced fallback, and generated demo scene. |
serde |
no | Serialization support where the focused crate supports it. |
use scenix::{
ArcballController, InputState, PerspectiveCamera, PointerButton, Vec2, Vec3,
ViewportMetrics,
};
let mut input = InputState::new(ViewportMetrics::new(Vec2::new(1280.0, 720.0), 2.0));
input.on_pointer_down(PointerButton::Left);
input.on_pointer_move(Vec2::new(24.0, 12.0));
input.on_scroll(-0.25);
let mut controls = ArcballController::new(Vec3::ZERO, 5.0);
let mut camera = PerspectiveCamera::default();
controls.update_from_input(&input, 1.0 / 60.0);
controls.apply_to_perspective(&mut camera);
input.end_frame();InputState also accepts fixed-capacity touch contacts, two-finger gestures,
four standard gamepads, and pointer-lock relative motion without allocating in
the event/control hot path.
use std::collections::BTreeMap;
use scenix::{
CameraId, CameraStores, Geometry, MaterialId, MeshId, NodeAnimationTarget, NodeAnimator,
PbrMaterial, PerspectiveCamera, SceneGraph, SceneNode, ScenixAnimationDriver, Vec3,
Vec3Track, box_geometry,
};
# fn run() -> Result<(), scenix::ValidationError> {
let mesh_id = MeshId::new(1);
let material_id = MaterialId::new(1);
let mut meshes = BTreeMap::<MeshId, Geometry>::new();
meshes.insert(mesh_id, box_geometry(1.0, 1.0, 1.0, 1, 1, 1));
let mut scene = SceneGraph::new();
let cube = scene.add(SceneNode::mesh("cube", mesh_id, material_id));
let mut driver = ScenixAnimationDriver::new();
driver.add_node(NodeAnimator::new(
cube,
NodeAnimationTarget::Translation(Vec3Track::tween(
Vec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
0.5,
)),
));
let camera_id = CameraId::new(1);
let mut perspective = BTreeMap::from([(
camera_id,
PerspectiveCamera::new(60.0, 1.0, 0.1, 100.0)
.position(Vec3::new(0.0, 0.0, 4.0))
.target(Vec3::ZERO),
)]);
let mut orthographic = BTreeMap::new();
let mut cameras = CameraStores {
perspective: &mut perspective,
orthographic: &mut orthographic,
};
let mut materials = BTreeMap::from([(material_id, PbrMaterial::new())]);
let mut skeletons = Vec::new();
driver.tick(0.5, &mut scene, &mut cameras, &mut materials, &mut skeletons)?;
scene.update_world_transforms();
# Ok(())
# }use scenix::{Renderer, RendererConfig, PerspectiveCamera, Vec3};
# async fn run(scene: &scenix::SceneGraph) -> Result<(), scenix::ScenixError> {
let mut renderer = Renderer::headless(RendererConfig::new(256, 256)).await?;
let camera = PerspectiveCamera::new(60.0, 1.0, 0.1, 100.0)
.position(Vec3::new(0.0, 0.0, 5.0))
.target(Vec3::ZERO);
let stats = renderer.render(scene, &camera)?;
assert!(stats.frame_index > 0);
# Ok(())
# }use scenix::{AssetManager, RendererAssetExt};
# async fn run() -> Result<(), scenix::ScenixError> {
let mut manager = AssetManager::new();
let package = manager.load_file("scene.glb")?;
let mut renderer = scenix::Renderer::headless(scenix::RendererConfig::new(512, 512)).await?;
let uploaded = renderer.register_asset_package(&package)?;
println!("meshes={}, textures={}", uploaded.meshes, uploaded.textures);
# Ok(())
# }The v1.4.0 AnimationMixer plays clip-based animation on top of Animato:
use scenix::{
AnimationClip, AnimationMixer, ClipChannel, ClipTrack, KeyframeInterpolation,
KeyframeVec3, LoopMode, NodeProperty, PropertyBinding, SceneGraph, SceneNode, Vec3,
};
let mut scene = SceneGraph::new();
let node = scene.add(SceneNode::new("mover"));
let clip = AnimationClip::empty("move").with_channel(ClipChannel {
binding: PropertyBinding::Node { node_id: node, property: NodeProperty::Translation },
track: ClipTrack::Vec3(KeyframeVec3::new(
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0)],
KeyframeInterpolation::Linear,
)),
});
let mut mixer = AnimationMixer::new();
let clip_index = mixer.add_clip(clip);
let action = mixer.add_action(clip_index);
mixer.action_mut(action).unwrap().set_loop_mode(LoopMode::REPEAT);
mixer.action_mut(action).unwrap().play(0.0);
// each frame:
// mixer.tick(dt, &mut scene, &mut cameras, &mut materials,
// &mut lights, &mut skeletons, &mut morphs)use std::collections::BTreeMap;
use scenix::{
Geometry, MaterialId, MeshId, PerspectiveCamera, Raycaster, SceneGraph, SceneNode,
Vec2, Vec3, box_geometry,
};
let mesh_id = MeshId::new(1);
let material_id = MaterialId::new(1);
let meshes = BTreeMap::<MeshId, Geometry>::from([(
mesh_id,
box_geometry(1.0, 1.0, 1.0, 1, 1, 1),
)]);
let mut scene = SceneGraph::new();
scene.add(SceneNode::mesh("cube", mesh_id, material_id));
scene.update_world_transforms();
let camera = PerspectiveCamera::new(60.0, 1.0, 0.1, 100.0)
.position(Vec3::new(0.0, 0.0, 4.0))
.target(Vec3::ZERO);
let ray = Raycaster::from_camera_ndc(&camera, Vec2::ZERO);
let mut raycaster = Raycaster::new();
raycaster.build_bvh(&scene, &meshes).unwrap();
assert!(raycaster.cast_ray(ray, &scene, &meshes).is_some());| Crate | Role |
|---|---|
scenix |
Facade crate with stable v1 feature flags. |
scenix-math |
no_std vectors, matrices, quaternions, transforms, rays, and bounds. |
scenix-core |
IDs, colors, errors, and shared traits. |
scenix-input |
Pointer, keyboard, touch, gesture, gamepad, pointer-lock, and viewport state. |
scenix-scene |
Scene graph plus editor metadata, layers, snapping, and selection state. |
scenix-camera |
Cameras plus orbit, fly, arcball, trackball, map, first-person, and pointer-lock controls. |
scenix-mesh |
Geometry buffers, primitives, instancing, batching, and morph targets. |
scenix-material |
GPU-free material descriptions and pipeline keys. |
scenix-light |
Lights, shadow settings, and light probes. |
scenix-texture |
CPU textures, samplers, atlases, video updates, and mipmaps. |
scenix-loader |
Optional CPU asset packages, asset manager, importers, exporters, diagnostics, and cache. |
scenix-renderer |
Optional wgpu renderer with on-demand ID/normal/depth editor picking. |
scenix-post |
Optional wgpu post-processing effects. |
scenix-raycaster |
BVH ray picking, marquee selection, drag planes, and transform interactions. |
scenix-helpers |
Debug lines, reusable gizmos, selection visuals, snap grids, and optional egui. |
scenix-animato |
Optional Animato bridge. |
scenix-wasm |
Optional browser canvas wrapper with WebGPU and WebGL paths. |
The facade crate registers the example set from ARCHITECTURE.md:
cargo run -p scenix --example hello_cube --features renderer
cargo run -p scenix --example pbr_sphere --features renderer
cargo run -p scenix --example physical_material --features renderer
cargo run -p scenix --example toon_shading --features renderer
cargo run -p scenix --example gltf_scene --features "loader renderer"
cargo run -p scenix --example asset_pipeline --features "loader renderer"
cargo run -p scenix --example asset_manager --features loader
cargo run -p scenix --example export_scene --features loader
cargo run -p scenix --example animation_import --features loader
cargo run -p scenix --example compressed_assets --features loader
cargo run -p scenix --example shadow_demo --features renderer
cargo run -p scenix --example raycasting
cargo run -p scenix --example post_processing --features "renderer post"
cargo run -p scenix --example instanced_mesh
cargo run -p scenix --example animato_integration --features animato
cargo run -p scenix --example animation_runtime --features "animato scene"
cargo run -p scenix --example animation_mixer --features "animato scene material light"
cargo run -p scenix --example skeleton_skinning --features "mesh animato"
cargo run -p scenix --example animation_events --features "animato scene light"
cargo run -p scenix --example orbit_camera
cargo run -p scenix --example lod_demo
cargo run -p scenix --example morph_targets
cargo run -p scenix --example fog_demo
cargo run -p scenix --example helpers_demo
cargo run -p scenix --example controls_showcase
cargo run -p scenix --example selection_and_drag
cargo run -p scenix --example transform_gizmo
cargo run -p scenix --example editor_inspector --features egui
cargo run -p scenix --example renderer_picking --features renderer
cargo run -p scenix --example sprite_particles
cargo run -p scenix --example environment_map --features renderer
cargo run -p scenix --example render_target_capture --features rendererThe browser example lives in examples/wasm_viewer:
rustup target add wasm32-unknown-unknown
cargo check --manifest-path examples/wasm_viewer/Cargo.toml --target wasm32-unknown-unknownThe static website is a standalone Leptos CSR app in website/. It is intentionally outside the main workspace so website dependencies do not affect normal library users.
cd website
trunk serve
trunk build --release --public-url /scenix/GitHub Pages deployment is handled by .github/workflows/pages.yml, which builds website/dist with Trunk and deploys it at /scenix/. The demo uses scenix-wasm: it tries WebGPU where safe, falls back to a WebGL2 renderer with the same generated scene/material/light controls when WebGPU is unavailable, uses reduced WebGL1 only as a last GPU path, and only uses the Canvas2D preview when both GPU paths are unavailable.
cargo fmt --check
cargo clippy --workspace --all-features -- -D warnings
cargo test --workspace
cargo test --workspace --all-features
cargo test -p scenix-math -p scenix-core -p scenix-input -p scenix-scene -p scenix-camera -p scenix-mesh -p scenix-material -p scenix-light -p scenix-texture -p scenix-raycaster -p scenix-helpers -p scenix-animato --no-default-features
cargo test -p scenix-loader --all-features
cargo test -p scenix-raycaster -p scenix-helpers --all-features
cargo test -p scenix-animato --all-features
cargo test -p scenix --test scenix_v15 --all-features
cargo check -p scenix --no-default-features --features interaction
cargo check -p scenix --no-default-features --features editor
cargo check -p scenix --no-default-features --features egui
cargo check -p scenix-wasm --target wasm32-unknown-unknown --all-features
cargo check --manifest-path examples/wasm_viewer/Cargo.toml --target wasm32-unknown-unknown
SCENIX_RUN_GPU_TESTS=1 WGPU_BACKEND=vulkan cargo test -p scenix-renderer -p scenix-post --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
cargo bench --workspace --no-run
cargo llvm-cov --workspace --all-features- Developer docs
- Architecture
- Roadmap
- Changelog
- Getting started
- Installation
- Quick start
- Concepts
- Guides
- API reference
- Examples
- Recipes
- Performance
- Deployment
- Migration
- Reference
- v1.5.0 release notes
- v1.4.0 release notes
- v1.3.0 release notes
- The renderer now uploads material textures, light data, environment descriptors, and render targets through real GPU resources. Advanced physical shading is a pragmatic realtime path, not an offline film renderer.
- Loader APIs produce CPU-side scenix data;
RendererAssetExtis a convenience bridge that still uploads into renderer-owned GPU resources explicitly. - Draco and meshopt compressed glTF assets currently produce explicit diagnostics unless preprocessed by an external converter.
- WebGL2 is the full browser fallback for the generated renderer scene when WebGPU is unavailable. WebGL1 remains a reduced last-resort fallback for older browsers.
- The website demo does not vendor large model assets.
- GPU tests require a Vulkan-capable device or Mesa lavapipe.
Licensed under either of:
- Apache License, Version 2.0
- MIT license