bevy_symbios_texture 0.6.0

Algorithmic texture generator for Bevy.
Documentation
//! Brick texture generator using grid-based SDF with per-cell hashing.
//!
//! The algorithm:
//! 1. Scale UV into a brick grid (`u * cols`, `v * rows`).
//! 2. Offset each row by `row_id × row_offset` to create bonding patterns.
//! 3. Compute a rounded-box SDF in cell-local space to separate brick from mortar.
//! 4. Hash each cell's integer ID to derive a per-brick colour variance.
//! 5. Blend toroidal surface-roughness FBM into the height field for micro-detail.

use noise::{Fbm, MultiFractal, Perlin};

use crate::{
    generator::{TextureError, TextureGenerator, TextureMap, Workspace, validate_dimensions},
    noise::{ToroidalNoise, normalize, sample_grid_into},
    surface::{SurfaceCell, SurfaceSample, generate_surface},
};

/// Configures the appearance of a [`BrickGenerator`].
///
/// For perfect vertical tiling the product `scale × row_offset` must be an
/// integer.  The default values (`scale = 4, row_offset = 0.5`) satisfy this
/// constraint (product = 2).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BrickConfig {
    /// PRNG seed for the deterministic noise pattern; different seeds give
    /// statistically-different textures from otherwise-identical configs.
    pub seed: u32,
    /// Number of brick rows across the tile (controls coarseness).
    pub scale: f64,
    /// Lateral offset per row as a fraction of brick width.
    /// `0.0` = stack bond, `0.5` = running bond, `0.333` = third bond.
    pub row_offset: f64,
    /// Brick width-to-height ratio (e.g. `2.0` = standard 2:1 brick).
    pub aspect_ratio: f64,
    /// Mortar gap as a fraction of cell height \[0, 0.4\].
    pub mortar_size: f64,
    /// Corner bevel radius as a fraction of `mortar_size` \[0, 1\].
    pub bevel: f64,
    /// Per-brick colour jitter \[0, 1\].  `0.0` = uniform, `1.0` = highly varied.
    pub cell_variance: f64,
    /// Surface pitting / roughness noise intensity \[0, 1\].
    pub roughness: f64,
    /// Brick face colour in linear RGB \[0, 1\].
    pub color_brick: [f32; 3],
    /// Mortar colour in linear RGB \[0, 1\].
    pub color_mortar: [f32; 3],
    /// Normal-map strength.
    pub normal_strength: f32,
}

impl Default for BrickConfig {
    fn default() -> Self {
        Self {
            seed: 42,
            scale: 4.0,
            row_offset: 0.5,
            aspect_ratio: 2.0,
            mortar_size: 0.05,
            bevel: 0.5,
            cell_variance: 0.15,
            roughness: 0.5,
            color_brick: [0.56, 0.28, 0.18],
            color_mortar: [0.76, 0.73, 0.67],
            normal_strength: 4.0,
        }
    }
}

/// Procedural brick-wall texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`BrickConfig`].  Construct
/// via [`BrickGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::brick`] task for non-blocking generation.
///
/// Noise objects are built in the constructor so that calling `generate`
/// multiple times (e.g. producing size variants of the same material)
/// does not repeat the initialisation cost.
pub struct BrickGenerator {
    config: BrickConfig,
    rough_noise: ToroidalNoise<Fbm<Perlin>>,
}

impl BrickGenerator {
    /// Create a new generator with the given configuration.
    ///
    /// Builds the noise objects up front so that repeated
    /// calls to [`generate`](TextureGenerator::generate) skip initialisation.
    pub fn new(config: BrickConfig) -> Self {
        let fbm_rough: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(50)).set_octaves(4);
        let rough_noise = ToroidalNoise::new(fbm_rough, config.scale * config.aspect_ratio * 2.0);

        Self {
            config,
            rough_noise,
        }
    }
}

/// Per-generation sampler: pitting grid + derived bond-layout constants.
struct BrickCell<'a> {
    config: &'a BrickConfig,
    rough_grid: &'a [f64],
    /// Bevel radius in cell-fraction space.
    bevel_r: f64,
    /// Inner half-extents for the rounded-box SDF.
    hx: f64,
    hy: f64,
    /// Integer row / column counts so the grid tiles exactly.
    scale: f64,
    cols: f64,
    width: usize,
}

impl SurfaceCell for BrickCell<'_> {
    fn sample(&self, x: u32, y: u32, u: f64, v: f64) -> SurfaceSample {
        let c = self.config;

        let v_scaled = v * self.scale;
        let row_id = v_scaled.floor();
        let v_frac = v_scaled.fract();

        let u_shifted = u * self.cols + row_id * c.row_offset;
        let brick_id_u = u_shifted.floor() as i64;
        let brick_id_v = row_id as i64;
        let u_frac = u_shifted.fract();

        // Cell-centered coordinates in [-0.5, 0.5].
        let cx = u_frac - 0.5;
        let cy = v_frac - 0.5;

        // Rounded-box SDF: negative inside brick, positive in mortar.
        let dx = cx.abs() - self.hx;
        let dy = cy.abs() - self.hy;
        let sdf =
            (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt() + dx.max(dy).min(0.0) - self.bevel_r;

        let raw_surf = normalize(self.rough_grid[y as usize * self.width + x as usize]);

        let (h_val, color) = if sdf < 0.0 {
            // Inside brick: bevel ramp + surface roughness.
            let edge_t = ((-sdf) / (self.bevel_r + 0.01)).clamp(0.0, 1.0);
            let noise_bump = (raw_surf - 0.5) * c.roughness * 0.4;
            let h_val = (edge_t + noise_bump * edge_t).clamp(0.0, 1.0);

            // Per-brick colour variance via integer cell hash.
            let cv = cell_hash(brick_id_u, brick_id_v, c.seed);
            let jitter = (cv - 0.5) * 2.0 * c.cell_variance;
            let color = [
                (c.color_brick[0] + jitter as f32).clamp(0.0, 1.0),
                (c.color_brick[1] + jitter as f32 * 0.7).clamp(0.0, 1.0),
                (c.color_brick[2] + jitter as f32 * 0.5).clamp(0.0, 1.0),
            ];
            (h_val, color)
        } else {
            // Mortar gap: subtle texture.
            (raw_surf * c.roughness * 0.04, c.color_mortar)
        };

        // ORM: roughness higher in mortar, lower on smooth brick.
        let rough_val = if sdf < 0.0 {
            0.45 + raw_surf as f32 * 0.3
        } else {
            0.90
        };

        SurfaceSample::matte(h_val, color, rough_val)
    }
}

impl BrickGenerator {
    fn generate_inner(
        &self,
        width: u32,
        height: u32,
        mut ws: Option<&mut Workspace>,
    ) -> Result<TextureMap, TextureError> {
        validate_dimensions(width, height)?;
        let c = &self.config;

        // Toroidal surface-roughness FBM for pitting detail.
        let mut rough_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
        sample_grid_into(&self.rough_noise, width, height, &mut rough_grid);

        // Bevel radius in cell-fraction space.
        let bevel_r = (c.bevel * c.mortar_size * 0.5).max(0.0);
        // Both row count and column count must be integers for the grid to tile.
        let scale = c.scale.round();
        let cell = BrickCell {
            config: c,
            rough_grid: &rough_grid,
            bevel_r,
            hx: (0.5 - c.mortar_size - bevel_r).max(0.0),
            hy: (0.5 - c.mortar_size - bevel_r).max(0.0),
            scale,
            cols: (scale * c.aspect_ratio).round(),
            width: width as usize,
        };
        let result = generate_surface(width, height, c.normal_strength, ws.as_deref_mut(), &cell);

        if let Some(ws) = ws {
            ws.return_grid(rough_grid);
        }
        result
    }
}

impl TextureGenerator for BrickGenerator {
    fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
        self.generate_inner(width, height, None)
    }

    fn generate_with_workspace(
        &self,
        width: u32,
        height: u32,
        workspace: &mut Workspace,
    ) -> Result<TextureMap, TextureError> {
        self.generate_inner(width, height, Some(workspace))
    }
}

// --- helpers ----------------------------------------------------------------

/// Deterministic integer hash → \[0, 1\].  Produces per-brick colour jitter
/// with good distribution and no visible lattice patterns.
fn cell_hash(bx: i64, by: i64, seed: u32) -> f64 {
    let mut h = seed as u64;
    h ^= (bx as u64).wrapping_mul(6_364_136_223_846_793_005);
    h ^= (by as u64).wrapping_mul(1_442_695_040_888_963_407);
    h ^= h >> 33;
    h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
    h ^= h >> 33;
    (h as f64) * (1.0 / u64::MAX as f64)
}