WebGPU Voxel Game
1use super::map::BlockKind;
2use crate::{
3 gfx::model::{Material, Mesh, ModelVertex},
4 Instance,
5};
6use block_mesh::{
7 greedy_quads, visible_block_faces, GreedyQuadsBuffer, MergeVoxel, Voxel, VoxelVisibility,
8 RIGHT_HANDED_Y_UP_CONFIG,
9};
10use glam::{ivec3, vec3, IVec3, Quat, UVec3, Vec3};
11use itertools::{iproduct, izip, Itertools};
12use ndshape::{ConstShape, ConstShape3u32, Shape};
13use wgpu::util::DeviceExt;
14
15// I have arbitrarily decided that this is (x,z,y) where +y is up.
16pub(crate) const CHUNK_SIZE: (usize, usize, usize) = (16, 16, 16);
17pub(crate) const CHUNK_SIZEEE: usize = 16;
18
19// A [Block; X*Y*Z] would be a much more efficient datatype, but, well...
20pub type Slice3 = [BlockKind; CHUNK_SIZE.0 * CHUNK_SIZE.1 * CHUNK_SIZE.2];
21
22pub fn sl3get(sl3: &Slice3, x: usize, y: usize, z: usize) -> BlockKind {
23 sl3[z + CHUNK_SIZE.2 * (y + CHUNK_SIZE.1 * x)]
24}
25pub fn sl3get_opt(sl3: &Slice3, x: usize, y: usize, z: usize) -> Option<BlockKind> {
26 sl3.get(z + CHUNK_SIZE.2 * (y + CHUNK_SIZE.1 * x)).copied()
27}
28// pub fn sl3set(sl3: &mut Slice3, x: usize, y: usize, z: usize, new: BlockKind) {
29// sl3[y + CHUNK_SIZE.2 * (z + CHUNK_SIZE.1 * x)] = new;
30// }
31
32pub struct ChunkOf<T: Copy> {
33 inner: [T; CHUNK_SIZE.0 * CHUNK_SIZE.1 * CHUNK_SIZE.2],
34}
35impl<T: Copy> ChunkOf<T> {
36 pub fn get(&self, pos: UVec3) -> Option<T> {
37 self.inner
38 .get((pos.y as usize) + CHUNK_SIZEEE * (pos.z as usize + CHUNK_SIZEEE * pos.x as usize))
39 .copied()
40 }
41 pub fn map<F, U: Copy>(&self, f: F) -> ChunkOf<U>
42 where
43 F: FnMut(T) -> U,
44 {
45 ChunkOf::<U> {
46 inner: self.inner.map(f),
47 }
48 }
49}
50
51#[allow(unused)]
52pub enum ChunkScramble {
53 Normal,
54 Inverse,
55 Random,
56}
57
58type ChunkShape = ndshape::ConstShape3u32<18, 18, 18>;
59
60#[derive(Clone)]
61pub struct Chunk {
62 pub blocks: Slice3,
63}
64
65#[derive(Clone, Copy, Eq, PartialEq)]
66struct BoolVoxel(bool);
67
68const EMPTY: BoolVoxel = BoolVoxel(false);
69const FULL: BoolVoxel = BoolVoxel(true);
70
71impl Voxel for BoolVoxel {
72 fn get_visibility(&self) -> VoxelVisibility {
73 if *self == EMPTY {
74 VoxelVisibility::Empty
75 } else {
76 VoxelVisibility::Opaque
77 }
78 }
79}
80
81impl MergeVoxel for BoolVoxel {
82 type MergeValue = Self;
83
84 fn merge_value(&self) -> Self::MergeValue {
85 *self
86 }
87}
88
89impl Chunk {
90 pub fn mesh(&self, chunk_pos: IVec3, material: usize, device: &wgpu::Device) -> Mesh {
91 let blocks = ChunkOf::<BlockKind> { inner: self.blocks };
92
93 let size = CHUNK_SIZEEE as i32;
94 let voxels = iproduct!(-1..size + 1, -1..size + 1, -1..size + 1)
95 .map(|(x, y, z)| {
96 let idx = ivec3(x, y, z).as_uvec3();
97 let block = blocks.get(idx);
98 match block {
99 Some(BlockKind::Brick) => FULL,
100 _ => EMPTY,
101 }
102 })
103 .collect_vec();
104
105 let mut output = GreedyQuadsBuffer::new(voxels.len());
106 greedy_quads(
107 &voxels[..],
108 &ChunkShape {},
109 [0; 3],
110 [17; 3],
111 &RIGHT_HANDED_Y_UP_CONFIG.faces,
112 &mut output,
113 );
114
115 let coordinate_spaces = RIGHT_HANDED_Y_UP_CONFIG;
116
117 let num_indices = output.quads.num_quads() * 6;
118 let num_vertices = output.quads.num_quads() * 4;
119 let mut indices = Vec::with_capacity(num_indices);
120 let mut vertices = Vec::with_capacity(num_vertices);
121 let mut positions = Vec::with_capacity(num_vertices / 3);
122 // let mut normals = Vec::with_capacity(num_vertices);
123 // let mut tex_coords = Vec::with_capacity(num_vertices);
124 for (group, face) in output
125 .quads
126 .groups
127 .into_iter()
128 .zip(coordinate_spaces.faces.into_iter())
129 {
130 for quad in group.into_iter() {
131 let ps = face.quad_mesh_positions(&quad, 1.0);
132 let ns = face.quad_mesh_normals();
133 let ts = face.tex_coords(coordinate_spaces.u_flip_face, true, &quad);
134
135 vertices.extend(izip!(ps, ns, ts).map(|(position, normal, tex_coords)| {
136 let Vec3 { x, y, z } = chunk_pos.as_vec3() * Vec3::splat(16.0);
137 let [px, py, pz] = position;
138 ModelVertex {
139 position: [x + px, y + py, z + pz],
140 tex_coords,
141 normal,
142 }
143 }));
144 indices.extend_from_slice(&face.quad_mesh_indices(positions.len() as u32));
145
146 // hack
147 positions.extend_from_slice(&face.quad_mesh_positions(&quad, 1.0));
148 }
149 }
150
151 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
152 label: Some(&format!("Chunk Vertex Buffer")),
153 contents: bytemuck::cast_slice(&vertices),
154 usage: wgpu::BufferUsages::VERTEX,
155 });
156
157 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
158 label: Some(&format!("Chunk Index Buffer")),
159 contents: bytemuck::cast_slice(&indices),
160 usage: wgpu::BufferUsages::INDEX,
161 });
162
163 Mesh {
164 name: "Chunk".to_string(),
165 vertex_buffer,
166 index_buffer,
167 num_elements: num_indices as _,
168 material,
169 }
170 }
171
172 fn generate_normal(world_pos: IVec3) -> Chunk {
173 let blocks = itertools::iproduct!(0..CHUNK_SIZE.0, 0..CHUNK_SIZE.1, 0..CHUNK_SIZE.2)
174 .map(|(x, z, y)| {
175 let tile_pos = ivec3(x as _, y as _, z as _);
176 let tile_pos_worldspace = (tile_pos + (world_pos * CHUNK_SIZE.0 as i32)).as_vec3();
177
178 let sines =
179 f32::sin(tile_pos_worldspace.x * 0.1) + f32::sin(tile_pos_worldspace.z * 0.1);
180
181 // Pretty arbitrary numbers! Just trying to get something interesting
182 let n = (((sines / 4. + 0.5) * CHUNK_SIZE.2 as f32).round() as i32)
183 <= -tile_pos_worldspace.y as _;
184
185 if n {
186 BlockKind::Brick
187 } else {
188 BlockKind::Air
189 }
190 })
191 .collect_array()
192 .unwrap();
193
194 Chunk { blocks }
195 }
196
197 fn generate_callback(method: ChunkScramble) -> fn(IVec3) -> Chunk {
198 use ChunkScramble as C;
199 match method {
200 C::Normal => Chunk::generate_normal,
201 C::Inverse => |p| {
202 let blocks = Chunk::generate_normal(p)
203 .blocks
204 .iter()
205 .map(|b| match b {
206 BlockKind::Air => BlockKind::Brick,
207 BlockKind::Brick => BlockKind::Air,
208 })
209 .collect_array()
210 .unwrap();
211 Chunk { blocks }
212 },
213 C::Random => |p| {
214 #[cfg(not(target_arch = "wasm32"))]
215 let blocks = {
216 use rand::Rng;
217 rand::rng().random()
218 };
219
220 Chunk { blocks }
221 },
222 }
223 }
224 pub fn generate(map_pos: IVec3, method: ChunkScramble) -> Chunk {
225 Chunk::generate_callback(method)(map_pos)
226 }
227
228 // pub fn save(
229 // &self,
230 // map_pos: IVec3,
231 // conn: &mut ConnectionOnlyOnNative,
232 // ) -> Result<(), Box<dyn std::error::Error>> {
233 // let config = bincode::config::standard();
234
235 // #[cfg(not(target_arch = "wasm32"))]
236 // {
237 // let encoded = bincode::encode_to_vec(self, config)?;
238
239 // let mut stmt = conn.prepare_cached(
240 // r#"
241 // INSERT INTO chunks (x,y,z,data)
242 // VALUES (?,?,?,?)
243 // "#,
244 // )?;
245 // stmt.insert((map_pos.x, map_pos.y, map_pos.z, encoded))?;
246 // }
247
248 // // We are going to use LocalStorage for web. I don't like it either.
249 // #[cfg(target_arch = "wasm32")]
250 // {
251 // use base64::prelude::{Engine, BASE64_STANDARD};
252 // let encoded = bincode::encode_to_vec(self, config)?;
253 // let encoded = BASE64_STANDARD.encode(encoded);
254
255 // let store = web_sys::window().unwrap().local_storage().unwrap().unwrap();
256 // store.set(&file_name, &encoded);
257 // }
258 // Ok(())
259 // }
260 // fn load_from_file(
261 // map_pos: IVec3,
262 // conn: &mut ConnectionOnlyOnNative,
263 // ) -> Result<Option<Chunk>, Box<dyn std::error::Error>> {
264 // let config = bincode::config::standard();
265 // let file_hash = calculate_hash(&map_pos);
266 // let file_name = format!("chunk_{}.bl0ck", file_hash);
267
268 // #[cfg(not(target_arch = "wasm32"))]
269 // {
270 // // let file_path = Path::new("./save/chunk/").join(Path::new(&file_name));
271 // // if file_path.exists() {
272 // // log::warn!("Load Chunk!");
273 // // let mut file = File::open(file_path).unwrap();
274
275 // let mut stmt = conn.prepare_cached(
276 // r#"
277 // SELECT (data) from chunks
278 // WHERE (x,y,z) == (?,?,?)
279 // "#,
280 // )?;
281 // let i: Vec<u8> =
282 // match stmt.query_row((map_pos.x, map_pos.y, map_pos.z), |f| f.get("data")) {
283 // Ok(x) => x,
284 // Err(rusqlite::Error::QueryReturnedNoRows) => {
285 // return Ok(None);
286 // }
287 // Err(e) => {
288 // return Err(e.into());
289 // }
290 // };
291
292 // let (decoded, _) = bincode::decode_from_slice(i.as_slice(), config)?;
293
294 // Ok(Some(decoded))
295 // // } else {
296 // // log::warn!("Chunk not loaded!");
297 // // Ok(None)
298 // // }
299 // }
300 // #[cfg(target_arch = "wasm32")]
301 // {
302 // use base64::prelude::{Engine, BASE64_STANDARD};
303 // let store = web_sys::window().unwrap().local_storage().unwrap().unwrap();
304 // if let Ok(Some(s)) = store.get(&file_name) {
305 // let s = BASE64_STANDARD.decode(s)?;
306 // let (decoded, _) = bincode::decode_from_slice(&s[..], config)?;
307
308 // Ok(Some(decoded))
309 // } else {
310 // Ok(None)
311 // }
312 // }
313 // }
314
315 pub fn load(map_pos: IVec3) -> Result<Chunk, Box<dyn std::error::Error>> {
316 // #[cfg(not(target_arch = "wasm32"))]
317 // let cached = CHUNK_FILE_CACHE.lock().unwrap().contains_key(&map_pos);
318 // #[cfg(target_arch = "wasm32")]
319 // let cached = false;
320
321 let chunk = Chunk::generate(map_pos, ChunkScramble::Normal);
322 Ok(chunk)
323
324 // if cached {
325 // // log::warn!("Cache hit!");
326 // #[cfg(not(target_arch = "wasm32"))]
327 // //return Ok(CHUNK_FILE_CACHE.lock().unwrap()[&map_pos]);
328 // #[cfg(target_arch = "wasm32")]
329 // return unreachable!();
330 // } else {
331 // }
332 }
333}