concinnity_asset/
voxel_world.rs1use crate::{AssetId, MaterialHandle, de_opt_material_handle};
4use alloc::vec::Vec;
5
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20#[serde(default)]
21pub struct VoxelWorld {
22 pub seed: u64,
25 pub chunk_blocks: [u32; 3],
27 pub block_size: f32,
29 pub view_radius: u32,
31 pub impostor_radius: u32,
36 pub impostor_step: u32,
39 pub load_budget: u32,
41 pub palette: Vec<AssetId>,
44 #[serde(deserialize_with = "de_opt_material_handle")]
46 pub material: Option<MaterialHandle>,
47}
48
49impl Default for VoxelWorld {
50 fn default() -> Self {
51 Self {
52 seed: 0,
53 chunk_blocks: [16, 24, 16],
54 block_size: 1.0,
55 view_radius: 5,
56 impostor_radius: 0,
57 impostor_step: 4,
58 load_budget: 3,
59 palette: Vec::new(),
60 material: None,
61 }
62 }
63}
64
65impl VoxelWorld {
68 pub fn chunk_blocks(&self) -> [u32; 3] {
70 [
71 self.chunk_blocks[0].max(1),
72 self.chunk_blocks[1].max(1),
73 self.chunk_blocks[2].max(1),
74 ]
75 }
76
77 pub fn block_size(&self) -> f32 {
79 self.block_size.max(0.01)
80 }
81
82 pub fn chunk_world_size(&self) -> (f32, f32) {
84 let b = self.chunk_blocks();
85 let s = self.block_size();
86 (b[0] as f32 * s, b[2] as f32 * s)
87 }
88
89 pub fn view_radius(&self) -> i32 {
92 (self.view_radius as i32).clamp(0, 32)
93 }
94
95 pub fn impostor_radius(&self) -> i32 {
99 (self.impostor_radius as i32)
100 .clamp(0, 96)
101 .max(self.view_radius())
102 }
103
104 pub fn impostor_step(&self) -> u32 {
108 self.impostor_step.clamp(1, 64)
109 }
110
111 pub fn impostors_enabled(&self) -> bool {
114 self.impostor_radius() > self.view_radius()
115 }
116
117 pub fn load_budget(&self) -> usize {
120 (self.load_budget as usize).max(1)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn defaults_stream_a_small_radius_with_impostors_off() {
130 let w = VoxelWorld::default();
131 assert_eq!(w.chunk_blocks(), [16, 24, 16]);
132 assert_eq!(w.block_size(), 1.0);
133 assert_eq!(w.chunk_world_size(), (16.0, 16.0));
134 assert_eq!(w.view_radius(), 5);
135 assert_eq!(w.load_budget(), 3);
136 assert_eq!(w.impostor_step(), 4);
137 assert!(!w.impostors_enabled());
139 assert_eq!(w.impostor_radius(), w.view_radius());
140 assert!(w.palette.is_empty());
141 assert_eq!(w.material, None);
142 }
143
144 #[test]
145 fn a_degenerate_chunk_size_is_floored_to_something_meshable() {
146 let w: VoxelWorld =
149 serde_json::from_str(r#"{"chunk_blocks":[0,0,0],"block_size":0.0}"#).unwrap();
150 assert_eq!(w.chunk_blocks(), [1, 1, 1]);
151 assert_eq!(w.block_size(), 0.01);
152 assert_eq!(w.chunk_world_size(), (0.01, 0.01));
153 }
154
155 #[test]
156 fn chunk_world_size_is_the_horizontal_footprint() {
157 let w: VoxelWorld =
159 serde_json::from_str(r#"{"chunk_blocks":[8,64,4],"block_size":0.5}"#).unwrap();
160 assert_eq!(w.chunk_world_size(), (4.0, 2.0));
161 }
162
163 #[test]
164 fn radii_are_clamped_to_what_the_streamer_can_hold() {
165 let w: VoxelWorld =
166 serde_json::from_str(r#"{"view_radius":999,"impostor_radius":999}"#).unwrap();
167 assert_eq!(w.view_radius(), 32);
168 assert_eq!(w.impostor_radius(), 96);
169 assert!(w.impostors_enabled());
170 }
171
172 #[test]
173 fn an_impostor_radius_never_falls_inside_the_view_radius() {
174 let w: VoxelWorld =
177 serde_json::from_str(r#"{"view_radius":10,"impostor_radius":2}"#).unwrap();
178 assert_eq!(w.impostor_radius(), 10);
179 assert!(!w.impostors_enabled());
180 }
181
182 #[test]
183 fn a_zero_impostor_step_or_load_budget_cannot_wedge_streaming() {
184 let w: VoxelWorld = serde_json::from_str(r#"{"impostor_step":0,"load_budget":0}"#).unwrap();
185 assert_eq!(w.impostor_step(), 1);
186 assert_eq!(w.load_budget(), 1);
187 let w: VoxelWorld = serde_json::from_str(r#"{"impostor_step":999}"#).unwrap();
188 assert_eq!(w.impostor_step(), 64);
189 }
190
191 #[test]
192 fn an_authored_world_round_trips_through_postcard() {
193 crate::test_support::install_resolvers();
194 let w: VoxelWorld = serde_json::from_str(
195 r#"{"seed":42,"palette":["stone","dirt"],"material":"voxel_mat",
196 "view_radius":8,"impostor_radius":24}"#,
197 )
198 .unwrap();
199 assert_eq!(w.palette, [AssetId(5), AssetId(4)]);
200 assert_eq!(w.material, Some(MaterialHandle(9)));
201
202 let bytes = postcard::to_allocvec(&w).unwrap();
203 let back: VoxelWorld = postcard::from_bytes(&bytes).unwrap();
204 assert_eq!(back.seed, 42);
205 assert_eq!(back.palette, [AssetId(5), AssetId(4)]);
206 assert_eq!(back.material, Some(MaterialHandle(9)));
207 assert!(back.impostors_enabled());
208 }
209}