Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decals

Live Demo: Decals

Decals are textures projected onto scene geometry using deferred projection through the depth buffer. They are used for bullet holes, blood splatters, footprints, scorch marks, and environmental details without modifying the underlying mesh geometry.

How Decal Rendering Works

Each decal is rendered as a unit cube positioned and oriented in world space. The fragment shader reconstructs the world position of the scene geometry behind the cube by sampling the depth buffer, then transforms that position into the decal's local space using the inverse model matrix. If the reconstructed point falls within the decal's projection volume (±1 in XY, 0 to depth in Z), the decal texture is sampled at those local XY coordinates and blended onto the scene.

The normal threshold test compares the scene surface normal (from the depth buffer gradients) against the decal's forward direction. Surfaces angled beyond the threshold are rejected, preventing decals from wrapping around sharp edges.

Distance fade uses a smoothstep between fade_start and fade_end based on the camera-to-decal distance.

Decal Component

#![allow(unused)]
fn main() {
pub struct Decal {
    pub texture: Option<String>,
    pub emissive_texture: Option<String>,
    pub emissive_strength: f32,
    pub color: [f32; 4],
    pub size: Vec2,
    pub depth: f32,
    pub normal_threshold: f32,
    pub fade_start: f32,
    pub fade_end: f32,
}
}
FieldDefaultDescription
textureNoneTexture name in the texture cache
emissive_textureNoneOptional emissive texture for glowing decals
emissive_strength1.0HDR multiplier for emissive texture
color[1, 1, 1, 1]RGBA tint multiplied with the texture
size(1.0, 1.0)Width and height of the projected decal
depth1.0Projection depth (how far the decal penetrates into surfaces)
normal_threshold0.5Surface angle cutoff (0 = accept all, 1 = perpendicular only)
fade_start50.0Distance where fade begins
fade_end100.0Distance where the decal is fully transparent

Spawning Decals

#![allow(unused)]
fn main() {
fn spawn_bullet_hole(world: &mut World, position: Vec3, normal: Vec3) -> Entity {
    let entity = world.spawn_entities(
        LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | DECAL,
        1
    )[0];

    let rotation = nalgebra_glm::quat_look_at(&normal, &Vec3::y());

    world.core.set_local_transform(entity, LocalTransform {
        translation: position,
        rotation,
        ..Default::default()
    });

    world.core.set_decal(entity, Decal::new("bullet_hole")
        .with_size(0.2, 0.2)
        .with_depth(0.1)
        .with_normal_threshold(0.5)
        .with_fade(20.0, 30.0));

    entity
}
}

Builder API

The Decal struct supports a builder pattern:

#![allow(unused)]
fn main() {
let decal = Decal::new("texture_name")
    .with_size(0.5, 0.5)
    .with_depth(0.2)
    .with_color([1.0, 0.0, 0.0, 1.0])
    .with_normal_threshold(0.3)
    .with_fade(30.0, 50.0)
    .with_emissive("rune_glow", 3.0);
}

Common Use Cases

Blood Splatter

#![allow(unused)]
fn main() {
fn spawn_blood(world: &mut World, position: Vec3, normal: Vec3) -> Entity {
    let entity = world.spawn_entities(
        LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | DECAL,
        1
    )[0];

    let rotation = nalgebra_glm::quat_look_at(&normal, &Vec3::y());
    let random_angle = rand::random::<f32>() * std::f32::consts::TAU;
    let rotation = rotation * nalgebra_glm::quat_angle_axis(random_angle, &Vec3::z());

    world.core.set_local_transform(entity, LocalTransform {
        translation: position,
        rotation,
        ..Default::default()
    });

    world.core.set_decal(entity, Decal::new("blood")
        .with_size(0.8, 0.8)
        .with_depth(0.1)
        .with_normal_threshold(0.3)
        .with_fade(30.0, 50.0));

    entity
}
}

Emissive Rune

#![allow(unused)]
fn main() {
fn spawn_magic_rune(world: &mut World, position: Vec3) -> Entity {
    let entity = world.spawn_entities(
        LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | DECAL,
        1
    )[0];

    world.core.set_local_transform(entity, LocalTransform {
        translation: position,
        rotation: nalgebra_glm::quat_angle_axis(
            -std::f32::consts::FRAC_PI_2,
            &Vec3::x(),
        ),
        ..Default::default()
    });

    world.core.set_decal(entity, Decal::new("rune")
        .with_size(2.0, 2.0)
        .with_depth(0.5)
        .with_normal_threshold(0.7)
        .with_fade(50.0, 80.0)
        .with_emissive("rune_glow", 3.0));

    entity
}
}

Footprints

#![allow(unused)]
fn main() {
fn spawn_footprint(world: &mut World, position: Vec3, direction: Vec3, left: bool) -> Entity {
    let entity = world.spawn_entities(
        LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | DECAL,
        1
    )[0];

    let rotation = nalgebra_glm::quat_look_at(&Vec3::y(), &direction);
    let flip = if left { 1.0 } else { -1.0 };

    world.core.set_local_transform(entity, LocalTransform {
        translation: position,
        rotation,
        scale: Vec3::new(flip, 1.0, 1.0),
    });

    world.core.set_decal(entity, Decal::new("footprint")
        .with_size(0.15, 0.3)
        .with_depth(0.05)
        .with_normal_threshold(0.8)
        .with_fade(15.0, 25.0));

    entity
}
}