hydra-rs 0.0.2

Rust bindings to OpenUSD's Hydra rendering layer: scene-index ingestion, render-delegate enumeration, headless render to RGBA via Storm.
//! Rust bindings to OpenUSD's Hydra rendering layer.
//!
//! Two layers of API:
//!
//! 1. The stateless [`render_to_rgba`] convenience: open a stage, render
//!    once with a hardcoded camera and default light, throw the engine away.
//! 2. The stateful [`Renderer`]: holds a stage + HGI + `UsdImagingGLEngine`
//!    across calls so a viewport can re-render at interactive rates without
//!    rebuilding any of the scaffolding. Camera, lights, time code, output
//!    size, and clear color are all mutable between renders.
//!
//! Plus low-level inspection: [`SceneIndex::from_path`] for ingesting a stage
//! into a `UsdImagingStageSceneIndex` and walking what Hydra sees, and
//! [`list_render_delegates`] for enumerating the registered render plugins.

use std::path::Path;

use cxx::UniquePtr;

#[cxx::bridge(namespace = "hydra_rs")]
mod ffi {
    unsafe extern "C++" {
        include!("hydra_bridge.h");

        type SceneIndex;
        type Renderer;

        fn populate_from_path(usd_path: &str) -> Result<UniquePtr<SceneIndex>>;
        fn list_render_delegate_ids() -> UniquePtr<CxxVector<CxxString>>;
        fn create_renderer(
            usd_path: &str,
            render_delegate_id: &str,
        ) -> Result<UniquePtr<Renderer>>;
        fn render_to_rgba(
            usd_path: &str,
            render_delegate_id: &str,
            width: u32,
            height: u32,
        ) -> Result<UniquePtr<CxxVector<u8>>>;

        fn stage_root(self: &SceneIndex) -> String;
        fn prim_count(self: &SceneIndex) -> usize;
        fn prim_paths(self: &SceneIndex) -> UniquePtr<CxxVector<CxxString>>;

        fn set_size(self: Pin<&mut Renderer>, width: u32, height: u32);
        fn set_camera_matrices(
            self: Pin<&mut Renderer>,
            view: &[f32],
            projection: &[f32],
        );
        fn set_time(self: Pin<&mut Renderer>, time: f64);
        fn use_default_time(self: Pin<&mut Renderer>);
        fn clear_lights(self: Pin<&mut Renderer>);
        fn use_default_light(self: Pin<&mut Renderer>);
        fn add_distant_light(
            self: Pin<&mut Renderer>,
            dx: f32,
            dy: f32,
            dz: f32,
            r: f32,
            g: f32,
            b: f32,
            intensity: f32,
        );
        fn add_positional_light(
            self: Pin<&mut Renderer>,
            px: f32,
            py: f32,
            pz: f32,
            r: f32,
            g: f32,
            b: f32,
            intensity: f32,
        );
        fn set_clear_color(self: Pin<&mut Renderer>, r: f32, g: f32, b: f32, a: f32);
        fn current_renderer(self: &Renderer) -> String;
        fn set_renderer_plugin(self: &Renderer, plugin_id: &str) -> bool;
        fn render_color(self: &Renderer) -> Result<UniquePtr<CxxVector<u8>>>;
    }
}

/// Wraps a `UsdImagingStageSceneIndex` and the `UsdStage` that backs it.
pub struct SceneIndex {
    inner: UniquePtr<ffi::SceneIndex>,
}

impl SceneIndex {
    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, cxx::Exception> {
        let p = path.as_ref().to_string_lossy();
        Ok(Self {
            inner: ffi::populate_from_path(&p)?,
        })
    }

    pub fn stage_root(&self) -> String {
        self.inner.stage_root()
    }

    pub fn prim_count(&self) -> usize {
        self.inner.prim_count()
    }

    pub fn prim_paths(&self) -> Vec<String> {
        self.inner
            .prim_paths()
            .iter()
            .map(|s| s.to_string())
            .collect()
    }
}

/// Render delegate plugin IDs registered with USD's plug system in this
/// process (e.g. `"HdStormRendererPlugin"`, `"HdEmbreeRendererPlugin"`).
pub fn list_render_delegates() -> Vec<String> {
    ffi::list_render_delegate_ids()
        .iter()
        .map(|s| s.to_string())
        .collect()
}

/// Stateful Hydra renderer. Hold one of these across frames in a viewport;
/// call `set_camera_matrices` / `set_size` / `set_time` between renders.
pub struct Renderer {
    inner: UniquePtr<ffi::Renderer>,
}

impl Renderer {
    /// Create a renderer for `usd_path` using the default render delegate
    /// (Storm on macOS, Linux with GL/Vulkan, Windows with DX/GL).
    pub fn new(usd_path: impl AsRef<Path>) -> Result<Self, cxx::Exception> {
        Self::with_delegate(usd_path, "")
    }

    /// Create a renderer for `usd_path` using the named render delegate
    /// (e.g. `"HdStormRendererPlugin"`, `"HdEmbreeRendererPlugin"`).
    pub fn with_delegate(
        usd_path: impl AsRef<Path>,
        delegate_id: &str,
    ) -> Result<Self, cxx::Exception> {
        let p = usd_path.as_ref().to_string_lossy();
        Ok(Self {
            inner: ffi::create_renderer(&p, delegate_id)?,
        })
    }

    /// Output size in pixels. Resets the default-camera projection so the
    /// aspect ratio stays correct if the caller hasn't supplied their own.
    pub fn set_size(&mut self, width: u32, height: u32) {
        self.inner.pin_mut().set_size(width, height);
    }

    /// Set the view and projection matrices as row-major 4x4 floats. Each
    /// slice must be exactly 16 entries long. Convention is GfMatrix4d's:
    /// `m[r * 4 + c]` is row `r`, column `c`. Pixar uses row-vector
    /// convention internally, so a "view matrix" here is what you'd plug
    /// into `vec_world * view = vec_camera`.
    pub fn set_camera_matrices(&mut self, view: &[f32; 16], projection: &[f32; 16]) {
        self.inner.pin_mut().set_camera_matrices(view, projection);
    }

    /// Override the time code rendered. Defaults to `UsdTimeCode::Default`.
    pub fn set_time(&mut self, time: f64) {
        self.inner.pin_mut().set_time(time);
    }

    /// Restore the default time code.
    pub fn use_default_time(&mut self) {
        self.inner.pin_mut().use_default_time();
    }

    /// Drop all explicit lights AND disable the default headlight, so the
    /// next render is unlit (just diffuse albedo).
    pub fn clear_lights(&mut self) {
        self.inner.pin_mut().clear_lights();
    }

    /// Drop explicit lights and re-enable the built-in default headlight.
    pub fn use_default_light(&mut self) {
        self.inner.pin_mut().use_default_light();
    }

    /// Add a directional light with direction `(dx, dy, dz)` (need not be
    /// normalized), color `(r, g, b)`, and a multiplicative `intensity`.
    /// Disables the default headlight; multiple `add_*_light` calls
    /// accumulate.
    pub fn add_distant_light(
        &mut self,
        direction: [f32; 3],
        color: [f32; 3],
        intensity: f32,
    ) {
        self.inner.pin_mut().add_distant_light(
            direction[0],
            direction[1],
            direction[2],
            color[0],
            color[1],
            color[2],
            intensity,
        );
    }

    /// Add a positional (point) light at `(px, py, pz)` with the same
    /// `(r, g, b) * intensity` shading.
    pub fn add_positional_light(
        &mut self,
        position: [f32; 3],
        color: [f32; 3],
        intensity: f32,
    ) {
        self.inner.pin_mut().add_positional_light(
            position[0],
            position[1],
            position[2],
            color[0],
            color[1],
            color[2],
            intensity,
        );
    }

    /// Background color used by the AOV clear. Tuple is `(r, g, b, a)` in
    /// linear space.
    pub fn set_clear_color(&mut self, color: [f32; 4]) {
        self.inner
            .pin_mut()
            .set_clear_color(color[0], color[1], color[2], color[3]);
    }

    /// The currently-active render delegate plugin id.
    pub fn current_renderer(&self) -> String {
        self.inner.current_renderer()
    }

    /// Switch the render delegate. Returns `false` if the plugin id isn't
    /// registered.
    pub fn set_renderer_plugin(&self, plugin_id: &str) -> bool {
        self.inner.set_renderer_plugin(plugin_id)
    }

    /// Render the current state and return RGBA8 pixels of size
    /// `width * height * 4`.
    pub fn render(&self) -> Result<Vec<u8>, cxx::Exception> {
        Ok(self.inner.render_color()?.iter().copied().collect())
    }
}

/// Open a stage, render once with the default camera and headlight, return
/// RGBA8 pixels. Convenience wrapper around [`Renderer`] for simple cases.
pub fn render_to_rgba(
    usd_path: impl AsRef<Path>,
    render_delegate: &str,
    width: u32,
    height: u32,
) -> Result<Vec<u8>, cxx::Exception> {
    let p = usd_path.as_ref().to_string_lossy();
    Ok(ffi::render_to_rgba(&p, render_delegate, width, height)?
        .iter()
        .copied()
        .collect())
}