From efe1d87d762c4888e5fb85af21d5e3e902d4c7a0 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Sat, 26 Apr 2025 16:34:12 +0300 Subject: [PATCH] Better asset loading --- Cargo.lock | 7 + Cargo.toml | 1 + src/error.rs | 3 + src/main.rs | 48 ++++++- src/render/asset/block.rs | 264 ++++++++++++++---------------------- src/render/asset/mod.rs | 118 ++++++++++++++++ src/render/asset/model.rs | 13 ++ src/render/asset/texture.rs | 131 ++++++++++-------- src/render/mesh.rs | 5 +- src/render/mod.rs | 53 ++------ src/world/chunk.rs | 99 ++++++++++++++ src/world/generator.rs | 66 +++++++++ src/world/level.rs | 60 ++++++++ src/world/mod.rs | 215 +---------------------------- 14 files changed, 612 insertions(+), 471 deletions(-) create mode 100644 src/world/chunk.rs create mode 100644 src/world/generator.rs create mode 100644 src/world/level.rs diff --git a/Cargo.lock b/Cargo.lock index 77f3027..30a2f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,6 +610,7 @@ dependencies = [ "bytemuck", "env_logger", "glam", + "glob", "log", "noise", "png", @@ -665,6 +666,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "half" version = "2.6.0" diff --git a/Cargo.toml b/Cargo.toml index 3fe4c12..7072565 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ bitflags = "2.9.0" bytemuck = { version = "1.22.0", features = ["derive"] } env_logger = "0.11.8" glam = { version = "0.30.2", features = ["bytemuck"] } +glob = "0.3.2" log = "0.4.27" noise = "0.9.0" png = "0.17.16" diff --git a/src/error.rs b/src/error.rs index 58a80f5..2676d00 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,6 +42,9 @@ pub enum Error { AllocateImage(#[from] AllocateImageError), #[error("GPU memory allocation error: {0}")] MemoryAllocator(#[from] MemoryAllocatorError), + + #[error("Missing asset/directory: {0}")] + AssetMissing(String), } impl From> for Error { diff --git a/src/main.rs b/src/main.rs index 687f289..29b7fbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,14 @@ #![feature(duration_constants)] +#![allow(clippy::new_without_default)] -use std::{process::ExitCode, sync::Arc}; +use std::{process::ExitCode, sync::Arc, time::Instant}; use error::Error; -use render::Renderer; +use glam::Vec3; +use render::{ + asset::{AssetLoader, TextureLoadStage}, + Renderer, +}; use winit::{ application::ApplicationHandler, dpi::PhysicalSize, @@ -11,26 +16,32 @@ use winit::{ event_loop::{ActiveEventLoop, EventLoop}, window::{Window, WindowId}, }; +use world::{Chunk, ChunkGenerator, NoiseChunkGenerator, World}; pub mod error; pub mod render; pub mod world; -#[derive(Default)] pub struct Engine { window: Option>, renderer: Option, + assets: Option>, + world: Option, + camera_position: Vec3, + generator: Box, + start: Instant, } impl Engine { pub fn ensure_renderer(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Error> { if self.window.is_none() { + let assets = self.assets.take().expect("asset loader"); let window_attributes = Window::default_attributes() .with_resizable(false) .with_inner_size(PhysicalSize::new(800, 600)); let window = Arc::new(event_loop.create_window(window_attributes)?); - let render = Renderer::from_window(window.clone())?; + let render = Renderer::from_window(window.clone(), assets)?; self.window = Some(window); self.renderer = Some(render); @@ -88,8 +99,19 @@ impl ApplicationHandler for Engine { } } WindowEvent::RedrawRequested => { - if let Some(render) = self.renderer.as_mut() { - if let Err(error) = render.render() { + if let (Some(render), Some(world)) = (self.renderer.as_mut(), self.world.as_mut()) { + let delta = self.start.elapsed(); + let dt = delta.as_secs_f64(); + + self.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, + ); + + world.update_with_camera(self.camera_position, &mut *self.generator); + + if let Err(error) = render.render(self.camera_position, world) { log::error!("Render error: {error}"); event_loop.exit(); } @@ -124,8 +146,20 @@ impl ApplicationHandler for Engine { } fn run() -> Result<(), Error> { + let loader = AssetLoader::new(); + let (loader, block_registry) = loader.load_block_definitions()?; + let world = World::new(block_registry); + let event_loop = EventLoop::new()?; - let mut engine = Engine::default(); + let mut engine = Engine { + window: None, + renderer: None, + assets: Some(loader), + world: Some(world), + camera_position: Vec3::ZERO, + generator: Box::new(NoiseChunkGenerator::from_seed(1234)), + start: Instant::now(), + }; event_loop.run_app(&mut engine)?; diff --git a/src/render/asset/block.rs b/src/render/asset/block.rs index b21344e..4fcb4a0 100644 --- a/src/render/asset/block.rs +++ b/src/render/asset/block.rs @@ -1,178 +1,124 @@ -use std::{collections::HashMap, fs::File, path::PathBuf, sync::Arc}; +use std::collections::HashMap; use serde::Deserialize; -use vulkano::{device::Queue, image::view::ImageView}; -use crate::error::Error; +use crate::render::asset::texture::FALLBACK_TEXTURE_ID; -use super::{ - model::{BlockModelRegistry, CubeTextures}, - texture::TextureRegistryBuilder, -}; +use super::model::CubeTextures; #[derive(Debug, Deserialize)] -pub struct BlockDefinition { +pub struct InputBlockDefinition { pub model: BlockModelDefinition, } +pub struct BlockDefinition { + pub model: SidedModelDefinition, +} + +#[derive(Debug, Deserialize)] +pub struct SidedModelDefinition { + pub front: String, + pub left: String, + pub back: String, + pub right: String, + pub bottom: String, + pub top: String, +} + +#[derive(Debug, Deserialize)] +pub struct GrassLikeModelDefinition { + pub side: String, + pub top: String, + pub bottom: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BlockModelDefinition { + Sided(SidedModelDefinition), + GrassLike(GrassLikeModelDefinition), + Simple(String), +} + 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 new() -> Self { + Self { + name_to_id: HashMap::new(), + } + } + + pub fn add(&mut self, name: String, id: u8) { + self.name_to_id.insert(name, id); + } + pub fn get(&self, name: &str) -> Option { self.name_to_id.get(name).copied() } } +impl BlockModelDefinition { + pub fn to_sided(self) -> SidedModelDefinition { + match self { + Self::Simple(texture) => SidedModelDefinition { + front: texture.clone(), + left: texture.clone(), + back: texture.clone(), + right: texture.clone(), + bottom: texture.clone(), + top: texture, + }, + Self::Sided(sided) => sided, + Self::GrassLike(grass) => SidedModelDefinition { + front: grass.side.clone(), + left: grass.side.clone(), + back: grass.side.clone(), + right: grass.side, + bottom: grass.bottom, + top: grass.top, + }, + } + } +} + +impl SidedModelDefinition { + pub fn resolve(&self, texture_map: &HashMap) -> CubeTextures { + let front = texture_map + .get(&self.front) + .copied() + .unwrap_or(FALLBACK_TEXTURE_ID); + let left = texture_map + .get(&self.left) + .copied() + .unwrap_or(FALLBACK_TEXTURE_ID); + let back = texture_map + .get(&self.back) + .copied() + .unwrap_or(FALLBACK_TEXTURE_ID); + let right = texture_map + .get(&self.right) + .copied() + .unwrap_or(FALLBACK_TEXTURE_ID); + let bottom = texture_map + .get(&self.bottom) + .copied() + .unwrap_or(FALLBACK_TEXTURE_ID); + let top = texture_map + .get(&self.top) + .copied() + .unwrap_or(FALLBACK_TEXTURE_ID); + + CubeTextures { + front, + left, + back, + right, + bottom, + top, + } + } +} + diff --git a/src/render/asset/mod.rs b/src/render/asset/mod.rs index ff3c4cf..445c970 100644 --- a/src/render/asset/mod.rs +++ b/src/render/asset/mod.rs @@ -1,4 +1,122 @@ +use std::{collections::HashMap, fs::File, path::Path, sync::Arc}; + +use block::{BlockDefinition, BlockRegistry, InputBlockDefinition}; +use model::BlockModelRegistry; +use texture::TextureDependencies; +use vulkano::{device::Queue, image::view::ImageView}; + +use crate::error::Error; + +use super::util::Allocators; + pub mod block; pub mod model; pub mod texture; +#[derive(Debug, Default)] +pub struct BlockDefinitionStage { + definitions: HashMap, +} + +pub struct TextureLoadStage { + dependencies: TextureDependencies, + definitions: HashMap, +} +pub struct RuntimeStage; + +pub struct AssetLoader { + stage: S, +} + +impl AssetLoader { + pub fn new() -> Self { + Self { + stage: Default::default(), + } + } + + pub fn load_block_definitions( + mut self, + ) -> Result<(AssetLoader, BlockRegistry), Error> { + const PATH: &str = "assets/blocks"; + for entry in + glob::glob(&format!("{PATH}/*.json")).map_err(|_| Error::AssetMissing(PATH.into()))? + { + let Ok(entry) = entry else { continue }; + let Some(name) = entry.file_stem().and_then(|e| e.to_str()) else { + continue; + }; + + if let Err(error) = self.load_definition(name, &entry) { + log::error!("block:{name} load error: {error}"); + } + } + + let mut block_registry = BlockRegistry::new(); + let mut definitions = HashMap::new(); + let mut id = 1; + for (name, definition) in self.stage.definitions.into_iter() { + let sided_model = definition.model.to_sided(); + + block_registry.add(name, id); + definitions.insert(id, BlockDefinition { model: sided_model }); + + id += 1; + } + + let texture_dependencies = + TextureDependencies::from_block_definitions(definitions.values()); + + Ok(( + AssetLoader { + stage: TextureLoadStage { + dependencies: texture_dependencies, + definitions, + }, + }, + block_registry, + )) + } + + fn load_definition>(&mut self, name: &str, path: P) -> Result<(), Error> { + if self.stage.definitions.contains_key(name) { + log::warn!("Ignoring duplicate block:{name}"); + return Ok(()); + } + let file = File::open(path)?; + let definition = serde_json::from_reader(file).unwrap(); + self.stage.definitions.insert(name.into(), definition); + Ok(()) + } +} + +impl AssetLoader { + pub fn load_textures( + self, + allocators: Arc, + queue: Arc, + ) -> Result< + ( + AssetLoader, + BlockModelRegistry, + Arc, + ), + Error, + > { + let (texture_map, texture_view) = self.stage.dependencies.load(allocators, queue)?; + + let mut block_model_registry = BlockModelRegistry::new(); + for (id, block) in self.stage.definitions { + block_model_registry.add(id, block.model.resolve(&texture_map)); + } + + Ok(( + AssetLoader { + stage: RuntimeStage, + }, + block_model_registry, + texture_view, + )) + } +} + diff --git a/src/render/asset/model.rs b/src/render/asset/model.rs index 9ba1f37..b24a286 100644 --- a/src/render/asset/model.rs +++ b/src/render/asset/model.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use super::texture::FALLBACK_TEXTURE_ID; + pub struct BlockModelRegistry { pub id_to_model: HashMap, pub fallback: CubeTextures, @@ -28,6 +30,17 @@ impl CubeTextures { } impl BlockModelRegistry { + pub fn new() -> Self { + Self { + id_to_model: HashMap::new(), + fallback: CubeTextures::single(FALLBACK_TEXTURE_ID), + } + } + + pub fn add(&mut self, id: u8, model: CubeTextures) { + self.id_to_model.insert(id, model); + } + 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 index 2fee8c1..c86f23c 100644 --- a/src/render/asset/texture.rs +++ b/src/render/asset/texture.rs @@ -1,5 +1,5 @@ use std::{ - collections::{hash_map::Entry, HashMap}, + collections::{HashMap, HashSet}, fs::File, path::{Path, PathBuf}, sync::Arc, @@ -18,6 +18,10 @@ use vulkano::{ use crate::{error::Error, render::util::Allocators}; +use super::block::{BlockDefinition, SidedModelDefinition}; + +pub const FALLBACK_TEXTURE_ID: u32 = 0; + pub trait TextureReader { fn format(&self) -> Format; fn width(&self) -> u32; @@ -34,64 +38,14 @@ pub struct TextureArrayBuilder { readers: Vec>, } -pub struct TextureRegistryBuilder { - inner: TextureArrayBuilder, - map: HashMap, - last_texture_id: u32, +pub struct TextureDependencies { + set: HashSet, } 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)?; @@ -214,3 +168,74 @@ impl TextureArrayBuilder { } } +impl TextureDependencies { + pub fn new() -> Self { + Self { + set: HashSet::from_iter(["fallback".into()]), + } + } + + pub fn from_block_definitions<'a, I: IntoIterator>( + iter: I, + ) -> Self { + let mut this = Self::new(); + for def in iter { + this.from_definition(&def.model); + } + this + } + + pub fn add(&mut self, name: &str) { + self.set.insert(name.into()); + } + + pub fn from_definition(&mut self, definition: &SidedModelDefinition) { + self.add(&definition.front); + self.add(&definition.left); + self.add(&definition.back); + self.add(&definition.right); + self.add(&definition.bottom); + self.add(&definition.top); + } + + pub fn load( + self, + allocators: Arc, + queue: Arc, + ) -> Result<(HashMap, Arc), Error> { + const TEXTURE_PATH: &str = "assets/textures/blocks"; + let base = PathBuf::from(TEXTURE_PATH); + + let device = queue.device().clone(); + let mut builder = TextureArrayBuilder::new(device, allocators, 32, 32); + + // Add fallback texture + builder.push(Box::new(PngTextureReader::open(base.join("fallback.png"))?)); + + let mut id = 1; + let mut map = HashMap::new(); + for name in self.set { + if name == "fallback" { + continue; + } + + let reader = match PngTextureReader::open(base.join(format!("{name}.png"))) { + Ok(reader) => reader, + Err(error) => { + log::error!("Texture \"{name}\" load error: {error}"); + continue; + } + }; + + builder.push(Box::new(reader)); + log::info!("{name} -> {id}"); + map.insert(name, id); + id += 1; + } + + let texture_view = builder.upload(queue)?; + + Ok((map, texture_view)) + } +} + diff --git a/src/render/mesh.rs b/src/render/mesh.rs index c1262d9..2442ce4 100644 --- a/src/render/mesh.rs +++ b/src/render/mesh.rs @@ -90,7 +90,7 @@ impl ChunkMesh { 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); + builder.add_cube(bx, y as i32, bz, nq, textures); } } } @@ -208,7 +208,8 @@ impl MeshBuilder { } pub fn upload(self, allocators: &Arc) -> Result, Error> { - log::info!("Upload {} vertices", self.vertices.len()); + log::debug!("Upload {} vertices", self.vertices.len()); + let buffer = Buffer::from_iter( allocators.memory.clone(), BufferCreateInfo { diff --git a/src/render/mod.rs b/src/render/mod.rs index 91998c3..4c1c802 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use asset::{block::BlockRegistryBuilder, texture::TextureRegistryBuilder}; +use asset::{AssetLoader, TextureLoadStage}; use glam::{Mat4, Vec3}; use mesh::WorldMeshState; use util::{Allocators, ViewportExt}; @@ -38,7 +38,7 @@ use winit::window::Window; use crate::{ error::Error, - world::{Chunk, ChunkCoords, NoiseChunkGenerator, World}, + world::{Chunk, ChunkCoords, World}, }; pub mod asset; @@ -73,12 +73,8 @@ pub struct Renderer { fragment_shader: EntryPoint, pipeline: Arc, - world: World, - g: NoiseChunkGenerator, - render_world: WorldMeshState, - start: Instant, fps: Fps, } @@ -111,7 +107,10 @@ impl Fps { } impl Renderer { - pub fn from_window(window: Arc) -> Result { + pub fn from_window( + window: Arc, + asset_loader: AssetLoader, + ) -> Result { let instance_extensions = Surface::required_extensions(&window)?; let device_extensions = DeviceExtensions { khr_swapchain: true, @@ -187,21 +186,8 @@ impl Renderer { .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 (_, block_model_registry, texture_view) = + asset_loader.load_textures(allocators.clone(), queue.clone())?; let render_world = WorldMeshState::new(block_model_registry); @@ -225,8 +211,6 @@ impl Renderer { vec![sampler.clone()], )?; - let start = Instant::now(); - Ok(Self { window, device, @@ -251,16 +235,13 @@ impl Renderer { sampler, texture_view, - world, - g, render_world, - start, fps: Fps::new(), }) } - pub fn render(&mut self) -> Result<(), Error> { + pub fn render(&mut self, camera_position: Vec3, world: &mut World) -> Result<(), Error> { self.fps.new_frame(); self.fps.update(); @@ -287,8 +268,6 @@ impl Renderer { let dimensions = self.window.inner_size(); let aspect = dimensions.width as f32 / dimensions.height as f32; - 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, 1000.0); @@ -296,14 +275,6 @@ impl Renderer { Mat4::look_at_rh(camera, target, Vec3::Y) } - 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, - ); - - self.world.update_with_camera(camera_position, &mut self.g); - let view = make_view( camera_position, Vec3::new((Chunk::SIZE / 2) as f32, 32.0, (Chunk::SIZE / 2) as f32), @@ -387,9 +358,9 @@ impl Renderer { 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)?; + let mesh = self + .render_world + .get_or_upload(&self.allocators, coords, world)?; if let Some(mesh) = mesh { builder.bind_vertex_buffers(0, mesh.vertex_buffer.clone())?; diff --git a/src/world/chunk.rs b/src/world/chunk.rs new file mode 100644 index 0000000..37dc654 --- /dev/null +++ b/src/world/chunk.rs @@ -0,0 +1,99 @@ +use super::NeighborQuery; + +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) + } +} + +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 + } +} + diff --git a/src/world/generator.rs b/src/world/generator.rs new file mode 100644 index 0000000..53f82b1 --- /dev/null +++ b/src/world/generator.rs @@ -0,0 +1,66 @@ +use noise::NoiseFn; + +use crate::render::asset::block::BlockRegistry; + +use super::chunk::{Chunk, ChunkCoords}; + +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, + ); +} + diff --git a/src/world/level.rs b/src/world/level.rs new file mode 100644 index 0000000..1387bbf --- /dev/null +++ b/src/world/level.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; + +use glam::Vec3; + +use crate::render::asset::block::BlockRegistry; + +use super::{Chunk, ChunkCoords, ChunkGenerator}; + +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)); + log::debug!("Generate {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) { + self.generate(coords, g); + } + } + } + } +} + diff --git a/src/world/mod.rs b/src/world/mod.rs index 28118a0..11dccba 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -1,144 +1,12 @@ -use std::{collections::HashMap, hash::Hash}; - use bitflags::bitflags; -use glam::Vec3; -use noise::NoiseFn; -use crate::render::asset::block::BlockRegistry; +pub mod chunk; +pub mod generator; +pub mod level; -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, - ); -} +pub use chunk::{Chunk, ChunkCoords}; +pub use generator::{ChunkGenerator, NoiseChunkGenerator}; +pub use level::World; bitflags! { #[derive(Debug)] @@ -152,74 +20,3 @@ bitflags! { } } -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 - } -} -