plumesplat 0.1.1

Advanced terrain splatting for Bevy with support for 256+ materials using texture arrays
Documentation
// Triplanar mapping utilities for PlumeSplat
//
// Based on techniques from:
// - Ben Golus: https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a
// - Inigo Quilez: https://iquilezles.org/articles/biplanar/

#define_import_path plumesplat::triplanar

// Compute triplanar blend weights from world normal
fn triplanar_weights(world_normal: vec3<f32>, sharpness: f32) -> vec3<f32> {
    // Take absolute value of normal for blend weights
    var weights = abs(world_normal);

    // Apply sharpness (power) to create sharper transitions
    weights = pow(weights, vec3<f32>(sharpness));

    // Normalize so weights sum to 1
    let sum = weights.x + weights.y + weights.z;
    return weights / sum;
}

// Sample a 2D array texture using triplanar projection
fn sample_triplanar(
    tex: texture_2d_array<f32>,
    tex_sampler: sampler,
    world_pos: vec3<f32>,
    world_normal: vec3<f32>,
    layer: i32,
    uv_scale: f32,
    sharpness: f32
) -> vec4<f32> {
    let weights = triplanar_weights(world_normal, sharpness);
    let scaled_pos = world_pos * uv_scale;

    // Sample from each projection plane
    let sample_x = textureSample(tex, tex_sampler, scaled_pos.zy, layer);
    let sample_y = textureSample(tex, tex_sampler, scaled_pos.xz, layer);
    let sample_z = textureSample(tex, tex_sampler, scaled_pos.xy, layer);

    // Blend based on surface orientation
    return sample_x * weights.x + sample_y * weights.y + sample_z * weights.z;
}

// Sample a normal map using triplanar projection with proper tangent space handling
fn sample_triplanar_normal(
    tex: texture_2d_array<f32>,
    tex_sampler: sampler,
    world_pos: vec3<f32>,
    world_normal: vec3<f32>,
    layer: i32,
    uv_scale: f32,
    sharpness: f32
) -> vec3<f32> {
    let weights = triplanar_weights(world_normal, sharpness);
    let scaled_pos = world_pos * uv_scale;

    // Sample normal maps from each plane
    let normal_x = textureSample(tex, tex_sampler, scaled_pos.zy, layer).xyz * 2.0 - 1.0;
    let normal_y = textureSample(tex, tex_sampler, scaled_pos.xz, layer).xyz * 2.0 - 1.0;
    let normal_z = textureSample(tex, tex_sampler, scaled_pos.xy, layer).xyz * 2.0 - 1.0;

    // Swizzle to world space for each projection axis
    // This uses the "Whiteout" blending method
    let sign_x = sign(world_normal.x);
    let sign_y = sign(world_normal.y);
    let sign_z = sign(world_normal.z);

    let world_normal_x = vec3<f32>(normal_x.z * sign_x, normal_x.y, normal_x.x);
    let world_normal_y = vec3<f32>(normal_y.x, normal_y.z * sign_y, normal_y.y);
    let world_normal_z = vec3<f32>(normal_z.x, normal_z.y, normal_z.z * sign_z);

    // Blend normals
    var blended = world_normal_x * weights.x + world_normal_y * weights.y + world_normal_z * weights.z;

    return normalize(blended);
}

// Biplanar mapping (faster than triplanar, uses only 2 texture samples)
fn biplanar_weights(world_normal: vec3<f32>) -> vec2<f32> {
    let n = abs(world_normal);

    // Find the two dominant axes
    if n.x > n.y && n.x > n.z {
        // X is dominant - skip X plane, use Y and Z
        let w = vec2<f32>(n.y, n.z);
        return w / (w.x + w.y);
    } else if n.y > n.z {
        // Y is dominant - skip Y plane, use X and Z
        let w = vec2<f32>(n.x, n.z);
        return w / (w.x + w.y);
    } else {
        // Z is dominant - skip Z plane, use X and Y
        let w = vec2<f32>(n.x, n.y);
        return w / (w.x + w.y);
    }
}