#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct Color4 {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color4 {
#[allow(dead_code)]
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
#[allow(dead_code)]
pub fn white() -> Self {
Self::new(1.0, 1.0, 1.0, 1.0)
}
#[allow(dead_code)]
pub fn black() -> Self {
Self::new(0.0, 0.0, 0.0, 1.0)
}
#[allow(dead_code)]
pub fn to_array(&self) -> [f32; 4] {
[self.r, self.g, self.b, self.a]
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrMaterial {
pub name: String,
pub base_color: Color4,
pub metallic: f32,
pub roughness: f32,
pub emissive: [f32; 3],
pub alpha_cutoff: Option<f32>,
pub double_sided: bool,
}
impl PbrMaterial {
#[allow(dead_code)]
pub fn default_skin() -> Self {
PbrMaterial {
name: "skin".to_string(),
base_color: Color4::new(0.85, 0.65, 0.55, 1.0),
metallic: 0.0,
roughness: 0.6,
emissive: [0.0, 0.0, 0.0],
alpha_cutoff: None,
double_sided: false,
}
}
#[allow(dead_code)]
pub fn default_cloth() -> Self {
PbrMaterial {
name: "cloth".to_string(),
base_color: Color4::new(0.5, 0.5, 0.5, 1.0),
metallic: 0.0,
roughness: 0.9,
emissive: [0.0, 0.0, 0.0],
alpha_cutoff: None,
double_sided: false,
}
}
#[allow(dead_code)]
pub fn default_metal() -> Self {
PbrMaterial {
name: "metal".to_string(),
base_color: Color4::new(0.8, 0.8, 0.8, 1.0),
metallic: 1.0,
roughness: 0.2,
emissive: [0.0, 0.0, 0.0],
alpha_cutoff: None,
double_sided: false,
}
}
#[allow(dead_code)]
pub fn default_glass() -> Self {
PbrMaterial {
name: "glass".to_string(),
base_color: Color4::new(0.9, 0.95, 1.0, 0.15),
metallic: 0.0,
roughness: 0.05,
emissive: [0.0, 0.0, 0.0],
alpha_cutoff: Some(0.01),
double_sided: true,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct MaterialLibrary {
pub materials: Vec<PbrMaterial>,
}
impl MaterialLibrary {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn add(&mut self, mat: PbrMaterial) -> usize {
let idx = self.materials.len();
self.materials.push(mat);
idx
}
#[allow(dead_code)]
pub fn get(&self, idx: usize) -> Option<&PbrMaterial> {
self.materials.get(idx)
}
#[allow(dead_code)]
pub fn by_name(&self, name: &str) -> Option<&PbrMaterial> {
self.materials.iter().find(|m| m.name == name)
}
}
#[allow(dead_code)]
pub fn material_to_gltf_json(mat: &PbrMaterial) -> String {
let c = &mat.base_color;
let alpha_mode = if mat.alpha_cutoff.is_some() {
"\"MASK\""
} else if c.a < 1.0 {
"\"BLEND\""
} else {
"\"OPAQUE\""
};
let cutoff_fragment = mat
.alpha_cutoff
.map(|t| format!(", \"alphaCutoff\": {:.4}", t))
.unwrap_or_default();
format!(
r#"{{"name": "{name}", "pbrMetallicRoughness": {{"baseColorFactor": [{r:.4}, {g:.4}, {b:.4}, {a:.4}], "metallicFactor": {m:.4}, "roughnessFactor": {ro:.4}}}, "emissiveFactor": [{er:.4}, {eg:.4}, {eb:.4}], "alphaMode": {am}, "doubleSided": {ds}{cutoff}}}"#,
name = mat.name,
r = c.r,
g = c.g,
b = c.b,
a = c.a,
m = mat.metallic,
ro = mat.roughness,
er = mat.emissive[0],
eg = mat.emissive[1],
eb = mat.emissive[2],
am = alpha_mode,
ds = mat.double_sided,
cutoff = cutoff_fragment,
)
}
#[allow(dead_code)]
pub fn color_to_hex(c: &Color4) -> String {
let r = (c.r.clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (c.g.clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (c.b.clamp(0.0, 1.0) * 255.0).round() as u8;
let a = (c.a.clamp(0.0, 1.0) * 255.0).round() as u8;
format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
}
#[allow(dead_code)]
pub fn lerp_material(a: &PbrMaterial, b: &PbrMaterial, t: f32) -> PbrMaterial {
let lerp_f32 = |x: f32, y: f32| x + (y - x) * t;
let lerp_color = |ca: &Color4, cb: &Color4| Color4 {
r: lerp_f32(ca.r, cb.r),
g: lerp_f32(ca.g, cb.g),
b: lerp_f32(ca.b, cb.b),
a: lerp_f32(ca.a, cb.a),
};
PbrMaterial {
name: a.name.clone(),
base_color: lerp_color(&a.base_color, &b.base_color),
metallic: lerp_f32(a.metallic, b.metallic),
roughness: lerp_f32(a.roughness, b.roughness),
emissive: [
lerp_f32(a.emissive[0], b.emissive[0]),
lerp_f32(a.emissive[1], b.emissive[1]),
lerp_f32(a.emissive[2], b.emissive[2]),
],
alpha_cutoff: a.alpha_cutoff,
double_sided: a.double_sided,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color4_white() {
let c = Color4::white();
assert!((c.r - 1.0).abs() < 1e-6);
assert!((c.g - 1.0).abs() < 1e-6);
assert!((c.b - 1.0).abs() < 1e-6);
assert!((c.a - 1.0).abs() < 1e-6);
}
#[test]
fn color4_to_array() {
let c = Color4::new(0.1, 0.2, 0.3, 0.4);
let arr = c.to_array();
assert_eq!(arr, [0.1, 0.2, 0.3, 0.4]);
}
#[test]
fn color_to_hex_white() {
assert_eq!(color_to_hex(&Color4::white()), "#FFFFFFFF");
}
#[test]
fn color_to_hex_black() {
assert_eq!(color_to_hex(&Color4::black()), "#000000FF");
}
#[test]
fn default_skin_low_metallic() {
let skin = PbrMaterial::default_skin();
assert!(skin.metallic < 0.1);
}
#[test]
fn default_cloth_not_metallic() {
let cloth = PbrMaterial::default_cloth();
assert!((cloth.metallic - 0.0).abs() < 1e-6);
}
#[test]
fn default_metal_metallic_one() {
let metal = PbrMaterial::default_metal();
assert!((metal.metallic - 1.0).abs() < 1e-6);
}
#[test]
fn default_glass_has_alpha() {
let glass = PbrMaterial::default_glass();
assert!(glass.base_color.a < 1.0, "glass should be transparent");
}
#[test]
fn default_glass_has_alpha_cutoff() {
let glass = PbrMaterial::default_glass();
assert!(glass.alpha_cutoff.is_some());
}
#[test]
fn material_to_gltf_json_contains_pbr_key() {
let mat = PbrMaterial::default_skin();
let json = material_to_gltf_json(&mat);
assert!(json.contains("pbrMetallicRoughness"));
}
#[test]
fn library_add_and_get() {
let mut lib = MaterialLibrary::new();
let idx = lib.add(PbrMaterial::default_skin());
assert_eq!(idx, 0);
assert!(lib.get(0).is_some());
}
#[test]
fn library_by_name() {
let mut lib = MaterialLibrary::new();
lib.add(PbrMaterial::default_skin());
assert!(lib.by_name("skin").is_some());
assert!(lib.by_name("nonexistent").is_none());
}
#[test]
fn library_get_out_of_bounds() {
let lib = MaterialLibrary::new();
assert!(lib.get(99).is_none());
}
#[test]
fn lerp_material_midpoint() {
let a = PbrMaterial::default_skin(); let b = PbrMaterial::default_metal(); let mid = lerp_material(&a, &b, 0.5);
assert!((mid.metallic - 0.5).abs() < 1e-5);
}
#[test]
fn lerp_material_at_t0_matches_a() {
let a = PbrMaterial::default_cloth();
let b = PbrMaterial::default_metal();
let result = lerp_material(&a, &b, 0.0);
assert!((result.metallic - a.metallic).abs() < 1e-6);
assert!((result.roughness - a.roughness).abs() < 1e-6);
}
#[test]
fn lerp_material_at_t1_matches_b() {
let a = PbrMaterial::default_cloth();
let b = PbrMaterial::default_metal();
let result = lerp_material(&a, &b, 1.0);
assert!((result.metallic - b.metallic).abs() < 1e-6);
assert!((result.roughness - b.roughness).abs() < 1e-6);
}
}