use anyhow::Result;
use openusd::schemas::lux::{
CylinderLight, DiskLight, DistantLight, DomeLight, GeometryLight, Light, LightAPI, LightListAPI,
LightListCacheBehavior, PortalLight, RectLight, ShadowAPI, ShapingAPI, SphereLight, TextureFormat,
};
use openusd::sdf;
use openusd::usd::Stage;
const FIXTURE: &str = "fixtures/usdLux_scene.usda";
fn open() -> Result<Stage> {
Stage::open(FIXTURE)
}
fn from_usda(usda: &str) -> Result<Stage> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("scene.usda");
std::fs::write(&path, usda)?;
std::mem::forget(dir);
Stage::open(path.to_str().unwrap())
}
#[test]
fn distant_light_inputs_and_angle() -> Result<()> {
let stage = open()?;
let sun = DistantLight::get(&stage, sdf::path("/World/Sun")?)?.expect("DistantLight");
assert_eq!(sun.intensity_attr().get::<f32>()?, Some(12000.0));
assert_eq!(sun.exposure_attr().get::<f32>()?, Some(1.5));
assert_eq!(sun.color_attr().get::<[f32; 3]>()?, Some([1.0, 0.95, 0.85]));
assert_eq!(sun.enable_color_temperature_attr().get::<bool>()?, Some(true));
assert_eq!(sun.color_temperature_attr().get::<f32>()?, Some(5500.0));
assert_eq!(sun.angle_attr().get::<sdf::Value>()?, Some(sdf::Value::Float(0.53)));
Ok(())
}
#[test]
fn distant_light_unauthored_intensity_is_none() -> Result<()> {
let stage = from_usda("#usda 1.0\ndef DistantLight \"Bare\" {}\n")?;
let bare = DistantLight::get(&stage, sdf::path("/Bare")?)?.expect("DistantLight");
assert_eq!(bare.intensity_attr().get::<sdf::Value>()?, None);
Ok(())
}
#[test]
fn sphere_light_radius_and_treat_as_point() -> Result<()> {
let stage = open()?;
let s = SphereLight::get(&stage, sdf::path("/World/Sphere")?)?.expect("SphereLight");
assert_eq!(s.radius_attr().get()?, Some(sdf::Value::Float(0.25)));
assert_eq!(s.treat_as_point_attr().get()?, Some(sdf::Value::Bool(true)));
assert_eq!(s.intensity_attr().get()?, Some(sdf::Value::Float(800.0)));
Ok(())
}
#[test]
fn rect_light_with_shaping_and_shadow() -> Result<()> {
let stage = open()?;
let prim = sdf::path("/World/Rect")?;
let rect = RectLight::get(&stage, &prim)?.expect("RectLight");
assert_eq!(rect.width_attr().get()?, Some(sdf::Value::Float(2.0)));
assert_eq!(rect.height_attr().get()?, Some(sdf::Value::Float(1.0)));
assert_eq!(
rect.texture_file_attr().get()?,
Some(sdf::Value::AssetPath("./textures/softbox.exr".into()))
);
let shaping = ShapingAPI::get(&stage, &prim)?.expect("ShapingAPI");
assert_eq!(shaping.cone_angle_attr().get()?, Some(sdf::Value::Float(45.0)));
assert_eq!(shaping.cone_softness_attr().get()?, Some(sdf::Value::Float(0.2)));
assert_eq!(
shaping.ies_file_attr().get()?,
Some(sdf::Value::AssetPath("./ies/profile.ies".into()))
);
assert_eq!(shaping.ies_normalize_attr().get()?, Some(sdf::Value::Bool(true)));
let shadow = ShadowAPI::get(&stage, &prim)?.expect("ShadowAPI");
assert_eq!(shadow.enable_attr().get()?, Some(sdf::Value::Bool(true)));
assert_eq!(shadow.distance_attr().get()?, Some(sdf::Value::Float(10.0)));
assert_eq!(shadow.falloff_attr().get()?, Some(sdf::Value::Float(2.0)));
Ok(())
}
#[test]
fn shaping_and_shadow_absent_on_non_applied_prims() -> Result<()> {
let stage = open()?;
let prim = sdf::path("/World/Sphere")?;
assert!(ShapingAPI::get(&stage, &prim)?.is_none());
assert!(ShadowAPI::get(&stage, &prim)?.is_none());
Ok(())
}
#[test]
fn disk_and_cylinder_lights() -> Result<()> {
let stage = open()?;
let disk = DiskLight::get(&stage, sdf::path("/World/Disk")?)?.expect("DiskLight");
assert_eq!(disk.radius_attr().get()?, Some(sdf::Value::Float(0.75)));
let tube = CylinderLight::get(&stage, sdf::path("/World/Tube")?)?.expect("CylinderLight");
assert_eq!(tube.length_attr().get()?, Some(sdf::Value::Float(3.0)));
assert_eq!(tube.radius_attr().get()?, Some(sdf::Value::Float(0.05)));
assert_eq!(tube.treat_as_line_attr().get()?, Some(sdf::Value::Bool(true)));
Ok(())
}
#[test]
fn dome_light_texture_format_and_portals() -> Result<()> {
let stage = open()?;
let dome = DomeLight::get(&stage, sdf::path("/World/Dome")?)?.expect("DomeLight");
assert_eq!(
dome.texture_file_attr().get()?,
Some(sdf::Value::AssetPath("./hdri/studio.hdr".into()))
);
assert_eq!(
dome.texture_format_attr()
.get::<String>()?
.as_deref()
.and_then(TextureFormat::from_token),
Some(TextureFormat::Latlong)
);
assert_eq!(dome.guide_radius_attr().get()?, Some(sdf::Value::Float(50.0)));
assert_eq!(dome.portals_rel().targets()?, vec![sdf::path("/World/Dome/Portal")?]);
Ok(())
}
#[test]
fn portal_light_dimensions() -> Result<()> {
let stage = open()?;
let portal = PortalLight::get(&stage, sdf::path("/World/Dome/Portal")?)?.expect("PortalLight");
assert_eq!(portal.width_attr().get()?, Some(sdf::Value::Float(1.2)));
assert_eq!(portal.height_attr().get()?, Some(sdf::Value::Float(2.4)));
Ok(())
}
#[test]
fn geometry_light_target() -> Result<()> {
let stage = open()?;
let g = GeometryLight::get(&stage, sdf::path("/World/MeshLight")?)?.expect("GeometryLight");
assert_eq!(g.geometry_rel().targets()?, vec![sdf::path("/World/Emitter")?]);
assert_eq!(g.intensity_attr().get()?, Some(sdf::Value::Float(200.0)));
Ok(())
}
#[test]
fn get_rejects_wrong_type() -> Result<()> {
let stage = open()?;
assert!(DistantLight::get(&stage, sdf::path("/World/Emitter")?)?.is_none());
assert!(SphereLight::get(&stage, sdf::path("/World/Sun")?)?.is_none());
Ok(())
}
#[test]
fn light_list_api() -> Result<()> {
let stage = open()?;
let list = LightListAPI::get(&stage, sdf::path("/World")?)?.expect("LightListAPI");
assert_eq!(
list.cache_behavior_attr()
.get::<String>()?
.as_deref()
.and_then(LightListCacheBehavior::from_token),
Some(LightListCacheBehavior::ConsumeAndContinue)
);
let lights = list.light_list_rel().targets()?;
assert!(lights.contains(&sdf::path("/World/Sun")?));
assert!(lights.contains(&sdf::path("/World/Dome/Portal")?));
Ok(())
}
#[test]
fn light_api_skips_non_light() -> Result<()> {
let stage = open()?;
assert!(LightAPI::get(&stage, sdf::path("/World")?)?.is_none());
Ok(())
}
#[test]
fn light_api_via_applied_schema() -> Result<()> {
let stage = from_usda(
"#usda 1.0\ndef Mesh \"Panel\" (\n prepend apiSchemas = [\"LightAPI\"]\n) {\n float inputs:intensity = 750\n}\n",
)?;
let light = LightAPI::get(&stage, sdf::path("/Panel")?)?.expect("LightAPI applied to Mesh");
assert_eq!(light.intensity_attr().get()?, Some(sdf::Value::Float(750.0)));
Ok(())
}
#[test]
fn light_api_mesh_and_volume() -> Result<()> {
let stage = from_usda(concat!(
"#usda 1.0\n",
"def Mesh \"Panel\" (\n prepend apiSchemas = [\"MeshLightAPI\"]\n) {\n float inputs:intensity = 321\n}\n",
"def Volume \"Fog\" (\n prepend apiSchemas = [\"VolumeLightAPI\"]\n) {\n float inputs:intensity = 123\n}\n",
))?;
let panel = LightAPI::get(&stage, sdf::path("/Panel")?)?.expect("MeshLightAPI");
assert_eq!(panel.intensity_attr().get()?, Some(sdf::Value::Float(321.0)));
let fog = LightAPI::get(&stage, sdf::path("/Fog")?)?.expect("VolumeLightAPI");
assert_eq!(fog.intensity_attr().get()?, Some(sdf::Value::Float(123.0)));
Ok(())
}
#[test]
fn animated_intensity_via_get_at() -> Result<()> {
let stage = from_usda(concat!(
"#usda 1.0\n",
"def SphereLight \"Flicker\" {\n",
" float inputs:intensity.timeSamples = {\n 0: 100.0,\n 10: 1000.0,\n }\n",
"}\n",
))?;
let light = SphereLight::get(&stage, sdf::path("/Flicker")?)?.expect("SphereLight");
assert_eq!(light.intensity_attr().get::<sdf::Value>()?, None);
assert_eq!(light.intensity_attr().get_at(0.0)?, Some(sdf::Value::Double(100.0)));
assert_eq!(light.intensity_attr().get_at(10.0)?, Some(sdf::Value::Double(1000.0)));
assert_eq!(light.intensity_attr().get_at(5.0)?, Some(sdf::Value::Double(550.0)));
Ok(())
}
#[test]
fn token_round_trips() {
assert_eq!(TextureFormat::default(), TextureFormat::Automatic);
assert_eq!(TextureFormat::Latlong.as_token(), "latlong");
assert_eq!(TextureFormat::from_token("angular"), Some(TextureFormat::Angular));
assert_eq!(TextureFormat::from_token("bogus"), None);
assert_eq!(
LightListCacheBehavior::default(),
LightListCacheBehavior::ConsumeAndContinue
);
assert_eq!(
LightListCacheBehavior::from_token("ignore"),
Some(LightListCacheBehavior::Ignore)
);
}