616 lines
22 KiB
Rust
616 lines
22 KiB
Rust
//! Versioned material, material-instance, and surface-shader asset documents.
|
|
|
|
use bevy::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
EditorAssetRef, MaterialDesc, MaterialInputSchema, MaterialInputSet, MaterialInputSource,
|
|
MaterialInputTarget, MaterialParameter, MaterialParameterValue, MaterialRef,
|
|
MaterialRenderState, MaterialShaderKind, MaterialTextureBinding, ShaderRefDesc, TextureChannel,
|
|
};
|
|
|
|
pub const MATERIAL_ASSET_SCHEMA_VERSION: u32 = 2;
|
|
pub const MATERIAL_INSTANCE_SCHEMA_VERSION: u32 = 2;
|
|
pub const SURFACE_SHADER_SCHEMA_VERSION: u32 = 2;
|
|
|
|
const fn default_material_schema_version() -> u32 {
|
|
MATERIAL_ASSET_SCHEMA_VERSION
|
|
}
|
|
|
|
const fn default_material_instance_schema_version() -> u32 {
|
|
MATERIAL_INSTANCE_SCHEMA_VERSION
|
|
}
|
|
|
|
const fn default_surface_shader_schema_version() -> u32 {
|
|
SURFACE_SHADER_SCHEMA_VERSION
|
|
}
|
|
|
|
/// Converts the runtime/legacy descriptor into the lightweight authored input representation.
|
|
pub fn material_inputs_from_desc(material: &MaterialDesc) -> MaterialInputSet {
|
|
let mut values = vec![
|
|
MaterialParameter {
|
|
name: "base_color".into(),
|
|
value: MaterialParameterValue::Color(material.base_color),
|
|
},
|
|
MaterialParameter {
|
|
name: "metallic".into(),
|
|
value: MaterialParameterValue::Float(material.metallic),
|
|
},
|
|
MaterialParameter {
|
|
name: "roughness".into(),
|
|
value: MaterialParameterValue::Float(material.roughness),
|
|
},
|
|
MaterialParameter {
|
|
name: "emissive_color".into(),
|
|
value: MaterialParameterValue::Color(material.emissive_color),
|
|
},
|
|
MaterialParameter {
|
|
name: "emissive_intensity".into(),
|
|
value: MaterialParameterValue::Float(material.emissive_intensity),
|
|
},
|
|
MaterialParameter {
|
|
name: "uv_offset".into(),
|
|
value: MaterialParameterValue::Vec2(material.uv_offset),
|
|
},
|
|
MaterialParameter {
|
|
name: "uv_tiling".into(),
|
|
value: MaterialParameterValue::Vec2(material.uv_tiling),
|
|
},
|
|
];
|
|
for parameter in &material.parameters {
|
|
if !is_standard_value_name(¶meter.name) {
|
|
upsert_parameter(&mut values, parameter.clone());
|
|
}
|
|
}
|
|
|
|
let mut textures = Vec::new();
|
|
if let Some(path) = material.base_color_texture.as_deref() {
|
|
textures.push(path_texture_binding(
|
|
"base_color",
|
|
path,
|
|
TextureChannel::Rgba,
|
|
));
|
|
}
|
|
if let Some(path) = material.normal_map_texture.as_deref() {
|
|
textures.push(path_texture_binding("normal", path, TextureChannel::Rgb));
|
|
}
|
|
if let Some(path) = material.metallic_roughness_texture.as_deref() {
|
|
textures.push(path_texture_binding("occlusion", path, TextureChannel::R));
|
|
textures.push(path_texture_binding("roughness", path, TextureChannel::G));
|
|
textures.push(path_texture_binding("metallic", path, TextureChannel::B));
|
|
}
|
|
if let Some(path) = material.occlusion_texture.as_deref() {
|
|
upsert_texture(
|
|
&mut textures,
|
|
path_texture_binding("occlusion", path, TextureChannel::R),
|
|
);
|
|
}
|
|
if let Some(path) = material.emissive_texture.as_deref() {
|
|
textures.push(path_texture_binding("emissive", path, TextureChannel::Rgb));
|
|
}
|
|
for texture in &material.textures {
|
|
if !is_standard_texture_name(&texture.name) {
|
|
upsert_texture(&mut textures, texture.clone());
|
|
}
|
|
}
|
|
MaterialInputSet { values, textures }
|
|
}
|
|
|
|
/// Applies authored values to the runtime/legacy material representation.
|
|
pub fn apply_material_inputs_to_desc(inputs: &MaterialInputSet, material: &mut MaterialDesc) {
|
|
material.parameters.clear();
|
|
material.textures.clear();
|
|
for parameter in &inputs.values {
|
|
match (parameter.name.as_str(), ¶meter.value) {
|
|
("base_color", MaterialParameterValue::Color(value)) => material.base_color = *value,
|
|
("metallic", MaterialParameterValue::Float(value)) => material.metallic = *value,
|
|
("roughness", MaterialParameterValue::Float(value)) => material.roughness = *value,
|
|
("emissive_color", MaterialParameterValue::Color(value)) => {
|
|
material.emissive_color = *value
|
|
}
|
|
("emissive_intensity", MaterialParameterValue::Float(value)) => {
|
|
material.emissive_intensity = *value
|
|
}
|
|
("uv_offset", MaterialParameterValue::Vec2(value)) => material.uv_offset = *value,
|
|
("uv_tiling", MaterialParameterValue::Vec2(value)) => material.uv_tiling = *value,
|
|
_ => upsert_parameter(&mut material.parameters, parameter.clone()),
|
|
}
|
|
}
|
|
material.base_color_texture = binding_path(inputs.texture("base_color"));
|
|
material.normal_map_texture = binding_path(inputs.texture("normal"));
|
|
material.emissive_texture = binding_path(inputs.texture("emissive"));
|
|
let roughness = binding_path(inputs.texture("roughness"));
|
|
let metallic = binding_path(inputs.texture("metallic"));
|
|
material.metallic_roughness_texture = match (roughness, metallic) {
|
|
(Some(roughness), Some(metallic)) if roughness == metallic => Some(roughness),
|
|
(Some(path), None) | (None, Some(path)) => Some(path),
|
|
_ => None,
|
|
};
|
|
material.occlusion_texture = binding_path(inputs.texture("occlusion"));
|
|
for texture in &inputs.textures {
|
|
if !is_standard_texture_name(&texture.name) {
|
|
upsert_texture(&mut material.textures, texture.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MaterialInputSource for MaterialDesc {
|
|
fn to_material_inputs(&self) -> MaterialInputSet {
|
|
material_inputs_from_desc(self)
|
|
}
|
|
}
|
|
|
|
impl MaterialInputTarget for MaterialDesc {
|
|
fn apply_material_inputs(&mut self, inputs: &MaterialInputSet) {
|
|
apply_material_inputs_to_desc(inputs, self);
|
|
}
|
|
}
|
|
|
|
fn path_texture_binding(name: &str, path: &str, channel: TextureChannel) -> MaterialTextureBinding {
|
|
let label = std::path::Path::new(path)
|
|
.file_stem()
|
|
.and_then(|value| value.to_str())
|
|
.unwrap_or(path);
|
|
MaterialTextureBinding {
|
|
name: name.into(),
|
|
texture: Some(
|
|
EditorAssetRef::new(String::new(), format!("texture:{name}"), label)
|
|
.with_source_path(path),
|
|
),
|
|
channel,
|
|
}
|
|
}
|
|
|
|
fn binding_path(binding: Option<&MaterialTextureBinding>) -> Option<String> {
|
|
binding
|
|
.and_then(|binding| binding.texture.as_ref())
|
|
.and_then(|reference| reference.source_path.clone())
|
|
}
|
|
|
|
fn is_standard_value_name(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"base_color"
|
|
| "metallic"
|
|
| "roughness"
|
|
| "emissive_color"
|
|
| "emissive_intensity"
|
|
| "uv_offset"
|
|
| "uv_tiling"
|
|
)
|
|
}
|
|
|
|
fn is_standard_texture_name(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"base_color"
|
|
| "base_color_texture"
|
|
| "normal"
|
|
| "normal_map_texture"
|
|
| "occlusion"
|
|
| "roughness"
|
|
| "metallic"
|
|
| "metallic_roughness"
|
|
| "metallic_roughness_texture"
|
|
| "emissive"
|
|
| "emissive_texture"
|
|
)
|
|
}
|
|
|
|
/// RON material asset with schema-driven authoring inputs.
|
|
#[derive(Asset, TypePath, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MaterialAsset {
|
|
#[serde(default = "default_material_schema_version")]
|
|
pub schema_version: u32,
|
|
pub label: String,
|
|
#[serde(default)]
|
|
pub shader: ShaderRefDesc,
|
|
#[serde(default)]
|
|
pub shader_ref: Option<EditorAssetRef>,
|
|
#[serde(default)]
|
|
pub render_state: MaterialRenderState,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub provenance: Option<MaterialProvenance>,
|
|
#[serde(default)]
|
|
pub inputs: MaterialInputSet,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct MaterialProvenance {
|
|
pub source_path: String,
|
|
pub source_fingerprint: String,
|
|
pub source_sub_asset_id: String,
|
|
pub source_label: String,
|
|
}
|
|
|
|
impl MaterialAsset {
|
|
pub fn load_from_path(catalog_path: &str) -> Result<Self, String> {
|
|
let text = std::fs::read_to_string(catalog_path)
|
|
.map_err(|err| format!("could not read {catalog_path}: {err}"))?;
|
|
ron::from_str(&text).map_err(|err| format!("invalid material RON in {catalog_path}: {err}"))
|
|
}
|
|
}
|
|
|
|
/// Loads a project Material or direct-base Material Instance into one effective descriptor.
|
|
///
|
|
/// The returned descriptor retains the selected asset path so primitive/brush authoring can locate
|
|
/// the shared source while renderer slots continue to preserve their stable [`MaterialRef`].
|
|
pub fn load_resolved_material_from_path(
|
|
catalog_path: &str,
|
|
) -> Result<(MaterialDesc, MaterialRenderState), String> {
|
|
if let Ok(asset) = MaterialAsset::load_from_path(catalog_path) {
|
|
let mut material = MaterialDesc {
|
|
shader: asset.shader,
|
|
..Default::default()
|
|
};
|
|
asset.inputs.apply_to_material_desc(&mut material);
|
|
material.material_asset_path = Some(catalog_path.to_string());
|
|
return Ok((material, asset.render_state));
|
|
}
|
|
|
|
let instance = MaterialInstanceAsset::load_from_path(catalog_path)?;
|
|
let base_path = instance.base.0.source_path.as_deref().ok_or_else(|| {
|
|
format!(
|
|
"material instance {} has no loadable base path",
|
|
instance.label
|
|
)
|
|
})?;
|
|
let base = MaterialAsset::load_from_path(base_path)?;
|
|
let mut material = MaterialDesc {
|
|
shader: base.shader,
|
|
..Default::default()
|
|
};
|
|
base.inputs.apply_to_material_desc(&mut material);
|
|
instance.apply_to(&mut material);
|
|
material.material_asset_path = Some(catalog_path.to_string());
|
|
Ok((material, base.render_state))
|
|
}
|
|
|
|
/// Explicit reusable overrides over one project Material asset.
|
|
#[derive(Asset, TypePath, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MaterialInstanceAsset {
|
|
#[serde(default = "default_material_instance_schema_version")]
|
|
pub schema_version: u32,
|
|
pub label: String,
|
|
pub base: MaterialRef,
|
|
#[serde(default)]
|
|
pub overrides: MaterialInputSet,
|
|
}
|
|
|
|
impl MaterialInstanceAsset {
|
|
pub fn load_from_path(catalog_path: &str) -> Result<Self, String> {
|
|
let text = std::fs::read_to_string(catalog_path)
|
|
.map_err(|err| format!("could not read {catalog_path}: {err}"))?;
|
|
ron::from_str(&text)
|
|
.map_err(|err| format!("invalid material-instance RON in {catalog_path}: {err}"))
|
|
}
|
|
|
|
/// Applies this instance's sparse overrides without mutating the shared base asset.
|
|
pub fn apply_to(&self, material: &mut MaterialDesc) {
|
|
for parameter in &self.overrides.values {
|
|
match (parameter.name.as_str(), ¶meter.value) {
|
|
("base_color", crate::MaterialParameterValue::Color(value)) => {
|
|
material.base_color = *value;
|
|
}
|
|
("metallic", crate::MaterialParameterValue::Float(value)) => {
|
|
material.metallic = *value;
|
|
}
|
|
("roughness", crate::MaterialParameterValue::Float(value)) => {
|
|
material.roughness = *value;
|
|
}
|
|
("emissive_color", crate::MaterialParameterValue::Color(value)) => {
|
|
material.emissive_color = *value;
|
|
}
|
|
("emissive_intensity", crate::MaterialParameterValue::Float(value)) => {
|
|
material.emissive_intensity = *value;
|
|
}
|
|
("uv_offset", crate::MaterialParameterValue::Vec2(value)) => {
|
|
material.uv_offset = *value;
|
|
}
|
|
("uv_tiling", crate::MaterialParameterValue::Vec2(value)) => {
|
|
material.uv_tiling = *value;
|
|
}
|
|
_ => upsert_parameter(&mut material.parameters, parameter.clone()),
|
|
}
|
|
}
|
|
for texture in &self.overrides.textures {
|
|
let path = texture
|
|
.texture
|
|
.as_ref()
|
|
.and_then(|reference| reference.source_path.clone());
|
|
match texture.name.as_str() {
|
|
"base_color" | "base_color_texture" => material.base_color_texture = path,
|
|
"emissive" | "emissive_texture" => material.emissive_texture = path,
|
|
"normal" | "normal_map_texture" => material.normal_map_texture = path,
|
|
"roughness" | "metallic" | "metallic_roughness_texture" => {
|
|
material.metallic_roughness_texture = path
|
|
}
|
|
_ => upsert_texture(&mut material.textures, texture.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Overlays sparse runtime/property-block values onto this direct-base instance.
|
|
pub fn merge_overrides(
|
|
&mut self,
|
|
parameters: impl IntoIterator<Item = MaterialParameter>,
|
|
textures: impl IntoIterator<Item = MaterialTextureBinding>,
|
|
) {
|
|
for parameter in parameters {
|
|
upsert_parameter(&mut self.overrides.values, parameter);
|
|
}
|
|
for texture in textures {
|
|
upsert_texture(&mut self.overrides.textures, texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn upsert_parameter(values: &mut Vec<MaterialParameter>, value: MaterialParameter) {
|
|
if let Some(existing) = values.iter_mut().find(|entry| entry.name == value.name) {
|
|
*existing = value;
|
|
} else {
|
|
values.push(value);
|
|
}
|
|
}
|
|
|
|
fn upsert_texture(values: &mut Vec<MaterialTextureBinding>, value: MaterialTextureBinding) {
|
|
if let Some(existing) = values.iter_mut().find(|entry| entry.name == value.name) {
|
|
*existing = value;
|
|
} else {
|
|
values.push(value);
|
|
}
|
|
}
|
|
|
|
/// RON shader schema describing material inspector parameters.
|
|
#[derive(Asset, TypePath, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ShaderSchemaAsset {
|
|
#[serde(default = "default_surface_shader_schema_version")]
|
|
pub schema_version: u32,
|
|
pub label: String,
|
|
pub kind: MaterialShaderKind,
|
|
#[serde(default)]
|
|
pub wgsl_path: Option<String>,
|
|
#[serde(default)]
|
|
pub schema: MaterialInputSchema,
|
|
}
|
|
|
|
impl ShaderSchemaAsset {
|
|
pub fn load_from_path(catalog_path: &str) -> Result<Self, String> {
|
|
let text = std::fs::read_to_string(catalog_path)
|
|
.map_err(|err| format!("could not read {catalog_path}: {err}"))?;
|
|
ron::from_str(&text)
|
|
.map_err(|err| format!("invalid shader schema RON in {catalog_path}: {err}"))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
standard_lit_input_schema, MaterialAlphaMode, MaterialInputPresentation, ShaderPropertyType,
|
|
};
|
|
|
|
#[test]
|
|
fn material_v2_defaults_to_opaque() {
|
|
let parsed = MaterialAsset {
|
|
schema_version: MATERIAL_ASSET_SCHEMA_VERSION,
|
|
label: "Material".into(),
|
|
shader: ShaderRefDesc::default(),
|
|
shader_ref: None,
|
|
render_state: MaterialRenderState::default(),
|
|
provenance: None,
|
|
inputs: MaterialInputSet::default(),
|
|
};
|
|
|
|
assert_eq!(parsed.render_state.alpha_mode, MaterialAlphaMode::Opaque);
|
|
}
|
|
|
|
#[test]
|
|
fn material_instance_requires_an_explicit_base_ref() {
|
|
let instance = MaterialInstanceAsset {
|
|
schema_version: MATERIAL_INSTANCE_SCHEMA_VERSION,
|
|
label: "Red Variant".into(),
|
|
base: MaterialRef::new(EditorAssetRef::new(
|
|
"material-id",
|
|
"material:source",
|
|
"Base",
|
|
)),
|
|
overrides: MaterialInputSet::default(),
|
|
};
|
|
|
|
assert!(instance.base.is_resolved());
|
|
}
|
|
|
|
#[test]
|
|
fn material_instance_applies_standard_and_custom_overrides() {
|
|
let mut material = MaterialDesc::default();
|
|
let instance = MaterialInstanceAsset {
|
|
schema_version: MATERIAL_INSTANCE_SCHEMA_VERSION,
|
|
label: "Variant".into(),
|
|
base: MaterialRef::default(),
|
|
overrides: MaterialInputSet {
|
|
values: vec![
|
|
MaterialParameter {
|
|
name: "roughness".into(),
|
|
value: crate::MaterialParameterValue::Float(0.2),
|
|
},
|
|
MaterialParameter {
|
|
name: "edge_width".into(),
|
|
value: crate::MaterialParameterValue::Float(3.0),
|
|
},
|
|
],
|
|
textures: vec![MaterialTextureBinding {
|
|
name: "base_color_texture".into(),
|
|
texture: Some(
|
|
EditorAssetRef::new("texture", "texture:source", "Grid")
|
|
.with_source_path("assets/textures/grid.png"),
|
|
),
|
|
channel: TextureChannel::Rgba,
|
|
}],
|
|
},
|
|
};
|
|
|
|
instance.apply_to(&mut material);
|
|
|
|
assert_eq!(material.roughness, 0.2);
|
|
assert_eq!(
|
|
material.base_color_texture.as_deref(),
|
|
Some("assets/textures/grid.png")
|
|
);
|
|
assert!(material
|
|
.parameters
|
|
.iter()
|
|
.any(|value| value.name == "edge_width"));
|
|
}
|
|
|
|
#[test]
|
|
fn material_inputs_round_trip_shared_uv_transform() {
|
|
let source = MaterialDesc {
|
|
uv_offset: Vec2::new(0.25, -0.5),
|
|
uv_tiling: Vec2::new(4.0, 2.0),
|
|
..Default::default()
|
|
};
|
|
let inputs = MaterialInputSet::from_material_desc(&source);
|
|
let mut resolved = MaterialDesc::default();
|
|
|
|
inputs.apply_to_material_desc(&mut resolved);
|
|
|
|
assert_eq!(resolved.uv_offset, source.uv_offset);
|
|
assert_eq!(resolved.uv_tiling, source.uv_tiling);
|
|
}
|
|
|
|
#[test]
|
|
fn standard_lit_surface_inputs_match_penpot_bindings() {
|
|
let schema = standard_lit_input_schema();
|
|
let surface = schema
|
|
.inputs
|
|
.iter()
|
|
.filter(|input| input.group == "surface_inputs")
|
|
.collect::<Vec<_>>();
|
|
let rows = surface
|
|
.iter()
|
|
.filter(|input| matches!(input.presentation, MaterialInputPresentation::Row))
|
|
.count();
|
|
assert_eq!(surface.len(), 7);
|
|
assert_eq!(rows, 6);
|
|
assert_eq!(surface[0].name, "base_color");
|
|
assert_eq!(surface[4].name, "normal");
|
|
assert!(matches!(
|
|
surface[4].property_type,
|
|
ShaderPropertyType::Float {
|
|
min: Some(0.0),
|
|
max: Some(1.0)
|
|
}
|
|
));
|
|
assert_eq!(surface[6].name, "emissive_intensity");
|
|
assert_eq!(
|
|
surface[6].presentation,
|
|
MaterialInputPresentation::Companion {
|
|
owner: "emissive_color".into()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn promotion_merge_replaces_existing_values_without_duplicates() {
|
|
let mut instance = MaterialInstanceAsset {
|
|
schema_version: MATERIAL_INSTANCE_SCHEMA_VERSION,
|
|
label: "Variant".into(),
|
|
base: MaterialRef::default(),
|
|
overrides: MaterialInputSet {
|
|
values: vec![MaterialParameter {
|
|
name: "roughness".into(),
|
|
value: crate::MaterialParameterValue::Float(0.8),
|
|
}],
|
|
textures: Vec::new(),
|
|
},
|
|
};
|
|
instance.merge_overrides(
|
|
[MaterialParameter {
|
|
name: "roughness".into(),
|
|
value: crate::MaterialParameterValue::Float(0.2),
|
|
}],
|
|
[MaterialTextureBinding {
|
|
name: "normal_map_texture".into(),
|
|
texture: None,
|
|
channel: TextureChannel::Rgb,
|
|
}],
|
|
);
|
|
|
|
assert_eq!(instance.overrides.values.len(), 1);
|
|
assert_eq!(
|
|
instance.overrides.values[0].value,
|
|
crate::MaterialParameterValue::Float(0.2)
|
|
);
|
|
assert_eq!(instance.overrides.textures.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn resolved_material_instance_applies_sparse_overrides_and_keeps_instance_path() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-resolved-material-instance-{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
let base_path = root.join("base.ron");
|
|
let instance_path = root.join("instance.ron");
|
|
std::fs::create_dir_all(&root).unwrap();
|
|
let base = MaterialAsset {
|
|
schema_version: MATERIAL_ASSET_SCHEMA_VERSION,
|
|
label: "Base".into(),
|
|
shader: ShaderRefDesc::default(),
|
|
shader_ref: None,
|
|
render_state: MaterialRenderState {
|
|
double_sided: true,
|
|
..Default::default()
|
|
},
|
|
provenance: None,
|
|
inputs: MaterialInputSet::from_material_desc(&MaterialDesc {
|
|
roughness: 0.8,
|
|
..Default::default()
|
|
}),
|
|
};
|
|
std::fs::write(
|
|
&base_path,
|
|
ron::ser::to_string_pretty(&base, ron::ser::PrettyConfig::default()).unwrap(),
|
|
)
|
|
.unwrap();
|
|
let instance = MaterialInstanceAsset {
|
|
schema_version: MATERIAL_INSTANCE_SCHEMA_VERSION,
|
|
label: "Instance".into(),
|
|
base: MaterialRef::new(
|
|
EditorAssetRef::new("base", "material:source", "Base")
|
|
.with_source_path(base_path.to_string_lossy().into_owned()),
|
|
),
|
|
overrides: MaterialInputSet {
|
|
values: vec![MaterialParameter {
|
|
name: "roughness".into(),
|
|
value: crate::MaterialParameterValue::Float(0.25),
|
|
}],
|
|
textures: Vec::new(),
|
|
},
|
|
};
|
|
std::fs::write(
|
|
&instance_path,
|
|
ron::ser::to_string_pretty(&instance, ron::ser::PrettyConfig::default()).unwrap(),
|
|
)
|
|
.unwrap();
|
|
|
|
let (resolved, state) =
|
|
load_resolved_material_from_path(instance_path.to_str().unwrap()).unwrap();
|
|
|
|
assert_eq!(resolved.roughness, 0.25);
|
|
assert_eq!(
|
|
resolved.material_asset_path.as_deref(),
|
|
instance_path.to_str()
|
|
);
|
|
assert!(state.double_sided);
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
}
|