hydra-rs 0.0.4

Rust bindings to OpenUSD's Hydra rendering layer: scene-index ingestion, render-delegate enumeration, headless render to RGBA via Storm.
#pragma once

#include "rust/cxx.h"
// Forward-declared structs that cxx generates from the bridge module
// have to be available here so the C++ Renderer signatures below
// reference them by name. cxx emits them in this header automatically
// (the file is included downstream of the rust-side bridge module).

#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/vec4f.h>
#include <pxr/imaging/glf/simpleLight.h>
#include <pxr/imaging/glf/simpleMaterial.h>
#include <pxr/imaging/hd/sceneIndex.h>
#include <pxr/imaging/hgi/hgi.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usdImaging/usdImaging/stageSceneIndex.h>
#include <pxr/usdImaging/usdImagingGL/engine.h>

#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>

namespace hydra_rs {

struct SceneIndex {
    // Owns the UsdStageRefPtr that backs the scene index — the imaging
    // delegate keeps a non-owning reference, so the stage must outlive it.
    pxr::UsdStageRefPtr stage_owner;
    pxr::HdSceneIndexBaseRefPtr scene_index;

    rust::String stage_root() const;
    size_t prim_count() const;
    std::unique_ptr<std::vector<std::string>> prim_paths() const;
};

std::unique_ptr<SceneIndex> populate_from_path(rust::Str usd_path);

// Render delegate plugin IDs registered in this process.
std::unique_ptr<std::vector<std::string>> list_render_delegate_ids();

// Stateful renderer. Holds a stage, an HGI, and a UsdImagingGLEngine across
// render() calls so a viewport can re-render at interactive rates without
// rebuilding any of that scaffolding. Construction order in the struct is
// load-bearing: hgi MUST destruct after engine, because engine references it.
struct Renderer {
    pxr::UsdStageRefPtr stage;
    pxr::HgiUniquePtr hgi;
    std::unique_ptr<pxr::UsdImagingGLEngine> engine;

    pxr::GfMatrix4d view_matrix;
    pxr::GfMatrix4d proj_matrix;

    pxr::GlfSimpleLightVector explicit_lights;
    pxr::GlfSimpleMaterial material;
    pxr::GfVec4f scene_ambient = pxr::GfVec4f(0.05f, 0.05f, 0.05f, 1.0f);
    pxr::GfVec4f clear_color = pxr::GfVec4f(0.1f, 0.1f, 0.15f, 1.0f);
    bool use_default_lighting = true;

    uint32_t width = 256;
    uint32_t height = 256;
    double frame = 0.0;
    bool default_frame = true;

    void set_size(uint32_t w, uint32_t h);
    void set_camera_matrices(rust::Slice<const float> view,
                             rust::Slice<const float> projection);
    void set_time(double time);
    void use_default_time();

    void clear_lights();
    void use_default_light();
    void add_distant_light(float dx, float dy, float dz,
                           float r, float g, float b, float intensity);
    void add_positional_light(float px, float py, float pz,
                              float r, float g, float b, float intensity);

    void set_clear_color(float r, float g, float b, float a);
    // Scene ambient — a constant additive RGB applied to every shaded
    // surface, regardless of light direction. Storm uses it as the
    // ambient term inside `GlfSimpleLightingContext`. Default is the
    // 0.05 grey set in the struct initializer; bump it (~0.15-0.25)
    // when the consumer has no IBL and wants the shadow side of the
    // model to read instead of going to black.
    void set_scene_ambient(float r, float g, float b, float a);

    // Image-based environment lighting — authors a `UsdLuxDomeLight`
    // into the stage's session layer pointing at `hdri_path`. Storm
    // renders it both as the dome background behind geometry and as
    // an IBL contributor (importance-sampled). `intensity` and
    // `exposure` map to the corresponding UsdLux attrs; `rotation_y`
    // is applied as an `xformOp:rotateY` in degrees (positive =
    // counter-clockwise looking down -Y, matching the UsdGeomXformOp
    // sign convention).
    //
    // Idempotent: replaces the existing dome's attributes on each
    // call rather than spawning a new prim, so calling it every frame
    // is cheap. Session-layer authoring means the underlying USD file
    // on disk is never modified. Enables scene lights in the render
    // params automatically — explicit `add_distant_light` lights stay
    // active and stack with the dome.
    void set_dome_light(rust::Str hdri_path,
                        float intensity,
                        float exposure,
                        float rotation_y_degrees);
    // Tear down any dome authored by `set_dome_light` and turn off
    // scene-lights routing. Explicit GlfSimpleLights are unaffected.
    void clear_dome_light();

    // Author a `UsdPreviewSurface` material into the stage's session
    // layer and bind it to every `UsdGeomMesh` under the pseudo-root.
    // Each non-empty asset path becomes a `UsdUVTexture` shader
    // wired into the matching PBR input (`diffuseColor` /
    // `roughness` / `metallic` / `normal`). The four texture nodes
    // share one `UsdPrimvarReader_float2` reading the `st` primvar.
    //
    // Asset paths may contain the `<UDIM>` token; USD's resolver
    // substitutes the per-tile id (1001, 1002, …) at sample time, so
    // a single material binding covers multi-tile UDIM layouts. Pass
    // an empty string to skip a channel — the corresponding PBR
    // input keeps its UsdPreviewSurface default.
    //
    // Idempotent: replaces the previous painted material rather than
    // stacking, so the consumer can call it every time it re-exports
    // its paint targets without leaking prim trees.
    void set_painted_material(rust::Str base_color_asset_path,
                              rust::Str roughness_asset_path,
                              rust::Str metallic_asset_path,
                              rust::Str normal_asset_path);

    // Unbind the painted material from every mesh that carries it
    // and remove the material prim from the session layer. After
    // this the stage's originally-authored material bindings (if
    // any) drive shading again.
    void clear_painted_material();

    // Reference an external material defined in another USD layer
    // (the library file under `assets/materials/`) into the session
    // layer at `/_hydraExternalMaterial`, and bind it to every mesh
    // under the pseudo-root. `source_usd_path` is the on-disk USD
    // file (or any URI the asset resolver can chase). `prim_path` is
    // the SdfPath of the `UsdShade.Material` inside that file (e.g.
    // `/Materials/Brick`). Empty `source_usd_path` is treated as
    // `clear_external_material`.
    //
    // Idempotent. Overwrites the previous reference + re-binds to
    // every mesh on each call, so re-pointing at a different
    // material is one bridge call.
    void set_external_material(rust::Str source_usd_path,
                               rust::Str prim_path);
    // Drop the external material reference and unbind from every
    // mesh. The stage's original material bindings (if any) take
    // over again. Doesn't touch the painted-material binding —
    // those are different prim paths.
    void clear_external_material();

    // Author analytic user lights into the stage's session layer
    // from a packed flat-float payload. 16 floats per light — see
    // the Rust-side `set_user_lights` doc for the field layout. Each
    // light becomes either a `UsdLuxDistantLight` (type tag 0) or a
    // `UsdLuxSphereLight` with `shaping:cone:*` (type tag 1) at
    // `/_hydraLight<i>`. Throws on a payload whose length isn't a
    // multiple of 16.
    //
    // Idempotent: prim attributes overwrite in place when the slot
    // is already populated; previously-authored slots past the new
    // count get removed. Triggers `enableSceneLights` next render.
    void set_user_lights(rust::Slice<const float> data);
    // Remove every previously-authored user-light prim. Dome light
    // is untouched.
    void clear_user_lights();

    rust::String current_renderer() const;
    bool set_renderer_plugin(rust::Str plugin_id) const;

    // `UsdGeomImageable::purpose` filters: anything marked `default`
    // always draws; the three optional purposes opt-in via these
    // flags. Defaults match what consumers usually want for a
    // "show me the asset" preview (render + proxy on, guides off).
    // Pipeline assets that wrap their detail in a
    // `Scope { purpose = "render" }` only show up when render is on.
    void set_show_render(bool show);
    void set_show_proxy(bool show);
    void set_show_guides(bool show);

    std::unique_ptr<std::vector<uint8_t>> render_color() const;

    // True once the active render delegate considers the current
    // frame fully resolved. Rasterising delegates (Storm) return
    // true after a single `render_color` call; sampling delegates
    // (hdNSI, Embree, Arnold, …) return true after their sample
    // budget is hit. Headless one-shots that want a converged image
    // call `render_color` in a loop until this flips on; interactive
    // viewports ignore it and let the continuous-repaint loop drive
    // progressive convergence on its own.
    bool is_converged() const;

    // Tracks whether `set_dome_light` was called since the last
    // `clear_dome_light`. Drives `enableSceneLights` in render
    // params so the UsdLux prim we authored is consumed by Storm.
    bool dome_light_active = false;

    // `UsdImagingGLRenderParams::show*` mirrors. Toggled per-frame
    // by `set_show_render` / `_proxy` / `_guides`; consumed inside
    // `render_color`.
    bool show_render_purpose = true;
    bool show_proxy_purpose = true;
    bool show_guides_purpose = false;

    // Number of `/_hydraLight<i>` prims currently authored by
    // `set_user_lights`. Used to clean up slots past the new count
    // on each re-author, and to drive `enableSceneLights` (the dome
    // flag is OR'd with whether any user light is present).
    uint32_t user_light_count = 0;
};

std::unique_ptr<Renderer> create_renderer(rust::Str usd_path,
                                          rust::Str render_delegate_id);

// Convenience wrapper: spin up a renderer, render once, throw away.
std::unique_ptr<std::vector<uint8_t>> render_to_rgba(
    rust::Str usd_path,
    rust::Str render_delegate_id,
    uint32_t width,
    uint32_t height);

}  // namespace hydra_rs