Skip to main content

concinnity_asset/
glass_panel.rs

1// Coloured glass-panel schema.
2
3use crate::AssetId;
4
5/// A flat translucent panel of coloured glass. A fixed-orientation rectangular
6/// quad that refracts and tints the scene behind it and brightens the
7/// grazing-angle rim with a Fresnel highlight.
8///
9/// Unlike [WaterSurface](#watersurface) it has no animation, no surface
10/// displacement, and no depth-based colour. It's a simple building block for
11/// translucent surfaces such as windows, ice, holograms, or force fields.
12///
13/// The panel is positioned by `centre`, oriented by `normal` (the facing
14/// direction), and sized by `half_size` (half-width along the panel's tangent,
15/// half-height along its bitangent).
16///
17/// ```rust
18/// # use concinnity_asset::GlassPanel;
19/// GlassPanel {
20///     centre: [0.0, 2.0, -3.0],
21///     normal: [0.0, 0.0, 1.0],
22///     half_size: [2.0, 1.5],
23///     tint: [0.6, 0.85, 0.9],
24///     opacity: 0.45,
25///     refraction_strength: 0.04,
26///     ..Default::default()
27/// };
28/// ```
29#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
30#[serde(default)]
31pub struct GlassPanel {
32    /// Asset identity; injected via `inject_name`. Not part of `args`.
33    #[serde(skip)]
34    pub asset_id: AssetId,
35    /// World-space position of the panel's centre.
36    pub centre: [f32; 3],
37    /// Facing direction of the panel. Normalised on load; defaults to +Z when
38    /// degenerate.
39    pub normal: [f32; 3],
40    /// Half-width and half-height of the panel, in world units.
41    pub half_size: [f32; 2],
42    /// Linear-space RGB colour the glass tints the scene behind it.
43    pub tint: [f32; 3],
44    /// How opaque the glass is, in [0, 1]. 0 = clear, 1 = fully opaque tint.
45    pub opacity: f32,
46    /// How strongly the glass bends the view of what's behind it. 0 = no
47    /// refraction.
48    pub refraction_strength: f32,
49    /// Sharpness of the grazing-angle rim highlight. Higher values confine the
50    /// brightening to steeper viewing angles.
51    pub fresnel_power: f32,
52    /// When false the panel is skipped each frame.
53    pub visible: bool,
54}
55
56impl Default for GlassPanel {
57    fn default() -> Self {
58        Self {
59            asset_id: AssetId::default(),
60            centre: [0.0, 1.0, 0.0],
61            normal: [0.0, 0.0, 1.0],
62            half_size: [1.0, 1.0],
63            tint: [0.7, 0.85, 0.95],
64            opacity: 0.5,
65            refraction_strength: 0.04,
66            fresnel_power: 4.0,
67            visible: true,
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn a_blank_panel_is_a_visible_half_transparent_unit_square() {
78        let g = GlassPanel::default();
79        assert_eq!(g.centre, [0.0, 1.0, 0.0]);
80        assert_eq!(g.normal, [0.0, 0.0, 1.0]);
81        assert_eq!(g.half_size, [1.0, 1.0]);
82        assert_eq!(g.opacity, 0.5);
83        assert_eq!(g.refraction_strength, 0.04);
84        assert_eq!(g.fresnel_power, 4.0);
85        assert!(g.visible);
86        // The default tint is a faint cool cast, not neutral white.
87        assert_eq!(g.tint, [0.7, 0.85, 0.95]);
88    }
89
90    #[test]
91    fn an_authored_panel_parses_and_round_trips_through_postcard() {
92        let g: GlassPanel = serde_json::from_str(
93            r#"{"centre":[2,1,-3],"normal":[1,0,0],"half_size":[0.6,1.2],
94                "tint":[1,1,1],"opacity":0.2,"refraction_strength":0.1,"visible":false}"#,
95        )
96        .unwrap();
97        assert_eq!(g.normal, [1.0, 0.0, 0.0]);
98        assert!(!g.visible);
99
100        let bytes = postcard::to_allocvec(&g).unwrap();
101        let back: GlassPanel = postcard::from_bytes(&bytes).unwrap();
102        assert_eq!(back.centre, [2.0, 1.0, -3.0]);
103        assert_eq!(back.half_size, [0.6, 1.2]);
104        assert_eq!(back.tint, [1.0, 1.0, 1.0]);
105        assert_eq!(back.opacity, 0.2);
106        assert_eq!(back.refraction_strength, 0.1);
107        // Fresnel was not authored, so it keeps the schema default.
108        assert_eq!(back.fresnel_power, 4.0);
109        assert_eq!(back.asset_id, AssetId::default());
110    }
111}