use openusd::Result;
use crate::SchemaError;
use openusd::{gf, sdf, usd};
use super::tokens::*;
use super::{Connectable, Input, Material, ProducerFilter, Shader};
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Channel<T> {
#[default]
Unset,
Value(T),
Texture(String),
}
impl<T> Channel<T> {
pub fn value(&self) -> Option<&T> {
match self {
Channel::Value(v) => Some(v),
_ => None,
}
}
pub fn texture(&self) -> Option<&str> {
match self {
Channel::Texture(p) => Some(p.as_str()),
_ => None,
}
}
pub fn is_set(&self) -> bool {
!matches!(self, Channel::Unset)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ReadPreviewSurface {
pub shader: String,
pub diffuse_color: Channel<gf::Vec3f>,
pub emissive_color: Channel<gf::Vec3f>,
pub specular_color: Channel<gf::Vec3f>,
pub metallic: Channel<f32>,
pub roughness: Channel<f32>,
pub clearcoat: Channel<f32>,
pub clearcoat_roughness: Channel<f32>,
pub opacity: Channel<f32>,
pub opacity_threshold: Channel<f32>,
pub ior: Channel<f32>,
pub normal: Channel<gf::Vec3f>,
pub occlusion: Channel<f32>,
}
pub fn read_preview_surface(
stage: &usd::Stage,
material: &sdf::Path,
) -> Result<Option<ReadPreviewSurface>, SchemaError> {
let Some(material) = Material::get(stage, material.clone())? else {
return Ok(None);
};
let Some(terminal) = resolve_surface_terminal(&material)? else {
return Ok(None);
};
let Some(source) = terminal.sources().first() else {
return Ok(None);
};
let Some(shader) = source.shader() else {
return Ok(None);
};
if shader.id()?.as_deref() != Some(SHADER_ID_PREVIEW_SURFACE) {
return Ok(None);
}
Ok(Some(ReadPreviewSurface {
shader: shader.path().as_str().to_string(),
diffuse_color: read_color_channel(shader, PS_DIFFUSE_COLOR)?,
emissive_color: read_color_channel(shader, PS_EMISSIVE_COLOR)?,
specular_color: read_color_channel(shader, PS_SPECULAR_COLOR)?,
metallic: read_scalar_channel(shader, PS_METALLIC)?,
roughness: read_scalar_channel(shader, PS_ROUGHNESS)?,
clearcoat: read_scalar_channel(shader, PS_CLEARCOAT)?,
clearcoat_roughness: read_scalar_channel(shader, PS_CLEARCOAT_ROUGHNESS)?,
opacity: read_scalar_channel(shader, PS_OPACITY)?,
opacity_threshold: read_scalar_channel(shader, PS_OPACITY_THRESHOLD)?,
ior: read_scalar_channel(shader, PS_IOR)?,
normal: read_color_channel(shader, PS_NORMAL)?,
occlusion: read_scalar_channel(shader, PS_OCCLUSION)?,
}))
}
fn resolve_surface_terminal(material: &Material) -> Result<Option<super::ResolvedTerminal>, SchemaError> {
if let Some(terminal) = material.compute_surface_source(&[])? {
return Ok(Some(terminal));
}
let suffix = format!(":{}", TERMINAL_SURFACE);
let mut contexts: Vec<String> = material
.surface_outputs()?
.iter()
.filter_map(|output| output.base_name().strip_suffix(&suffix))
.map(String::from)
.collect();
contexts.sort();
if contexts.is_empty() {
return Ok(None);
}
let contexts: Vec<&str> = contexts.iter().map(String::as_str).collect();
material.compute_surface_source(&contexts)
}
fn connected_texture_file(shader: &Shader, base: &str) -> Result<Option<String>, SchemaError> {
let produced = shader
.input(base)
.value_producing_attributes(ProducerFilter::ShaderOutputsOnly)?;
let Some(source) = produced.first() else {
return Ok(None);
};
let stage = shader.stage();
let Some(tex) = Shader::get(stage, source.path().prim_path())? else {
return Ok(None);
};
if tex.id()?.as_deref() != Some(SHADER_ID_UV_TEXTURE) {
return Ok(None);
}
resolve_asset_value(&tex.input(TEX_FILE))
}
fn resolve_asset_value(input: &Input) -> Result<Option<String>, SchemaError> {
let produced = input.value_producing_attributes(ProducerFilter::Any)?;
let Some(source) = produced.first() else {
return Ok(None);
};
Ok(source
.attribute()
.get::<sdf::Value>()?
.as_ref()
.and_then(sdf::Value::as_str)
.map(str::to_owned))
}
fn read_color_channel(shader: &Shader, base: &str) -> Result<Channel<gf::Vec3f>, SchemaError> {
if let Some(file) = connected_texture_file(shader, base)? {
return Ok(Channel::Texture(file));
}
Ok(match shader.input(base).get::<sdf::Value>()? {
Some(sdf::Value::Vec3f(v)) => Channel::Value(v),
Some(sdf::Value::Vec3d(v)) => Channel::Value(gf::vec3f(v.x as f32, v.y as f32, v.z as f32)),
Some(sdf::Value::Vec3h(v)) => Channel::Value(gf::vec3f(v.x.to_f32(), v.y.to_f32(), v.z.to_f32())),
_ => Channel::Unset,
})
}
fn read_scalar_channel(shader: &Shader, base: &str) -> Result<Channel<f32>, SchemaError> {
if let Some(file) = connected_texture_file(shader, base)? {
return Ok(Channel::Texture(file));
}
Ok(match shader.input(base).get::<sdf::Value>()? {
Some(sdf::Value::Float(f)) => Channel::Value(f),
Some(sdf::Value::Double(d)) => Channel::Value(d as f32),
Some(sdf::Value::Half(h)) => Channel::Value(h.to_f32()),
Some(sdf::Value::Int(i)) => Channel::Value(i as f32),
_ => Channel::Unset,
})
}
#[cfg(test)]
mod tests {
use super::*;
use openusd::Result;
#[test]
fn context_only_surface() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let surf = Shader::define(&stage, "/Mat/Surface")?;
surf.create_id_attr()?.set(sdf::Value::token("UsdPreviewSurface"))?;
surf.create_input("roughness", "float")?.set(sdf::Value::Float(0.4))?;
surf.create_output("surface", "token")?;
Material::define(&stage, "/Mat")?
.create_surface_output_for("ri")?
.set_connections(["/Mat/Surface.outputs:surface"])?;
let ps = read_preview_surface(&stage, &sdf::path("/Mat")?)?.expect("context-only decodes");
assert_eq!(ps.shader, "/Mat/Surface");
assert_eq!(ps.roughness.value(), Some(&0.4));
Ok(())
}
#[test]
fn scalar_and_textured_channels() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let tex = Shader::define(&stage, "/Mat/DiffuseTex")?;
tex.create_id_attr()?.set(sdf::Value::token("UsdUVTexture"))?;
tex.create_input("file", "asset")?
.set(sdf::Value::AssetPath("./albedo.png".into()))?;
tex.create_output("rgb", "float3")?;
let surf = Shader::define(&stage, "/Mat/Surface")?;
surf.create_id_attr()?.set(sdf::Value::token("UsdPreviewSurface"))?;
surf.create_input("diffuseColor", "color3f")?
.set_connections(["/Mat/DiffuseTex.outputs:rgb"])?;
surf.create_input("roughness", "float")?.set(sdf::Value::Float(0.4))?;
surf.create_input("metallic", "float")?.set(sdf::Value::Float(1.0))?;
surf.create_output("surface", "token")?;
Material::define(&stage, "/Mat")?
.create_surface_output()?
.set_connections(["/Mat/Surface.outputs:surface"])?;
let ps = read_preview_surface(&stage, &sdf::path("/Mat")?)?.expect("UsdPreviewSurface");
assert_eq!(ps.shader, "/Mat/Surface");
assert_eq!(ps.diffuse_color.texture(), Some("./albedo.png"));
assert_eq!(ps.roughness.value(), Some(&0.4));
assert_eq!(ps.metallic.value(), Some(&1.0));
assert!(!ps.opacity.is_set());
assert!(!ps.ior.is_set());
Ok(())
}
#[test]
fn interface_driven_texture() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let mat = Material::define(&stage, "/Mat")?;
mat.create_input("diffuseTexFile", "asset")?
.set(sdf::Value::AssetPath("./albedo.png".into()))?;
let tex = Shader::define(&stage, "/Mat/DiffuseTex")?;
tex.create_id_attr()?.set(sdf::Value::token("UsdUVTexture"))?;
tex.create_input("file", "asset")?
.set_connections(["/Mat.inputs:diffuseTexFile"])?;
tex.create_output("rgb", "float3")?;
let surf = Shader::define(&stage, "/Mat/Surface")?;
surf.create_id_attr()?.set(sdf::Value::token("UsdPreviewSurface"))?;
surf.create_input("diffuseColor", "color3f")?
.set_connections(["/Mat/DiffuseTex.outputs:rgb"])?;
surf.create_output("surface", "token")?;
mat.create_surface_output()?
.set_connections(["/Mat/Surface.outputs:surface"])?;
let ps = read_preview_surface(&stage, &sdf::path("/Mat")?)?.expect("UsdPreviewSurface");
assert_eq!(ps.diffuse_color.texture(), Some("./albedo.png"));
Ok(())
}
#[test]
fn non_preview_surface_none() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let surf = Shader::define(&stage, "/Mat/Surface")?;
surf.create_id_attr()?
.set(sdf::Value::token("ND_standard_surface_surfaceshader"))?;
surf.create_output("surface", "token")?;
Material::define(&stage, "/Mat")?
.create_surface_output()?
.set_connections(["/Mat/Surface.outputs:surface"])?;
assert!(read_preview_surface(&stage, &sdf::path("/Mat")?)?.is_none());
Ok(())
}
}