From 7d2bd71c3f45c31e2eb7c65d119783dc78a2a2c8 Mon Sep 17 00:00:00 2001 From: hameron Date: Wed, 12 Aug 2026 21:28:10 +0200 Subject: [PATCH] Complete UsdShade NodeDef source queries Shader implementation data previously exposed only raw universal attributes, leaving callers to interpret implementationSource and construct source-type-specific property names themselves. The composed queries now select the active identifier, source-asset, or source-code family and fall back to identifier mode for unauthored or invalid selectors. Source assets, asset sub-identifiers, and inline source code support source-type-specific lookup with universal fallback. Source-type discovery follows the active implementation family, defined properties correctly block fallback, and asset results retain their authored, evaluated, and resolved paths. Shader, Input, and Output expose composed sdrMetadata maps, keyed lookups, and presence queries. Focused and integration tests cover selection, fallback, metadata composition, malformed data, and resolved source assets, with the UsdShade documentation and roadmap updated for the completed query surface. --- ROADMAP.md | 2 +- src/schemas/shade/mod.rs | 5 + src/schemas/shade/node_def.rs | 423 ++++++++++++++++++++++++++++++++++ src/schemas/shade/schema.rs | 8 +- src/schemas/shade/tokens.rs | 6 + tests/shade_reader.rs | 36 ++- 6 files changed, 475 insertions(+), 5 deletions(-) create mode 100644 src/schemas/shade/node_def.rs diff --git a/ROADMAP.md b/ROADMAP.md index 3fde9df3..718c4469 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -183,7 +183,7 @@ Features from the C++ reference implementation not covered by the core specifica | Parallelism (Rayon) | :construction: | | Composition graph is `&`-only, ready for parallel execution | | [Incremental invalidation](https://openusd.org/release/api/class_pcp_changes.html) | :white_check_mark: | `0.6.0` | `pcp::Changes` classifies each `sdf::ChangeList` into tiered invalidations scoped by the reverse dependency map (`pcp::Dependencies`), so an edit drops only the affected indices
Remaining — per-prim composition revision; `query_errors` compute-once; spec-stack-refresh splice | | [UsdGeom](https://openusd.org/release/api/usd_geom_page_front.html) (geometry, transforms, cameras) | :white_check_mark: | `0.4.0` | Trait-views behind `geom` (the `SchemaBase → … → Curves` chain, read + author): all intrinsic shapes, Camera, Xform/Scope, Mesh + GeomSubset, curve/point/patch types, PointInstancer; full `xformOpOrder` evaluator
Remaining — PrimvarsAPI / ModelAPI / MotionAPI / VisibilityAPI / BBoxCache / XformCache | -| [UsdShade](https://openusd.org/release/api/usd_shade_page_front.html) (materials, shaders) | :white_check_mark: | `0.5.0` | Trait-views behind `shade` (read + author): `Connectable`, `Input`, `Output`, `ConnectionTarget`, `ConnectedSources`, `ShadingAttribute`, `ProducerFilter`, `ResolvedTerminal`, `InterfaceInputConsumersMap`, Shader / NodeGraph / Material, MaterialBindingAPI, and the UsdPreviewSurface reader
Remaining — Material base-material; `CoordSysAPI`; renderer shader dialects (MDL / MaterialX) | +| [UsdShade](https://openusd.org/release/api/usd_shade_page_front.html) (materials, shaders) | :white_check_mark: | `0.5.0` | Trait-views behind `shade` (read + author): `Connectable`, `Input`, `Output`, `ConnectionTarget`, `ConnectedSources`, `ShadingAttribute`, `ProducerFilter`, `ResolvedTerminal`, `InterfaceInputConsumersMap`, `ImplementationSource`, `SdrMetadata`, Shader / NodeGraph / Material, MaterialBindingAPI, and the UsdPreviewSurface reader
Remaining — Material base-material; `CoordSysAPI`; renderer shader dialects (MDL / MaterialX) | | [UsdLux](https://openusd.org/release/api/usd_lux_page_front.html) (lighting) | :white_check_mark: | `0.4.0` | Trait-views behind `lux` feature (built on the `geom` chain — lights are `Xformable` / `Boundable` prims plus the `Light` interface, with read + author on the same handle): all 8 concrete light prims + LightFilter, and the applied LightAPI / ShapingAPI / ShadowAPI / LightListAPI | | [UsdSkel](https://openusd.org/release/api/usd_skel_page_front.html) (skeletons, skinning) | :white_check_mark: | `0.5.0` | Trait-views behind `skel` (read + author; `skel = ["geom"]`): SkelRoot / Skeleton as `Boundable`, SkelAnimation / BlendShape (incl. inbetween shapes), namespace-inherited SkelBindingAPI — plus the object model (Topology, AnimMapper, SkeletonResolver, SkinningResolver, SkelAnimQuery, `discover_bindings`, pure-math LBS / blend shapes)
Remaining — stage-level interpolation of SkelAnimation time samples | | [UsdVol](https://openusd.org/release/api/usd_vol_page_front.html) (volumes) | :white_check_mark: | `0.5.0` | Trait-views behind `vol` feature (built on the `geom` chain): `Volume` (a `Gprim` with `field:` relationships) and the file-backed `OpenVDBAsset` / `Field3DAsset` (the shared `FieldAsset` attrs)
Remaining — the `ParticleField*` Gaussian-splat schemas | diff --git a/src/schemas/shade/mod.rs b/src/schemas/shade/mod.rs index 393fc71a..b6284578 100644 --- a/src/schemas/shade/mod.rs +++ b/src/schemas/shade/mod.rs @@ -29,6 +29,9 @@ //! as far as [`ProducerFilter`] admits. [`NodeGraphInterface`] maps interface //! inputs in the reverse direction to their consumers. Specialized consumers //! include [`Material::compute_surface_source`] and [`read_preview_surface`]. +//! [`Shader::implementation_source`] and the source queries interpret the +//! active `NodeDef` implementation family, while [`SdrMetadata`] exposes the +//! composed shader-registry metadata on shaders, inputs, and outputs. //! To find every shading prim on a stage, traverse it and gate each prim //! through the typed `get` (e.g. [`Material::get`]), mirroring C++ //! `prim.IsA()`. @@ -63,6 +66,7 @@ mod binding; mod connectable; mod input; mod interface; +mod node_def; mod output; mod preview; mod schema; @@ -76,6 +80,7 @@ pub use connectable::{ }; pub use input::Input; pub use interface::{InterfaceInputConsumersMap, NodeGraphInterface}; +pub use node_def::SdrMetadata; pub use output::Output; pub use preview::{Channel, ReadPreviewSurface, read_preview_surface}; pub use schema::{Material, NodeGraph, ResolvedTerminal, Shader, TerminalKind, TerminalSource}; diff --git a/src/schemas/shade/node_def.rs b/src/schemas/shade/node_def.rs new file mode 100644 index 00000000..e0e47063 --- /dev/null +++ b/src/schemas/shade/node_def.rs @@ -0,0 +1,423 @@ +//! Composed UsdShade `NodeDef` source and shader-registry metadata queries. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::error::Error; + +use anyhow::Result; + +use crate::{sdf, tf, usd}; + +use super::tokens as tok; +use super::{ImplementationSource, Input, Output, Shader}; + +/// String-valued metadata passed to an Sdr shader or property definition +/// (C++ `SdrTokenMap`). +pub type SdrMetadata = HashMap; + +impl Shader { + /// The active shader implementation family. + /// + /// An unauthored, mistyped, or unrecognized + /// `info:implementationSource` resolves to [`ImplementationSource::Id`], + /// matching C++ `UsdShadeNodeDefAPI::GetImplementationSource`. + pub fn implementation_source(&self) -> Result { + implementation_source(self) + } + + /// The composed source asset for `source_type` when source-asset mode is + /// active. + /// + /// An empty source type selects universal `info:sourceAsset`. A requested + /// source type selects `info::sourceAsset` and falls back to + /// the universal attribute only when that specific attribute is not + /// defined. The returned [`sdf::AssetPath`] retains its authored, + /// evaluated, and resolved paths. + pub fn source_asset(&self, source_type: impl AsRef) -> Result> { + source_value( + self, + ImplementationSource::SourceAsset, + source_type.as_ref(), + tok::A_INFO_SOURCE_ASSET, + tok::IMPL_SOURCE_SOURCE_ASSET, + ) + } + + /// The composed source-asset sub-identifier for `source_type` when + /// source-asset mode is active. + /// + /// An empty source type selects the universal sub-identifier. A missing + /// source-type-specific attribute falls back to it. + pub fn source_asset_subidentifier(&self, source_type: impl AsRef) -> Result> { + source_value( + self, + ImplementationSource::SourceAsset, + source_type.as_ref(), + tok::A_INFO_SOURCE_ASSET_SUBIDENTIFIER, + tok::SOURCE_ASSET_SUBIDENTIFIER, + ) + } + + /// The composed inline source for `source_type` when source-code mode is + /// active. + /// + /// An empty source type selects universal `info:sourceCode`. A missing + /// source-type-specific attribute falls back to it. + pub fn source_code(&self, source_type: impl AsRef) -> Result> { + source_value( + self, + ImplementationSource::SourceCode, + source_type.as_ref(), + tok::A_INFO_SOURCE_CODE, + tok::IMPL_SOURCE_SOURCE_CODE, + ) + } + + /// The source types authored for the active source-asset or source-code + /// family, in composed property order. + /// + /// Universal properties have no source type and are not included. + /// Identifier mode returns an empty list. + pub fn source_types(&self) -> Result> { + let implementation = implementation_source(self)?; + let suffix = match implementation { + ImplementationSource::Id => return Ok(Vec::new()), + ImplementationSource::SourceAsset => tok::IMPL_SOURCE_SOURCE_ASSET, + ImplementationSource::SourceCode => tok::IMPL_SOURCE_SOURCE_CODE, + }; + + Ok(self + .authored_property_names()? + .into_iter() + .filter_map(|name| source_type(name.as_str(), suffix).map(tf::Token::from)) + .collect()) + } + + /// The composed shader-level `sdrMetadata` dictionary. + /// + /// UsdShade permits string values in this dictionary. Entries of another + /// value type are malformed and are omitted from the returned map. + pub fn sdr_metadata(&self) -> Result { + Ok(metadata_map(prim_metadata_value(self)?)) + } + + /// The composed shader-level `sdrMetadata` value for `key`. + pub fn sdr_metadata_by_key(&self, key: impl AsRef) -> Result> { + Ok(metadata_value(prim_metadata_value(self)?, key.as_ref())) + } + + /// Whether a composed shader-level `sdrMetadata` field exists. + pub fn has_sdr_metadata(&self) -> Result { + Ok(prim_metadata_value(self)?.is_some()) + } + + /// Whether the composed shader-level `sdrMetadata` dictionary contains + /// `key`, regardless of the entry's value type. + pub fn has_sdr_metadata_by_key(&self, key: impl AsRef) -> Result { + Ok(metadata_has_key(prim_metadata_value(self)?, key.as_ref())) + } +} + +macro_rules! impl_attribute_sdr_metadata { + ($ty:ty) => { + impl $ty { + /// The composed `sdrMetadata` dictionary on this shading + /// attribute. + /// + /// UsdShade permits string values in this dictionary. Entries of + /// another value type are malformed and are omitted from the + /// returned map. + pub fn sdr_metadata(&self) -> Result { + Ok(metadata_map( + self.attribute() + .get_metadata::(tok::META_SDR_METADATA)?, + )) + } + + /// The composed `sdrMetadata` value for `key` on this shading + /// attribute. + pub fn sdr_metadata_by_key(&self, key: impl AsRef) -> Result> { + Ok(metadata_value( + self.attribute() + .get_metadata::(tok::META_SDR_METADATA)?, + key.as_ref(), + )) + } + + /// Whether a composed `sdrMetadata` field exists on this shading + /// attribute. + pub fn has_sdr_metadata(&self) -> Result { + Ok(self + .attribute() + .get_metadata::(tok::META_SDR_METADATA)? + .is_some()) + } + + /// Whether this shading attribute's composed `sdrMetadata` + /// dictionary contains `key`, regardless of its value type. + pub fn has_sdr_metadata_by_key(&self, key: impl AsRef) -> Result { + Ok(metadata_has_key( + self.attribute() + .get_metadata::(tok::META_SDR_METADATA)?, + key.as_ref(), + )) + } + } + }; +} + +impl_attribute_sdr_metadata!(Input); +impl_attribute_sdr_metadata!(Output); + +/// Reads an implementation-specific value, with universal fallback when the +/// requested attribute is not defined. +fn source_value( + prim: &usd::Prim, + implementation: ImplementationSource, + source_type: &str, + universal_name: &'static str, + suffix: &str, +) -> Result> +where + T: TryFrom, + T::Error: Error + Send + Sync + 'static, +{ + if implementation_source(prim)? != implementation { + return Ok(None); + } + + let name = source_property_name(source_type, universal_name, suffix); + let attribute = prim.attribute(name.as_ref()); + if source_type.is_empty() || attribute.is_defined()? { + return attribute.get(); + } + prim.attribute(universal_name).get() +} + +/// Resolves the active implementation family on a `NodeDef` prim. +fn implementation_source(prim: &usd::Prim) -> Result { + let value = prim.attribute(tok::A_INFO_IMPLEMENTATION_SOURCE).get::()?; + Ok(value + .and_then(sdf::Value::try_as_token) + .and_then(ImplementationSource::from_token) + .unwrap_or_default()) +} + +/// Builds a universal or source-type-specific `NodeDef` property name. +fn source_property_name(source_type: &str, universal_name: &'static str, suffix: &str) -> Cow<'static, str> { + if source_type.is_empty() { + Cow::Borrowed(universal_name) + } else { + Cow::Owned(format!("{}{source_type}:{suffix}", tok::NS_INFO)) + } +} + +/// Extracts the source type from an exact active-family property name. +fn source_type<'a>(name: &'a str, suffix: &str) -> Option<&'a str> { + let mut parts = name.split(':'); + match (parts.next(), parts.next(), parts.next(), parts.next()) { + (Some("info"), Some(source_type), Some(found), None) if !source_type.is_empty() && found == suffix => { + Some(source_type) + } + _ => None, + } +} + +/// The raw composed prim-level shader-registry metadata value. +fn prim_metadata_value(prim: &usd::Prim) -> Result> { + prim.stage().field(prim.path(), tok::META_SDR_METADATA) +} + +/// Converts a valid string-valued metadata dictionary to its public map. +fn metadata_map(value: Option) -> SdrMetadata { + let Some(sdf::Value::Dictionary(dictionary)) = value else { + return SdrMetadata::new(); + }; + dictionary + .into_iter() + .filter_map(|(key, value)| value.try_as_string().map(|value| (tf::Token::from(key), value))) + .collect() +} + +/// Extracts one valid string-valued metadata entry. +fn metadata_value(value: Option, key: &str) -> Option { + let sdf::Value::Dictionary(mut dictionary) = value? else { + return None; + }; + dictionary.remove(key)?.try_as_string() +} + +/// Tests one composed dictionary key without interpreting its value. +fn metadata_has_key(value: Option, key: &str) -> bool { + matches!(value, Some(sdf::Value::Dictionary(dictionary)) if dictionary.contains_key(key)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schemas::shade::Connectable; + use crate::usd::SchemaBase; + + fn dictionary(entries: &[(&str, &str)]) -> sdf::Value { + sdf::Value::Dictionary( + entries + .iter() + .map(|&(key, value)| (key.to_string(), sdf::Value::String(value.to_string()))) + .collect(), + ) + } + + #[test] + fn implementation_selects_id() -> Result<()> { + let stage = usd::Stage::builder().in_memory("anon.usda")?; + let shader = Shader::define(&stage, "/Shader")?; + shader.create_id_attr()?.set(sdf::Value::token("Example"))?; + + assert_eq!(shader.implementation_source()?, ImplementationSource::Id); + assert_eq!(shader.id()?.as_deref(), Some("Example")); + + shader + .create_implementation_source_attr()? + .set(sdf::Value::token("invalid"))?; + assert_eq!(shader.implementation_source()?, ImplementationSource::Id); + assert_eq!(shader.id()?.as_deref(), Some("Example")); + + shader + .implementation_source_attr() + .set(ImplementationSource::SourceAsset)?; + assert_eq!(shader.id()?, None); + Ok(()) + } + + #[test] + fn source_asset_fallback() -> Result<()> { + let stage = usd::Stage::builder().in_memory("anon.usda")?; + let shader = Shader::define(&stage, "/Shader")?; + shader + .create_implementation_source_attr()? + .set(ImplementationSource::SourceAsset)?; + shader + .create_source_asset_attr()? + .set(sdf::Value::AssetPath("./universal.osl".into()))?; + shader + .create_attribute("info:osl:sourceAsset", "asset")? + .set(sdf::Value::AssetPath("./specific.osl".into()))?; + shader.create_attribute("info:mdl:sourceAsset", "asset")?; + shader + .create_attribute("info:osl:sourceAsset:subIdentifier", "token")? + .set(sdf::Value::token("Specific"))?; + shader + .create_source_asset_subidentifier_attr()? + .set(sdf::Value::token("Universal"))?; + shader + .create_attribute("info:ri:sourceCode", "string")? + .set("inactive")?; + + assert_eq!( + shader.source_asset("osl")?.expect("OSL asset").authored_path, + "./specific.osl" + ); + assert_eq!( + shader.source_asset("ri")?.expect("fallback asset").authored_path, + "./universal.osl" + ); + assert_eq!( + shader + .source_asset("bad type")? + .expect("malformed source type falls back") + .authored_path, + "./universal.osl" + ); + assert_eq!(shader.source_asset("mdl")?, None); + assert_eq!(shader.source_asset_subidentifier("osl")?.as_deref(), Some("Specific")); + assert_eq!(shader.source_asset_subidentifier("ri")?.as_deref(), Some("Universal")); + assert_eq!( + shader.source_types()?, + vec![tf::Token::from("osl"), tf::Token::from("mdl")] + ); + Ok(()) + } + + #[test] + fn source_code_selection() -> Result<()> { + let stage = usd::Stage::builder().in_memory("anon.usda")?; + let shader = Shader::define(&stage, "/Shader")?; + shader + .create_implementation_source_attr()? + .set(ImplementationSource::SourceCode)?; + shader.create_source_code_attr()?.set("universal")?; + shader + .create_attribute("info:osl:sourceCode", "string")? + .set("specific")?; + shader + .create_attribute("info:mdl:sourceAsset", "asset")? + .set(sdf::Value::AssetPath("./inactive.mdl".into()))?; + + assert_eq!(shader.source_code("osl")?.as_deref(), Some("specific")); + assert_eq!(shader.source_code("ri")?.as_deref(), Some("universal")); + assert_eq!(shader.source_asset("mdl")?, None); + assert_eq!(shader.source_types()?, vec![tf::Token::from("osl")]); + Ok(()) + } + + #[test] + fn metadata_composes() -> Result<()> { + let stage = usd::Stage::builder().in_memory("root.usda")?; + let root = stage.root_layer().identifier().to_string(); + let mut weak = sdf::Layer::new_in_memory("weak.usda"); + weak.edit(|edit| { + let mut shader = sdf::PrimSpec::new(edit.data_mut(), "/Shader", sdf::Specifier::Def, tok::T_SHADER)?; + shader.set( + tok::META_SDR_METADATA, + dictionary(&[("label", "weak"), ("page", "weak")]), + ); + let mut input = sdf::AttributeSpec::new( + edit.data_mut(), + "/Shader.inputs:value", + "float", + sdf::Variability::Varying, + false, + )?; + input.set(tok::META_SDR_METADATA, dictionary(&[("widget", "slider")])); + Ok(()) + })?; + stage.insert_layer(&root, 0, weak, sdf::LayerOffset::IDENTITY)?; + + let shader = Shader::define(&stage, "/Shader")?; + let mut strong_metadata = dictionary(&[("page", "strong")]) + .try_as_dictionary() + .expect("dictionary helper result"); + strong_metadata.insert("malformed".to_string(), sdf::Value::Int(7)); + shader + .prim() + .clone() + .set_metadata(tok::META_SDR_METADATA, sdf::Value::Dictionary(strong_metadata))?; + let input = shader.create_input("value", "float")?; + let output = shader.create_output("result", "float")?; + output + .clone() + .into_attribute() + .set_metadata(tok::META_SDR_METADATA, dictionary(&[("role", "result")]))?; + + let metadata = shader.sdr_metadata()?; + assert_eq!( + metadata.get(&tf::Token::from("label")).map(String::as_str), + Some("weak") + ); + assert_eq!( + metadata.get(&tf::Token::from("page")).map(String::as_str), + Some("strong") + ); + assert_eq!(shader.sdr_metadata_by_key("page")?.as_deref(), Some("strong")); + assert_eq!(shader.sdr_metadata_by_key("malformed")?, None); + assert!(shader.has_sdr_metadata()?); + assert!(shader.has_sdr_metadata_by_key("label")?); + assert!(shader.has_sdr_metadata_by_key("malformed")?); + + assert_eq!(input.sdr_metadata_by_key("widget")?.as_deref(), Some("slider")); + assert!(input.has_sdr_metadata()?); + assert_eq!(output.sdr_metadata_by_key("role")?.as_deref(), Some("result")); + assert!(output.has_sdr_metadata_by_key("role")?); + Ok(()) + } +} diff --git a/src/schemas/shade/schema.rs b/src/schemas/shade/schema.rs index f1c02266..050568f3 100644 --- a/src/schemas/shade/schema.rs +++ b/src/schemas/shade/schema.rs @@ -49,9 +49,13 @@ impl Shader { .set_variability(sdf::Variability::Uniform)?) } - /// The composed `info:id` as a string, if authored — the convenience - /// behind dispatching on shader type (C++ `UsdShadeShader::GetShaderId`). + /// The composed `info:id` as a string when identifier mode is active — the + /// convenience behind dispatching on shader type (C++ + /// `UsdShadeShader::GetShaderId`). pub fn id(&self) -> Result> { + if self.implementation_source()? != super::ImplementationSource::Id { + return Ok(None); + } Ok(self.id_attr().get::()?.map(Into::into)) } diff --git a/src/schemas/shade/tokens.rs b/src/schemas/shade/tokens.rs index 43180970..e66854f8 100644 --- a/src/schemas/shade/tokens.rs +++ b/src/schemas/shade/tokens.rs @@ -14,6 +14,8 @@ pub const API_NODE_DEF: &str = "NodeDefAPI"; pub const API_MATERIAL_BINDING: &str = "MaterialBindingAPI"; // Property namespace prefixes +/// Node-definition properties are authored under `info:`. +pub const NS_INFO: &str = "info:"; /// Input attributes are authored as `inputs:`. pub const NS_INPUTS: &str = "inputs:"; /// Output attributes are authored as `outputs:`. @@ -30,6 +32,8 @@ pub const A_INFO_SOURCE_ASSET_SUBIDENTIFIER: &str = "info:sourceAsset:subIdentif pub const IMPL_SOURCE_ID: &str = "id"; pub const IMPL_SOURCE_SOURCE_ASSET: &str = "sourceAsset"; pub const IMPL_SOURCE_SOURCE_CODE: &str = "sourceCode"; +/// Source-asset sub-identifier suffix used after an optional source type. +pub const SOURCE_ASSET_SUBIDENTIFIER: &str = "sourceAsset:subIdentifier"; // Material / NodeGraph terminal output base names pub const TERMINAL_SURFACE: &str = "surface"; @@ -53,6 +57,8 @@ pub const CONNECTABILITY_INTERFACE_ONLY: &str = "interfaceOnly"; // `renderType` metadata on a connectable input / output — a renderer-specific // type hint (C++ `UsdShadeInput`/`UsdShadeOutput::SetRenderType`). pub const META_RENDER_TYPE: &str = "renderType"; +/// Shader-registry metadata on a shader, input, or output. +pub const META_SDR_METADATA: &str = "sdrMetadata"; // MaterialBindingAPI relationship names + binding metadata pub const REL_MATERIAL_BINDING: &str = "material:binding"; diff --git a/tests/shade_reader.rs b/tests/shade_reader.rs index 80b4d1da..1dac03d8 100644 --- a/tests/shade_reader.rs +++ b/tests/shade_reader.rs @@ -2,9 +2,11 @@ //! hand-authored UsdShade fixture, and a full author → read-back roundtrip on //! an in-memory stage. +use std::fs; + use anyhow::Result; -use openusd::schemas::shade::{self, Channel, Connectable, Material, MaterialBindingAPI, Shader}; -use openusd::{sdf, usd}; +use openusd::schemas::shade::{self, Channel, Connectable, ImplementationSource, Material, MaterialBindingAPI, Shader}; +use openusd::{sdf, tf, usd}; const FIXTURE: &str = "fixtures/usdShade_scene.usda"; @@ -134,3 +136,33 @@ fn author_then_read_back_roundtrip() -> Result<()> { assert_eq!(shaders(&stage)?.len(), 2); Ok(()) } + +#[test] +fn reads_node_def_source() -> Result<()> { + let directory = tempfile::tempdir()?; + let source_path = directory.path().join("shader.osl"); + fs::write(&source_path, "shader Example() {}")?; + let scene_path = directory.path().join("scene.usda"); + fs::write( + &scene_path, + r#"#usda 1.0 +def Shader "Source" +{ + uniform token info:implementationSource = "sourceAsset" + uniform asset info:osl:sourceAsset = @./shader.osl@ +} +"#, + )?; + + let scene = scene_path.to_string_lossy(); + let stage = usd::Stage::open(scene.as_ref())?; + let shader = Shader::get(&stage, "/Source")?.expect("Shader"); + assert_eq!(shader.implementation_source()?, ImplementationSource::SourceAsset); + assert_eq!(shader.source_types()?, vec![tf::Token::from("osl")]); + + let asset = shader.source_asset("osl")?.expect("OSL source asset"); + assert_eq!(asset.authored_path, "./shader.osl"); + let resolved = source_path.canonicalize()?; + assert_eq!(asset.resolved_path(), Some(resolved.to_string_lossy().as_ref())); + Ok(()) +}