Basic texture rendering

This commit is contained in:
2025-04-26 15:13:22 +03:00
parent d57cb33b98
commit 9b4c2fb9dd
24 changed files with 1260 additions and 125 deletions
Generated
+110 -2
View File
@@ -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",
]
+6
View File
@@ -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"
+5
View File
@@ -0,0 +1,5 @@
{
"model": {
"simple": "dirt"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"model": {
"grass_like": {
"side": "grass_side",
"bottom": "dirt",
"top": "grass_top"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"model": {
"simple": "stone"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+7
View File
@@ -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}")]
+3
View File
@@ -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 {
+178
View File
@@ -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<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 {
pub fn get(&self, name: &str) -> Option<u8> {
self.name_to_id.get(name).copied()
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod block;
pub mod model;
pub mod texture;
+35
View File
@@ -0,0 +1,35 @@
use std::collections::HashMap;
pub struct BlockModelRegistry {
pub id_to_model: HashMap<u8, CubeTextures>,
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)
}
}
+216
View File
@@ -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<Device>,
allocators: Arc<Allocators>,
width: u32,
height: u32,
readers: Vec<Box<dyn TextureReader>>,
}
pub struct TextureRegistryBuilder {
inner: TextureArrayBuilder,
map: HashMap<String, u32>,
last_texture_id: u32,
}
pub struct PngTextureReader {
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 {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
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<Device>, allocators: Arc<Allocators>, width: u32, height: u32) -> Self {
Self {
device,
allocators,
width,
height,
readers: Vec::new(),
}
}
pub fn push(&mut self, reader: Box<dyn TextureReader>) {
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<Queue>) -> Result<Arc<ImageView>, 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::<DeviceSize>();
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)
}
}
+179 -65
View File
@@ -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<InputVertex>,
}
pub struct ChunkMesh {
pub vertex_buffer: Subbuffer<[InputVertex]>,
}
pub struct WorldMeshState {
pub chunks: HashMap<ChunkCoords, ChunkMesh>,
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<Allocators>,
coords: ChunkCoords,
world: &World,
) -> Result<Option<&ChunkMesh>, 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<Allocators>,
block_model_registry: &BlockModelRegistry,
coords: ChunkCoords,
chunk: &Chunk,
) -> Result<Self, Error> {
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<Allocators>) -> Result<Subbuffer<[InputVertex]>, 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,
}
+179 -50
View File
@@ -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<Arc<Image>>,
swapchain_dirty: bool,
vertex_buffer: Subbuffer<[InputVertex]>,
uniform_allocator: SubbufferAllocator,
render_pass: Arc<RenderPass>,
viewport: Viewport,
framebuffers: Vec<Arc<Framebuffer>>,
sampler: Arc<Sampler>,
texture_view: Arc<ImageView>,
vertex_shader: EntryPoint,
fragment_shader: EntryPoint,
pipeline: Arc<GraphicsPipeline>,
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::<InputVertex>(
&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::<shaders::plain::SceneData>()?;
{
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::<shaders::plain::SceneData>()?;
{
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(())
+9
View File
@@ -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<V: Vertex>(
viewport: Viewport,
vertex_shader: &EntryPoint,
fragment_shader: &EntryPoint,
samplers: Vec<Arc<Sampler>>,
) -> Result<Arc<GraphicsPipeline>, Error> {
let vertex_input_state = V::per_vertex().definition(vertex_shader)?;
let stages = [
@@ -55,12 +57,19 @@ pub fn create_two_stage_3d_pipeline<V: Vertex>(
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())?,
+7 -2
View File
@@ -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;
}
+11 -4
View File
@@ -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));
}
+70
View File
@@ -205,3 +205,73 @@ pub fn create_3d_framebuffers(
Ok(framebuffers)
}
// pub fn load_png_texture<P: AsRef<Path>>(
// allocators: &Allocators,
// queue: &Arc<Queue>,
// path: P,
// usage: ImageUsage,
// ) -> Result<Arc<ImageView>, 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)
// }
//
+2 -2
View File
@@ -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,
}
+225
View File
@@ -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<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! {
#[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<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
}
}