ferrum-wgpu 0.2.1

3D rendering engine library
Documentation

ferrum-wgpu

A 3D rendering engine library built with Rust and wgpu (WebGPU).

Crates.io Rust wgpu License: GPL v3

Cross-platform PBR rendering engine that runs on desktop (Windows, Linux, macOS), browser (WebAssembly via WebGPU) and Raspberry Pi.

Installation

cargo add ferrum-wgpu

Or in Cargo.toml:

[dependencies]
ferrum-wgpu = "x.x.x"

Features

Feature Default Description
rpi no Enables OpenGL ES backend for Raspberry Pi. Disables Vulkan/Metal/DX12.

Enable with:

ferrum-wgpu = { version = "x.x.x", features = ["rpi"] }

Quick Start

In this case you can see the basic window example to setup your app. For more complete examples, see the /examples directory.

use ferrum_wgpu::{
    State,
    config::{WindowSize, config::FerrumConfig},
};
use std::collections::HashMap;
use {
    ferrum_wgpu::KeyCode,
    std::sync::Arc,
    winit::{
        application::ApplicationHandler,
        event::{KeyEvent, WindowEvent},
        event_loop::{ActiveEventLoop, EventLoop},
        keyboard::PhysicalKey,
        window::{Window, WindowId},
    },
};

fn main() -> anyhow::Result<()> {
    let demo_models: HashMap<&str, usize> = HashMap::new();

    let app_config: FerrumConfig = FerrumConfig {
        size: WindowSize::new(1000, 500),
        asset: ferrum_wgpu::assets::Asset::new("/res".to_string()),
        ..Default::default()
    };

    App::new(app_config)
        .ferrum_setup(move |state: &mut State| setup(state, &demo_models))
        .ferrum_update(|state: &mut State| update(state))
        .run()?;

    Ok(())
}

pub fn setup(_state: &mut State, _demo_models: &HashMap<&str, usize>) {}
pub fn update(_state: &mut State) {}

pub type SetupFn = Box<dyn FnOnce(&mut State)>;
pub type UpdateFn = Box<dyn FnMut(&mut State)>;

#[derive(Default)]
pub struct App {
    pub state: Option<State>,
    setup: Option<SetupFn>,
    update: Option<UpdateFn>,
    window: Option<Arc<Window>>,
    config: FerrumConfig,
}

impl App {
    pub fn new(config: FerrumConfig) -> Self {
        Self {
            state: None,
            setup: None,
            update: None,
            window: None,
            config,
        }
    }

    pub fn ferrum_setup<F: FnOnce(&mut State) + 'static>(mut self, f: F) -> Self {
        self.setup = Some(Box::new(f));
        self
    }

    pub fn ferrum_update<F: FnMut(&mut State) + 'static>(mut self, f: F) -> Self {
        self.update = Some(Box::new(f));
        self
    }

    pub fn run(mut self) -> anyhow::Result<()> {
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

        let event_loop: EventLoop<State> = EventLoop::<State>::with_user_event().build()?;
        event_loop.run_app(&mut self)?;
        Ok(())
    }
}

impl ApplicationHandler<State> for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let window_attributes = Window::default_attributes()
            .with_title("Ferrum")
            .with_inner_size(ferrum_wgpu::PhysicalSize::new(
                self.config.size.width,
                self.config.size.height,
            ));

        let window: Arc<Window> = Arc::new(event_loop.create_window(window_attributes).unwrap());
        self.window = Some(Arc::clone(&window));

        let inner_size = window.inner_size();
        let size = WindowSize::new(inner_size.width, inner_size.height);
        let setup = self.setup.take();
        let asset = self.config.asset.clone();

        self.state = Some(
            pollster::block_on(async move {
                let mut state = State::new(window, size, asset).await?;
                if let Some(s) = setup {
                    s(&mut state);
                }
                anyhow::Ok(state)
            })
            .unwrap(),
        );
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        let state = match &mut self.state {
            Some(s) => s,
            None => return,
        };

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::RedrawRequested => {
                state.evolbe();
                if let Some(update) = &mut self.update {
                    update(state);
                }
                match state.render() {
                    Ok(_) => {}
                    Err(ferrum_wgpu::SurfaceError::Lost | ferrum_wgpu::SurfaceError::Outdated) => {
                        if let Some(window) = &self.window {
                            let size = window.inner_size();
                            state.resize(size.height, size.width);
                        }
                    }
                    Err(e) => eprint!("Render error: {e}"),
                }
            }
            WindowEvent::Resized(size) => state.resize(size.height, size.width),
            WindowEvent::KeyboardInput {
                event:
                    KeyEvent {
                        physical_key: PhysicalKey::Code(code),
                        state: key_state,
                        ..
                    },
                ..
            } => {
                if code == KeyCode::Escape && key_state.is_pressed() {
                    #[cfg(not(target_arch = "wasm32"))]
                    event_loop.exit();
                } else {
                    state
                        .camera
                        .controller
                        .handle_key(code, key_state.is_pressed());
                }
            }
            _ => {}
        }
    }

    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: State) {
        self.state = Some(event);
    }

    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        if let Some(window) = &self.window {
            window.request_redraw();
        }
    }
}

Docs

Full documentation is not available yet. For now, refer to the demo example to understand basic usage. Below is a brief summary of the main concepts:

API Overview

Method Description
State::new(window, size) Initialize GPU device, surface, pipelines and sky
state.spawn_model(desc) Async-load a .obj model and add it to the scene
state.evolbe() Per-frame tick: collect loaded models, update uniforms
state.render() Submit render pass and present the frame
state.render_with_overlay(callback) Render with an egui overlay pass
state.set_wind(direction, intensity) Set wind vector that animates foliage
state.resize(width, height) Handle window resize

Capabilities

  • PBR rendering with diffuse/specular lighting, tangent-space normal maps, HDR pipeline and ACES tonemapping
  • Skybox from equirectangular HDR/EXR images converted to cubemap via compute shaders
  • Animated directional light with orbital rotation and shadow maps
  • Instancing for efficient multi-object rendering
  • Free camera with WASD / arrow key controls
  • Async resource loading on both native and WASM targets

Graphics Backends

Platform Backend
Windows / macOS / Linux Vulkan, Metal, DX12
Web (WASM) WebGPU (required — not WebGL2)
Raspberry Pi OpenGL ES (enable rpi feature)

Demo & Source

Full project, live demo and Raspberry Pi integration: github.com/karlosvas/ferrum

License

GNU General Public License v3.0 — see LICENSE.