use bevy::asset::Handle;
use bevy::ecs::component::Component;
use bevy::image::Image;
use bevy::pbr::StandardMaterial;
use crate::material::PlumeSplatSettings;
#[derive(Debug, Clone)]
pub struct MaterialLayer {
pub albedo: Handle<Image>,
pub normal: Option<Handle<Image>>,
pub pbr: Option<Handle<Image>>,
}
impl MaterialLayer {
#[must_use]
pub fn new(albedo: Handle<Image>) -> Self {
Self {
albedo,
normal: None,
pbr: None,
}
}
#[must_use]
pub fn with_normal(mut self, normal: Handle<Image>) -> Self {
self.normal = Some(normal);
self
}
#[must_use]
pub fn with_pbr(mut self, pbr: Handle<Image>) -> Self {
self.pbr = Some(pbr);
self
}
#[must_use]
pub fn has_normal(&self) -> bool {
self.normal.is_some()
}
#[must_use]
pub fn has_pbr(&self) -> bool {
self.pbr.is_some()
}
pub fn all_handles(&self) -> Vec<&Handle<Image>> {
let mut handles = vec![&self.albedo];
if let Some(ref normal) = self.normal {
handles.push(normal);
}
if let Some(ref pbr) = self.pbr {
handles.push(pbr);
}
handles
}
}
#[derive(Debug, Clone, Default)]
pub struct SplatMaterialBuilder {
layers: Vec<MaterialLayer>,
settings: PlumeSplatSettings,
base_material: StandardMaterial,
}
impl SplatMaterialBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add_layer(mut self, layer: MaterialLayer) -> Self {
assert!(
self.layers.len() < 256,
"Cannot add more than 256 material layers"
);
self.layers.push(layer);
self
}
#[must_use]
pub fn add_layers(mut self, layers: impl IntoIterator<Item = MaterialLayer>) -> Self {
for layer in layers {
self = self.add_layer(layer);
}
self
}
#[must_use]
pub fn with_uv_scale(mut self, scale: f32) -> Self {
self.settings.uv_scale = scale;
self
}
#[must_use]
pub fn with_triplanar_sharpness(mut self, sharpness: f32) -> Self {
self.settings.triplanar_sharpness = sharpness;
self
}
#[must_use]
pub fn with_height_blending(mut self, sharpness: f32) -> Self {
self.settings.height_blend_sharpness = sharpness;
self
}
#[must_use]
pub fn with_blend_offset(mut self, offset: f32) -> Self {
self.settings.blend_offset = offset.clamp(0.0, 0.5);
self
}
#[must_use]
pub fn with_blend_exponent(mut self, exponent: f32) -> Self {
self.settings.blend_exponent = exponent.clamp(1.0, 8.0);
self
}
#[must_use]
pub fn with_base_material(mut self, base: StandardMaterial) -> Self {
self.base_material = base;
self
}
#[must_use]
pub fn layer_count(&self) -> usize {
self.layers.len()
}
#[must_use]
pub fn has_any_normals(&self) -> bool {
self.layers.iter().any(MaterialLayer::has_normal)
}
#[must_use]
pub fn has_any_pbr(&self) -> bool {
self.layers.iter().any(MaterialLayer::has_pbr)
}
#[must_use]
pub fn build(self) -> PendingSplatMaterial {
assert!(!self.layers.is_empty(), "At least one material layer is required");
PendingSplatMaterial {
layers: self.layers,
settings: self.settings,
base_material: self.base_material,
}
}
}
#[derive(Component, Debug, Clone)]
pub struct PendingSplatMaterial {
pub layers: Vec<MaterialLayer>,
pub settings: PlumeSplatSettings,
pub base_material: StandardMaterial,
}
impl PendingSplatMaterial {
#[must_use]
pub fn is_ready(&self, images: &bevy::asset::Assets<Image>) -> bool {
self.layers
.iter()
.flat_map(MaterialLayer::all_handles)
.all(|handle| images.contains(handle.id()))
}
#[must_use]
pub fn total_texture_count(&self) -> usize {
self.layers
.iter()
.map(|layer| layer.all_handles().len())
.sum()
}
#[must_use]
pub fn loaded_texture_count(&self, images: &bevy::asset::Assets<Image>) -> usize {
self.layers
.iter()
.flat_map(MaterialLayer::all_handles)
.filter(|handle| images.contains(handle.id()))
.count()
}
#[must_use]
pub fn layer_count(&self) -> usize {
self.layers.len()
}
#[must_use]
pub fn has_normals(&self) -> bool {
self.layers.iter().any(MaterialLayer::has_normal)
}
#[must_use]
pub fn has_pbr(&self) -> bool {
self.layers.iter().any(MaterialLayer::has_pbr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::asset::Handle;
#[test]
fn test_material_layer_new() {
let handle = Handle::default();
let layer = MaterialLayer::new(handle.clone());
assert!(!layer.has_normal());
assert!(!layer.has_pbr());
assert_eq!(layer.all_handles().len(), 1);
}
#[test]
fn test_material_layer_with_all_textures() {
let albedo = Handle::default();
let normal = Handle::default();
let pbr = Handle::default();
let layer = MaterialLayer::new(albedo)
.with_normal(normal)
.with_pbr(pbr);
assert!(layer.has_normal());
assert!(layer.has_pbr());
assert_eq!(layer.all_handles().len(), 3);
}
#[test]
fn test_builder_empty() {
let builder = SplatMaterialBuilder::new();
assert_eq!(builder.layer_count(), 0);
assert!(!builder.has_any_normals());
assert!(!builder.has_any_pbr());
}
#[test]
fn test_builder_add_layers() {
let builder = SplatMaterialBuilder::new()
.add_layer(MaterialLayer::new(Handle::default()))
.add_layer(MaterialLayer::new(Handle::default()))
.add_layer(MaterialLayer::new(Handle::default()));
assert_eq!(builder.layer_count(), 3);
}
#[test]
fn test_builder_settings() {
let pending = SplatMaterialBuilder::new()
.add_layer(MaterialLayer::new(Handle::default()))
.with_uv_scale(2.0)
.with_triplanar_sharpness(8.0)
.with_height_blending(0.5)
.with_blend_offset(0.2)
.with_blend_exponent(3.0)
.build();
assert_eq!(pending.settings.uv_scale, 2.0);
assert_eq!(pending.settings.triplanar_sharpness, 8.0);
assert_eq!(pending.settings.height_blend_sharpness, 0.5);
assert_eq!(pending.settings.blend_offset, 0.2);
assert_eq!(pending.settings.blend_exponent, 3.0);
}
#[test]
fn test_builder_clamps_values() {
let pending = SplatMaterialBuilder::new()
.add_layer(MaterialLayer::new(Handle::default()))
.with_blend_offset(1.0) .with_blend_exponent(10.0) .build();
assert_eq!(pending.settings.blend_offset, 0.5);
assert_eq!(pending.settings.blend_exponent, 8.0);
}
#[test]
#[should_panic(expected = "At least one material layer is required")]
fn test_builder_panics_with_no_layers() {
let _ = SplatMaterialBuilder::new().build();
}
#[test]
fn test_pending_material_counts() {
let pending = SplatMaterialBuilder::new()
.add_layer(
MaterialLayer::new(Handle::default())
.with_normal(Handle::default())
)
.add_layer(
MaterialLayer::new(Handle::default())
.with_normal(Handle::default())
.with_pbr(Handle::default())
)
.build();
assert_eq!(pending.layer_count(), 2);
assert_eq!(pending.total_texture_count(), 5); assert!(pending.has_normals());
assert!(pending.has_pbr());
}
}