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.
# hydra-rs

Rust bindings to OpenUSD's Hydra rendering layer. Sibling crate of [`rust-usd`](https://crates.io/crates/rust-usd); the two are designed to be used together but currently interop via path strings rather than shared cxx types.

What's here:

* `SceneIndex::from_path(path)` opens a stage and ingests it into a `UsdImagingStageSceneIndex`. `prim_paths()` and `prim_count()` walk the index so callers can verify Hydra sees the prims they expect, including filtering of shaders inside materials.
* `list_render_delegates()` enumerates the `HdRendererPlugin` plugins USD's plug system found at startup. On a typical macOS dev box that returns `["HdStormRendererPlugin"]`.
* `Renderer` is a stateful renderer that holds a `UsdStageRefPtr`, the `Hgi`, and a `UsdImagingGLEngine` across calls so a viewport can re-render at interactive rates without rebuilding any of the scaffolding. Camera matrices, output size, lights, time code, and clear color are all mutable between renders.
* `render_to_rgba(usd_path, delegate, w, h)` is a stateless convenience wrapper around `Renderer` for one shot renders.

The build environment matches `rust-usd`. Set `USD_INCLUDE_DIR`, `USD_LIB_DIR`, `USD_LIB_PREFIX`, and the Python framework variables documented in the `rust-usd` README before running cargo.

## Renderer

```rust
use hydra_rs::Renderer;

let mut renderer = Renderer::new("scene.usda")?;
renderer.set_size(1920, 1080);

// Switch out the default headlight for a key plus fill.
renderer.clear_lights();
renderer.add_distant_light([1.0, 1.0, 0.5],  [1.0, 1.0, 0.95], 1.5);
renderer.add_distant_light([-1.0, -0.3, 1.0], [0.4, 0.5, 0.8], 0.8);

// Camera matrices are row major 16-float arrays in GfMatrix4d's row vector
// convention (`vec_world * view = vec_camera`).
renderer.set_camera_matrices(&view, &projection);

// Render returns RGBA8 of size `width * height * 4` in top-down order
// (origin = top-left), the same convention `image`, egui, wgpu sampled
// textures, and the PNG file format use.
let pixels = renderer.render()?;
```

The `hydra_render` example renders a single frame and writes raw RGBA next to the input asset. The `hydra_orbit` example reuses one `Renderer` to render four frames around the test sphere with key plus fill lights, demonstrating the stateful viewport pattern.

```
cd hydra-rs
USD_INCLUDE_DIR=... USD_LIB_DIR=... USD_LIB_PREFIX=pxr_ \
USD_PYTHON_INCLUDE_DIR=... USD_LINK_PYTHON=framework USD_PYTHON_FRAMEWORK_DIR=... \
cargo run --example hydra_orbit
ffmpeg -y -f rawvideo -pixel_format rgba -video_size 256x256 \
  -i examples/hydra_orbit_00.rgba examples/hydra_orbit_00.png
open examples/hydra_orbit_00.png
```

## Implementation notes

Storm renders into a Metal texture on macOS. Without an active Metal layer, the engine's default `HdxPresentTask` calls `HgiInteropMetal` which calls `glGetString` and segfaults headlessly, so `Renderer` constructs the engine with `SetEnablePresentation(false)`. Since presentation also drives some of the Hydra 2 scene index lighting setup, the renderer hands the engine a `GlfSimpleLightVector` via the legacy `SetLightingState` API and sets `enableSceneLights = false`. Authored scene lights are not yet routed through to Storm in this configuration; explicit `add_distant_light` and `add_positional_light` calls are the supported path for now.

The color AOV comes back as `HdFormatFloat16Vec4` on Apple Silicon and `HdFormatFloat32Vec4` on most others; both paths convert to RGBA8 inside `render_color`. Storm writes the AOV in OpenGL convention (origin = bottom-left); `render_color` reverses rows before returning so every consumer sees the more common top-down layout.

## What's not here yet

Scene authored lights routed through to the render delegate, additional AOVs (depth, normals, primId for picking), camera matrices read from a `UsdGeomCamera` prim, and Embree as a CPU fallback are the obvious next pulls. Each lands when a consumer pushes on it.