use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
use bevy::asset::Assets;
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::system::{Commands, Query, ResMut};
use bevy::image::Image;
use bevy::pbr::StandardMaterial;
use bevy::prelude::{AlphaMode, Color, Handle};
use bevy::render::render_resource::Face;
use crate::ashlar::AshlarConfig;
use crate::asphalt::AsphaltConfig;
use crate::async_gen::PendingTexture;
use crate::bark::BarkConfig;
use crate::brick::BrickConfig;
use crate::cache::{TextureCache, TextureCacheKey};
use crate::cobblestone::CobblestoneConfig;
use crate::concrete::ConcreteConfig;
use crate::corrugated::CorrugatedConfig;
use crate::encaustic::EncausticConfig;
use crate::generator::{map_to_images, map_to_images_card};
use crate::ground::GroundConfig;
use crate::iron_grille::IronGrilleConfig;
use crate::leaf::LeafConfig;
use crate::marble::MarbleConfig;
use crate::metal::MetalConfig;
use crate::pavers::PaversConfig;
use crate::plank::PlankConfig;
use crate::rock::RockConfig;
use crate::shingle::ShingleConfig;
use crate::stained_glass::StainedGlassConfig;
use crate::stucco::StuccoConfig;
use crate::thatch::ThatchConfig;
use crate::twig::TwigConfig;
use crate::wainscoting::WainscotingConfig;
use crate::window::WindowConfig;
#[derive(Copy, Clone, Debug)]
pub struct RenderProperties {
pub alpha_mode: AlphaMode,
pub double_sided: bool,
pub cull_mode: Option<Face>,
pub is_card: bool,
}
fn surface_render_properties() -> RenderProperties {
RenderProperties {
alpha_mode: AlphaMode::Opaque,
double_sided: false,
cull_mode: Some(Face::Back),
is_card: false,
}
}
fn card_render_properties() -> RenderProperties {
RenderProperties {
alpha_mode: AlphaMode::Mask(0.5),
double_sided: true,
cull_mode: None,
is_card: true,
}
}
macro_rules! define_texture_config {
($(($variant:ident, $module:ident, $config_ty:ty, $kind:ident)),* $(,)?) => {
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(tag = "$type")]
pub enum TextureConfig {
#[default]
None,
$(
#[doc = concat!("Procedural ", stringify!($variant), " generator config.")]
$variant($config_ty)
),*,
}
impl TextureConfig {
pub fn label(&self) -> &'static str {
match self {
Self::None => "None",
$(Self::$variant(_) => stringify!($variant)),*,
}
}
pub fn render_properties(&self) -> RenderProperties {
match self {
Self::None => surface_render_properties(),
$(Self::$variant(_) => kind_to_render_properties(TextureKind::$kind)),*,
}
}
pub fn spawn(&self, width: u32, height: u32) -> Option<PendingTexture> {
match self {
Self::None => None,
$(Self::$variant(c) =>
Some(PendingTexture::$module(c.clone(), width, height))),*,
}
}
pub fn fingerprint(&self) -> u64 {
let mut h = DefaultHasher::new();
self.label().hash(&mut h);
match self {
Self::None => {}
$(Self::$variant(c) => format!("{c:?}").hash(&mut h)),*,
}
h.finish()
}
}
};
}
enum TextureKind {
Surface,
Card,
}
fn kind_to_render_properties(kind: TextureKind) -> RenderProperties {
match kind {
TextureKind::Surface => surface_render_properties(),
TextureKind::Card => card_render_properties(),
}
}
define_texture_config!(
(Leaf, leaf, LeafConfig, Card),
(Twig, twig, TwigConfig, Card),
(Bark, bark, BarkConfig, Surface),
(Window, window, WindowConfig, Card),
(StainedGlass, stained_glass, StainedGlassConfig, Card),
(IronGrille, iron_grille, IronGrilleConfig, Card),
(Ground, ground, GroundConfig, Surface),
(Rock, rock, RockConfig, Surface),
(Brick, brick, BrickConfig, Surface),
(Plank, plank, PlankConfig, Surface),
(Shingle, shingle, ShingleConfig, Surface),
(Stucco, stucco, StuccoConfig, Surface),
(Concrete, concrete, ConcreteConfig, Surface),
(Metal, metal, MetalConfig, Surface),
(Pavers, pavers, PaversConfig, Surface),
(Ashlar, ashlar, AshlarConfig, Surface),
(Cobblestone, cobblestone, CobblestoneConfig, Surface),
(Thatch, thatch, ThatchConfig, Surface),
(Marble, marble, MarbleConfig, Surface),
(Corrugated, corrugated, CorrugatedConfig, Surface),
(Asphalt, asphalt, AsphaltConfig, Surface),
(Wainscoting, wainscoting, WainscotingConfig, Surface),
(Encaustic, encaustic, EncausticConfig, Surface),
);
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct MaterialSettings {
pub base_color: [f32; 3],
pub emission_color: [f32; 3],
pub emission_strength: f32,
pub roughness: f32,
pub metallic: f32,
pub uv_scale: f32,
#[serde(default)]
pub texture: TextureConfig,
}
impl Default for MaterialSettings {
fn default() -> Self {
Self {
base_color: [0.6, 0.4, 0.2],
emission_color: [0.0, 0.0, 0.0],
emission_strength: 0.0,
roughness: 0.5,
metallic: 0.0,
uv_scale: 1.0,
texture: TextureConfig::None,
}
}
}
#[derive(Component)]
pub struct PatchMaterialTextures {
pub target: Handle<StandardMaterial>,
pub cache_key: Option<TextureCacheKey>,
}
pub fn build_procedural_material_async(
commands: &mut Commands,
materials: &mut Assets<StandardMaterial>,
images: &mut Assets<Image>,
cache: Option<&mut TextureCache>,
settings: &MaterialSettings,
width: u32,
height: u32,
) -> Handle<StandardMaterial> {
let props = settings.texture.render_properties();
let emissive =
Color::srgb_from_array(settings.emission_color).to_linear() * settings.emission_strength;
let mut material = StandardMaterial {
base_color: Color::srgb_from_array(settings.base_color),
perceptual_roughness: settings.roughness,
metallic: settings.metallic,
emissive,
alpha_mode: props.alpha_mode,
double_sided: props.double_sided,
cull_mode: props.cull_mode,
..Default::default()
};
let cache_key = if matches!(settings.texture, TextureConfig::None) {
None
} else {
Some(TextureCacheKey {
kind: settings.texture.label(),
fingerprint: settings.texture.fingerprint(),
width,
height,
})
};
if let (Some(key), Some(cache_ref)) = (cache_key.as_ref(), cache.as_deref())
&& let Some(handles) = cache_ref.get_handles(key)
{
material.base_color_texture = Some(handles.albedo.clone());
material.normal_map_texture = Some(handles.normal.clone());
material.metallic_roughness_texture = Some(handles.roughness.clone());
return materials.add(material);
}
let handle = materials.add(material);
if let Some(pending) = settings.texture.spawn(width, height) {
commands.spawn((
pending,
PatchMaterialTextures {
target: handle.clone(),
cache_key,
},
));
}
let _ = images;
handle
}
pub fn patch_procedural_material_textures(
mut commands: Commands,
tasks: Query<(Entity, &PendingTexture, &PatchMaterialTextures)>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
mut cache: Option<ResMut<TextureCache>>,
) {
use std::sync::mpsc::TryRecvError;
for (entity, pending, patch) in &tasks {
let poll = pending
.rx
.lock()
.expect("texture thread poisoned")
.try_recv();
match poll {
Ok(Ok(map)) => {
let is_card = pending.is_card();
let handles = if is_card {
map_to_images_card(map, &mut images)
} else {
map_to_images(map, &mut images)
};
if let Some(cache_ref) = cache.as_deref_mut()
&& let Some(key) = patch.cache_key.clone()
{
cache_ref.insert(key, Arc::new(handles.clone()));
}
if let Some(mat) = materials.get_mut(&patch.target) {
mat.base_color_texture = Some(handles.albedo);
mat.normal_map_texture = Some(handles.normal);
mat.metallic_roughness_texture = Some(handles.roughness);
}
commands.entity(entity).despawn();
}
Ok(Err(e)) => {
bevy::log::error!("Procedural material texture generation failed: {e}");
commands.entity(entity).despawn();
}
Err(TryRecvError::Disconnected) => {
bevy::log::error!("Procedural material texture thread panicked");
commands.entity(entity).despawn();
}
Err(TryRecvError::Empty) => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn labels_are_unique_and_non_empty() {
let configs = [
TextureConfig::None,
TextureConfig::Bark(BarkConfig::default()),
TextureConfig::Leaf(LeafConfig::default()),
TextureConfig::Brick(BrickConfig::default()),
TextureConfig::StainedGlass(StainedGlassConfig::default()),
TextureConfig::IronGrille(IronGrilleConfig::default()),
];
let mut seen = std::collections::HashSet::new();
for cfg in &configs {
let label = cfg.label();
assert!(!label.is_empty());
assert!(seen.insert(label), "duplicate label: {label}");
}
}
#[test]
fn fingerprint_is_stable_and_distinguishes_seeds() {
let a = TextureConfig::Bark(BarkConfig::default());
let b = TextureConfig::Bark(BarkConfig::default());
assert_eq!(a.fingerprint(), b.fingerprint());
let mut differ = BarkConfig::default();
differ.seed = differ.seed.wrapping_add(1);
let c = TextureConfig::Bark(differ);
assert_ne!(a.fingerprint(), c.fingerprint());
}
#[test]
fn render_properties_match_kind() {
let card = TextureConfig::Leaf(LeafConfig::default()).render_properties();
assert!(card.is_card);
assert!(card.double_sided);
assert!(card.cull_mode.is_none());
let surface = TextureConfig::Bark(BarkConfig::default()).render_properties();
assert!(!surface.is_card);
assert!(!surface.double_sided);
assert!(matches!(surface.cull_mode, Some(Face::Back)));
}
#[test]
fn spawn_returns_none_for_none_variant() {
assert!(TextureConfig::None.spawn(8, 8).is_none());
assert!(
TextureConfig::Bark(BarkConfig::default())
.spawn(8, 8)
.is_some()
);
}
}