1use alloc::format;
17use alloc::string::String;
18use alloc::vec::Vec;
19
20pub struct PaletteSlot {
25 pub uv_top: [f32; 4],
27 pub uv_bottom: [f32; 4],
29 pub uv_side: [f32; 4],
31}
32
33type Verts = Vec<([f32; 3], [f32; 3], [f32; 3], [f32; 2])>;
36
37pub fn build_voxel_mesh(
44 dim: [u32; 3],
45 block_size: f32,
46 blocks: &[u32],
47 palette: &[Option<PaletteSlot>],
48) -> Result<(Verts, Vec<u16>), String> {
49 let [dx, dy, dz] = [dim[0] as usize, dim[1] as usize, dim[2] as usize];
50 let expected = dx.saturating_mul(dy).saturating_mul(dz);
51 if blocks.len() != expected {
52 return Err(format!(
53 "VoxelChunk: blocks length {} does not match dim {}x{}x{} ({} expected)",
54 blocks.len(),
55 dim[0],
56 dim[1],
57 dim[2],
58 expected
59 ));
60 }
61 for (i, &id) in blocks.iter().enumerate() {
62 if (id as usize) >= palette.len() {
63 return Err(format!(
64 "VoxelChunk: blocks[{}] = {} out of palette range (len {})",
65 i,
66 id,
67 palette.len()
68 ));
69 }
70 }
71
72 let mut verts: Verts = Vec::new();
73 let mut idxs: Vec<u16> = Vec::new();
74 let color = [0.75f32, 0.74, 0.72];
75 let bs = block_size;
76
77 let at = |x: i32, y: i32, z: i32| -> Option<&PaletteSlot> {
78 if x < 0 || y < 0 || z < 0 || x >= dx as i32 || y >= dy as i32 || z >= dz as i32 {
79 return None;
80 }
81 let i = (x as usize) + (y as usize) * dx + (z as usize) * dx * dy;
82 palette[blocks[i] as usize].as_ref()
83 };
84
85 let mut emit_quad = |corners: [[f32; 3]; 4], normal: [f32; 3], uv_rect: [f32; 4]| {
87 if verts.len() + 4 > u16::MAX as usize {
88 return;
89 }
90 let base = verts.len() as u16;
91 let [u0, v0, u1, v1] = uv_rect;
92 let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
94 for (i, p) in corners.iter().enumerate() {
95 verts.push((*p, normal, color, uvs[i]));
96 }
97 idxs.extend_from_slice(&[base, base + 1, base + 2, base + 2, base + 3, base]);
98 };
99
100 for z in 0..dz {
101 for y in 0..dy {
102 for x in 0..dx {
103 let slot = match at(x as i32, y as i32, z as i32) {
104 Some(s) => s,
105 None => continue,
106 };
107 let x0 = x as f32 * bs;
108 let y0 = y as f32 * bs;
109 let z0 = z as f32 * bs;
110 let x1 = x0 + bs;
111 let y1 = y0 + bs;
112 let z1 = z0 + bs;
113
114 if at(x as i32 + 1, y as i32, z as i32).is_none() {
116 emit_quad(
117 [[x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1]],
118 [1.0, 0.0, 0.0],
119 slot.uv_side,
120 );
121 }
122 if at(x as i32 - 1, y as i32, z as i32).is_none() {
124 emit_quad(
125 [[x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]],
126 [-1.0, 0.0, 0.0],
127 slot.uv_side,
128 );
129 }
130 if at(x as i32, y as i32 + 1, z as i32).is_none() {
132 emit_quad(
133 [[x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]],
134 [0.0, 1.0, 0.0],
135 slot.uv_top,
136 );
137 }
138 if at(x as i32, y as i32 - 1, z as i32).is_none() {
140 emit_quad(
141 [[x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]],
142 [0.0, -1.0, 0.0],
143 slot.uv_bottom,
144 );
145 }
146 if at(x as i32, y as i32, z as i32 + 1).is_none() {
148 emit_quad(
149 [[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]],
150 [0.0, 0.0, 1.0],
151 slot.uv_side,
152 );
153 }
154 if at(x as i32, y as i32, z as i32 - 1).is_none() {
156 emit_quad(
157 [[x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0]],
158 [0.0, 0.0, -1.0],
159 slot.uv_side,
160 );
161 }
162 }
163 }
164 }
165
166 Ok((verts, idxs))
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use alloc::vec;
173
174 fn solid_slot() -> PaletteSlot {
175 PaletteSlot {
176 uv_top: [0.0, 0.0, 1.0, 1.0],
177 uv_bottom: [0.0, 0.0, 1.0, 1.0],
178 uv_side: [0.0, 0.0, 1.0, 1.0],
179 }
180 }
181
182 #[test]
183 fn empty_chunk_produces_no_geometry() {
184 let palette = vec![None];
185 let (v, i) = build_voxel_mesh([2, 2, 2], 1.0, &[0; 8], &palette).unwrap();
186 assert!(v.is_empty());
187 assert!(i.is_empty());
188 }
189
190 #[test]
191 fn single_solid_block_has_six_faces() {
192 let palette = vec![None, Some(solid_slot())];
193 let (v, i) = build_voxel_mesh([1, 1, 1], 1.0, &[1], &palette).unwrap();
194 assert_eq!(v.len(), 24);
196 assert_eq!(i.len(), 36);
197 }
198
199 #[test]
200 fn interior_faces_are_culled() {
201 let palette = vec![None, Some(solid_slot())];
203 let (v, i) = build_voxel_mesh([2, 1, 1], 1.0, &[1, 1], &palette).unwrap();
204 assert_eq!(v.len(), 10 * 4);
205 assert_eq!(i.len(), 10 * 6);
206 }
207
208 #[test]
209 fn fully_filled_cube_has_only_outer_shell() {
210 let palette = vec![None, Some(solid_slot())];
212 let blocks = vec![1u32; 27];
213 let (v, _) = build_voxel_mesh([3, 3, 3], 1.0, &blocks, &palette).unwrap();
214 assert_eq!(v.len(), 54 * 4);
215 }
216
217 #[test]
218 fn air_block_around_solid_emits_all_six_faces() {
219 let palette = vec![None, Some(solid_slot())];
221 let mut blocks = vec![0u32; 27];
222 let center = 1 + 3 + 9;
223 blocks[center] = 1;
224 let (v, _) = build_voxel_mesh([3, 3, 3], 1.0, &blocks, &palette).unwrap();
225 assert_eq!(v.len(), 6 * 4);
226 }
227
228 #[test]
229 fn mismatched_blocks_length_errors() {
230 let palette = vec![None];
231 let result = build_voxel_mesh([2, 2, 2], 1.0, &[0; 4], &palette);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn block_index_out_of_palette_range_errors() {
237 let palette = vec![None];
238 let result = build_voxel_mesh([1, 1, 1], 1.0, &[7], &palette);
239 assert!(result.is_err());
240 }
241}