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}