bevy_symbios_texture 0.6.0

Algorithmic texture generator for Bevy.
Documentation
//! Bark texture generator using domain-warped FBM noise.
//!
//! The algorithm:
//!  1. Precompute toroidal sin/cos lookup tables (one entry per column, one per row).
//!  2. For each pixel, sample two FBM warp layers inline to produce offsets (du, dv).
//!  3. Sample the precomputed base FBM grid via bilinear interpolation at the warped UV coordinates for the final value.
//!  4. Derive colour, roughness and a height field from the result.
//!
//! The warp layers are computed inline (no intermediate grids).  The base FBM
//! layer is precomputed into a W×H grid (~536 MB at 8 K) once, then sampled
//! via bilinear interpolation at the warped coordinates.  This trades one
//! allocation for the elimination of O(W×H) `sin`/`cos` calls that would
//! otherwise occur when evaluating the toroidal base noise at arbitrary warped
//! positions.

use std::f64::consts::TAU;

use noise::core::worley::ReturnType;
use noise::{Fbm, MultiFractal, NoiseFn, Perlin, Worley};
use rayon::prelude::*;

use crate::{
    generator::{
        TextureError, TextureGenerator, TextureMap, Workspace, linear_to_srgb, validate_dimensions,
    },
    noise::{ToroidalNoise, bilinear_sample_torus, normalize, sample_grid_into},
    normal::{BoundaryMode, height_to_normal},
    surface::lerp,
};

/// Serde default for [`BarkConfig::warp_octaves`] — keeps configs saved
/// before the field existed deserialisable.
fn default_warp_octaves() -> usize {
    3
}

/// Configures the appearance of a [`BarkGenerator`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BarkConfig {
    /// PRNG seed for the deterministic noise pattern; different seeds give
    /// statistically-different textures from otherwise-identical configs.
    pub seed: u32,
    /// Overall spatial scale of the bark pattern.
    pub scale: f64,
    /// Octaves for the base FBM layer.
    pub octaves: usize,
    /// Octaves for the two warp FBM layers.  Warp output only displaces UV
    /// lookups into the base grid, so detail beyond ~3 octaves is visually
    /// invisible — and the warp layers dominate per-pixel cost.
    #[serde(default = "default_warp_octaves")]
    pub warp_octaves: usize,
    /// Horizontal warp strength (small — creates slight lateral texture).
    pub warp_u: f64,
    /// Vertical warp strength (large — creates the fibrous streaks).
    pub warp_v: f64,
    /// Base (light) bark colour in linear RGB \[0, 1\].
    pub color_light: [f32; 3],
    /// Dark groove colour in linear RGB \[0, 1\].
    pub color_dark: [f32; 3],
    /// Normal map strength.
    pub normal_strength: f32,
    /// Blend weight of the rhytidome furrow layer \[0, 1\].  0 = pure FBM fibre,
    /// 1 = pure Worley plates.
    pub furrow_multiplier: f64,
    /// Horizontal frequency of the Worley cells (higher = narrower plates).
    pub furrow_scale_u: f64,
    /// Vertical frequency of the Worley cells (lower = longer vertical plates).
    pub furrow_scale_v: f64,
    /// Power applied to the normalised plate height.  Values < 1 fatten the
    /// plates and sharpen the V-shaped cracks between them.
    pub furrow_shape: f64,
}

impl Default for BarkConfig {
    fn default() -> Self {
        Self {
            seed: 42,
            scale: 2.0,
            octaves: 6,
            warp_octaves: 3,
            warp_u: 0.15,
            warp_v: 0.55,
            color_light: [0.45, 0.28, 0.14],
            color_dark: [0.09, 0.05, 0.03],
            normal_strength: 3.0,
            furrow_multiplier: 0.78,
            furrow_scale_u: 2.0,
            furrow_scale_v: 0.48,
            furrow_shape: 2.0,
        }
    }
}

/// Procedural bark texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`BarkConfig`].  Construct
/// via [`BarkGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::bark`] 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.  Worley noise is still
/// constructed in `generate_inner()` because `Worley` is `!Send`.
pub struct BarkGenerator {
    config: BarkConfig,
    warp_u_noise: ToroidalNoise<Fbm<Perlin>>,
    warp_v_noise: ToroidalNoise<Fbm<Perlin>>,
    base_noise: ToroidalNoise<Fbm<Perlin>>,
}

impl BarkGenerator {
    /// 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: BarkConfig) -> Self {
        let fbm_warp_u: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(config.warp_octaves);
        let fbm_warp_v: Fbm<Perlin> =
            Fbm::new(config.seed.wrapping_add(100)).set_octaves(config.warp_octaves);
        let fbm_base: Fbm<Perlin> =
            Fbm::new(config.seed.wrapping_add(200)).set_octaves(config.octaves);
        let warp_u_noise = ToroidalNoise::new(fbm_warp_u, config.scale);
        let warp_v_noise = ToroidalNoise::new(fbm_warp_v, config.scale);
        let base_noise = ToroidalNoise::new(fbm_base, config.scale);
        Self {
            config,
            warp_u_noise,
            warp_v_noise,
            base_noise,
        }
    }
}

impl BarkGenerator {
    /// Core generation logic.  When `ws` is `Some`, borrows the base grid
    /// buffer from the workspace to avoid a fresh 128 MB allocation at 4K.
    /// Reuses workspace buffers across calls so that generating multiple
    /// size variants does not allocate new backing storage each time.
    fn generate_inner(
        &self,
        width: u32,
        height: u32,
        mut ws: Option<&mut Workspace>,
    ) -> Result<TextureMap, TextureError> {
        validate_dimensions(width, height)?;
        let c = &self.config;

        let w = width as usize;
        let h = height as usize;
        let n = w * h;

        // Precompute toroidal coordinates (W + H entries instead of W × H).
        let freq = c.scale;
        let col_cos: Vec<f64> = (0..w)
            .map(|x| (TAU * x as f64 / w as f64).cos() * freq)
            .collect();
        let col_sin: Vec<f64> = (0..w)
            .map(|x| (TAU * x as f64 / w as f64).sin() * freq)
            .collect();
        let row_cos: Vec<f64> = (0..h)
            .map(|y| (TAU * y as f64 / h as f64).cos() * freq)
            .collect();
        let row_sin: Vec<f64> = (0..h)
            .map(|y| (TAU * y as f64 / h as f64).sin() * freq)
            .collect();

        // Anisotropic torus tables for the Worley furrow layer.
        let f_freq_u = c.scale * c.furrow_scale_u;
        let f_freq_v = c.scale * c.furrow_scale_v;
        let f_col_cos: Vec<f64> = (0..w)
            .map(|x| (TAU * x as f64 / w as f64).cos() * f_freq_u)
            .collect();
        let f_col_sin: Vec<f64> = (0..w)
            .map(|x| (TAU * x as f64 / w as f64).sin() * f_freq_u)
            .collect();
        let f_row_cos: Vec<f64> = (0..h)
            .map(|y| (TAU * y as f64 / h as f64).cos() * f_freq_v)
            .collect();
        let f_row_sin: Vec<f64> = (0..h)
            .map(|y| (TAU * y as f64 / h as f64).sin() * f_freq_v)
            .collect();

        // Precompute the base noise on a regular grid using the torus LUTs.
        let mut base_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
        sample_grid_into(&self.base_noise, width, height, &mut base_grid);

        let mut heights = vec![0.0f64; n];
        let mut albedo = vec![0u8; n * 4];
        let mut roughness = vec![0u8; n * 4];

        heights
            .par_chunks_mut(w)
            .zip(albedo.par_chunks_mut(w * 4))
            .zip(roughness.par_chunks_mut(w * 4))
            .enumerate()
            .for_each(|(y, ((height_row, albedo_row), orm_row))| {
                // Worley noise for the rhytidome plates.  `noise::Worley`
                // holds an `Rc` and is `!Sync`, so each row constructs its
                // own instance — deterministic from the seed and only a few
                // microseconds each, which vanishes against the per-row FBM
                // cost.
                let worley =
                    Worley::new(c.seed.wrapping_add(300)).set_return_type(ReturnType::Distance);

                let nz = row_cos[y];
                let nw = row_sin[y];
                let v = y as f64 / h as f64;

                let f_nz = f_row_cos[y];
                let f_nw = f_row_sin[y];

                for (x, height_slot) in height_row.iter_mut().enumerate() {
                    let nx = col_cos[x];
                    let ny = col_sin[x];
                    let u = x as f64 / w as f64;

                    // Compute warp offsets using precomputed torus coordinates.
                    let du = self.warp_u_noise.get_precomputed(nx, ny, nz, nw) * c.warp_u;
                    let dv = self.warp_v_noise.get_precomputed(nx, ny, nz, nw) * c.warp_v;

                    // Sample the precomputed base grid at the warped UV coordinates.
                    // Bilinear interpolation wraps toroidally — no trig per pixel.
                    let raw = bilinear_sample_torus(&base_grid, w, h, u + du, v + dv);
                    let t = normalize(raw); // [0, 1]

                    // --- Worley rhytidome plates ---
                    // Sample anisotropic Worley on a 4D torus: U-axis uses high
                    // frequency (narrow plates), V-axis uses low frequency (tall plates).
                    let f_nx = f_col_cos[x];
                    let f_ny = f_col_sin[x];
                    let furrow_raw = worley.get([f_nx, f_ny, f_nz, f_nw]);
                    // Invert: boundaries (furrow_raw ≈ 1) → 0 (deep crack);
                    //         centres  (furrow_raw ≈ -1) → 1 (raised plate).
                    let furrow_norm = (0.5 - furrow_raw * 0.5).clamp(0.0, 1.0);
                    // powf < 1 widens the plateau and keeps cracks narrow and sharp.
                    let plate_height = furrow_norm.powf(c.furrow_shape);

                    // Blend fibrous FBM micro-detail with macro rhytidome plates.
                    let t_final =
                        t * (1.0 - c.furrow_multiplier) + plate_height * c.furrow_multiplier;

                    *height_slot = t_final;

                    // Colour: lerp between dark and light by height value.
                    let r = lerp(c.color_dark[0], c.color_light[0], t as f32);
                    let g = lerp(c.color_dark[1], c.color_light[1], t as f32);
                    let b = lerp(c.color_dark[2], c.color_light[2], t as f32);

                    let ai = x * 4;
                    albedo_row[ai] = linear_to_srgb(r);
                    albedo_row[ai + 1] = linear_to_srgb(g);
                    albedo_row[ai + 2] = linear_to_srgb(b);
                    albedo_row[ai + 3] = 255;

                    // Roughness: grooves (dark, low t) are rougher.
                    // Packed as ORM: R=Occlusion(1.0), G=Roughness, B=Metallic(0.0).
                    let rough = 0.6 + (1.0 - t as f32) * 0.35;
                    orm_row[ai] = 255; // Occlusion = 1.0 (no shadowing)
                    orm_row[ai + 1] = (rough * 255.0).round() as u8;
                    orm_row[ai + 2] = 0; // Metallic = 0.0
                    orm_row[ai + 3] = 255;
                }
            });

        // Return grid buffer to the workspace for reuse.
        if let Some(ws) = ws {
            ws.return_grid(base_grid);
        }

        let normal = height_to_normal(
            &heights,
            width,
            height,
            c.normal_strength,
            BoundaryMode::Wrap,
        );

        Ok(TextureMap {
            albedo,
            normal,
            roughness,
            width,
            height,
            mip_level_count: 1,
            emissive: None,
        })
    }
}

impl TextureGenerator for BarkGenerator {
    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))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Configs serialised before `warp_octaves` existed (pre-0.6) must still
    /// deserialise, receiving the serde default of 3.
    #[test]
    fn legacy_configs_without_warp_octaves_deserialize() {
        let legacy = r#"{
            "seed": 42, "scale": 2.0, "octaves": 6,
            "warp_u": 0.15, "warp_v": 0.55,
            "color_light": [0.45, 0.28, 0.14],
            "color_dark": [0.09, 0.05, 0.03],
            "normal_strength": 3.0,
            "furrow_multiplier": 0.78,
            "furrow_scale_u": 2.0,
            "furrow_scale_v": 0.48,
            "furrow_shape": 2.0
        }"#;
        let config: BarkConfig = serde_json::from_str(legacy).expect("legacy config loads");
        assert_eq!(config.warp_octaves, 3);
    }
}