From 9b4c2fb9ddb9ef1fb03e1cd41b245eb84dcf7c74 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Sat, 26 Apr 2025 15:13:22 +0300 Subject: [PATCH] Basic texture rendering --- Cargo.lock | 112 +++++++++++- Cargo.toml | 6 + assets/blocks/dirt.json | 5 + assets/blocks/grass.json | 9 + assets/blocks/stone.json | 5 + assets/textures/blocks/dirt.png | Bin 0 -> 2756 bytes assets/textures/blocks/fallback.png | Bin 0 -> 222 bytes assets/textures/blocks/grass_side.png | Bin 0 -> 2805 bytes assets/textures/blocks/grass_top.png | Bin 0 -> 2784 bytes assets/textures/blocks/stone.png | Bin 0 -> 1600 bytes src/error.rs | 7 + src/main.rs | 3 + src/render/asset/block.rs | 178 +++++++++++++++++++ src/render/asset/mod.rs | 4 + src/render/asset/model.rs | 35 ++++ src/render/asset/texture.rs | 216 +++++++++++++++++++++++ src/render/mesh.rs | 244 +++++++++++++++++++------- src/render/mod.rs | 229 ++++++++++++++++++------ src/render/shaders/mod.rs | 9 + src/render/shaders/plain.frag | 9 +- src/render/shaders/plain.vert | 15 +- src/render/util.rs | 70 ++++++++ src/render/vertex.rs | 4 +- src/world/mod.rs | 225 ++++++++++++++++++++++++ 24 files changed, 1260 insertions(+), 125 deletions(-) create mode 100644 assets/blocks/dirt.json create mode 100644 assets/blocks/grass.json create mode 100644 assets/blocks/stone.json create mode 100644 assets/textures/blocks/dirt.png create mode 100644 assets/textures/blocks/fallback.png create mode 100644 assets/textures/blocks/grass_side.png create mode 100644 assets/textures/blocks/grass_top.png create mode 100644 assets/textures/blocks/stone.png create mode 100644 src/render/asset/block.rs create mode 100644 src/render/asset/mod.rs create mode 100644 src/render/asset/model.rs create mode 100644 src/render/asset/texture.rs create mode 100644 src/world/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 9521b70..77f3027 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,7 @@ dependencies = [ "getrandom 0.2.16", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -606,10 +606,16 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" name = "gaym" version = "0.1.0" dependencies = [ + "bitflags 2.9.0", "bytemuck", "env_logger", "glam", "log", + "noise", + "png", + "rand 0.9.1", + "serde", + "serde_json", "thiserror 2.0.12", "vulkano 0.35.1", "vulkano-shaders", @@ -1001,6 +1007,17 @@ dependencies = [ "memoffset", ] +[[package]] +name = "noise" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da45c8333f2e152fc665d78a380be060eb84fad8ca4c9f7ac8ca29216cff0cc" +dependencies = [ + "num-traits", + "rand 0.8.5", + "rand_xorshift", +] + [[package]] name = "nom" version = "7.1.3" @@ -1011,6 +1028,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.5.11" @@ -1495,6 +1521,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1547,6 +1582,59 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -2915,7 +3003,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -2928,3 +3025,14 @@ dependencies = [ "quote", "syn 2.0.100", ] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/Cargo.toml b/Cargo.toml index 349903c..3fe4c12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,16 @@ version = "0.1.0" edition = "2024" [dependencies] +bitflags = "2.9.0" bytemuck = { version = "1.22.0", features = ["derive"] } env_logger = "0.11.8" glam = { version = "0.30.2", features = ["bytemuck"] } log = "0.4.27" +noise = "0.9.0" +png = "0.17.16" +rand = "0.9.1" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" thiserror = "2.0.12" vulkano = "0.35.1" vulkano-shaders = "0.35.0" diff --git a/assets/blocks/dirt.json b/assets/blocks/dirt.json new file mode 100644 index 0000000..2fa6495 --- /dev/null +++ b/assets/blocks/dirt.json @@ -0,0 +1,5 @@ +{ + "model": { + "simple": "dirt" + } +} diff --git a/assets/blocks/grass.json b/assets/blocks/grass.json new file mode 100644 index 0000000..4f6f8a2 --- /dev/null +++ b/assets/blocks/grass.json @@ -0,0 +1,9 @@ +{ + "model": { + "grass_like": { + "side": "grass_side", + "bottom": "dirt", + "top": "grass_top" + } + } +} diff --git a/assets/blocks/stone.json b/assets/blocks/stone.json new file mode 100644 index 0000000..5c4e565 --- /dev/null +++ b/assets/blocks/stone.json @@ -0,0 +1,5 @@ +{ + "model": { + "simple": "stone" + } +} diff --git a/assets/textures/blocks/dirt.png b/assets/textures/blocks/dirt.png new file mode 100644 index 0000000000000000000000000000000000000000..5500203786690bab9e4b97b17583ede2433c2511 GIT binary patch literal 2756 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 KMNUMnLSTX=n=0u5 literal 0 HcmV?d00001 diff --git a/assets/textures/blocks/fallback.png b/assets/textures/blocks/fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..f200c227169ed8e414ac4a90d0549e12206b857b GIT binary patch literal 222 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 yx++s_Hh7*#Z_i, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BlockModelDefinition { + Simple(String), + Sided { + front: String, + left: String, + back: String, + right: String, + bottom: String, + top: String, + }, + GrassLike { + side: String, + top: String, + bottom: String, + }, +} + +pub struct BlockRegistryBuilder { + texture_registry_builder: TextureRegistryBuilder, + definitions: HashMap, + name_to_id: HashMap, + last_block_id: u8, +} + +struct BlockSides<'a> { + front: &'a str, + left: &'a str, + back: &'a str, + right: &'a str, + bottom: &'a str, + top: &'a str, +} + +impl BlockModelDefinition { + fn sides(&self) -> BlockSides { + match self { + Self::Simple(texture) => BlockSides { + front: texture, + left: texture, + back: texture, + right: texture, + bottom: texture, + top: texture, + }, + Self::Sided { + front, + left, + back, + right, + bottom, + top, + } => BlockSides { + front, + left, + back, + right, + bottom, + top, + }, + Self::GrassLike { side, top, bottom } => BlockSides { + front: side, + left: side, + back: side, + right: side, + bottom, + top, + }, + } + } +} + +impl BlockRegistryBuilder { + pub fn new(texture_registry_builder: TextureRegistryBuilder) -> Self { + Self { + texture_registry_builder, + definitions: HashMap::new(), + name_to_id: HashMap::new(), + last_block_id: 1, + } + } + + pub fn load(&mut self, name: &str) -> Result<(), Error> { + let path = PathBuf::from("assets/blocks").join(format!("{name}.json")); + let file = File::open(path)?; + let definition = serde_json::from_reader(file).unwrap(); + + self.add(name, definition); + + Ok(()) + } + + pub fn add(&mut self, name: &str, definition: BlockDefinition) { + let id = self.last_block_id; + + let sides = definition.model.sides(); + let front = self + .texture_registry_builder + .add_texture_or_fallback(sides.front); + let left = self + .texture_registry_builder + .add_texture_or_fallback(sides.left); + let back = self + .texture_registry_builder + .add_texture_or_fallback(sides.back); + let right = self + .texture_registry_builder + .add_texture_or_fallback(sides.right); + let bottom = self + .texture_registry_builder + .add_texture_or_fallback(sides.bottom); + let top = self + .texture_registry_builder + .add_texture_or_fallback(sides.top); + + self.definitions.insert( + id, + CubeTextures { + front, + left, + back, + right, + bottom, + top, + }, + ); + self.name_to_id.insert(name.into(), id); + log::info!("{id} -> {name}"); + + self.last_block_id += 1; + } + + pub fn build( + self, + queue: Arc, + ) -> Result<(BlockModelRegistry, BlockRegistry, Arc), Error> { + let image_view = self.texture_registry_builder.upload(queue)?; + + let block_model_registry = BlockModelRegistry { + id_to_model: self.definitions, + fallback: CubeTextures::single(0), + }; + let block_registry = BlockRegistry { + name_to_id: self.name_to_id, + }; + + Ok((block_model_registry, block_registry, image_view)) + } +} + +impl BlockRegistry { + pub fn get(&self, name: &str) -> Option { + self.name_to_id.get(name).copied() + } +} + diff --git a/src/render/asset/mod.rs b/src/render/asset/mod.rs new file mode 100644 index 0000000..ff3c4cf --- /dev/null +++ b/src/render/asset/mod.rs @@ -0,0 +1,4 @@ +pub mod block; +pub mod model; +pub mod texture; + diff --git a/src/render/asset/model.rs b/src/render/asset/model.rs new file mode 100644 index 0000000..9ba1f37 --- /dev/null +++ b/src/render/asset/model.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; + +pub struct BlockModelRegistry { + pub id_to_model: HashMap, + pub fallback: CubeTextures, +} + +pub struct CubeTextures { + pub front: u32, + pub left: u32, + pub back: u32, + pub right: u32, + pub bottom: u32, + pub top: u32, +} + +impl CubeTextures { + pub fn single(id: u32) -> Self { + Self { + front: id, + left: id, + back: id, + right: id, + bottom: id, + top: id, + } + } +} + +impl BlockModelRegistry { + pub fn get_or_fallback(&self, id: u8) -> &CubeTextures { + self.id_to_model.get(&id).unwrap_or(&self.fallback) + } +} + diff --git a/src/render/asset/texture.rs b/src/render/asset/texture.rs new file mode 100644 index 0000000..2fee8c1 --- /dev/null +++ b/src/render/asset/texture.rs @@ -0,0 +1,216 @@ +use std::{ + collections::{hash_map::Entry, HashMap}, + fs::File, + path::{Path, PathBuf}, + sync::Arc, +}; + +use vulkano::{ + buffer::{Buffer, BufferCreateInfo, BufferUsage}, + command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, CopyBufferToImageInfo}, + device::{Device, Queue}, + format::Format, + image::{view::ImageView, Image, ImageCreateInfo, ImageType, ImageUsage}, + memory::allocator::{AllocationCreateInfo, MemoryTypeFilter}, + sync::{self, GpuFuture}, + DeviceSize, +}; + +use crate::{error::Error, render::util::Allocators}; + +pub trait TextureReader { + fn format(&self) -> Format; + fn width(&self) -> u32; + fn height(&self) -> u32; + fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error>; +} + +pub struct TextureArrayBuilder { + device: Arc, + allocators: Arc, + + width: u32, + height: u32, + readers: Vec>, +} + +pub struct TextureRegistryBuilder { + inner: TextureArrayBuilder, + map: HashMap, + last_texture_id: u32, +} + +pub struct PngTextureReader { + reader: png::Reader, +} + +impl TextureRegistryBuilder { + pub fn new( + device: Arc, + allocators: Arc, + width: u32, + height: u32, + ) -> Result { + let inner = TextureArrayBuilder::new(device, allocators, width, height); + let mut this = Self { + inner, + last_texture_id: 0, + map: HashMap::new(), + }; + + this.add_texture("fallback")?; + + Ok(this) + } + + pub fn add_texture(&mut self, name: &str) -> Result { + match self.map.entry(name.into()) { + Entry::Occupied(entry) => Ok(*entry.get()), + Entry::Vacant(entry) => { + let id = self.last_texture_id; + let path = PathBuf::from("assets/textures/blocks").join(format!("{name}.png")); + + self.inner.push(Box::new(PngTextureReader::open(&path)?)); + log::info!("{path:?} -> {id}"); + self.last_texture_id += 1; + + entry.insert(id); + + Ok(id) + } + } + } + + pub fn add_texture_or_fallback(&mut self, name: &str) -> u32 { + self.add_texture(name) + .inspect_err(|e| log::error!("Failed to load texture {name}: {e}")) + .unwrap_or(0) + } + + pub fn upload(self, queue: Arc) -> Result, Error> { + self.inner.upload(queue) + } +} + +impl PngTextureReader { + pub fn open>(path: P) -> Result { + let file = File::open(path)?; + let decoder = png::Decoder::new(file); + let reader = decoder.read_info()?; + Ok(Self { reader }) + } +} + +impl TextureReader for PngTextureReader { + fn width(&self) -> u32 { + self.reader.info().width + } + + fn height(&self) -> u32 { + self.reader.info().height + } + + fn format(&self) -> Format { + Format::R8G8B8A8_UNORM + } + + fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + self.reader.next_frame(buffer)?; + Ok(()) + } +} + +impl TextureArrayBuilder { + pub fn new(device: Arc, allocators: Arc, width: u32, height: u32) -> Self { + Self { + device, + allocators, + width, + height, + readers: Vec::new(), + } + } + + pub fn push(&mut self, reader: Box) { + if reader.width() != self.width || reader.height() != self.height { + todo!("Rescale textures to array size") + } + if reader.format() != Format::R8G8B8A8_UNORM { + todo!("Convert pixel format") + } + + self.readers.push(reader); + } + + pub fn upload(self, queue: Arc) -> Result, Error> { + let format = Format::R8G8B8A8_UNORM; + let extent = [self.width, self.height, 1]; + let array_layers = self.readers.len() as u32; + + let array_layer_size = format.block_size() + * extent + .into_iter() + .map(|e| e as DeviceSize) + .product::(); + let buffer_size = array_layer_size * array_layers as DeviceSize; + + let upload_buffer = Buffer::new_slice( + self.allocators.memory.clone(), + BufferCreateInfo { + usage: BufferUsage::TRANSFER_SRC, + ..Default::default() + }, + AllocationCreateInfo { + memory_type_filter: MemoryTypeFilter::PREFER_HOST + | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, + ..Default::default() + }, + buffer_size, + )?; + + { + let mut array_data = upload_buffer.write()?; + + for (i, mut reader) in self.readers.into_iter().enumerate() { + let offset = i * array_layer_size as usize; + reader.read_data(&mut array_data[offset..offset + array_layer_size as usize])?; + } + } + + let image = Image::new( + self.allocators.memory.clone(), + ImageCreateInfo { + image_type: ImageType::Dim2d, + format, + extent, + array_layers, + usage: ImageUsage::TRANSFER_DST | ImageUsage::SAMPLED, + ..Default::default() + }, + Default::default(), + )?; + + let mut upload = AutoCommandBufferBuilder::primary( + self.allocators.command_buffer.clone(), + queue.queue_family_index(), + CommandBufferUsage::OneTimeSubmit, + )?; + + upload.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image( + upload_buffer, + image.clone(), + ))?; + + let upload = upload.build()?; + + sync::now(self.device.clone()) + .then_execute(queue, upload)? + .then_signal_fence_and_flush()? + .wait(None)?; + + let image_view = ImageView::new_default(image)?; + + Ok(image_view) + } +} + diff --git a/src/render/mesh.rs b/src/render/mesh.rs index ad80573..c1262d9 100644 --- a/src/render/mesh.rs +++ b/src/render/mesh.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::Arc, +}; use glam::Vec3; use vulkano::{ @@ -6,15 +9,98 @@ use vulkano::{ memory::allocator::{AllocationCreateInfo, MemoryTypeFilter}, }; -use crate::error::Error; +use crate::{ + error::Error, + world::{Chunk, ChunkCoords, NeighborQuery, World}, +}; -use super::{util::Allocators, vertex::InputVertex}; +use super::{ + asset::model::{BlockModelRegistry, CubeTextures}, + util::Allocators, + vertex::InputVertex, +}; #[derive(Default)] pub struct MeshBuilder { vertices: Vec, } +pub struct ChunkMesh { + pub vertex_buffer: Subbuffer<[InputVertex]>, +} + +pub struct WorldMeshState { + pub chunks: HashMap, + pub block_model_registry: BlockModelRegistry, +} + +impl WorldMeshState { + pub fn new(block_model_registry: BlockModelRegistry) -> Self { + Self { + chunks: HashMap::new(), + block_model_registry, + } + } + + pub fn get_or_upload( + &mut self, + allocators: &Arc, + coords: ChunkCoords, + world: &World, + ) -> Result, Error> { + match self.chunks.entry(coords) { + Entry::Occupied(entry) => Ok(Some(entry.into_mut())), + Entry::Vacant(entry) => { + if let Some(chunk) = world.get(coords) { + let mesh = ChunkMesh::from_chunk( + allocators, + &self.block_model_registry, + coords, + chunk, + )?; + Ok(Some(entry.insert(mesh))) + } else { + Ok(None) + } + } + } + } +} + +impl ChunkMesh { + pub fn from_chunk( + allocators: &Arc, + block_model_registry: &BlockModelRegistry, + coords: ChunkCoords, + chunk: &Chunk, + ) -> Result { + let mut builder = MeshBuilder::new(); + + for x in 0..Chunk::SIZE as u32 { + for z in 0..Chunk::SIZE as u32 { + let h = chunk.top_height_at(x, z); + for y in 0..h { + let id = chunk.get_id(x, y, z); + + if id == 0 { + continue; + } + + let nq = chunk.neighbor_query(x, y, z); + let (bx, bz) = coords.block_coords(x, z); + + let textures = block_model_registry.get_or_fallback(id); + builder.add_cube(bx, y as i32, bz, nq, &textures); + } + } + } + + let vertex_buffer = builder.upload(allocators)?; + + Ok(Self { vertex_buffer }) + } +} + impl MeshBuilder { pub fn new() -> Self { Self { @@ -22,92 +108,107 @@ impl MeshBuilder { } } - pub fn add_cube(&mut self, x: i32, y: i32, z: i32) { + fn add_cube(&mut self, x: i32, y: i32, z: i32, nq: NeighborQuery, textures: &CubeTextures) { let position = Vec3::new(x as f32, y as f32, z as f32); + // Front - self.add_face( - position, - Vec3::new(0.0, 0.0, 0.0), - Vec3::new(1.0, 0.0, 0.0), - Vec3::new(1.0, 1.0, 0.0), - Vec3::new(0.0, 1.0, 0.0), - Vec3::new(1.0, 0.0, 0.0), - ); + if !nq.contains(NeighborQuery::FRONT) { + self.add_face(FaceInfo { + position, + p0: Vec3::new(0.0, 0.0, 0.0), + p1: Vec3::new(1.0, 0.0, 0.0), + p2: Vec3::new(1.0, 1.0, 0.0), + p3: Vec3::new(0.0, 1.0, 0.0), + tex: textures.front, + }); + } // Left - self.add_face( - position, - Vec3::new(0.0, 0.0, 0.0), - Vec3::new(0.0, 1.0, 0.0), - Vec3::new(0.0, 1.0, 1.0), - Vec3::new(0.0, 0.0, 1.0), - Vec3::new(0.0, 1.0, 0.0), - ); + if !nq.contains(NeighborQuery::LEFT) { + self.add_face(FaceInfo { + position, + p0: Vec3::new(0.0, 0.0, 0.0), + p1: Vec3::new(0.0, 0.0, 1.0), + p2: Vec3::new(0.0, 1.0, 1.0), + p3: Vec3::new(0.0, 1.0, 0.0), + tex: textures.left, + }); + } // Back - self.add_face( - position, - Vec3::new(0.0, 0.0, 1.0), - Vec3::new(1.0, 0.0, 1.0), - Vec3::new(1.0, 1.0, 1.0), - Vec3::new(0.0, 1.0, 1.0), - Vec3::new(0.0, 0.0, 1.0), - ); + if !nq.contains(NeighborQuery::BACK) { + self.add_face(FaceInfo { + position, + p0: Vec3::new(0.0, 0.0, 1.0), + p1: Vec3::new(1.0, 0.0, 1.0), + p2: Vec3::new(1.0, 1.0, 1.0), + p3: Vec3::new(0.0, 1.0, 1.0), + tex: textures.back, + }); + } // Right - self.add_face( - position, - Vec3::new(1.0, 0.0, 0.0), - Vec3::new(1.0, 1.0, 0.0), - Vec3::new(1.0, 1.0, 1.0), - Vec3::new(1.0, 0.0, 1.0), - Vec3::new(1.0, 1.0, 0.0), - ); + if !nq.contains(NeighborQuery::RIGHT) { + self.add_face(FaceInfo { + position, + p0: Vec3::new(1.0, 0.0, 0.0), + p1: Vec3::new(1.0, 0.0, 1.0), + p2: Vec3::new(1.0, 1.0, 1.0), + p3: Vec3::new(1.0, 1.0, 0.0), + tex: textures.right, + }); + } // Bottom - self.add_face( - position, - Vec3::new(0.0, 0.0, 0.0), - Vec3::new(1.0, 0.0, 0.0), - Vec3::new(1.0, 0.0, 1.0), - Vec3::new(0.0, 0.0, 1.0), - Vec3::new(1.0, 0.0, 1.0), - ); + if !nq.contains(NeighborQuery::BOTTOM) { + self.add_face(FaceInfo { + position, + p0: Vec3::new(0.0, 0.0, 0.0), + p1: Vec3::new(1.0, 0.0, 0.0), + p2: Vec3::new(1.0, 0.0, 1.0), + p3: Vec3::new(0.0, 0.0, 1.0), + tex: textures.bottom, + }); + } // Top - self.add_face( - position, - Vec3::new(0.0, 1.0, 0.0), - Vec3::new(1.0, 1.0, 0.0), - Vec3::new(1.0, 1.0, 1.0), - Vec3::new(0.0, 1.0, 1.0), - Vec3::new(0.0, 1.0, 1.0), - ); + if !nq.contains(NeighborQuery::TOP) { + self.add_face(FaceInfo { + position, + p0: Vec3::new(0.0, 1.0, 0.0), + p1: Vec3::new(1.0, 1.0, 0.0), + p2: Vec3::new(1.0, 1.0, 1.0), + p3: Vec3::new(0.0, 1.0, 1.0), + tex: textures.top, + }); + } } - fn add_face(&mut self, d: Vec3, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, c: Vec3) { + #[inline] + fn add_face(&mut self, face: FaceInfo) { self.vertices.push(InputVertex { - i_position: p0 + d, - i_color: c, + i_position: face.p0 + face.position, + i_texture: make_texture(0, 1, face.tex), }); self.vertices.push(InputVertex { - i_position: p1 + d, - i_color: c, + i_position: face.p1 + face.position, + i_texture: make_texture(1, 1, face.tex), }); self.vertices.push(InputVertex { - i_position: p2 + d, - i_color: c, + i_position: face.p2 + face.position, + i_texture: make_texture(1, 0, face.tex), }); self.vertices.push(InputVertex { - i_position: p2 + d, - i_color: c, + i_position: face.p2 + face.position, + i_texture: make_texture(1, 0, face.tex), }); self.vertices.push(InputVertex { - i_position: p3 + d, - i_color: c, + i_position: face.p3 + face.position, + i_texture: make_texture(0, 0, face.tex), }); self.vertices.push(InputVertex { - i_position: p0 + d, - i_color: c, + i_position: face.p0 + face.position, + i_texture: make_texture(0, 1, face.tex), }); } pub fn upload(self, allocators: &Arc) -> Result, Error> { + log::info!("Upload {} vertices", self.vertices.len()); let buffer = Buffer::from_iter( allocators.memory.clone(), BufferCreateInfo { @@ -126,3 +227,16 @@ impl MeshBuilder { } } +fn make_texture(u: u32, v: u32, t: u32) -> u32 { + (t << 2) | (u << 1) | v +} + +struct FaceInfo { + position: Vec3, + p0: Vec3, + p1: Vec3, + p2: Vec3, + p3: Vec3, + tex: u32, +} + diff --git a/src/render/mod.rs b/src/render/mod.rs index ba38c3b..91998c3 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,13 +1,17 @@ -use std::{sync::Arc, time::Instant}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use asset::{block::BlockRegistryBuilder, texture::TextureRegistryBuilder}; use glam::{Mat4, Vec3}; -use mesh::MeshBuilder; +use mesh::WorldMeshState; use util::{Allocators, ViewportExt}; use vertex::InputVertex; use vulkano::{ buffer::{ allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo}, - BufferUsage, Subbuffer, + BufferUsage, }, command_buffer::{ AutoCommandBufferBuilder, CommandBufferUsage, RenderPassBeginInfo, SubpassBeginInfo, @@ -16,7 +20,11 @@ use vulkano::{ descriptor_set::{DescriptorSet, WriteDescriptorSet}, device::{Device, DeviceExtensions, Queue}, format::Format, - image::Image, + image::{ + sampler::{Filter, Sampler, SamplerAddressMode, SamplerCreateInfo, SamplerMipmapMode}, + view::ImageView, + Image, + }, instance::{InstanceCreateFlags, InstanceCreateInfo}, memory::allocator::MemoryTypeFilter, pipeline::{graphics::viewport::Viewport, GraphicsPipeline, Pipeline, PipelineBindPoint}, @@ -28,8 +36,12 @@ use vulkano::{ }; use winit::window::Window; -use crate::error::Error; +use crate::{ + error::Error, + world::{Chunk, ChunkCoords, NoiseChunkGenerator, World}, +}; +pub mod asset; pub mod mesh; pub mod shaders; pub mod util; @@ -48,18 +60,54 @@ pub struct Renderer { swapchain_images: Vec>, swapchain_dirty: bool, - vertex_buffer: Subbuffer<[InputVertex]>, uniform_allocator: SubbufferAllocator, render_pass: Arc, viewport: Viewport, framebuffers: Vec>, + sampler: Arc, + texture_view: Arc, + vertex_shader: EntryPoint, fragment_shader: EntryPoint, pipeline: Arc, + world: World, + g: NoiseChunkGenerator, + + render_world: WorldMeshState, + start: Instant, + fps: Fps, +} + +pub struct Fps { + frames: usize, + last: Instant, +} + +impl Fps { + pub fn new() -> Self { + Self { + frames: 0, + last: Instant::now(), + } + } + + pub fn new_frame(&mut self) { + self.frames += 1; + } + + pub fn update(&mut self) { + let delta = self.last.elapsed(); + if delta >= Duration::SECOND { + let fps = self.frames as f64 / delta.as_secs_f64(); + log::info!("FPS: {fps}"); + self.frames = 0; + self.last = Instant::now(); + } + } } impl Renderer { @@ -85,16 +133,6 @@ impl Renderer { let allocators = Allocators::new_default(device.clone())?; - let vertex_buffer = { - let mut builder = MeshBuilder::new(); - builder.add_cube(0, 0, 0); - builder.add_cube(-1, 0, 0); - builder.add_cube(0, 0, -1); - builder.add_cube(-1, 0, -1); - builder.add_cube(0, 1, 0); - builder.upload(&allocators)? - }; - let uniform_allocator = SubbufferAllocator::new( allocators.memory.clone(), SubbufferAllocatorCreateInfo { @@ -148,12 +186,43 @@ impl Renderer { let fragment_shader = shaders::plain::fs::load(device.clone())? .entry_point("main") .expect("shader entry point"); + + let texture_registry_builder = + TextureRegistryBuilder::new(device.clone(), allocators.clone(), 32, 32)?; + let mut block_registry_builder = BlockRegistryBuilder::new(texture_registry_builder); + + for name in ["dirt", "grass", "stone"] { + if let Err(error) = block_registry_builder.load(name) { + log::error!("Block {name} load error: {error}"); + } + } + + let (block_model_registry, block_registry, texture_view) = + block_registry_builder.build(queue.clone())?; + + let world = World::new(block_registry); + let g = NoiseChunkGenerator::from_seed(1234); + + let render_world = WorldMeshState::new(block_model_registry); + + let sampler = Sampler::new( + device.clone(), + SamplerCreateInfo { + mag_filter: Filter::Nearest, + min_filter: Filter::Nearest, + mipmap_mode: SamplerMipmapMode::Linear, + address_mode: [SamplerAddressMode::Repeat; 3], + ..Default::default() + }, + )?; + let pipeline = shaders::create_two_stage_3d_pipeline::( &device, &render_pass, viewport.clone(), &vertex_shader, &fragment_shader, + vec![sampler.clone()], )?; let start = Instant::now(); @@ -169,7 +238,6 @@ impl Renderer { swapchain_images, swapchain_dirty: false, - vertex_buffer, uniform_allocator, render_pass, @@ -180,11 +248,22 @@ impl Renderer { fragment_shader, pipeline, + sampler, + texture_view, + + world, + g, + render_world, + start, + fps: Fps::new(), }) } pub fn render(&mut self) -> Result<(), Error> { + self.fps.new_frame(); + self.fps.update(); + self.refresh_swapchain()?; let (image_index, suboptimal, acquire_future) = @@ -211,36 +290,58 @@ impl Renderer { let delta = self.start.elapsed(); let dt = delta.as_secs_f64() * 1.0; let model = Mat4::IDENTITY; - let projection = Mat4::perspective_rh_gl(45.0f32.to_radians(), aspect, 0.01, 100.0); + let projection = Mat4::perspective_rh_gl(45.0f32.to_radians(), aspect, 0.01, 1000.0); fn make_view(camera: Vec3, target: Vec3) -> Mat4 { Mat4::look_at_rh(camera, target, Vec3::Y) } - let view = make_view( - Vec3::new((dt.cos() * 3.0) as f32, 3.0, (dt.sin() * 3.0) as f32), - Vec3::new(0.0, 0.0, 0.0), + let camera_position = Vec3::new( + (dt.cos() * 32.0) as f32 + (Chunk::SIZE / 2) as f32, + 48.0, + (dt.sin() * 32.0) as f32 + (Chunk::SIZE / 2) as f32, ); - let uniform_buffer = self - .uniform_allocator - .allocate_sized::()?; - { - let mut write = uniform_buffer.write()?; - *write = shaders::plain::SceneData { - model: model.to_cols_array_2d(), - view: view.to_cols_array_2d(), - projection: projection.to_cols_array_2d(), - }; - } + self.world.update_with_camera(camera_position, &mut self.g); - let descriptor_layout = &self.pipeline.layout().set_layouts()[0]; - let set0 = DescriptorSet::new( - self.allocators.descriptor_set.clone(), - descriptor_layout.clone(), - [WriteDescriptorSet::buffer(0, uniform_buffer)], - [], - )?; + let view = make_view( + camera_position, + Vec3::new((Chunk::SIZE / 2) as f32, 32.0, (Chunk::SIZE / 2) as f32), + ); + + let set0 = { + let uniform_buffer = self + .uniform_allocator + .allocate_sized::()?; + { + let mut write = uniform_buffer.write()?; + *write = shaders::plain::SceneData { + model: model.to_cols_array_2d(), + view: view.to_cols_array_2d(), + projection: projection.to_cols_array_2d(), + }; + } + + let descriptor_layout = &self.pipeline.layout().set_layouts()[0]; + + DescriptorSet::new( + self.allocators.descriptor_set.clone(), + descriptor_layout.clone(), + [WriteDescriptorSet::buffer(0, uniform_buffer)], + [], + )? + }; + + let set1 = { + let descriptor_layout = &self.pipeline.layout().set_layouts()[1]; + + DescriptorSet::new( + self.allocators.descriptor_set.clone(), + descriptor_layout.clone(), + [WriteDescriptorSet::image_view(1, self.texture_view.clone())], + [], + )? + }; let mut builder = AutoCommandBufferBuilder::primary( self.allocators.command_buffer.clone(), @@ -259,18 +360,45 @@ impl Renderer { }, )?; - builder - .bind_pipeline_graphics(self.pipeline.clone())? - .bind_descriptor_sets( - PipelineBindPoint::Graphics, - self.pipeline.layout().clone(), - 0, - set0.offsets([0]), - )? - .bind_vertex_buffers(0, self.vertex_buffer.clone())?; + builder.bind_pipeline_graphics(self.pipeline.clone())?; - unsafe { - builder.draw(self.vertex_buffer.len() as _, 1, 0, 0)?; + builder.bind_descriptor_sets( + PipelineBindPoint::Graphics, + self.pipeline.layout().clone(), + 0, + set0.offsets([0]), + )?; + builder.bind_descriptor_sets( + PipelineBindPoint::Graphics, + self.pipeline.layout().clone(), + 1, + set1, + )?; + + const VIEW_DISTANCE: i32 = 5; + + fn cpos(x: f32) -> i32 { + (x as i32 + Chunk::SIZE as i32 / 2) / Chunk::SIZE as i32 + } + + let camera_cx = cpos(camera_position.x); + let camera_cz = cpos(camera_position.z); + + for x in -VIEW_DISTANCE..=VIEW_DISTANCE { + for z in -VIEW_DISTANCE..=VIEW_DISTANCE { + let coords = ChunkCoords::new(x + camera_cx, z + camera_cz); + let mesh = + self.render_world + .get_or_upload(&self.allocators, coords, &self.world)?; + + if let Some(mesh) = mesh { + builder.bind_vertex_buffers(0, mesh.vertex_buffer.clone())?; + + unsafe { + builder.draw(mesh.vertex_buffer.len() as _, 1, 0, 0)?; + } + } + } } builder.end_render_pass(SubpassEndInfo::default())?; @@ -328,6 +456,7 @@ impl Renderer { self.viewport.clone(), &self.vertex_shader, &self.fragment_shader, + vec![self.sampler.clone()], )?; Ok(()) diff --git a/src/render/shaders/mod.rs b/src/render/shaders/mod.rs index f8d5139..008a5a5 100644 --- a/src/render/shaders/mod.rs +++ b/src/render/shaders/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use vulkano::{ descriptor_set::layout::DescriptorType, device::Device, + image::sampler::Sampler, pipeline::{ graphics::{ color_blend::{ColorBlendAttachmentState, ColorBlendState}, @@ -46,6 +47,7 @@ pub fn create_two_stage_3d_pipeline( viewport: Viewport, vertex_shader: &EntryPoint, fragment_shader: &EntryPoint, + samplers: Vec>, ) -> Result, Error> { let vertex_input_state = V::per_vertex().definition(vertex_shader)?; let stages = [ @@ -55,12 +57,19 @@ pub fn create_two_stage_3d_pipeline( let layout = { let mut layout_create_info = PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages); + layout_create_info.set_layouts[0] .bindings .get_mut(&0) .unwrap() .descriptor_type = DescriptorType::UniformBufferDynamic; + layout_create_info.set_layouts[1] + .bindings + .get_mut(&0) + .unwrap() + .immutable_samplers = samplers; + PipelineLayout::new( device.clone(), layout_create_info.into_pipeline_layout_create_info(device.clone())?, diff --git a/src/render/shaders/plain.frag b/src/render/shaders/plain.frag index 85107d8..752e255 100644 --- a/src/render/shaders/plain.frag +++ b/src/render/shaders/plain.frag @@ -1,9 +1,14 @@ #version 460 -layout(location = 0) in vec3 i_color; +layout(location = 0) in vec2 i_tex_coords; +layout(location = 1) in flat uint i_tex_index; layout(location = 0) out vec4 o_color; +layout(set = 1, binding = 0) uniform sampler u_sampler; +layout(set = 1, binding = 1) uniform texture2DArray u_texture; + void main() { - o_color = vec4(i_color, 1.0); + vec4 color = texture(sampler2DArray(u_texture, u_sampler), vec3(i_tex_coords, float(i_tex_index))); + o_color = color; } diff --git a/src/render/shaders/plain.vert b/src/render/shaders/plain.vert index a19907e..56e8ad5 100644 --- a/src/render/shaders/plain.vert +++ b/src/render/shaders/plain.vert @@ -1,9 +1,10 @@ #version 460 layout(location = 0) in vec3 i_position; -layout(location = 1) in vec3 i_color; +layout(location = 1) in uint i_texture; -layout(location = 0) out vec3 o_color; +layout(location = 0) out vec2 o_tex_coords; +layout(location = 1) out uint o_tex_index; layout(set = 0, binding = 0) uniform SceneData { mat4 model; @@ -13,8 +14,14 @@ layout(set = 0, binding = 0) uniform SceneData { void main() { vec4 m_position = vec4(i_position, 1.0); - vec4 s_position = u_scene.projection * u_scene.view * m_position; + vec4 w_position = u_scene.view * m_position; + vec4 s_position = u_scene.projection * w_position; gl_Position = s_position; - o_color = i_color; + + uint u = (i_texture >> 1) & 1; + uint v = i_texture & 1; + + o_tex_index = i_texture >> 2; + o_tex_coords = vec2(float(u), float(v)); } diff --git a/src/render/util.rs b/src/render/util.rs index fc0116c..03f7beb 100644 --- a/src/render/util.rs +++ b/src/render/util.rs @@ -205,3 +205,73 @@ pub fn create_3d_framebuffers( Ok(framebuffers) } +// pub fn load_png_texture>( +// allocators: &Allocators, +// queue: &Arc, +// path: P, +// usage: ImageUsage, +// ) -> Result, Error> { +// let file = File::open(path)?; +// let decoder = png::Decoder::new(file); +// let mut reader = decoder.read_info()?; +// let info = reader.info(); +// +// let extent = [info.width, info.height, 1]; +// +// let image = Image::new( +// allocators.memory.clone(), +// ImageCreateInfo { +// usage: ImageUsage::TRANSFER_DST | usage, +// image_type: ImageType::Dim2d, +// extent, +// format: Format::R8G8B8A8_UNORM, +// ..Default::default() +// }, +// Default::default(), +// )?; +// +// let buffer_size = (info.width * info.height * 4) as DeviceSize; +// +// let upload_buffer = Buffer::new_slice( +// allocators.memory.clone(), +// BufferCreateInfo { +// usage: BufferUsage::TRANSFER_SRC, +// ..Default::default() +// }, +// AllocationCreateInfo { +// memory_type_filter: MemoryTypeFilter::PREFER_HOST +// | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, +// ..Default::default() +// }, +// buffer_size, +// )?; +// +// { +// let mut write = upload_buffer.write()?; +// +// reader.next_frame(&mut write[..])?; +// } +// +// let mut upload = AutoCommandBufferBuilder::primary( +// allocators.command_buffer.clone(), +// queue.queue_family_index(), +// CommandBufferUsage::OneTimeSubmit, +// )?; +// +// upload.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image( +// upload_buffer, +// image.clone(), +// ))?; +// +// upload +// .build()? +// .execute(queue.clone())? +// .then_signal_fence_and_flush()? +// .wait(None)?; +// +// let view = ImageView::new_default(image)?; +// +// Ok(view) +// } +// + diff --git a/src/render/vertex.rs b/src/render/vertex.rs index 94d34b2..78b8c98 100644 --- a/src/render/vertex.rs +++ b/src/render/vertex.rs @@ -6,7 +6,7 @@ use vulkano::{buffer::BufferContents, pipeline::graphics::vertex_input::Vertex}; pub struct InputVertex { #[format(R32G32B32_SFLOAT)] pub i_position: Vec3, - #[format(R32G32B32_SFLOAT)] - pub i_color: Vec3, + #[format(R32_UINT)] + pub i_texture: u32, } diff --git a/src/world/mod.rs b/src/world/mod.rs new file mode 100644 index 0000000..28118a0 --- /dev/null +++ b/src/world/mod.rs @@ -0,0 +1,225 @@ +use std::{collections::HashMap, hash::Hash}; + +use bitflags::bitflags; +use glam::Vec3; +use noise::NoiseFn; + +use crate::render::asset::block::BlockRegistry; + +pub struct Chunk { + blocks: [u8; Self::SIZE * Self::SIZE * Self::HEIGHT], + height_map: [u32; Self::SIZE * Self::SIZE], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ChunkCoords { + pub x: i32, + pub z: i32, +} + +impl ChunkCoords { + pub const fn new(x: i32, z: i32) -> Self { + Self { x, z } + } + + pub fn origin_block(self) -> (i32, i32) { + (self.x * Chunk::SIZE as i32, self.z * Chunk::SIZE as i32) + } + + pub fn block_coords(self, x: u32, z: u32) -> (i32, i32) { + let (cx, cz) = self.origin_block(); + (cx + x as i32, cz + z as i32) + } +} + +pub struct World { + chunks: HashMap, + block_registry: BlockRegistry, +} + +impl World { + pub fn new(block_registry: BlockRegistry) -> Self { + Self { + chunks: HashMap::new(), + block_registry, + } + } + + pub fn generate(&mut self, coords: ChunkCoords, g: &mut G) { + assert!(!self.chunks.contains_key(&coords)); + + let mut chunk = Chunk::empty(); + g.generate_terrain(coords, &mut chunk, &self.block_registry); + self.chunks.insert(coords, chunk); + } + + pub fn get(&self, coords: ChunkCoords) -> Option<&Chunk> { + self.chunks.get(&coords) + } + + pub fn update_with_camera(&mut self, camera_position: Vec3, g: &mut G) { + const VIEW_DISTANCE: i32 = 5; + + fn cpos(x: f32) -> i32 { + (x as i32 + Chunk::SIZE as i32 / 2) / Chunk::SIZE as i32 + } + + let camera_cx = cpos(camera_position.x); + let camera_cz = cpos(camera_position.z); + + for cx in camera_cx - VIEW_DISTANCE..=camera_cx + VIEW_DISTANCE { + for cz in camera_cz - VIEW_DISTANCE..=camera_cz + VIEW_DISTANCE { + let coords = ChunkCoords::new(cx, cz); + + if !self.chunks.contains_key(&coords) { + log::info!("Generate {coords:?}"); + self.generate(coords, g); + } + } + } + } +} + +pub struct NoiseChunkGenerator { + noise: noise::Perlin, +} + +impl ChunkGenerator for NoiseChunkGenerator { + fn generate_terrain( + &mut self, + coords: ChunkCoords, + chunk: &mut Chunk, + block_registry: &BlockRegistry, + ) { + let grass = block_registry.get("grass").unwrap_or(0); + let dirt = block_registry.get("dirt").unwrap_or(0); + let stone = block_registry.get("stone").unwrap_or(0); + + for ix in 0..Chunk::SIZE as u32 { + for iz in 0..Chunk::SIZE as u32 { + let (x, z) = coords.block_coords(ix, iz); + let x = x as f64; + let z = z as f64; + + let n0 = self.noise.get([x / 256.0, 1.0, z / 256.0]) * 16.0; + let n1 = self.noise.get([x / 64.0, 0.0, z / 64.0]) * 4.0; + let n2 = self.noise.get([x / 16.0, 1.0, z / 16.0]) * 2.0; + let h = (n0 + n1 + n2 + 32.0) as u32; + + let sn0 = self.noise.get([x / 16.0, 0.0, z / 16.0]) * 2.0; + let stone_depth = (sn0 + 4.0) as u32; + + for y in 0..h { + let id = if y + 1 == h { + grass + } else if h - y > stone_depth { + stone + } else { + dirt + }; + chunk.set_id(ix, y, iz, id); + } + } + } + } +} + +impl NoiseChunkGenerator { + pub fn from_seed(seed: u64) -> Self { + let noise = noise::Perlin::new(seed as _); + Self { noise } + } +} + +pub trait ChunkGenerator { + fn generate_terrain( + &mut self, + coords: ChunkCoords, + chunk: &mut Chunk, + block_registry: &BlockRegistry, + ); +} + +bitflags! { + #[derive(Debug)] + pub struct NeighborQuery: u8 { + const FRONT = 1 << 0; + const LEFT = 1 << 1; + const RIGHT = 1 << 2; + const BACK = 1 << 3; + const BOTTOM = 1 << 4; + const TOP = 1 << 5; + } +} + +impl Chunk { + pub const SIZE: usize = 32; + pub const HEIGHT: usize = 128; + + pub fn empty() -> Self { + Self { + blocks: [0; Self::SIZE * Self::SIZE * Self::HEIGHT], + height_map: [0; Self::SIZE * Self::SIZE], + } + } + + pub fn fill_layer u8>(&mut self, y: u32, f: F) { + for x in 0..Self::SIZE as u32 { + for z in 0..Self::SIZE as u32 { + self.set_id(x, y, z, f(x, z)); + } + } + } + + pub fn get_id(&self, x: u32, y: u32, z: u32) -> u8 { + self.blocks[Self::to_index(x, y, z)] + } + + pub fn set_id(&mut self, x: u32, y: u32, z: u32, id: u8) { + self.blocks[Self::to_index(x, y, z)] = id; + + if id != 0 && y >= self.top_height_at(x, z) { + self.set_top_height_at(x, z, y + 1); + } + } + + pub fn set_top_height_at(&mut self, x: u32, z: u32, h: u32) { + self.height_map[Self::to_height_index(x, z)] = h; + } + + pub fn top_height_at(&self, x: u32, z: u32) -> u32 { + self.height_map[Self::to_height_index(x, z)] + } + + pub fn neighbor_query(&self, x: u32, y: u32, z: u32) -> NeighborQuery { + let mut q = NeighborQuery::empty(); + if z != 0 && self.get_id(x, y, z - 1) != 0 { + q |= NeighborQuery::FRONT; + } + if x != 0 && self.get_id(x - 1, y, z) != 0 { + q |= NeighborQuery::LEFT; + } + if z as usize != Self::SIZE - 1 && self.get_id(x, y, z + 1) != 0 { + q |= NeighborQuery::BACK; + } + if x as usize != Self::SIZE - 1 && self.get_id(x + 1, y, z) != 0 { + q |= NeighborQuery::RIGHT; + } + if y != 0 && self.get_id(x, y - 1, z) != 0 { + q |= NeighborQuery::BOTTOM; + } + if y as usize != Self::HEIGHT - 1 && self.get_id(x, y + 1, z) != 0 { + q |= NeighborQuery::TOP; + } + q + } + + const fn to_index(x: u32, y: u32, z: u32) -> usize { + x as usize + (z as usize + y as usize * Self::SIZE) * Self::SIZE + } + + const fn to_height_index(x: u32, z: u32) -> usize { + x as usize + z as usize * Self::SIZE + } +} +