Skip to main content

concinnity_core/components/
decal.rs

1// Projected-decal schema.
2
3use crate::ecs::TextureHandle;
4use crate::ecs::asset_id::AssetId;
5use crate::ecs::de_opt_texture_handle;
6
7/// A projected texture stamped onto whatever scene geometry sits inside the
8/// decal's oriented box.
9///
10/// The decal is a box volume positioned by `position`/`rotation_deg`/`size` in
11/// world space. The texture is projected down the box's local +Y axis onto the
12/// local X-Z plane and stamped onto the surfaces inside the box; anything
13/// outside the box is unaffected. Surfaces near the box's top and bottom faces
14/// fade out so the stamp doesn't show a hard edge on a curved surface.
15///
16/// The defaults orient the decal as a ground stamp: a flat 1×1 m square laid on
17/// the world X-Z plane, projecting down from +Y. To stamp a wall, rotate so
18/// local +Y points into the surface (e.g. `rotation_deg:[0,0,90]` for a +X
19/// wall).
20///
21/// Decals blend over the lit image without affecting depth, so they layer on
22/// top of the surfaces they stamp.
23///
24/// ```rust
25/// # use concinnity_core::components::Decal;
26/// Decal {
27///     position: [2.0, 0.01, -1.5],
28///     size: [1.5, 0.5, 1.5],
29///     ..Default::default()
30/// };
31/// ```
32#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
33#[serde(default)]
34pub struct Decal {
35    /// Asset identity; injected via `inject_name`. Not part of `args`.
36    #[serde(skip)]
37    pub asset_id: AssetId,
38    /// The [Texture](#texture) asset projected onto the scene.
39    #[serde(deserialize_with = "de_opt_texture_handle")]
40    pub texture: Option<TextureHandle>,
41    /// World-space position of the decal box's centre.
42    pub position: [f32; 3],
43    /// Euler rotation in degrees [pitch, yaw, roll], YXZ order, same as
44    /// [Prop](#prop).
45    pub rotation_deg: [f32; 3],
46    /// Local-space box extents. Local +Y is the projection axis; the texture
47    /// is sampled on the local X-Z plane. A non-positive component disables
48    /// the decal.
49    pub size: [f32; 3],
50    /// Linear-space RGBA tint multiplied with the sampled texture. The alpha
51    /// channel scales the final blend, so `[1,1,1,0]` hides the decal.
52    pub tint: [f32; 4],
53    /// When false the decal is skipped each frame.
54    pub visible: bool,
55}
56
57impl Default for Decal {
58    fn default() -> Self {
59        Self {
60            asset_id: AssetId::default(),
61            texture: None,
62            position: [0.0, 0.0, 0.0],
63            rotation_deg: [0.0, 0.0, 0.0],
64            size: [1.0, 1.0, 1.0],
65            tint: [1.0, 1.0, 1.0, 1.0],
66            visible: true,
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn a_blank_decal_is_a_visible_untinted_ground_stamp() {
77        let d = Decal::default();
78        assert_eq!(d.rotation_deg, [0.0, 0.0, 0.0]);
79        assert_eq!(d.size, [1.0, 1.0, 1.0]);
80        // An identity tint leaves the sampled texture alone; alpha 1 keeps it
81        // fully blended.
82        assert_eq!(d.tint, [1.0, 1.0, 1.0, 1.0]);
83        assert!(d.visible);
84        assert!(d.texture.is_none());
85    }
86
87    #[test]
88    fn a_wall_stamp_parses_and_round_trips_through_postcard() {
89        crate::test_support::install_resolvers();
90        let d: Decal = serde_json::from_str(
91            r#"{"texture":"tex_bullet","position":[3,1.6,-2],"rotation_deg":[0,0,90],
92                "size":[0.4,0.2,0.4],"tint":[1,1,1,0.5],"visible":false}"#,
93        )
94        .unwrap();
95        assert_eq!(d.texture, Some(TextureHandle(10)));
96        assert_eq!(d.rotation_deg, [0.0, 0.0, 90.0]);
97        assert!(!d.visible);
98
99        let bytes = postcard::to_allocvec(&d).unwrap();
100        let back: Decal = postcard::from_bytes(&bytes).unwrap();
101        assert_eq!(back.texture, Some(TextureHandle(10)));
102        assert_eq!(back.position, [3.0, 1.6, -2.0]);
103        assert_eq!(back.size, [0.4, 0.2, 0.4]);
104        assert_eq!(back.tint, [1.0, 1.0, 1.0, 0.5]);
105        assert_eq!(back.asset_id, AssetId::default());
106    }
107}