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}