Skip to main content

kvas_engine/
decal.rs

1//! Projected decals: a box that paints whatever surface lies inside it (any shape of geometry,
2//! no UVs needed). The box projects along its local -Y axis, so its +Y should point out of the
3//! surface; its X and Z scale give the size, its Y scale the depth it reaches.
4//!
5//! A decal can carry an image, a normal map and an ORM map, or draw a built-in procedural shape,
6//! and blends with the surface in one of several modes (normal, multiply, lighten, overlay...).
7//! Decals are drawn after the opaque scene by `builtin:decal`, sorted by `order`, culled by the
8//! view frustum and by `max_distance`, and grouped by image so thousands stay cheap.
9//!
10//! ```
11//! use kvas_engine::decal::{Decal, DecalBlend};
12//! let d = Decal { blend: DecalBlend::Multiply, opacity: 0.8, ..Default::default() };
13//! assert_eq!(d.shader_params()[0], DecalBlend::Multiply as i32 as f32);
14//! ```
15
16use crate::components::Component;
17use crate::reflect::{AssetKind, Field};
18use serde::{Deserialize, Serialize};
19
20crate::choice_enum! {
21    /// How a decal combines with the surface under it.
22    DecalBlend { Normal, Multiply, Screen, Lighten, Darken, Overlay, Add, SoftLight, ReliefOnly, ColorDodge, ColorBurn, Tint } default Normal
23}
24
25crate::choice_enum! {
26    /// Built-in shapes for decals without an image.
27    DecalShape { Image, Blob, Scorch, BulletHole, Cracks, Puddle, Grunge, PaintSplat, Moss, Rust, Leaks, Ring } default Image
28}
29
30/// A projected decal. Put it on an entity and move, turn and scale it like any object.
31#[derive(Clone, Serialize, Deserialize)]
32#[serde(default)]
33pub struct Decal {
34    /// Image (its alpha cuts the shape). Empty: the procedural `shape` is drawn.
35    pub texture: String,
36    /// Optional tangent-space normal map (relief of the decal).
37    pub normal_map: String,
38    /// Optional occlusion / roughness / metallic map.
39    pub orm_map: String,
40    /// Procedural shape used when there is no image.
41    pub shape: DecalShape,
42    /// How the decal combines with the surface.
43    pub blend: DecalBlend,
44    /// Tint (multiplied with the image), linear RGBA.
45    pub color: [f32; 4],
46    /// Overall strength 0..1.
47    pub opacity: f32,
48    /// Surface roughness under the decal.
49    pub roughness: f32,
50    /// Metalness under the decal.
51    pub metallic: f32,
52    /// Glow added on top (lights up in the dark).
53    pub emission: f32,
54    /// Strength of the normal map or of the shape's relief.
55    pub normal_strength: f32,
56    /// Fade on surfaces turned away from the projection, 0 = paint everything, 1 = only faces
57    /// looking straight along the decal.
58    pub angle_fade: f32,
59    /// Random variation of the procedural shapes.
60    pub seed: i32,
61    /// Higher numbers draw on top of lower ones.
62    pub order: i32,
63    /// Not drawn beyond this distance from the camera (0 = always); saves time with many decals.
64    pub max_distance: f32,
65}
66
67impl Default for Decal {
68    fn default() -> Self {
69        Self {
70            texture: String::new(),
71            normal_map: String::new(),
72            orm_map: String::new(),
73            shape: DecalShape::Blob,
74            blend: DecalBlend::Normal,
75            color: [0.35, 0.05, 0.04, 1.0],
76            opacity: 1.0,
77            roughness: 0.6,
78            metallic: 0.0,
79            emission: 0.0,
80            normal_strength: 1.0,
81            angle_fade: 0.3,
82            seed: 1,
83            order: 0,
84            max_distance: 80.0,
85        }
86    }
87}
88
89impl Decal {
90    /// The eight shader numbers of the decal (`object.user0`, `object.user1` of `builtin:decal`).
91    pub fn shader_params(&self) -> [f32; 8] {
92        [
93            self.blend as i32 as f32,
94            self.shape as i32 as f32,
95            self.angle_fade.clamp(0.0, 1.0),
96            self.normal_strength,
97            self.seed as f32,
98            self.emission,
99            (!self.normal_map.is_empty()) as i32 as f32,
100            (!self.orm_map.is_empty()) as i32 as f32,
101        ]
102    }
103}
104
105impl Component for Decal {
106    fn fields(&mut self) -> Vec<Field<'_>> {
107        vec![
108            Field::asset("texture", &mut self.texture, AssetKind::Texture),
109            Field::asset("normal_map", &mut self.normal_map, AssetKind::Texture),
110            Field::asset("orm_map", &mut self.orm_map, AssetKind::Texture),
111            Field::choice("shape", &mut self.shape),
112            Field::choice("blend", &mut self.blend),
113            Field::color("color", &mut self.color),
114            Field::f32("opacity", &mut self.opacity),
115            Field::f32("roughness", &mut self.roughness),
116            Field::f32("metallic", &mut self.metallic),
117            Field::f32("emission", &mut self.emission),
118            Field::f32("normal_strength", &mut self.normal_strength),
119            Field::f32("angle_fade", &mut self.angle_fade),
120            Field::i32("seed", &mut self.seed),
121            Field::i32("order", &mut self.order),
122            Field::f32("max_distance", &mut self.max_distance),
123        ]
124    }
125}
126
127/// A ready look for common uses: `"blood"`, `"scorch"`, `"bullet_hole"`, `"cracks"`, `"puddle"`,
128/// `"dirt"`, `"paint"`, `"moss"`, `"rust"`, `"leaks"`, `"graffiti_glow"`, `"light_spot"`.
129pub fn preset(name: &str) -> Decal {
130    let d = Decal::default();
131    match name {
132        "scorch" => Decal { shape: DecalShape::Scorch, blend: DecalBlend::Multiply, color: [0.05, 0.04, 0.035, 1.0], roughness: 0.95, ..d },
133        "bullet_hole" => Decal { shape: DecalShape::BulletHole, color: [0.06, 0.055, 0.05, 1.0], normal_strength: 2.0, roughness: 0.9, ..d },
134        "cracks" => Decal { shape: DecalShape::Cracks, blend: DecalBlend::Multiply, color: [0.12, 0.11, 0.1, 1.0], normal_strength: 1.5, ..d },
135        "puddle" => Decal { shape: DecalShape::Puddle, color: [0.25, 0.24, 0.22, 1.0], roughness: 0.04, opacity: 0.85, angle_fade: 0.7, ..d },
136        "dirt" => Decal { shape: DecalShape::Grunge, blend: DecalBlend::Multiply, color: [0.45, 0.38, 0.28, 1.0], opacity: 0.8, ..d },
137        "paint" => Decal { shape: DecalShape::PaintSplat, color: [0.9, 0.15, 0.4, 1.0], roughness: 0.35, ..d },
138        "moss" => Decal { shape: DecalShape::Moss, color: [0.1, 0.25, 0.05, 1.0], roughness: 0.95, normal_strength: 1.2, ..d },
139        "rust" => Decal { shape: DecalShape::Rust, color: [0.45, 0.18, 0.06, 1.0], roughness: 0.9, ..d },
140        "leaks" => Decal { shape: DecalShape::Leaks, blend: DecalBlend::Multiply, color: [0.35, 0.33, 0.3, 1.0], angle_fade: 0.0, ..d },
141        "graffiti_glow" => Decal { shape: DecalShape::Ring, color: [0.2, 0.9, 1.0, 1.0], emission: 3.0, ..d },
142        "light_spot" => Decal { shape: DecalShape::Blob, blend: DecalBlend::Lighten, color: [1.0, 0.85, 0.6, 1.0], opacity: 0.6, ..d },
143        _ => d,
144    }
145}
146
147/// Names accepted by [`preset`].
148pub const PRESETS: &[&str] = &["blood", "scorch", "bullet_hole", "cracks", "puddle", "dirt", "paint", "moss", "rust", "leaks", "graffiti_glow", "light_spot"];
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn presets_and_params_round_trip() {
156        for name in PRESETS {
157            let d = preset(name);
158            let json = serde_json::to_string(&d).unwrap();
159            let back: Decal = serde_json::from_str(&json).unwrap();
160            assert_eq!(back.shader_params(), d.shader_params(), "{name}");
161        }
162        let d = Decal { normal_map: "n.png".into(), ..Default::default() };
163        assert_eq!(d.shader_params()[6], 1.0);
164    }
165}