Skip to main content

oxide_renderer/
descriptor.rs

1//! Material descriptor loading for shader-driven assets
2
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7use crate::shader::{BuiltinShader, ShaderSource};
8
9#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum MaterialType {
12    Lit,
13    Unlit,
14    Basic,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "source", rename_all = "snake_case")]
19pub enum ShaderDescriptor {
20    Builtin { shader: String },
21    File { path: String },
22    Inline { wgsl: String },
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct MaterialDescriptor {
27    pub name: String,
28    pub material_type: MaterialType,
29    pub shader: ShaderDescriptor,
30    pub fallback_shader: Option<String>,
31    /// Path to the albedo (diffuse) texture file.
32    #[serde(default)]
33    pub albedo_texture: Option<String>,
34    /// Path to the normal map texture file.
35    #[serde(default)]
36    pub normal_texture: Option<String>,
37    /// Path to the roughness texture file.
38    #[serde(default)]
39    pub roughness_texture: Option<String>,
40}
41
42#[derive(thiserror::Error, Debug)]
43pub enum MaterialDescriptorError {
44    #[error("Failed to read descriptor '{path}': {source}")]
45    Io {
46        path: String,
47        source: std::io::Error,
48    },
49    #[error("Failed to parse JSON descriptor '{path}': {source}")]
50    ParseJson {
51        path: String,
52        source: serde_json::Error,
53    },
54    #[error("Failed to parse RON descriptor '{path}': {source}")]
55    ParseRon {
56        path: String,
57        source: Box<ron::error::SpannedError>,
58    },
59    #[error("Failed to parse TOML descriptor '{path}': {source}")]
60    ParseToml {
61        path: String,
62        source: toml::de::Error,
63    },
64    #[error("Unsupported descriptor format '{ext}' for file '{path}'")]
65    UnsupportedFormat { path: String, ext: String },
66    #[error("Unknown builtin shader '{name}'")]
67    UnknownBuiltinShader { name: String },
68}
69
70impl MaterialDescriptor {
71    pub fn shader_source(&self) -> Result<ShaderSource, MaterialDescriptorError> {
72        match &self.shader {
73            ShaderDescriptor::Builtin { shader } => {
74                Ok(ShaderSource::Builtin(parse_builtin_shader(shader)?))
75            }
76            ShaderDescriptor::File { path } => Ok(ShaderSource::File(path.into())),
77            ShaderDescriptor::Inline { wgsl } => Ok(ShaderSource::WgslOwned(wgsl.clone())),
78        }
79    }
80
81    pub fn fallback_shader(&self) -> Result<BuiltinShader, MaterialDescriptorError> {
82        match &self.fallback_shader {
83            Some(name) => parse_builtin_shader(name),
84            None => Ok(BuiltinShader::Fallback),
85        }
86    }
87}
88
89pub fn load_material_descriptor(
90    path: impl AsRef<Path>,
91) -> Result<MaterialDescriptor, MaterialDescriptorError> {
92    let path = path.as_ref();
93    let raw = std::fs::read_to_string(path).map_err(|source| MaterialDescriptorError::Io {
94        path: path.display().to_string(),
95        source,
96    })?;
97
98    let ext = path
99        .extension()
100        .and_then(|e| e.to_str())
101        .unwrap_or("")
102        .to_lowercase();
103    match ext.as_str() {
104        "json" => serde_json::from_str(&raw).map_err(|source| MaterialDescriptorError::ParseJson {
105            path: path.display().to_string(),
106            source,
107        }),
108        "ron" => ron::from_str(&raw).map_err(|source| MaterialDescriptorError::ParseRon {
109            path: path.display().to_string(),
110            source: Box::new(source),
111        }),
112        "toml" => toml::from_str(&raw).map_err(|source| MaterialDescriptorError::ParseToml {
113            path: path.display().to_string(),
114            source,
115        }),
116        _ => Err(MaterialDescriptorError::UnsupportedFormat {
117            path: path.display().to_string(),
118            ext,
119        }),
120    }
121}
122
123fn parse_builtin_shader(name: &str) -> Result<BuiltinShader, MaterialDescriptorError> {
124    match name.trim().to_ascii_lowercase().as_str() {
125        "basic" => Ok(BuiltinShader::Basic),
126        "lit" => Ok(BuiltinShader::Lit),
127        "unlit" => Ok(BuiltinShader::Unlit),
128        "sky_gradient" | "skygradient" | "sky" => Ok(BuiltinShader::SkyGradient),
129        "sprite_ui" | "spriteui" | "ui" => Ok(BuiltinShader::SpriteUi),
130        "fallback" => Ok(BuiltinShader::Fallback),
131        _ => Err(MaterialDescriptorError::UnknownBuiltinShader {
132            name: name.to_string(),
133        }),
134    }
135}