#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ItemSettings {
pub hidden: bool,
pub unlit: bool,
pub opacity: f32,
pub wireframe: bool,
pub pick_id: crate::renderer::PickId,
pub selected: bool,
pub cast_shadows: bool,
pub receive_shadows: bool,
}
impl Default for ItemSettings {
fn default() -> Self {
Self {
hidden: false,
unlit: false,
opacity: 1.0,
wireframe: false,
pick_id: crate::renderer::PickId::NONE,
selected: false,
cast_shadows: true,
receive_shadows: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ShadingModel {
Phong,
Pbr,
Matcap(crate::resources::MatcapId),
Flat,
}
impl Default for ShadingModel {
fn default() -> Self {
Self::Phong
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ParamVisMode {
Checker = 1,
Grid = 2,
LocalChecker = 3,
LocalRadial = 4,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParamVis {
pub mode: ParamVisMode,
pub scale: f32,
}
impl Default for ParamVis {
fn default() -> Self {
Self {
mode: ParamVisMode::Checker,
scale: 8.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BackfacePattern {
Checker = 0,
Hatching = 1,
Crosshatch = 2,
Stripes = 3,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PatternConfig {
pub pattern: BackfacePattern,
pub colour: [f32; 3],
pub scale: f32,
}
impl Default for PatternConfig {
fn default() -> Self {
Self {
pattern: BackfacePattern::Checker,
colour: [1.0, 0.5, 0.0],
scale: 20.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BackfacePolicy {
Cull,
Identical,
DifferentColour([f32; 3]),
Tint(f32),
Pattern(PatternConfig),
}
impl Default for BackfacePolicy {
fn default() -> Self {
BackfacePolicy::Cull
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AlphaMode {
Opaque,
Mask(f32),
Blend,
}
impl Default for AlphaMode {
fn default() -> Self {
AlphaMode::Opaque
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Material {
pub base_colour: [f32; 3],
pub ambient: f32,
pub diffuse: f32,
pub specular: f32,
pub shininess: f32,
pub metallic: f32,
pub roughness: f32,
pub texture_id: Option<u64>,
pub normal_map_id: Option<u64>,
pub ao_map_id: Option<u64>,
pub metallic_roughness_texture_id: Option<u64>,
pub emissive: [f32; 3],
pub emissive_texture_id: Option<u64>,
pub alpha_mode: AlphaMode,
pub shading_model: ShadingModel,
pub param_vis: Option<ParamVis>,
pub backface_policy: BackfacePolicy,
}
impl Default for Material {
fn default() -> Self {
Self {
base_colour: [0.7, 0.7, 0.7],
ambient: 0.15,
diffuse: 0.75,
specular: 0.4,
shininess: 32.0,
metallic: 0.0,
roughness: 0.5,
texture_id: None,
normal_map_id: None,
ao_map_id: None,
metallic_roughness_texture_id: None,
emissive: [0.0, 0.0, 0.0],
emissive_texture_id: None,
alpha_mode: AlphaMode::Opaque,
shading_model: ShadingModel::Phong,
param_vis: None,
backface_policy: BackfacePolicy::Cull,
}
}
}
impl Material {
pub fn is_two_sided(&self) -> bool {
!matches!(self.backface_policy, BackfacePolicy::Cull)
}
pub fn is_blend(&self) -> bool {
matches!(self.alpha_mode, AlphaMode::Blend)
}
pub fn from_colour(colour: [f32; 3]) -> Self {
Self {
base_colour: colour,
..Default::default()
}
}
pub fn pbr(base_colour: [f32; 3], metallic: f32, roughness: f32) -> Self {
Self {
base_colour,
shading_model: ShadingModel::Pbr,
metallic,
roughness,
..Default::default()
}
}
pub fn is_pbr(&self) -> bool {
matches!(self.shading_model, ShadingModel::Pbr)
}
pub fn is_flat(&self) -> bool {
matches!(self.shading_model, ShadingModel::Flat)
}
pub fn flat(base_colour: [f32; 3]) -> Self {
Self {
base_colour,
shading_model: ShadingModel::Flat,
..Default::default()
}
}
pub fn matcap_id(&self) -> Option<crate::resources::MatcapId> {
match self.shading_model {
ShadingModel::Matcap(id) => Some(id),
_ => None,
}
}
pub fn solid(colour: [f32; 3]) -> Self {
Self::from_colour(colour)
}
pub fn textured(texture_id: u64) -> Self {
Self {
texture_id: Some(texture_id),
..Default::default()
}
}
pub fn pbr_with_ao(
base_colour: [f32; 3],
metallic: f32,
roughness: f32,
ao_map: Option<u64>,
) -> Self {
Self {
base_colour,
shading_model: ShadingModel::Pbr,
metallic,
roughness,
ao_map_id: ao_map,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn item_settings_defaults() {
let a = ItemSettings::default();
assert!(!a.hidden);
assert!(!a.unlit);
assert!((a.opacity - 1.0).abs() < 1e-6);
assert!(!a.wireframe);
assert!(!a.selected);
}
#[test]
fn item_settings_spread_syntax() {
let a = ItemSettings {
unlit: true,
..Default::default()
};
assert!(a.unlit);
assert!(!a.hidden);
assert!((a.opacity - 1.0).abs() < 1e-6);
}
#[test]
fn flat_constructor_and_helpers() {
let m = Material::flat([0.4, 0.5, 0.6]);
assert!(m.is_flat());
assert!(!m.is_pbr());
assert!(m.matcap_id().is_none());
assert!(matches!(m.shading_model, ShadingModel::Flat));
assert_eq!(m.base_colour, [0.4, 0.5, 0.6]);
let p = Material::default();
assert!(!p.is_flat());
}
#[test]
fn default_values() {
let m = Material::default();
assert!((m.base_colour[0] - 0.7).abs() < 1e-6);
assert!((m.ambient - 0.15).abs() < 1e-6);
assert!((m.diffuse - 0.75).abs() < 1e-6);
assert!(!m.is_pbr());
assert!(m.texture_id.is_none());
assert!(m.normal_map_id.is_none());
assert!(m.ao_map_id.is_none());
assert!(m.matcap_id().is_none());
assert!(m.param_vis.is_none());
}
#[test]
fn from_colour_sets_base_colour() {
let m = Material::from_colour([1.0, 0.0, 0.5]);
assert!((m.base_colour[0] - 1.0).abs() < 1e-6);
assert!((m.base_colour[1]).abs() < 1e-6);
assert!((m.base_colour[2] - 0.5).abs() < 1e-6);
assert!((m.ambient - 0.15).abs() < 1e-6);
}
#[test]
fn pbr_constructor() {
let m = Material::pbr([0.8, 0.2, 0.1], 0.9, 0.3);
assert!(m.is_pbr());
assert!((m.metallic - 0.9).abs() < 1e-6);
assert!((m.roughness - 0.3).abs() < 1e-6);
assert!((m.base_colour[0] - 0.8).abs() < 1e-6);
}
#[test]
fn is_two_sided_cull() {
let m = Material::default();
assert!(!m.is_two_sided());
}
#[test]
fn is_two_sided_identical() {
let m = Material {
backface_policy: BackfacePolicy::Identical,
..Default::default()
};
assert!(m.is_two_sided());
}
#[test]
fn is_two_sided_different_colour() {
let m = Material {
backface_policy: BackfacePolicy::DifferentColour([1.0, 0.0, 0.0]),
..Default::default()
};
assert!(m.is_two_sided());
}
#[test]
fn is_two_sided_tint() {
let m = Material {
backface_policy: BackfacePolicy::Tint(0.3),
..Default::default()
};
assert!(m.is_two_sided());
}
#[test]
fn is_two_sided_pattern() {
let m = Material {
backface_policy: BackfacePolicy::Pattern(PatternConfig {
pattern: BackfacePattern::Hatching,
colour: [0.5, 0.5, 0.5],
..Default::default()
}),
..Default::default()
};
assert!(m.is_two_sided());
}
#[test]
fn param_vis_default() {
let pv = ParamVis::default();
assert_eq!(pv.mode, ParamVisMode::Checker);
assert!((pv.scale - 8.0).abs() < 1e-6);
}
#[test]
fn solid_delegates_to_from_colour() {
let colour = [1.0_f32, 0.0, 0.5];
let a = Material::solid(colour);
let b = Material::from_colour(colour);
assert!((a.base_colour[0] - b.base_colour[0]).abs() < 1e-6);
assert!((a.base_colour[1] - b.base_colour[1]).abs() < 1e-6);
assert!((a.base_colour[2] - b.base_colour[2]).abs() < 1e-6);
assert_eq!(a.is_pbr(), b.is_pbr());
assert_eq!(a.texture_id, b.texture_id);
assert_eq!(a.ao_map_id, b.ao_map_id);
assert!((a.ambient - b.ambient).abs() < 1e-6);
}
#[test]
fn textured_sets_texture_id() {
let m = Material::textured(42);
assert_eq!(m.texture_id, Some(42));
let def = Material::default();
assert!((m.base_colour[0] - def.base_colour[0]).abs() < 1e-6);
assert!(!m.is_pbr());
}
#[test]
fn pbr_with_ao_sets_fields() {
let m = Material::pbr_with_ao([0.8, 0.2, 0.1], 0.9, 0.3, Some(7));
assert!(m.is_pbr());
assert!((m.metallic - 0.9).abs() < 1e-6);
assert!((m.roughness - 0.3).abs() < 1e-6);
assert_eq!(m.ao_map_id, Some(7));
assert!((m.base_colour[0] - 0.8).abs() < 1e-6);
}
#[test]
fn pbr_with_ao_accepts_none() {
let m = Material::pbr_with_ao([0.5; 3], 0.0, 1.0, None);
assert_eq!(m.ao_map_id, None);
}
}