Skip to main content

oxihuman_export/
material.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4/// PBR metallic-roughness material properties.
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
6pub struct PbrMaterial {
7    pub name: String,
8    /// Base color RGBA [0..1]
9    pub base_color: [f32; 4],
10    /// Metallic factor [0..1]
11    pub metallic: f32,
12    /// Roughness factor [0..1]
13    pub roughness: f32,
14    /// Emissive RGB [0..1]
15    pub emissive: [f32; 3],
16    /// Double-sided rendering
17    pub double_sided: bool,
18    /// Alpha mode: "OPAQUE", "MASK", or "BLEND"
19    pub alpha_mode: String,
20}
21
22impl PbrMaterial {
23    /// Skin-like material (pinkish, low metallic, medium roughness)
24    #[allow(dead_code)]
25    pub fn skin() -> Self {
26        Self {
27            name: "skin".to_string(),
28            base_color: [0.94, 0.76, 0.69, 1.0],
29            metallic: 0.0,
30            roughness: 0.5,
31            emissive: [0.0, 0.0, 0.0],
32            double_sided: true,
33            alpha_mode: "OPAQUE".to_string(),
34        }
35    }
36
37    /// Clothing-like material (dark, low metallic, high roughness)
38    #[allow(dead_code)]
39    pub fn clothing() -> Self {
40        Self {
41            name: "clothing".to_string(),
42            base_color: [0.15, 0.15, 0.20, 1.0],
43            metallic: 0.0,
44            roughness: 0.85,
45            emissive: [0.0, 0.0, 0.0],
46            double_sided: false,
47            alpha_mode: "OPAQUE".to_string(),
48        }
49    }
50
51    /// Default neutral material
52    #[allow(dead_code)]
53    pub fn default_material() -> Self {
54        Self {
55            name: "default".to_string(),
56            base_color: [0.8, 0.8, 0.8, 1.0],
57            metallic: 0.0,
58            roughness: 0.5,
59            emissive: [0.0, 0.0, 0.0],
60            double_sided: false,
61            alpha_mode: "OPAQUE".to_string(),
62        }
63    }
64
65    /// Convert to GLTF JSON material node
66    #[allow(dead_code)]
67    pub fn to_gltf_json(&self) -> serde_json::Value {
68        serde_json::json!({
69            "name": self.name,
70            "pbrMetallicRoughness": {
71                "baseColorFactor": [
72                    self.base_color[0],
73                    self.base_color[1],
74                    self.base_color[2],
75                    self.base_color[3]
76                ],
77                "metallicFactor": self.metallic,
78                "roughnessFactor": self.roughness
79            },
80            "emissiveFactor": [
81                self.emissive[0],
82                self.emissive[1],
83                self.emissive[2]
84            ],
85            "doubleSided": self.double_sided,
86            "alphaMode": self.alpha_mode
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn skin_material_has_correct_alpha() {
97        assert_eq!(PbrMaterial::skin().base_color[3], 1.0);
98    }
99
100    #[test]
101    fn to_gltf_json_has_pbr_key() {
102        let skin = PbrMaterial::skin();
103        let json = skin.to_gltf_json();
104        assert!(
105            json["pbrMetallicRoughness"].is_object(),
106            "pbrMetallicRoughness should be an object"
107        );
108    }
109
110    #[test]
111    fn clothing_is_opaque() {
112        assert_eq!(PbrMaterial::clothing().alpha_mode, "OPAQUE");
113    }
114
115    #[test]
116    fn gltf_json_metallic_is_float() {
117        let skin = PbrMaterial::skin();
118        let json = skin.to_gltf_json();
119        assert!(
120            json["pbrMetallicRoughness"]["metallicFactor"]
121                .as_f64()
122                .is_some(),
123            "metallicFactor should be a float"
124        );
125    }
126}