nightshade 0.48.0

A cross-platform data-oriented game engine.
Documentation
//! Procedurally generated sprite textures for VFX particles.
//!
//! The particle renderer multiplies a particle's gradient color by its texture,
//! so these textures are white with a shaped alpha channel: the gradient
//! provides color, the alpha provides the silhouette. [`upload_vfx_textures`]
//! generates and uploads them once; presets reference the slots by constant.

use crate::ecs::particles::components::ParticleTextureUpload;
use crate::ecs::world::World;

/// Soft round glow: a bright core with a smooth falloff. Fire, plasma, orbs.
pub const GLOW_SLOT: u32 = 1;
/// Four-point sparkle: a tight core with cross streaks. Sparks, embers, magic.
pub const FLARE_SLOT: u32 = 2;
/// Wispy noise cloud with soft edges. Smoke, dust, mist, snow.
pub const SMOKE_SLOT: u32 = 3;

const SIZE: u32 = 512;

/// Generates the built-in particle sprites and queues them for GPU upload.
/// Call once during setup before spawning textured effects.
pub fn upload_vfx_textures(world: &mut World) {
    upload(world, GLOW_SLOT, glow_texture());
    upload(world, FLARE_SLOT, flare_texture());
    upload(world, SMOKE_SLOT, smoke_texture());
}

fn upload(world: &mut World, slot: u32, rgba_data: Vec<u8>) {
    world
        .resources
        .loading
        .pending_particle_textures
        .push(ParticleTextureUpload {
            slot,
            rgba_data,
            width: SIZE,
            height: SIZE,
        });
}

fn build(alpha: impl Fn(f32, f32) -> f32) -> Vec<u8> {
    let mut data = vec![0u8; (SIZE * SIZE * 4) as usize];
    let half = SIZE as f32 * 0.5;
    for y in 0..SIZE {
        for x in 0..SIZE {
            let nx = (x as f32 + 0.5 - half) / half;
            let ny = (y as f32 + 0.5 - half) / half;
            let a = alpha(nx, ny).clamp(0.0, 1.0);
            let index = ((y * SIZE + x) * 4) as usize;
            data[index] = 255;
            data[index + 1] = 255;
            data[index + 2] = 255;
            data[index + 3] = (a * 255.0) as u8;
        }
    }
    data
}

fn glow_texture() -> Vec<u8> {
    build(|nx, ny| {
        let distance = (nx * nx + ny * ny).sqrt();
        let edge = (1.0 - distance).clamp(0.0, 1.0);
        (-distance * distance * 4.5).exp() * edge * edge
    })
}

fn flare_texture() -> Vec<u8> {
    build(|nx, ny| {
        let distance = (nx * nx + ny * ny).sqrt();
        let core = (-distance * distance * 26.0).exp();
        let horizontal = (-ny.abs() * 26.0).exp() * (1.0 - nx.abs()).clamp(0.0, 1.0);
        let vertical = (-nx.abs() * 26.0).exp() * (1.0 - ny.abs()).clamp(0.0, 1.0);
        core + (horizontal + vertical) * 0.6
    })
}

fn smoke_texture() -> Vec<u8> {
    build(|nx, ny| {
        let distance = (nx * nx + ny * ny).sqrt();
        let edge = (1.0 - distance).clamp(0.0, 1.0);
        let cloud = fbm((nx * 0.5 + 0.5) * 5.0, (ny * 0.5 + 0.5) * 5.0);
        edge * edge * (0.35 + 0.75 * cloud)
    })
}

fn hash2(x: i32, y: i32) -> f32 {
    let mut h = x
        .wrapping_mul(374_761_393)
        .wrapping_add(y.wrapping_mul(668_265_263));
    h = (h ^ (h >> 13)).wrapping_mul(1_274_126_177);
    ((h ^ (h >> 16)) as u32) as f32 / u32::MAX as f32
}

fn value_noise(px: f32, py: f32) -> f32 {
    let x0 = px.floor() as i32;
    let y0 = py.floor() as i32;
    let fx = px - x0 as f32;
    let fy = py - y0 as f32;
    let ux = fx * fx * (3.0 - 2.0 * fx);
    let uy = fy * fy * (3.0 - 2.0 * fy);
    let a = hash2(x0, y0);
    let b = hash2(x0 + 1, y0);
    let c = hash2(x0, y0 + 1);
    let d = hash2(x0 + 1, y0 + 1);
    let top = a + (b - a) * ux;
    let bottom = c + (d - c) * ux;
    top + (bottom - top) * uy
}

fn fbm(px: f32, py: f32) -> f32 {
    let mut value = 0.0;
    let mut amplitude = 0.5;
    let mut frequency = 1.0;
    for _ in 0..4 {
        value += amplitude * value_noise(px * frequency, py * frequency);
        amplitude *= 0.5;
        frequency *= 2.0;
    }
    value
}