Better asset loading

This commit is contained in:
2025-04-26 16:34:12 +03:00
parent 9b4c2fb9dd
commit efe1d87d76
14 changed files with 612 additions and 471 deletions
Generated
+7
View File
@@ -610,6 +610,7 @@ dependencies = [
"bytemuck", "bytemuck",
"env_logger", "env_logger",
"glam", "glam",
"glob",
"log", "log",
"noise", "noise",
"png", "png",
@@ -665,6 +666,12 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]] [[package]]
name = "half" name = "half"
version = "2.6.0" version = "2.6.0"
+1
View File
@@ -8,6 +8,7 @@ bitflags = "2.9.0"
bytemuck = { version = "1.22.0", features = ["derive"] } bytemuck = { version = "1.22.0", features = ["derive"] }
env_logger = "0.11.8" env_logger = "0.11.8"
glam = { version = "0.30.2", features = ["bytemuck"] } glam = { version = "0.30.2", features = ["bytemuck"] }
glob = "0.3.2"
log = "0.4.27" log = "0.4.27"
noise = "0.9.0" noise = "0.9.0"
png = "0.17.16" png = "0.17.16"
+3
View File
@@ -42,6 +42,9 @@ pub enum Error {
AllocateImage(#[from] AllocateImageError), AllocateImage(#[from] AllocateImageError),
#[error("GPU memory allocation error: {0}")] #[error("GPU memory allocation error: {0}")]
MemoryAllocator(#[from] MemoryAllocatorError), MemoryAllocator(#[from] MemoryAllocatorError),
#[error("Missing asset/directory: {0}")]
AssetMissing(String),
} }
impl From<Box<ValidationError>> for Error { impl From<Box<ValidationError>> for Error {
+41 -7
View File
@@ -1,9 +1,14 @@
#![feature(duration_constants)] #![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 error::Error;
use render::Renderer; use glam::Vec3;
use render::{
asset::{AssetLoader, TextureLoadStage},
Renderer,
};
use winit::{ use winit::{
application::ApplicationHandler, application::ApplicationHandler,
dpi::PhysicalSize, dpi::PhysicalSize,
@@ -11,26 +16,32 @@ use winit::{
event_loop::{ActiveEventLoop, EventLoop}, event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId}, window::{Window, WindowId},
}; };
use world::{Chunk, ChunkGenerator, NoiseChunkGenerator, World};
pub mod error; pub mod error;
pub mod render; pub mod render;
pub mod world; pub mod world;
#[derive(Default)]
pub struct Engine { pub struct Engine {
window: Option<Arc<Window>>, window: Option<Arc<Window>>,
renderer: Option<Renderer>, renderer: Option<Renderer>,
assets: Option<AssetLoader<TextureLoadStage>>,
world: Option<World>,
camera_position: Vec3,
generator: Box<dyn ChunkGenerator>,
start: Instant,
} }
impl Engine { impl Engine {
pub fn ensure_renderer(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Error> { pub fn ensure_renderer(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Error> {
if self.window.is_none() { if self.window.is_none() {
let assets = self.assets.take().expect("asset loader");
let window_attributes = Window::default_attributes() let window_attributes = Window::default_attributes()
.with_resizable(false) .with_resizable(false)
.with_inner_size(PhysicalSize::new(800, 600)); .with_inner_size(PhysicalSize::new(800, 600));
let window = Arc::new(event_loop.create_window(window_attributes)?); 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.window = Some(window);
self.renderer = Some(render); self.renderer = Some(render);
@@ -88,8 +99,19 @@ impl ApplicationHandler for Engine {
} }
} }
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
if let Some(render) = self.renderer.as_mut() { if let (Some(render), Some(world)) = (self.renderer.as_mut(), self.world.as_mut()) {
if let Err(error) = render.render() { 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}"); log::error!("Render error: {error}");
event_loop.exit(); event_loop.exit();
} }
@@ -124,8 +146,20 @@ impl ApplicationHandler for Engine {
} }
fn run() -> Result<(), Error> { 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 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)?; event_loop.run_app(&mut engine)?;
+105 -159
View File
@@ -1,178 +1,124 @@
use std::{collections::HashMap, fs::File, path::PathBuf, sync::Arc}; use std::collections::HashMap;
use serde::Deserialize; use serde::Deserialize;
use vulkano::{device::Queue, image::view::ImageView};
use crate::error::Error; use crate::render::asset::texture::FALLBACK_TEXTURE_ID;
use super::{ use super::model::CubeTextures;
model::{BlockModelRegistry, CubeTextures},
texture::TextureRegistryBuilder,
};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BlockDefinition { pub struct InputBlockDefinition {
pub model: BlockModelDefinition, 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 { pub struct BlockRegistry {
name_to_id: HashMap<String, u8>, name_to_id: HashMap<String, u8>,
} }
#[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<u8, CubeTextures>,
name_to_id: HashMap<String, u8>,
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<Queue>,
) -> Result<(BlockModelRegistry, BlockRegistry, Arc<ImageView>), 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 { 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<u8> { pub fn get(&self, name: &str) -> Option<u8> {
self.name_to_id.get(name).copied() 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<String, u32>) -> 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,
}
}
}
+118
View File
@@ -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 block;
pub mod model; pub mod model;
pub mod texture; pub mod texture;
#[derive(Debug, Default)]
pub struct BlockDefinitionStage {
definitions: HashMap<String, InputBlockDefinition>,
}
pub struct TextureLoadStage {
dependencies: TextureDependencies,
definitions: HashMap<u8, BlockDefinition>,
}
pub struct RuntimeStage;
pub struct AssetLoader<S> {
stage: S,
}
impl AssetLoader<BlockDefinitionStage> {
pub fn new() -> Self {
Self {
stage: Default::default(),
}
}
pub fn load_block_definitions(
mut self,
) -> Result<(AssetLoader<TextureLoadStage>, 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<P: AsRef<Path>>(&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<TextureLoadStage> {
pub fn load_textures(
self,
allocators: Arc<Allocators>,
queue: Arc<Queue>,
) -> Result<
(
AssetLoader<RuntimeStage>,
BlockModelRegistry,
Arc<ImageView>,
),
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,
))
}
}
+13
View File
@@ -1,5 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use super::texture::FALLBACK_TEXTURE_ID;
pub struct BlockModelRegistry { pub struct BlockModelRegistry {
pub id_to_model: HashMap<u8, CubeTextures>, pub id_to_model: HashMap<u8, CubeTextures>,
pub fallback: CubeTextures, pub fallback: CubeTextures,
@@ -28,6 +30,17 @@ impl CubeTextures {
} }
impl BlockModelRegistry { 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 { pub fn get_or_fallback(&self, id: u8) -> &CubeTextures {
self.id_to_model.get(&id).unwrap_or(&self.fallback) self.id_to_model.get(&id).unwrap_or(&self.fallback)
} }
+78 -53
View File
@@ -1,5 +1,5 @@
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{HashMap, HashSet},
fs::File, fs::File,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@@ -18,6 +18,10 @@ use vulkano::{
use crate::{error::Error, render::util::Allocators}; use crate::{error::Error, render::util::Allocators};
use super::block::{BlockDefinition, SidedModelDefinition};
pub const FALLBACK_TEXTURE_ID: u32 = 0;
pub trait TextureReader { pub trait TextureReader {
fn format(&self) -> Format; fn format(&self) -> Format;
fn width(&self) -> u32; fn width(&self) -> u32;
@@ -34,64 +38,14 @@ pub struct TextureArrayBuilder {
readers: Vec<Box<dyn TextureReader>>, readers: Vec<Box<dyn TextureReader>>,
} }
pub struct TextureRegistryBuilder { pub struct TextureDependencies {
inner: TextureArrayBuilder, set: HashSet<String>,
map: HashMap<String, u32>,
last_texture_id: u32,
} }
pub struct PngTextureReader { pub struct PngTextureReader {
reader: png::Reader<File>, reader: png::Reader<File>,
} }
impl TextureRegistryBuilder {
pub fn new(
device: Arc<Device>,
allocators: Arc<Allocators>,
width: u32,
height: u32,
) -> Result<Self, Error> {
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<u32, Error> {
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<Queue>) -> Result<Arc<ImageView>, Error> {
self.inner.upload(queue)
}
}
impl PngTextureReader { impl PngTextureReader {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let file = File::open(path)?; 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<Item = &'a BlockDefinition>>(
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<Allocators>,
queue: Arc<Queue>,
) -> Result<(HashMap<String, u32>, Arc<ImageView>), 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))
}
}
+3 -2
View File
@@ -90,7 +90,7 @@ impl ChunkMesh {
let (bx, bz) = coords.block_coords(x, z); let (bx, bz) = coords.block_coords(x, z);
let textures = block_model_registry.get_or_fallback(id); 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<Allocators>) -> Result<Subbuffer<[InputVertex]>, Error> { pub fn upload(self, allocators: &Arc<Allocators>) -> Result<Subbuffer<[InputVertex]>, Error> {
log::info!("Upload {} vertices", self.vertices.len()); log::debug!("Upload {} vertices", self.vertices.len());
let buffer = Buffer::from_iter( let buffer = Buffer::from_iter(
allocators.memory.clone(), allocators.memory.clone(),
BufferCreateInfo { BufferCreateInfo {
+12 -41
View File
@@ -3,7 +3,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use asset::{block::BlockRegistryBuilder, texture::TextureRegistryBuilder}; use asset::{AssetLoader, TextureLoadStage};
use glam::{Mat4, Vec3}; use glam::{Mat4, Vec3};
use mesh::WorldMeshState; use mesh::WorldMeshState;
use util::{Allocators, ViewportExt}; use util::{Allocators, ViewportExt};
@@ -38,7 +38,7 @@ use winit::window::Window;
use crate::{ use crate::{
error::Error, error::Error,
world::{Chunk, ChunkCoords, NoiseChunkGenerator, World}, world::{Chunk, ChunkCoords, World},
}; };
pub mod asset; pub mod asset;
@@ -73,12 +73,8 @@ pub struct Renderer {
fragment_shader: EntryPoint, fragment_shader: EntryPoint,
pipeline: Arc<GraphicsPipeline>, pipeline: Arc<GraphicsPipeline>,
world: World,
g: NoiseChunkGenerator,
render_world: WorldMeshState, render_world: WorldMeshState,
start: Instant,
fps: Fps, fps: Fps,
} }
@@ -111,7 +107,10 @@ impl Fps {
} }
impl Renderer { impl Renderer {
pub fn from_window(window: Arc<Window>) -> Result<Self, Error> { pub fn from_window(
window: Arc<Window>,
asset_loader: AssetLoader<TextureLoadStage>,
) -> Result<Self, Error> {
let instance_extensions = Surface::required_extensions(&window)?; let instance_extensions = Surface::required_extensions(&window)?;
let device_extensions = DeviceExtensions { let device_extensions = DeviceExtensions {
khr_swapchain: true, khr_swapchain: true,
@@ -187,21 +186,8 @@ impl Renderer {
.entry_point("main") .entry_point("main")
.expect("shader entry point"); .expect("shader entry point");
let texture_registry_builder = let (_, block_model_registry, texture_view) =
TextureRegistryBuilder::new(device.clone(), allocators.clone(), 32, 32)?; asset_loader.load_textures(allocators.clone(), queue.clone())?;
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 render_world = WorldMeshState::new(block_model_registry);
@@ -225,8 +211,6 @@ impl Renderer {
vec![sampler.clone()], vec![sampler.clone()],
)?; )?;
let start = Instant::now();
Ok(Self { Ok(Self {
window, window,
device, device,
@@ -251,16 +235,13 @@ impl Renderer {
sampler, sampler,
texture_view, texture_view,
world,
g,
render_world, render_world,
start,
fps: Fps::new(), 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.new_frame();
self.fps.update(); self.fps.update();
@@ -287,8 +268,6 @@ impl Renderer {
let dimensions = self.window.inner_size(); let dimensions = self.window.inner_size();
let aspect = dimensions.width as f32 / dimensions.height as f32; 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 model = Mat4::IDENTITY;
let projection = Mat4::perspective_rh_gl(45.0f32.to_radians(), aspect, 0.01, 1000.0); 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) 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( let view = make_view(
camera_position, camera_position,
Vec3::new((Chunk::SIZE / 2) as f32, 32.0, (Chunk::SIZE / 2) as f32), 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 x in -VIEW_DISTANCE..=VIEW_DISTANCE {
for z in -VIEW_DISTANCE..=VIEW_DISTANCE { for z in -VIEW_DISTANCE..=VIEW_DISTANCE {
let coords = ChunkCoords::new(x + camera_cx, z + camera_cz); let coords = ChunkCoords::new(x + camera_cx, z + camera_cz);
let mesh = let mesh = self
self.render_world .render_world
.get_or_upload(&self.allocators, coords, &self.world)?; .get_or_upload(&self.allocators, coords, world)?;
if let Some(mesh) = mesh { if let Some(mesh) = mesh {
builder.bind_vertex_buffers(0, mesh.vertex_buffer.clone())?; builder.bind_vertex_buffers(0, mesh.vertex_buffer.clone())?;
+99
View File
@@ -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<F: Fn(u32, u32) -> 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
}
}
+66
View File
@@ -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,
);
}
+60
View File
@@ -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<ChunkCoords, Chunk>,
block_registry: BlockRegistry,
}
impl World {
pub fn new(block_registry: BlockRegistry) -> Self {
Self {
chunks: HashMap::new(),
block_registry,
}
}
pub fn generate<G: ChunkGenerator + ?Sized>(&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<G: ChunkGenerator + ?Sized>(
&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);
}
}
}
}
}
+6 -209
View File
@@ -1,144 +1,12 @@
use std::{collections::HashMap, hash::Hash};
use bitflags::bitflags; 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 { pub use chunk::{Chunk, ChunkCoords};
blocks: [u8; Self::SIZE * Self::SIZE * Self::HEIGHT], pub use generator::{ChunkGenerator, NoiseChunkGenerator};
height_map: [u32; Self::SIZE * Self::SIZE], pub use level::World;
}
#[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<ChunkCoords, Chunk>,
block_registry: BlockRegistry,
}
impl World {
pub fn new(block_registry: BlockRegistry) -> Self {
Self {
chunks: HashMap::new(),
block_registry,
}
}
pub fn generate<G: ChunkGenerator>(&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<G: ChunkGenerator>(&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! { bitflags! {
#[derive(Debug)] #[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<F: Fn(u32, u32) -> 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
}
}