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 0000000..5500203 Binary files /dev/null and b/assets/textures/blocks/dirt.png differ diff --git a/assets/textures/blocks/fallback.png b/assets/textures/blocks/fallback.png new file mode 100644 index 0000000..f200c22 Binary files /dev/null and b/assets/textures/blocks/fallback.png differ diff --git a/assets/textures/blocks/grass_side.png b/assets/textures/blocks/grass_side.png new file mode 100644 index 0000000..0d7ba60 Binary files /dev/null and b/assets/textures/blocks/grass_side.png differ diff --git a/assets/textures/blocks/grass_top.png b/assets/textures/blocks/grass_top.png new file mode 100644 index 0000000..e20661c Binary files /dev/null and b/assets/textures/blocks/grass_top.png differ diff --git a/assets/textures/blocks/stone.png b/assets/textures/blocks/stone.png new file mode 100644 index 0000000..f07bc10 Binary files /dev/null and b/assets/textures/blocks/stone.png differ diff --git a/src/error.rs b/src/error.rs index 4ad3890..58a80f5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::io; + use vulkano::{ buffer::AllocateBufferError, command_buffer::CommandBufferExecError, image::AllocateImageError, memory::allocator::MemoryAllocatorError, pipeline::layout::IntoPipelineLayoutCreateInfoError, @@ -13,6 +15,11 @@ pub enum Error { #[error("Window creation error: {0}")] WinitOs(#[from] winit::error::OsError), + #[error("{0}")] + Io(#[from] io::Error), + #[error("PNG decode error: {0}")] + PngDecode(#[from] png::DecodingError), + #[error("Vulkan loading error: {0}")] Loading(#[from] LoadingError), #[error("Window handle error: {0}")] diff --git a/src/main.rs b/src/main.rs index f136542..687f289 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![feature(duration_constants)] + use std::{process::ExitCode, sync::Arc}; use error::Error; @@ -12,6 +14,7 @@ use winit::{ pub mod error; pub mod render; +pub mod world; #[derive(Default)] pub struct Engine { diff --git a/src/render/asset/block.rs b/src/render/asset/block.rs new file mode 100644 index 0000000..b21344e --- /dev/null +++ b/src/render/asset/block.rs @@ -0,0 +1,178 @@ +use std::{collections::HashMap, fs::File, path::PathBuf, sync::Arc}; + +use serde::Deserialize; +use vulkano::{device::Queue, image::view::ImageView}; + +use crate::error::Error; + +use super::{ + model::{BlockModelRegistry, CubeTextures}, + texture::TextureRegistryBuilder, +}; + +#[derive(Debug, Deserialize)] +pub struct BlockDefinition { + pub model: BlockModelDefinition, +} + +pub struct BlockRegistry { + name_to_id: HashMap, +} + +#[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 + } +} +