viewport-lib is a gpu-accelerated 3D viewport library for rust. It works with any GUI framework that gives you access to a wgpu device, queue, and render target: eframe/egui, winit, Iced, Slint, and others.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
viewport-lib covers rendering, cameras, and post-processing. Your application owns the window, event loop, and tool state.
WARNING (Feb 2026): The viewport has only recently been extracted as a stand-alone library from a separate project and the API is still unstable. Several key things are still a work in progress. When things become more solid I will release v1.0.0.
UPDATE (May 2026): A ton of work has been put in and things are looking better and better. There are still some decisions to be made and problems to solved but on every front things are looking really good. If all goes well, v1.0.0 will be ready some time in July of 2026.
- Geometry: mesh, point cloud, polyline, volume, glyph, and streamtube rendering
- Lighting: directional, point, and spot lights; shadow maps;
- Materials: PBR and Blinn-Phong shading, normal maps, transparency
- Scene tools: clip planes, section views, scalar colouring, and colourmaps
- Camera: arcball orbit, orthographic projection, view presets, smooth animation, and frame-to-selection
- Interaction: CPU/GPU picking, rectangle selection, transform gizmos, and snapping
- Overlays: labels, scalar bar, rulers, and axes indicator
The examples/ directory contains working integrations for several GUI frameworks.
- eframe-showcase: run this first: demonstrates many of the viewport's built-in capabilities across multiple showcases (not exhaustive).
- eframe-minimal: the simplest integration: start here if you want to understand the minimal setup.
- eframe-primitives: demonstrates the built-in geometry primitives.
- eframe-viewport: a mid-complexity example with scene graph, picking, and gizmos.
- eframe-input-controllers: shows custom input bindings and controller configuration.
Other examples: winit-viewport, winit-showcase, winit-primitives, winit-multi-viewport, iced-viewport, slint-viewport, gtk4-viewport
Run examples with:
cargo run --release --example eframe-showcase
In a typical app you will need to use both the renderer (to build and submit a FrameData) and the input handler to define and handle keys and events.
use viewport_lib::{
Camera,
CameraFrame,
FrameData,
SceneFrame,
SceneRenderItem,
primitives,
};
// Upload the cube primitive mesh once at startup
let mesh_id = renderer.resources_mut().upload_mesh_data(&device, &primitives::cube(1.0))?;
// Build a frame each render tick
let camera = Camera::default();
let model = glam::Mat4::from_translation(glam::vec3(1.0, 2.0, 0.0));
let item = SceneRenderItem {
mesh_id,
model: model.to_cols_array_2d(),
..SceneRenderItem::default()
};
let fd = FrameData::new(
CameraFrame::from_camera(&camera, [width, height]),
SceneFrame::from_surface_items(vec![item]),
);
// prepare once per frame (uploads uniforms, batches instances)
renderer.pass().prepare(&device, &queue, &fd);
// then paint into a render pass -- exact call depends on your GUI framework:
// renderer.pass_view().paint(&mut render_pass, &fd); // eframe / iced / raw wgpu
// renderer.owned().render(&device, &queue, &output_view, &fd); // winit HDROrbitCameraController is one of the available built-in controllers. You can also build your own controller directly on top of ViewportInput and ViewportBinding if you need different navigation behaviour - but OrbitCameraController is a good starting point. Push events each frame, then call apply_to_camera to orbit/pan/zoom and get back an ActionFrame for the rest of your input logic.
use viewport_lib::{BindingPreset, ManipulationContext, ManipulationController, ManipResult, OrbitCameraController, ViewportContext, ViewportEvent};
// --- app state ---
let mut orbit = OrbitCameraController::new(BindingPreset::ViewportAll);
let mut manip = ManipulationController::new();
// prime the controller before the first frame
orbit.begin_frame(ViewportContext { hovered: true, focused: true, viewport_size: [width, height] });
// --- each frame ---
// 1. drive camera navigation; get the action frame for this frame
let frame = if manip.is_active() {
// suppress orbit while a manipulation is in progress
orbit.resolve()
} else {
orbit.apply_to_camera(&mut camera)
};
// 2. drive the manipulation controller
let ctx = ManipulationContext {
camera: camera.clone(),
viewport_size: glam::Vec2::new(width, height),
cursor_viewport: Some(cursor_pos),
pointer_delta,
selection_center: selected_object_center,
gizmo: None,
drag_started,
dragging,
clicked,
};
match manip.update(&frame, ctx) {
ManipResult::Update(delta) => {
// apply incremental transform to selected objects each frame
object_translation += delta.translation;
object_rotation = delta.rotation * object_rotation;
object_scale *= delta.scale;
}
ManipResult::ConstraintChanged => {
// axis constraint changed mid-session: restore objects to their
// pre-session transforms (same as cancel but keep the session alive)
restore_snapshot();
}
ManipResult::Commit => finalize_and_push_undo(),
ManipResult::Cancel => restore_snapshot(),
ManipResult::None => {}
}
// 3. reset for next frame
orbit.begin_frame(ViewportContext { hovered, focused, viewport_size: [width, height] });This project is licensed under the GNU General Public License v3.0. See LICENSE for details. Get in contact for details on purchasing a commercial license.





