Skip to main content

concinnity_asset/
decal.rs

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