@ -0,0 +1,2 @@ |
||||
/target/ |
||||
|
@ -0,0 +1,48 @@ |
||||
{ |
||||
"version": "0.2.0", |
||||
"configurations": [ |
||||
{ |
||||
"type": "lldb", |
||||
"request": "launch", |
||||
"name": "Debug", |
||||
"cargo": { |
||||
"args": [ |
||||
"build", |
||||
"--bin=bevy-bloxel-test", |
||||
"--package=bevy-bloxel-test" |
||||
], |
||||
"filter": { |
||||
"name": "bevy-bloxel-test", |
||||
"kind": "bin" |
||||
} |
||||
}, |
||||
"args": [], |
||||
"cwd": "${workspaceFolder}", |
||||
"env": { |
||||
// When Bevy looks for assets, it checks BEVY_ASSET_ROOT, CARGO_MANIFEST_DIR, and |
||||
// then falls back to the executable directory. When debugging, the cargo manifest |
||||
// directory is not set, so we need to specify this environment variable. |
||||
"BEVY_ASSET_ROOT": "${workspaceFolder}", |
||||
}, |
||||
}, |
||||
{ |
||||
"type": "lldb", |
||||
"request": "launch", |
||||
"name": "Debug unit tests", |
||||
"cargo": { |
||||
"args": [ |
||||
"test", |
||||
"--no-run", |
||||
"--bin=bevy-bloxel-test", |
||||
"--package=bevy-bloxel-test" |
||||
], |
||||
"filter": { |
||||
"name": "bevy-bloxel-test", |
||||
"kind": "bin" |
||||
} |
||||
}, |
||||
"args": [], |
||||
"cwd": "${workspaceFolder}", |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,19 @@ |
||||
[package] |
||||
name = "bevy-bloxel-test" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# Enable a small amount of optimization in the dev profile. |
||||
[profile.dev] |
||||
opt-level = 1 |
||||
|
||||
# Enable a large amount of optimization in the dev profile for dependencies. |
||||
[profile.dev.package."*"] |
||||
opt-level = 3 |
||||
|
||||
[dependencies] |
||||
bevy = { version = "0.15.3", features = [ "file_watcher", "embedded_watcher" ] } |
||||
bitvec = "1.0.1" |
||||
overload = "0.1.1" |
||||
rand = "0.9.0" |
||||
zorder = "0.2.2" |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
@ -0,0 +1,7 @@ |
||||
use bevy::prelude::*; |
||||
|
||||
#[derive(Component)] |
||||
pub struct Block; |
||||
|
||||
#[derive(Component, Deref)] |
||||
pub struct BlockTexture(pub Handle<Image>); |
@ -0,0 +1,26 @@ |
||||
use bevy::{prelude::*, utils::HashMap}; |
||||
|
||||
use super::{ |
||||
math::{ChunkPos, CHUNK_LENGTH}, |
||||
storage::PaletteBloxelStorage, |
||||
}; |
||||
|
||||
#[derive(Component, Default)] |
||||
pub struct Chunk; |
||||
|
||||
#[derive(Component, Deref, DerefMut)] |
||||
#[require(Chunk)] |
||||
pub struct ChunkData(PaletteBloxelStorage<Entity>); |
||||
|
||||
impl ChunkData { |
||||
pub fn new(default: Entity) -> Self { |
||||
Self(PaletteBloxelStorage::new( |
||||
UVec3::splat(CHUNK_LENGTH as u32), |
||||
default, |
||||
)) |
||||
} |
||||
} |
||||
|
||||
#[derive(Component, Default, Deref, DerefMut)] |
||||
#[require(Transform, Visibility)] |
||||
pub struct ChunkMap(HashMap<ChunkPos, Entity>); |
@ -0,0 +1,5 @@ |
||||
mod pos; |
||||
mod region; |
||||
|
||||
pub use pos::Pos; |
||||
pub use region::Region; |
@ -0,0 +1,212 @@ |
||||
use std::{ |
||||
marker::PhantomData, |
||||
ops::{Add, AddAssign, Index, IndexMut, Sub, SubAssign}, |
||||
}; |
||||
|
||||
use bevy::{ |
||||
ecs::component::Component, |
||||
math::{Dir3, IVec3, InvalidDirectionError}, |
||||
}; |
||||
|
||||
use super::Region; |
||||
|
||||
pub struct Pos<T> { |
||||
pub x: i32, |
||||
pub y: i32, |
||||
pub z: i32, |
||||
_marker: PhantomData<T>, |
||||
} |
||||
|
||||
impl<T> Pos<T> { |
||||
pub const ORIGIN: Self = Self::new(0, 0, 0); |
||||
|
||||
pub const fn new(x: i32, y: i32, z: i32) -> Self { |
||||
Self { |
||||
x, |
||||
y, |
||||
z, |
||||
_marker: PhantomData, |
||||
} |
||||
} |
||||
|
||||
pub fn region(self) -> Region<T> { |
||||
Region::new_unchecked(self.into(), self.into()) |
||||
} |
||||
|
||||
pub fn distance_to(self, other: Self) -> f32 { |
||||
(self.distance_to_squared(other) as f32).sqrt() |
||||
} |
||||
|
||||
pub fn distance_to_squared(self, other: Self) -> i32 { |
||||
IVec3::distance_squared(self.into(), other.into()) |
||||
} |
||||
|
||||
pub fn direction_to(self, other: Self) -> Result<Dir3, InvalidDirectionError> { |
||||
Dir3::new((other - self).as_vec3()) |
||||
} |
||||
|
||||
pub fn min(self, rhs: Self) -> Self { |
||||
Self::new(self.x.min(rhs.x), self.y.min(rhs.y), self.z.min(rhs.z)) |
||||
} |
||||
|
||||
pub fn max(self, rhs: Self) -> Self { |
||||
Self::new(self.x.max(rhs.x), self.y.max(rhs.y), self.z.max(rhs.z)) |
||||
} |
||||
} |
||||
|
||||
impl<T> std::fmt::Display for Pos<T> { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "[{}, {}, {}]", self.x, self.y, self.z) |
||||
} |
||||
} |
||||
|
||||
impl<T> std::fmt::Debug for Pos<T> { |
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
fmt.debug_tuple(stringify!(Pos<T>)) |
||||
.field(&self.x) |
||||
.field(&self.y) |
||||
.field(&self.z) |
||||
.finish() |
||||
} |
||||
} |
||||
|
||||
// Trait implementations that SHOULD be handled by #[derive]
|
||||
|
||||
use bevy::ecs::component::StorageType; |
||||
impl<T: Send + Sync + 'static> Component for Pos<T> { |
||||
const STORAGE_TYPE: StorageType = StorageType::Table; |
||||
} |
||||
|
||||
impl<T> Default for Pos<T> { |
||||
fn default() -> Self { |
||||
Self { |
||||
x: 0, |
||||
y: 0, |
||||
z: 0, |
||||
_marker: PhantomData, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl<T> Clone for Pos<T> { |
||||
fn clone(&self) -> Self { |
||||
*self |
||||
} |
||||
} |
||||
|
||||
impl<T> Copy for Pos<T> {} |
||||
|
||||
impl<T> PartialEq for Pos<T> { |
||||
fn eq(&self, other: &Self) -> bool { |
||||
(self.x, self.y, self.z).eq(&(other.x, other.y, other.z)) |
||||
} |
||||
} |
||||
|
||||
impl<T> Eq for Pos<T> {} |
||||
|
||||
impl<T> std::hash::Hash for Pos<T> { |
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
||||
(self.x, self.y, self.z).hash(state) |
||||
} |
||||
} |
||||
|
||||
// Conversion
|
||||
|
||||
impl<T> From<(i32, i32, i32)> for Pos<T> { |
||||
fn from((x, y, z): (i32, i32, i32)) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl<T> From<Pos<T>> for (i32, i32, i32) { |
||||
fn from(Pos { x, y, z, .. }: Pos<T>) -> Self { |
||||
(x, y, z) |
||||
} |
||||
} |
||||
|
||||
impl<T> From<[i32; 3]> for Pos<T> { |
||||
fn from([x, y, z]: [i32; 3]) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl<T> From<Pos<T>> for [i32; 3] { |
||||
fn from(Pos { x, y, z, .. }: Pos<T>) -> Self { |
||||
[x, y, z] |
||||
} |
||||
} |
||||
|
||||
impl<T> From<IVec3> for Pos<T> { |
||||
fn from(IVec3 { x, y, z }: IVec3) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl<T> From<Pos<T>> for IVec3 { |
||||
fn from(Pos { x, y, z, .. }: Pos<T>) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
|
||||
// Offsetting
|
||||
|
||||
impl<T> Add<IVec3> for Pos<T> { |
||||
type Output = Pos<T>; |
||||
fn add(self, rhs: IVec3) -> Self::Output { |
||||
Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z) |
||||
} |
||||
} |
||||
|
||||
impl<T> Sub<IVec3> for Pos<T> { |
||||
type Output = Pos<T>; |
||||
fn sub(self, rhs: IVec3) -> Self::Output { |
||||
Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z) |
||||
} |
||||
} |
||||
|
||||
impl<T> AddAssign<IVec3> for Pos<T> { |
||||
fn add_assign(&mut self, rhs: IVec3) { |
||||
self.x += rhs.x; |
||||
self.y += rhs.y; |
||||
self.z += rhs.z; |
||||
} |
||||
} |
||||
|
||||
impl<T> SubAssign<IVec3> for Pos<T> { |
||||
fn sub_assign(&mut self, rhs: IVec3) { |
||||
self.x -= rhs.x; |
||||
self.y -= rhs.y; |
||||
self.z -= rhs.z; |
||||
} |
||||
} |
||||
|
||||
// Difference
|
||||
|
||||
impl<T> Sub<Pos<T>> for Pos<T> { |
||||
type Output = IVec3; |
||||
fn sub(self, rhs: Self) -> IVec3 { |
||||
IVec3::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z) |
||||
} |
||||
} |
||||
|
||||
// Indexing
|
||||
|
||||
impl<T> Index<usize> for Pos<T> { |
||||
type Output = i32; |
||||
fn index(&self, index: usize) -> &Self::Output { |
||||
match index { |
||||
0 => &self.x, |
||||
1 => &self.y, |
||||
2 => &self.z, |
||||
_ => panic!("index out of bounds"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl<T> IndexMut<usize> for Pos<T> { |
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output { |
||||
match index { |
||||
0 => &mut self.x, |
||||
1 => &mut self.y, |
||||
2 => &mut self.z, |
||||
_ => panic!("index out of bounds"), |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,175 @@ |
||||
use std::{ |
||||
marker::PhantomData, |
||||
ops::{Add, AddAssign, Sub, SubAssign}, |
||||
}; |
||||
|
||||
use bevy::{ |
||||
ecs::component::Component, |
||||
math::{IVec3, UVec3}, |
||||
}; |
||||
|
||||
use super::Pos; |
||||
|
||||
pub struct Region<T> { |
||||
min: IVec3, |
||||
max: IVec3, |
||||
_marker: PhantomData<T>, |
||||
} |
||||
|
||||
impl<T> Region<T> { |
||||
pub fn new_unchecked(min: IVec3, max: IVec3) -> Self { |
||||
Self { |
||||
min, |
||||
max, |
||||
_marker: PhantomData, |
||||
} |
||||
} |
||||
|
||||
pub fn new_checked(min: Pos<T>, max: Pos<T>) -> Result<Self, ()> { |
||||
let min: IVec3 = min.into(); |
||||
let max: IVec3 = max.into(); |
||||
if min.x <= max.x && min.y <= max.y && min.z <= max.z { |
||||
Ok(Self::new_unchecked(min, max)) |
||||
} else { |
||||
Err(()) |
||||
} |
||||
} |
||||
|
||||
pub fn new(a: Pos<T>, b: Pos<T>) -> Self { |
||||
let a: IVec3 = a.into(); |
||||
let b: IVec3 = b.into(); |
||||
let min = IVec3::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z)); |
||||
let max = IVec3::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z)); |
||||
Self::new_unchecked(min, max) |
||||
} |
||||
|
||||
pub fn size(&self) -> UVec3 { |
||||
(self.max - self.min + IVec3::ONE).as_uvec3() |
||||
} |
||||
|
||||
pub fn contains(&self, pos: Pos<T>) -> bool { |
||||
let pos: IVec3 = pos.into(); |
||||
(pos.x >= self.min.x && pos.x <= self.max.x) |
||||
&& (pos.y >= self.min.y && pos.y <= self.max.y) |
||||
&& (pos.z >= self.min.z && pos.z <= self.max.z) |
||||
} |
||||
|
||||
pub fn intersects(&self, other: Self) -> bool { |
||||
(other.max.x > self.min.x && other.min.x < self.max.x) |
||||
&& (other.max.y > self.min.y && other.min.y < self.max.y) |
||||
&& (other.max.z > self.min.z && other.min.z < self.max.z) |
||||
} |
||||
|
||||
pub fn distance_to(&self, pos: Pos<T>) -> f32 { |
||||
(self.distance_to_squared(pos) as f32).sqrt() |
||||
} |
||||
|
||||
pub fn distance_to_squared(&self, pos: Pos<T>) -> i32 { |
||||
let pos: IVec3 = pos.into(); |
||||
let clamped = pos.max(self.min).min(self.max); |
||||
pos.distance_squared(clamped) |
||||
} |
||||
|
||||
pub fn expand(&self, amount: i32) -> Result<Self, ()> { |
||||
Self::new_checked( |
||||
(self.min - IVec3::splat(amount)).into(), |
||||
(self.max + IVec3::splat(amount)).into(), |
||||
) |
||||
} |
||||
} |
||||
|
||||
impl<T> std::fmt::Display for Region<T> { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "{} to {}", self.min, self.max) |
||||
} |
||||
} |
||||
|
||||
impl<T> std::fmt::Debug for Region<T> { |
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
fmt.debug_struct(stringify!(Region<T>)) |
||||
.field("min", &self.min) |
||||
.field("max", &self.max) |
||||
.finish() |
||||
} |
||||
} |
||||
|
||||
// Trait implementations that SHOULD be handled by #[derive]
|
||||
|
||||
use bevy::ecs::component::StorageType; |
||||
impl<T: Send + Sync + 'static> Component for Region<T> { |
||||
const STORAGE_TYPE: StorageType = StorageType::Table; |
||||
} |
||||
|
||||
impl<T> Default for Region<T> { |
||||
fn default() -> Self { |
||||
Self { |
||||
min: IVec3::ZERO, |
||||
max: IVec3::ZERO, |
||||
_marker: PhantomData, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl<T> Clone for Region<T> { |
||||
fn clone(&self) -> Self { |
||||
*self |
||||
} |
||||
} |
||||
|
||||
impl<T> Copy for Region<T> {} |
||||
|
||||
impl<T> PartialEq for Region<T> { |
||||
fn eq(&self, other: &Self) -> bool { |
||||
(self.min, self.max).eq(&(other.min, other.max)) |
||||
} |
||||
} |
||||
|
||||
impl<T> Eq for Region<T> {} |
||||
|
||||
impl<T> std::hash::Hash for Region<T> { |
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
||||
(self.min, self.max).hash(state) |
||||
} |
||||
} |
||||
|
||||
// Offsetting
|
||||
|
||||
impl<T> Add<IVec3> for Region<T> { |
||||
type Output = Region<T>; |
||||
fn add(self, rhs: IVec3) -> Self::Output { |
||||
Self::new_unchecked(self.min + rhs, self.max + rhs) |
||||
} |
||||
} |
||||
impl<T> Add<IVec3> for &Region<T> { |
||||
type Output = Region<T>; |
||||
fn add(self, rhs: IVec3) -> Self::Output { |
||||
Region::new_unchecked(self.min + rhs, self.max + rhs) |
||||
} |
||||
} |
||||
|
||||
impl<T> Sub<IVec3> for Region<T> { |
||||
type Output = Region<T>; |
||||
fn sub(self, rhs: IVec3) -> Self::Output { |
||||
Self::new_unchecked(self.min - rhs, self.max - rhs) |
||||
} |
||||
} |
||||
impl<T> Sub<IVec3> for &Region<T> { |
||||
type Output = Region<T>; |
||||
fn sub(self, rhs: IVec3) -> Self::Output { |
||||
Region::new_unchecked(self.min - rhs, self.max - rhs) |
||||
} |
||||
} |
||||
|
||||
impl<T> AddAssign<IVec3> for Region<T> { |
||||
fn add_assign(&mut self, rhs: IVec3) { |
||||
self.min += rhs; |
||||
self.max += rhs; |
||||
} |
||||
} |
||||
|
||||
impl<T> SubAssign<IVec3> for Region<T> { |
||||
fn sub_assign(&mut self, rhs: IVec3) { |
||||
self.min -= rhs; |
||||
self.max -= rhs; |
||||
} |
||||
} |
@ -0,0 +1,129 @@ |
||||
use std::ops::{self, Index, IndexMut}; |
||||
|
||||
use bevy::{ |
||||
ecs::component::Component, |
||||
math::{Dir3, IVec3, InvalidDirectionError}, |
||||
}; |
||||
use overload::overload; |
||||
|
||||
use super::BlockRegion; |
||||
|
||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Hash, Debug)] |
||||
pub struct BlockPos { |
||||
pub x: i32, |
||||
pub y: i32, |
||||
pub z: i32, |
||||
} |
||||
|
||||
impl BlockPos { |
||||
pub const ORIGIN: BlockPos = Self::new(0, 0, 0); |
||||
|
||||
pub const fn new(x: i32, y: i32, z: i32) -> Self { |
||||
Self { x, y, z } |
||||
} |
||||
|
||||
pub fn region(self) -> BlockRegion { |
||||
BlockRegion::new_unchecked(self, self) |
||||
} |
||||
|
||||
pub fn distance_to(self, other: Self) -> f32 { |
||||
(self.distance_to_squared(other) as f32).sqrt() |
||||
} |
||||
|
||||
pub fn distance_to_squared(self, other: Self) -> i32 { |
||||
IVec3::distance_squared(self.into(), other.into()) |
||||
} |
||||
|
||||
pub fn direction_to(self, other: Self) -> Result<Dir3, InvalidDirectionError> { |
||||
Dir3::new((other - self).as_vec3()) |
||||
} |
||||
|
||||
pub fn min(self, rhs: Self) -> Self { |
||||
Self::new(self.x.min(rhs.x), self.y.min(rhs.y), self.z.min(rhs.z)) |
||||
} |
||||
|
||||
pub fn max(self, rhs: Self) -> Self { |
||||
Self::new(self.x.max(rhs.x), self.y.max(rhs.y), self.z.max(rhs.z)) |
||||
} |
||||
|
||||
pub fn offset(self, x: i32, y: i32, z: i32) -> Self { |
||||
Self::new(self.x + x, self.y + y, self.z + z) |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for BlockPos { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "[{}, {}, {}]", self.x, self.y, self.z) |
||||
} |
||||
} |
||||
|
||||
// Offsetting
|
||||
|
||||
overload!((l: BlockPos) + (r: IVec3) -> BlockPos { BlockPos::new(l.x + r.x, l.y + r.y, l.z + r.z) }); |
||||
overload!((l: BlockPos) - (r: IVec3) -> BlockPos { BlockPos::new(l.x - r.x, l.y - r.y, l.z - r.z) }); |
||||
overload!((s: &mut BlockPos) += (v: IVec3) { s.x += v.x; s.y += v.y; s.z += v.z; }); |
||||
overload!((s: &mut BlockPos) -= (v: IVec3) { s.x -= v.x; s.y -= v.y; s.z -= v.z; }); |
||||
|
||||
// Difference
|
||||
|
||||
overload!((l: BlockPos) - (r: BlockPos) -> IVec3 { IVec3::new(l.x - r.x, l.y - r.y, l.z - r.z) }); |
||||
|
||||
// Indexing
|
||||
|
||||
impl Index<usize> for BlockPos { |
||||
type Output = i32; |
||||
fn index(&self, index: usize) -> &Self::Output { |
||||
match index { |
||||
0 => &self.x, |
||||
1 => &self.y, |
||||
2 => &self.z, |
||||
_ => panic!("index out of bounds"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl IndexMut<usize> for BlockPos { |
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output { |
||||
match index { |
||||
0 => &mut self.x, |
||||
1 => &mut self.y, |
||||
2 => &mut self.z, |
||||
_ => panic!("index out of bounds"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Conversion
|
||||
|
||||
impl From<(i32, i32, i32)> for BlockPos { |
||||
fn from((x, y, z): (i32, i32, i32)) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<BlockPos> for (i32, i32, i32) { |
||||
fn from(BlockPos { x, y, z }: BlockPos) -> Self { |
||||
(x, y, z) |
||||
} |
||||
} |
||||
|
||||
impl From<[i32; 3]> for BlockPos { |
||||
fn from([x, y, z]: [i32; 3]) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<BlockPos> for [i32; 3] { |
||||
fn from(BlockPos { x, y, z }: BlockPos) -> Self { |
||||
[x, y, z] |
||||
} |
||||
} |
||||
|
||||
impl From<IVec3> for BlockPos { |
||||
fn from(IVec3 { x, y, z }: IVec3) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<BlockPos> for IVec3 { |
||||
fn from(BlockPos { x, y, z }: BlockPos) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
@ -0,0 +1,87 @@ |
||||
use std::ops; |
||||
|
||||
use bevy::{ |
||||
ecs::component::Component, |
||||
math::{IVec3, UVec3}, |
||||
}; |
||||
use overload::overload; |
||||
|
||||
use super::BlockPos; |
||||
|
||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Hash, Debug)] |
||||
pub struct BlockRegion { |
||||
min: BlockPos, |
||||
max: BlockPos, |
||||
} |
||||
|
||||
impl BlockRegion { |
||||
pub fn new_unchecked(min: BlockPos, max: BlockPos) -> Self { |
||||
Self { min, max } |
||||
} |
||||
|
||||
pub fn new_checked(min: BlockPos, max: BlockPos) -> Result<Self, ()> { |
||||
if min.x <= max.x && min.y <= max.y && min.z <= max.z { |
||||
Ok(Self::new_unchecked(min, max)) |
||||
} else { |
||||
Err(()) |
||||
} |
||||
} |
||||
|
||||
pub fn new(a: BlockPos, b: BlockPos) -> Self { |
||||
let min = BlockPos::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z)); |
||||
let max = BlockPos::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z)); |
||||
Self::new_unchecked(min, max) |
||||
} |
||||
|
||||
pub fn size(&self) -> UVec3 { |
||||
(self.max - self.min + IVec3::ONE).as_uvec3() |
||||
} |
||||
|
||||
pub fn contains(&self, pos: BlockPos) -> bool { |
||||
(pos.x >= self.min.x && pos.x <= self.max.x) |
||||
&& (pos.y >= self.min.y && pos.y <= self.max.y) |
||||
&& (pos.z >= self.min.z && pos.z <= self.max.z) |
||||
} |
||||
|
||||
pub fn intersects(&self, other: Self) -> bool { |
||||
(other.max.x > self.min.x && other.min.x < self.max.x) |
||||
&& (other.max.y > self.min.y && other.min.y < self.max.y) |
||||
&& (other.max.z > self.min.z && other.min.z < self.max.z) |
||||
} |
||||
|
||||
pub fn distance_to(&self, pos: BlockPos) -> f32 { |
||||
(self.distance_to_squared(pos) as f32).sqrt() |
||||
} |
||||
|
||||
pub fn distance_to_squared(&self, pos: BlockPos) -> i32 { |
||||
let clamped = pos.max(self.min).min(self.max); |
||||
pos.distance_to_squared(clamped) |
||||
} |
||||
|
||||
pub fn offset(&self, x: i32, y: i32, z: i32) -> Self { |
||||
Self { |
||||
min: self.min.offset(x, y, z), |
||||
max: self.max.offset(x, y, z), |
||||
} |
||||
} |
||||
|
||||
pub fn expand(&self, amount: i32) -> Result<Self, ()> { |
||||
Self::new_checked( |
||||
self.min - IVec3::splat(amount), |
||||
self.max + IVec3::splat(amount), |
||||
) |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for BlockRegion { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "{} to {}", self.min, self.max) |
||||
} |
||||
} |
||||
|
||||
// Offsetting
|
||||
|
||||
overload!((l: ?BlockRegion) + (r: IVec3) -> BlockRegion { BlockRegion::new_unchecked(l.min + r, l.max + r) }); |
||||
overload!((l: ?BlockRegion) - (r: IVec3) -> BlockRegion { BlockRegion::new_unchecked(l.min - r, l.max - r) }); |
||||
overload!((s: &mut BlockRegion) += (v: IVec3) { s.min += v; s.max += v; }); |
||||
overload!((s: &mut BlockRegion) -= (v: IVec3) { s.min -= v; s.max -= v; }); |
@ -0,0 +1,158 @@ |
||||
use std::ops::{self, Index, IndexMut}; |
||||
|
||||
use bevy::{ |
||||
ecs::component::Component, |
||||
math::{Dir3, IVec3, InvalidDirectionError}, |
||||
transform::components::Transform, |
||||
}; |
||||
use overload::overload; |
||||
|
||||
use super::{BlockPos, BlockRegion, ChunkRegion}; |
||||
|
||||
pub const CHUNK_SHIFT: i32 = 4; |
||||
pub const CHUNK_MASK: i32 = !(!0 << CHUNK_SHIFT); // = 0b1111
|
||||
pub const CHUNK_LENGTH: usize = 1 << CHUNK_SHIFT; // = 16
|
||||
|
||||
pub const CHUNK_MAX: IVec3 = IVec3::splat((CHUNK_LENGTH - 1) as i32); |
||||
|
||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Hash, Debug)] |
||||
pub struct ChunkPos { |
||||
pub x: i32, |
||||
pub y: i32, |
||||
pub z: i32, |
||||
} |
||||
|
||||
impl ChunkPos { |
||||
pub const ORIGIN: ChunkPos = Self::new(0, 0, 0); |
||||
|
||||
pub const fn new(x: i32, y: i32, z: i32) -> Self { |
||||
Self { x, y, z } |
||||
} |
||||
|
||||
pub fn region(self) -> ChunkRegion { |
||||
ChunkRegion::new_unchecked(self, self) |
||||
} |
||||
|
||||
pub fn distance_to(self, other: Self) -> f32 { |
||||
(self.distance_to_squared(other) as f32).sqrt() |
||||
} |
||||
|
||||
pub fn distance_to_squared(self, other: Self) -> i32 { |
||||
IVec3::distance_squared(self.into(), other.into()) |
||||
} |
||||
|
||||
pub fn direction_to(self, other: Self) -> Result<Dir3, InvalidDirectionError> { |
||||
Dir3::new((other - self).as_vec3()) |
||||
} |
||||
|
||||
pub fn transform(self) -> Transform { |
||||
let pos: IVec3 = self.into(); |
||||
Transform::from_translation((pos << CHUNK_SHIFT).as_vec3()) |
||||
} |
||||
|
||||
pub fn min(self, rhs: Self) -> Self { |
||||
Self::new(self.x.min(rhs.x), self.y.min(rhs.y), self.z.min(rhs.z)) |
||||
} |
||||
|
||||
pub fn max(self, rhs: Self) -> Self { |
||||
Self::new(self.x.max(rhs.x), self.y.max(rhs.y), self.z.max(rhs.z)) |
||||
} |
||||
|
||||
pub fn offset(self, x: i32, y: i32, z: i32) -> Self { |
||||
Self::new(self.x + x, self.y + y, self.z + z) |
||||
} |
||||
|
||||
pub fn from_block_pos(pos: BlockPos) -> (Self, IVec3) { |
||||
let pos: IVec3 = pos.into(); |
||||
((pos >> CHUNK_SHIFT).into(), pos & CHUNK_MASK) |
||||
} |
||||
|
||||
pub fn to_block_pos(self, relative: IVec3) -> BlockPos { |
||||
let pos: IVec3 = self.into(); |
||||
((pos << CHUNK_SHIFT) + relative).into() |
||||
} |
||||
|
||||
pub fn to_block_region(self) -> BlockRegion { |
||||
BlockRegion::new_unchecked( |
||||
self.to_block_pos(IVec3::ZERO), |
||||
self.to_block_pos(IVec3::splat(CHUNK_LENGTH as i32 - 1)), |
||||
) |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for ChunkPos { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "[{}, {}, {}]", self.x, self.y, self.z) |
||||
} |
||||
} |
||||
|
||||
// Offsetting
|
||||
|
||||
overload!((l: ChunkPos) + (r: IVec3) -> ChunkPos { ChunkPos::new(l.x + r.x, l.y + r.y, l.z + r.z) }); |
||||
overload!((l: ChunkPos) - (r: IVec3) -> ChunkPos { ChunkPos::new(l.x - r.x, l.y - r.y, l.z - r.z) }); |
||||
overload!((s: &mut ChunkPos) += (v: IVec3) { s.x += v.x; s.y += v.y; s.z += v.z; }); |
||||
overload!((s: &mut ChunkPos) -= (v: IVec3) { s.x -= v.x; s.y -= v.y; s.z -= v.z; }); |
||||
|
||||
// Difference
|
||||
|
||||
overload!((l: ChunkPos) - (r: ChunkPos) -> IVec3 { IVec3::new(l.x - r.x, l.y - r.y, l.z - r.z) }); |
||||
|
||||
// Indexing
|
||||
|
||||
impl Index<usize> for ChunkPos { |
||||
type Output = i32; |
||||
fn index(&self, index: usize) -> &Self::Output { |
||||
match index { |
||||
0 => &self.x, |
||||
1 => &self.y, |
||||
2 => &self.z, |
||||
_ => panic!("index out of bounds"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl IndexMut<usize> for ChunkPos { |
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output { |
||||
match index { |
||||
0 => &mut self.x, |
||||
1 => &mut self.y, |
||||
2 => &mut self.z, |
||||
_ => panic!("index out of bounds"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Conversion
|
||||
|
||||
impl From<(i32, i32, i32)> for ChunkPos { |
||||
fn from((x, y, z): (i32, i32, i32)) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ChunkPos> for (i32, i32, i32) { |
||||
fn from(ChunkPos { x, y, z }: ChunkPos) -> Self { |
||||
(x, y, z) |
||||
} |
||||
} |
||||
|
||||
impl From<[i32; 3]> for ChunkPos { |
||||
fn from([x, y, z]: [i32; 3]) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ChunkPos> for [i32; 3] { |
||||
fn from(ChunkPos { x, y, z }: ChunkPos) -> Self { |
||||
[x, y, z] |
||||
} |
||||
} |
||||
|
||||
impl From<IVec3> for ChunkPos { |
||||
fn from(IVec3 { x, y, z }: IVec3) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ChunkPos> for IVec3 { |
||||
fn from(ChunkPos { x, y, z }: ChunkPos) -> Self { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
@ -0,0 +1,94 @@ |
||||
use std::ops; |
||||
|
||||
use bevy::{ |
||||
ecs::component::Component, |
||||
math::{IVec3, UVec3}, |
||||
}; |
||||
use overload::overload; |
||||
|
||||
use super::{BlockRegion, ChunkPos, CHUNK_MAX}; |
||||
|
||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Hash, Debug)] |
||||
pub struct ChunkRegion { |
||||
min: ChunkPos, |
||||
max: ChunkPos, |
||||
} |
||||
|
||||
impl ChunkRegion { |
||||
pub fn new_unchecked(min: ChunkPos, max: ChunkPos) -> Self { |
||||
Self { min, max } |
||||
} |
||||
|
||||
pub fn new_checked(min: ChunkPos, max: ChunkPos) -> Result<Self, ()> { |
||||
if min.x <= max.x && min.y <= max.y && min.z <= max.z { |
||||
Ok(Self::new_unchecked(min, max)) |
||||
} else { |
||||
Err(()) |
||||
} |
||||
} |
||||
|
||||
pub fn new(a: ChunkPos, b: ChunkPos) -> Self { |
||||
let min = ChunkPos::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z)); |
||||
let max = ChunkPos::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z)); |
||||
Self::new_unchecked(min, max) |
||||
} |
||||
|
||||
pub fn size(&self) -> UVec3 { |
||||
(self.max - self.min + IVec3::ONE).as_uvec3() |
||||
} |
||||
|
||||
pub fn contains(&self, pos: ChunkPos) -> bool { |
||||
(pos.x >= self.min.x && pos.x <= self.max.x) |
||||
&& (pos.y >= self.min.y && pos.y <= self.max.y) |
||||
&& (pos.z >= self.min.z && pos.z <= self.max.z) |
||||
} |
||||
|
||||
pub fn intersects(&self, other: Self) -> bool { |
||||
(other.max.x > self.min.x && other.min.x < self.max.x) |
||||
&& (other.max.y > self.min.y && other.min.y < self.max.y) |
||||
&& (other.max.z > self.min.z && other.min.z < self.max.z) |
||||
} |
||||
|
||||
pub fn distance_to(&self, pos: ChunkPos) -> f32 { |
||||
(self.distance_to_squared(pos) as f32).sqrt() |
||||
} |
||||
|
||||
pub fn distance_to_squared(&self, pos: ChunkPos) -> i32 { |
||||
let clamped = pos.max(self.min).min(self.max); |
||||
pos.distance_to_squared(clamped) |
||||
} |
||||
|
||||
pub fn offset(&self, x: i32, y: i32, z: i32) -> Self { |
||||
Self { |
||||
min: self.min.offset(x, y, z), |
||||
max: self.max.offset(x, y, z), |
||||
} |
||||
} |
||||
|
||||
pub fn expand(&self, amount: i32) -> Result<Self, ()> { |
||||
Self::new_checked( |
||||
self.min - IVec3::splat(amount), |
||||
self.max + IVec3::splat(amount), |
||||
) |
||||
} |
||||
|
||||
pub fn to_block_region(&self) -> BlockRegion { |
||||
BlockRegion::new_unchecked( |
||||
self.min.to_block_pos(IVec3::ZERO), |
||||
self.min.to_block_pos(CHUNK_MAX), |
||||
) |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for ChunkRegion { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "{} to {}", self.min, self.max) |
||||
} |
||||
} |
||||
|
||||
// Offsetting
|
||||
|
||||
overload!((l: ?ChunkRegion) + (r: IVec3) -> ChunkRegion { ChunkRegion::new_unchecked(l.min + r, l.max + r) }); |
||||
overload!((l: ?ChunkRegion) - (r: IVec3) -> ChunkRegion { ChunkRegion::new_unchecked(l.min - r, l.max - r) }); |
||||
overload!((s: &mut ChunkRegion) += (v: IVec3) { s.min += v; s.max += v; }); |
||||
overload!((s: &mut ChunkRegion) -= (v: IVec3) { s.min -= v; s.max -= v; }); |
@ -0,0 +1,11 @@ |
||||
mod block_pos; |
||||
mod block_region; |
||||
mod chunk_pos; |
||||
mod chunk_region; |
||||
mod z_order_index; |
||||
|
||||
pub use block_pos::*; |
||||
pub use block_region::*; |
||||
pub use chunk_pos::*; |
||||
pub use chunk_region::*; |
||||
pub use z_order_index::*; |
@ -0,0 +1,211 @@ |
||||
use std::ops; |
||||
|
||||
use bevy::math::IVec3; |
||||
use overload::overload; |
||||
|
||||
use super::ChunkPos; |
||||
|
||||
/// Helper function that creates a [`ZOrderIndex`], panicking if the supplied values
|
||||
/// are not between [`ZOrderIndex::ELEMENT_MIN`] and [`ZOrderIndex::ELEMENT_MAX`].
|
||||
pub fn zorder(x: i32, y: i32, z: i32) -> ZOrderIndex { |
||||
ZOrderIndex::new(x, y, z).unwrap() |
||||
} |
||||
|
||||
/// Encodes 3 signed 21-bit integers into a 64-bit integer, their bits interleaved.
|
||||
///
|
||||
/// This struct wraps an integer which represents an index into a space-filling curve called
|
||||
/// [Z-Order Curve](https://en.wikipedia.org/wiki/Z-order_curve). This is also referred to as
|
||||
/// Morton order, code, or encoding.
|
||||
///
|
||||
/// By interleaving the 3 sub-elements into a single integer, some amount of packing can be
|
||||
/// achieved, at the loss of some bits per elements. For example, with this 64-bit integer,
|
||||
/// 21 bits per elements are available (`2_097_152` distinct values), which may be enough to
|
||||
/// represent block coordinates in a bloxel game world.
|
||||
///
|
||||
/// One upside of encoding separate coordinates into a single Z-Order index is that it can then
|
||||
/// be effectively used to index into octrees, and certain operations such as bitwise shifting
|
||||
/// are quite useful.
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
||||
pub struct ZOrderIndex(u64); |
||||
|
||||
impl ZOrderIndex { |
||||
pub const ZERO: Self = Self::from_raw(0); |
||||
|
||||
const BITS_PER_ELEMENT: usize = (size_of::<u64>() * 8) / 3; |
||||
pub const ELEMENT_MIN: i32 = !0 << (Self::BITS_PER_ELEMENT - 1); |
||||
pub const ELEMENT_MAX: i32 = !Self::ELEMENT_MIN; |
||||
|
||||
const TOTAL_USABLE_BITS: usize = Self::BITS_PER_ELEMENT * 3; |
||||
const USABLE_BITS_MASK: u64 = !(!0 << Self::TOTAL_USABLE_BITS); |
||||
const SIGN_BITS_MASK: u64 = 0b111 << (Self::TOTAL_USABLE_BITS - 3); |
||||
|
||||
pub const fn from_raw(value: u64) -> Self { |
||||
Self(value & Self::USABLE_BITS_MASK) |
||||
} |
||||
|
||||
pub fn new(x: i32, y: i32, z: i32) -> Result<Self, ()> { |
||||
if (x >= Self::ELEMENT_MIN && x <= Self::ELEMENT_MAX) |
||||
&& (y >= Self::ELEMENT_MIN && y <= Self::ELEMENT_MAX) |
||||
&& (z >= Self::ELEMENT_MIN && z <= Self::ELEMENT_MAX) |
||||
{ |
||||
let raw = zorder::index_of([x as u32, y as u32, z as u32]); |
||||
Ok(Self::from_raw(raw as u64)) |
||||
} else { |
||||
Err(()) |
||||
} |
||||
} |
||||
|
||||
pub fn raw(self) -> u64 { |
||||
self.0 |
||||
} |
||||
|
||||
pub fn offset(self, x: i32, y: i32, z: i32) -> Result<Self, ()> { |
||||
let (self_x, self_y, self_z) = self.into(); |
||||
Self::new(self_x + x, self_y + y, self_z + z) |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for ZOrderIndex { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
let (x, y, z) = (*self).into(); |
||||
write!(f, "[{}, {}, {}]", x, y, z) |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Debug for ZOrderIndex { |
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
let (x, y, z) = (*self).into(); |
||||
fmt.debug_tuple(stringify!(ZOrderIndex)) |
||||
.field(&x) |
||||
.field(&y) |
||||
.field(&z) |
||||
.finish() |
||||
} |
||||
} |
||||
|
||||
// Bitwise AND, OR, XOR
|
||||
|
||||
overload!((l: ZOrderIndex) & (r: u64) -> ZOrderIndex { ZOrderIndex::from_raw(l.0 & r) }); |
||||
overload!((l: ZOrderIndex) | (r: u64) -> ZOrderIndex { ZOrderIndex::from_raw(l.0 | r) }); |
||||
overload!((l: ZOrderIndex) ^ (r: u64) -> ZOrderIndex { ZOrderIndex::from_raw(l.0 ^ r) }); |
||||
overload!((l: ZOrderIndex) & (r: ZOrderIndex) -> ZOrderIndex { l & r.0 }); |
||||
overload!((l: ZOrderIndex) | (r: ZOrderIndex) -> ZOrderIndex { l | r.0 }); |
||||
overload!((l: ZOrderIndex) ^ (r: ZOrderIndex) -> ZOrderIndex { l ^ r.0 }); |
||||
|
||||
// Bitshifting
|
||||
|
||||
overload!((l: ZOrderIndex) << (r: usize) -> ZOrderIndex { |
||||
ZOrderIndex::from_raw(l.0 << (r * 3)) |
||||
}); |
||||
|
||||
overload!((l: ZOrderIndex) >> (r: usize) -> ZOrderIndex { |
||||
let mut result = l.0; |
||||
let sign_bits = result & ZOrderIndex::SIGN_BITS_MASK; |
||||
for _ in 0..r { result = (result >> 3) | sign_bits } |
||||
ZOrderIndex::from_raw(result) |
||||
}); |
||||
|
||||
// Conversion
|
||||
|
||||
impl TryFrom<(i32, i32, i32)> for ZOrderIndex { |
||||
type Error = (); |
||||
fn try_from((x, y, z): (i32, i32, i32)) -> Result<Self, Self::Error> { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ZOrderIndex> for (i32, i32, i32) { |
||||
fn from(value: ZOrderIndex) -> Self { |
||||
const SHIFT: usize = (size_of::<i32>() * 8) - ZOrderIndex::BITS_PER_ELEMENT; |
||||
zorder::coord_of(value.0 as u128) |
||||
.map(|i| ((i as i32) << SHIFT) >> SHIFT) |
||||
.into() |
||||
} |
||||
} |
||||
|
||||
impl TryFrom<[i32; 3]> for ZOrderIndex { |
||||
type Error = (); |
||||
fn try_from([x, y, z]: [i32; 3]) -> Result<Self, Self::Error> { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ZOrderIndex> for [i32; 3] { |
||||
fn from(value: ZOrderIndex) -> Self { |
||||
Into::<(i32, i32, i32)>::into(value).into() |
||||
} |
||||
} |
||||
|
||||
impl TryFrom<IVec3> for ZOrderIndex { |
||||
type Error = (); |
||||
fn try_from(IVec3 { x, y, z }: IVec3) -> Result<Self, Self::Error> { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ZOrderIndex> for IVec3 { |
||||
fn from(value: ZOrderIndex) -> Self { |
||||
Into::<(i32, i32, i32)>::into(value).into() |
||||
} |
||||
} |
||||
|
||||
impl TryFrom<ChunkPos> for ZOrderIndex { |
||||
type Error = (); |
||||
fn try_from(ChunkPos { x, y, z }: ChunkPos) -> Result<Self, Self::Error> { |
||||
Self::new(x, y, z) |
||||
} |
||||
} |
||||
impl From<ZOrderIndex> for ChunkPos { |
||||
fn from(value: ZOrderIndex) -> Self { |
||||
Into::<(i32, i32, i32)>::into(value).into() |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn encode_decode_into() { |
||||
assert_eq!((6, -16, 15), zorder(6, -16, 15).into()); |
||||
|
||||
let min = ZOrderIndex::ELEMENT_MIN; |
||||
let max = ZOrderIndex::ELEMENT_MAX; |
||||
assert_eq!((min, 0, max), zorder(min, 0, max).into()); |
||||
} |
||||
|
||||
#[test] |
||||
fn bounds_checking() { |
||||
let min = 0b11111111111_1_00000000000000000000u32 as i32; |
||||
let max = 0b00000000000_0_11111111111111111111u32 as i32; |
||||
|
||||
assert_eq!( |
||||
Ok((ZOrderIndex::ELEMENT_MIN, ZOrderIndex::ELEMENT_MAX, 0)), |
||||
ZOrderIndex::new(min, max, 0).map(Into::into) |
||||
); |
||||
|
||||
assert!(ZOrderIndex::new(min - 1, max, 0).is_err()); |
||||
assert!(ZOrderIndex::new(min, max + 1, 0).is_err()); |
||||
} |
||||
|
||||
#[test] |
||||
#[rustfmt::skip] |
||||
fn raw() { |
||||
let x_mask: u64 = 0b0_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_001_001_000; |
||||
let y_mask: u64 = 0b0_010_010_010_010_010_010_010_010_010_010_010_010_010_010_010_010_010_000_000_000_000; |
||||
let z_mask: u64 = 0b0_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_100_100_100_100; |
||||
|
||||
let raw = ZOrderIndex::from_raw(x_mask | y_mask | z_mask); |
||||
assert_eq!(raw, zorder(6, -16, 15)); |
||||
} |
||||
|
||||
#[test] |
||||
fn bitshift() { |
||||
let zero = ZOrderIndex::ZERO; |
||||
assert_eq!(zero, zero >> 2); |
||||
assert_eq!(zero, zero << 2); |
||||
|
||||
assert_eq!(zorder(4, 8, 12), zorder(1, 2, 3) << 2); |
||||
assert_eq!(zorder(1, 2, 3), zorder(4, 8, 12) >> 2); |
||||
|
||||
assert_eq!(zorder(-4, -8, -12), zorder(-1, -2, -3) << 2); |
||||
assert_eq!(zorder(-1, -2, -3), zorder(-4, -8, -12) >> 2); |
||||
} |
||||
} |
@ -0,0 +1,127 @@ |
||||
use bevy::{ |
||||
asset::RenderAssetUsages, prelude::*, render::mesh::Indices, render::mesh::PrimitiveTopology, |
||||
}; |
||||
|
||||
use crate::bloxel::{block::BlockTexture, prelude::*, storage::BloxelArray}; |
||||
|
||||
pub fn create_bloxel_mesh( |
||||
bloxels: &impl BloxelView<Entity>, |
||||
layout: &TextureAtlasLayout, |
||||
sources: &TextureAtlasSources, |
||||
block_lookup: &Query<&BlockTexture, With<Block>>, |
||||
) -> Mesh { |
||||
let offset = -(bloxels.size().as_vec3() / 2.); |
||||
|
||||
let mut positions: Vec<Vec3> = vec![]; |
||||
let mut normals: Vec<Vec3> = vec![]; |
||||
let mut uvs: Vec<Vec2> = vec![]; |
||||
let mut indices = vec![]; |
||||
let mut num_vertices = 0; |
||||
|
||||
let mut append_vertex = |pos: Vec3, normal: Dir3, uv: Vec2| { |
||||
positions.push(pos); |
||||
normals.push(*normal); |
||||
uvs.push(uv); |
||||
}; |
||||
|
||||
let mut append_quad = |show: bool, points: [Vec3; 4], normal: Dir3, uvs: Rect| { |
||||
if show { |
||||
append_vertex(points[0], normal, (uvs.min.x, uvs.min.y).into()); |
||||
append_vertex(points[1], normal, (uvs.max.x, uvs.min.y).into()); |
||||
append_vertex(points[2], normal, (uvs.max.x, uvs.max.y).into()); |
||||
append_vertex(points[3], normal, (uvs.min.x, uvs.max.y).into()); |
||||
|
||||
for i in [0, 1, 2, 2, 3, 0] { |
||||
indices.push(num_vertices + i); |
||||
} |
||||
num_vertices += 4; |
||||
} |
||||
}; |
||||
|
||||
let mut append_cube = |pos: IVec3, uvs: Rect, sides: [bool; 6]| { |
||||
let min = offset + pos.as_vec3(); |
||||
let max = offset + (pos + IVec3::ONE).as_vec3(); |
||||
|
||||
// left/right, bottom/top, back/front
|
||||
let lbb = Vec3::new(min.x, min.y, min.z); |
||||
let lbf = Vec3::new(min.x, max.y, max.z); |
||||
let ltb = Vec3::new(min.x, max.y, min.z); |
||||
let ltf = Vec3::new(min.x, min.y, max.z); |
||||
let rbb = Vec3::new(max.x, min.y, min.z); |
||||
let rbf = Vec3::new(max.x, max.y, max.z); |
||||
let rtb = Vec3::new(max.x, max.y, min.z); |
||||
let rtf = Vec3::new(max.x, min.y, max.z); |
||||
|
||||
append_quad(sides[0], [rbb, rtb, rbf, rtf], Dir3::X, uvs); // Right
|
||||
append_quad(sides[1], [ltf, lbf, ltb, lbb], Dir3::NEG_X, uvs); // Left
|
||||
append_quad(sides[2], [rtb, ltb, lbf, rbf], Dir3::Y, uvs); // Top
|
||||
append_quad(sides[3], [rtf, ltf, lbb, rbb], Dir3::NEG_Y, uvs); // Bottom
|
||||
append_quad(sides[4], [ltf, rtf, rbf, lbf], Dir3::Z, uvs); // Front
|
||||
append_quad(sides[5], [ltb, rtb, rbb, lbb], Dir3::NEG_Z, uvs); // Back
|
||||
}; |
||||
|
||||
// Shrinks the UV rectangle by a tiny amount to get rid of ugly seams at the edges of blocks,
|
||||
// due to floating point inaccuracy when the color is looked up in the shader or something.
|
||||
// Temporary fix until a more sophisticated graphics overhaul using texture arrays or so.
|
||||
let shrink_amount = -0.1 / layout.size.x as f32; |
||||
|
||||
let size = bloxels.size().as_ivec3(); |
||||
|
||||
// Capture all textures, which is currently all we need to know about a block to render it,
|
||||
// into a 3D array for us to look up, with a 1 block buffer so we can safely look up neighbors.
|
||||
let mut textures = BloxelArray::new_with_default(bloxels.size() + UVec3::new(2, 2, 2)); |
||||
for z in 0..size.z { |
||||
for y in 0..size.y { |
||||
for x in 0..size.x { |
||||
let pos = IVec3::new(x, y, z); |
||||
let block = bloxels.get(pos); |
||||
textures[pos + IVec3::ONE] = block_lookup.get(block).ok().map(|t| &t.0); |
||||
} |
||||
} |
||||
} |
||||
|
||||
for z in 0..size.z { |
||||
for y in 0..size.y { |
||||
for x in 0..size.x { |
||||
let pos = IVec3::new(x, y, z); |
||||
if let Some(texture) = textures[pos + IVec3::ONE] { |
||||
if let Some(uvs) = uv_rect(sources, layout, texture) { |
||||
append_cube( |
||||
pos, |
||||
uvs.inflate(shrink_amount), |
||||
[ |
||||
textures[pos + IVec3::new(2, 1, 1)].is_none(), |
||||
textures[pos + IVec3::new(0, 1, 1)].is_none(), |
||||
textures[pos + IVec3::new(1, 2, 1)].is_none(), |
||||
textures[pos + IVec3::new(1, 0, 1)].is_none(), |
||||
textures[pos + IVec3::new(1, 1, 2)].is_none(), |
||||
textures[pos + IVec3::new(1, 1, 0)].is_none(), |
||||
], |
||||
); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Mesh::new( |
||||
PrimitiveTopology::TriangleList, |
||||
RenderAssetUsages::default(), |
||||
) |
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) |
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) |
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) |
||||
.with_inserted_indices(Indices::U32(indices)) |
||||
} |
||||
|
||||
fn uv_rect( |
||||
sources: &TextureAtlasSources, |
||||
layout: &TextureAtlasLayout, |
||||
texture: impl Into<AssetId<Image>>, |
||||
) -> Option<Rect> { |
||||
sources.texture_rect(layout, texture).map(|rect| { |
||||
let rect = rect.as_rect(); |
||||
let size = layout.size.as_vec2(); |
||||
Rect::from_corners(rect.min / size, rect.max / size) |
||||
}) |
||||
} |
@ -0,0 +1,46 @@ |
||||
use std::ops::Deref; |
||||
|
||||
use bevy::prelude::*; |
||||
|
||||
use crate::{ |
||||
bloxel::{block::BlockTexture, prelude::*}, |
||||
TerrainAtlas, TerrainMaterial, |
||||
}; |
||||
|
||||
mod mesh_generator; |
||||
|
||||
pub use mesh_generator::*; |
||||
|
||||
/// Generates meshes for chunks that have `ChunkStorage` but no `Mesh3d` yet.
|
||||
pub fn generate_chunk_mesh( |
||||
mut commands: Commands, |
||||
mut meshes: ResMut<Assets<Mesh>>, |
||||
terrain_material: Option<Res<TerrainMaterial>>, |
||||
terrain_atlas: Option<Res<TerrainAtlas>>, |
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>, |
||||
chunks_without_meshes: Query<(Entity, &ChunkData), (With<Chunk>, Without<Mesh3d>)>, |
||||
block_lookup: Query<&BlockTexture, With<Block>>, |
||||
) { |
||||
// This system will run before `TerrainMaterial` is available, so to avoid
|
||||
// the system failing, we'll make the material and atlas an `Option`.
|
||||
let Some(terrain_material) = terrain_material else { |
||||
return; |
||||
}; |
||||
// Atlas and layout should exist at this point, if material does.
|
||||
let terrain_atlas = terrain_atlas.unwrap(); |
||||
let terrain_atlas_layout = atlas_layouts.get(&terrain_atlas.layout).unwrap(); |
||||
|
||||
for (entity, storage) in chunks_without_meshes.iter() { |
||||
let mesh = create_bloxel_mesh( |
||||
storage.deref(), |
||||
terrain_atlas_layout, |
||||
&terrain_atlas.sources, |
||||
&block_lookup, |
||||
); |
||||
|
||||
commands.entity(entity).insert(( |
||||
Mesh3d(meshes.add(mesh)), |
||||
MeshMaterial3d(terrain_material.0.clone()), |
||||
)); |
||||
} |
||||
} |
@ -0,0 +1,78 @@ |
||||
use bevy::prelude::*; |
||||
|
||||
pub mod block; |
||||
pub mod chunk; |
||||
pub mod generic_math; |
||||
pub mod math; |
||||
pub mod mesh; |
||||
pub mod storage; |
||||
pub mod worldgen; |
||||
|
||||
pub mod prelude { |
||||
pub use super::{ |
||||
block::*, |
||||
chunk::*, |
||||
math::*, |
||||
storage::{BloxelView, BloxelViewMut}, |
||||
BloxelPlugin, |
||||
}; |
||||
} |
||||
|
||||
use prelude::*; |
||||
|
||||
pub struct BloxelPlugin; |
||||
|
||||
impl Plugin for BloxelPlugin { |
||||
fn build(&self, app: &mut App) { |
||||
app.add_systems( |
||||
Update, |
||||
( |
||||
set_transform_on_chunkpos_changed, |
||||
worldgen::create_chunks_around_camera, |
||||
worldgen::generate_terrain, |
||||
mesh::generate_chunk_mesh, |
||||
), |
||||
) |
||||
.add_observer(add_chunks_to_chunkmap) |
||||
.add_observer(remove_chunks_from_chunkmap); |
||||
} |
||||
} |
||||
|
||||
fn set_transform_on_chunkpos_changed( |
||||
mut commands: Commands, |
||||
chunk_query: Query<(Entity, &ChunkPos), Changed<ChunkPos>>, |
||||
) { |
||||
for (chunk, pos) in chunk_query.iter() { |
||||
commands.entity(chunk).insert(pos.transform()); |
||||
} |
||||
} |
||||
|
||||
fn add_chunks_to_chunkmap( |
||||
trigger: Trigger<OnAdd, (Parent, ChunkPos)>, |
||||
chunk_query: Query<(&Parent, &ChunkPos)>, |
||||
mut map_query: Query<&mut ChunkMap>, |
||||
) { |
||||
let chunk = trigger.entity(); |
||||
let Ok((parent, pos)) = chunk_query.get(chunk) else { |
||||
// Trigger with a bundle doesn't work as expected. It triggers
|
||||
// when ANY component is added, rather than when ALL are present.
|
||||
return; |
||||
}; |
||||
let mut map = map_query.get_mut(parent.get()).unwrap(); |
||||
map.try_insert(*pos, chunk).expect("chunk already present"); |
||||
} |
||||
|
||||
fn remove_chunks_from_chunkmap( |
||||
trigger: Trigger<OnRemove, (Parent, ChunkPos)>, |
||||
chunk_query: Query<(&Parent, &ChunkPos)>, |
||||
mut map_query: Query<&mut ChunkMap>, |
||||
) { |
||||
let chunk = trigger.entity(); |
||||
let Ok((parent, pos)) = chunk_query.get(chunk) else { |
||||
// See above.
|
||||
return; |
||||
}; |
||||
let mut map = map_query.get_mut(parent.get()).unwrap(); |
||||
map.remove(pos).expect("chunk not found"); |
||||
println!("Chunk {chunk} @ {pos:?} removed from {}", parent.get()); |
||||
} |
@ -0,0 +1,74 @@ |
||||
use std::ops::{Index, IndexMut}; |
||||
|
||||
use bevy::math::{IVec3, UVec3}; |
||||
|
||||
use super::bloxel_view::{ivec3_to_index, BloxelView, BloxelViewMut}; |
||||
|
||||
pub struct BloxelArray<T: Copy> { |
||||
size: UVec3, |
||||
data: Vec<T>, |
||||
} |
||||
|
||||
impl<T: Copy> BloxelArray<T> { |
||||
pub fn new(size: UVec3, fill: T) -> Self { |
||||
assert!(size.x > 0 && size.y > 0 && size.z > 0); |
||||
let len = (size.x * size.y * size.z) as usize; |
||||
let data = vec![fill; len]; |
||||
Self { size, data } |
||||
} |
||||
|
||||
pub fn new_with_default(size: UVec3) -> Self |
||||
where |
||||
T: Default, |
||||
{ |
||||
Self::new(size, Default::default()) |
||||
} |
||||
|
||||
fn get_index(&self, pos: impl Into<IVec3>) -> usize { |
||||
let pos = pos.into(); |
||||
assert!(self.contains(pos)); |
||||
ivec3_to_index(pos, self.size) |
||||
} |
||||
} |
||||
|
||||
impl<T: Copy> Index<IVec3> for BloxelArray<T> { |
||||
type Output = T; |
||||
|
||||
fn index(&self, pos: IVec3) -> &Self::Output { |
||||
let index = self.get_index(pos); |
||||
&self.data[index] |
||||
} |
||||
} |
||||
|
||||
impl<T: Copy> IndexMut<IVec3> for BloxelArray<T> { |
||||
fn index_mut(&mut self, pos: IVec3) -> &mut Self::Output { |
||||
let index = self.get_index(pos); |
||||
&mut self.data[index] |
||||
} |
||||
} |
||||
|
||||
impl<T: Copy> BloxelView<T> for BloxelArray<T> { |
||||
fn size(&self) -> UVec3 { |
||||
self.size |
||||
} |
||||
|
||||
fn contains(&self, pos: impl Into<IVec3>) -> bool { |
||||
let pos = pos.into(); |
||||
let size = self.size.as_ivec3(); |
||||
(pos.x >= 0 && pos.x < size.x) |
||||
&& (pos.y >= 0 && pos.y < size.y) |
||||
&& (pos.z >= 0 && pos.z < size.z) |
||||
} |
||||
|
||||
fn get(&self, pos: impl Into<IVec3>) -> T { |
||||
let index = self.get_index(pos); |
||||
self.data[index] |
||||
} |
||||
} |
||||
|
||||
impl<T: Copy> BloxelViewMut<T> for BloxelArray<T> { |
||||
fn set(&mut self, pos: impl Into<IVec3>, value: T) { |
||||
let index = self.get_index(pos); |
||||
self.data[index] = value; |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
use bevy::math::{IVec3, UVec3}; |
||||
|
||||
pub trait BloxelView<T> { |
||||
fn size(&self) -> UVec3; |
||||
|
||||
fn contains(&self, pos: impl Into<IVec3>) -> bool; |
||||
|
||||
fn get(&self, pos: impl Into<IVec3>) -> T; |
||||
} |
||||
|
||||
pub trait BloxelViewMut<T>: BloxelView<T> { |
||||
fn set(&mut self, pos: impl Into<IVec3>, value: T); |
||||
} |
||||
|
||||
pub fn ivec3_to_index(pos: impl Into<IVec3>, size: UVec3) -> usize { |
||||
let pos = pos.into(); |
||||
let size = size.as_ivec3(); |
||||
(pos.x + (pos.y * size.x) + (pos.z * size.x * size.y)) as usize |
||||
} |
||||
|
||||
pub fn index_to_ivec3(index: usize, size: UVec3) -> IVec3 { |
||||
let size = size.as_ivec3(); |
||||
let i = index as i32; |
||||
let x = i % size.x; |
||||
let y = i / size.x % size.y; |
||||
let z = i / size.x / size.y; |
||||
IVec3::new(x, y, z) |
||||
} |
@ -0,0 +1,375 @@ |
||||
use std::{array::from_fn, collections::BinaryHeap}; |
||||
|
||||
use bevy::{ |
||||
math::IVec3, |
||||
utils::{HashMap, HashSet}, |
||||
}; |
||||
|
||||
use crate::bloxel::math::{zorder, ChunkPos, ChunkRegion, ZOrderIndex}; |
||||
|
||||
const START_INDEX_LOOKUP: [usize; 11] = [ |
||||
0, 1, 9, 73, 585, 4681, 37449, 299593, 2396745, 19173961, 153391689, |
||||
]; |
||||
|
||||
pub struct ChunkedOctree<T: Default + Copy + Eq> { |
||||
depth: usize, |
||||
regions: HashMap<ZOrderIndex, Vec<T>>, |
||||
} |
||||
|
||||
impl<T: Default + Copy + Eq> ChunkedOctree<T> { |
||||
pub fn new(depth: usize) -> Self { |
||||
assert!(depth > 0 && depth < START_INDEX_LOOKUP.len()); |
||||
Self { |
||||
depth, |
||||
regions: Default::default(), |
||||
} |
||||
} |
||||
|
||||
pub fn get(&self, node: impl TryInto<OctreeNode>) -> T { |
||||
if let Ok(node) = node.try_into() { |
||||
if node.level <= self.depth { |
||||
return self |
||||
.regions |
||||
.get(&(node.pos >> (self.depth - node.level))) |
||||
.map(|region| region[node.index(self.depth)]) |
||||
.unwrap_or_default(); |
||||
} |
||||
} |
||||
Default::default() |
||||
} |
||||
|
||||
pub fn update<F>(&mut self, chunk_pos: ChunkPos, update_fn: F) |
||||
where |
||||
F: Fn(OctreeNode, Option<&[T; 8]>, &mut T), |
||||
{ |
||||
let pos: ZOrderIndex = chunk_pos.try_into().unwrap(); |
||||
let region = self.regions.entry(pos >> self.depth).or_insert_with(|| { |
||||
let size = START_INDEX_LOOKUP[self.depth + 1] + 1; |
||||
vec![Default::default(); size] |
||||
}); |
||||
|
||||
let mut node = OctreeNode::new(0, pos); |
||||
while node.level <= self.depth { |
||||
let parent_index = node.index(self.depth); |
||||
let (parent, children) = if let Some(children_index) = node.children_index(self.depth) { |
||||
let (left, right) = region.split_at_mut(children_index); |
||||
let parent = &mut left[parent_index]; |
||||
let children = right[0..8].try_into().unwrap(); |
||||
(parent, Some(children)) |
||||
} else { |
||||
let parent = &mut region[parent_index]; |
||||
(parent, None) |
||||
}; |
||||
|
||||
let previous = *parent; |
||||
update_fn(node, children, parent); |
||||
if *parent == previous { |
||||
break; // If no change was made, we don't have anything to propagate.
|
||||
} |
||||
|
||||
node = node.parent(); |
||||
} |
||||
} |
||||
|
||||
pub fn find<'a, W, F>( |
||||
&'a self, |
||||
from: impl IntoIterator<Item = &'a ChunkPos>, |
||||
priority_fn: F, |
||||
) -> OctreeIterator<'a, T, W, F> |
||||
where |
||||
W: Ord, |
||||
F: Fn(OctreeNode, T) -> Option<W>, |
||||
{ |
||||
OctreeIterator::new(self, from, priority_fn) |
||||
} |
||||
} |
||||
|
||||
pub struct OctreeIterator<'a, T, P, F> |
||||
where |
||||
T: Default + Copy + Eq, |
||||
P: Ord, |
||||
F: Fn(OctreeNode, T) -> Option<P>, |
||||
{ |
||||
octree: &'a ChunkedOctree<T>, |
||||
checked: HashSet<ZOrderIndex>, |
||||
queue: BinaryHeap<PriorityItem<T, P>>, |
||||
priority_fn: F, |
||||
} |
||||
|
||||
impl<'a, T, P, F> OctreeIterator<'a, T, P, F> |
||||
where |
||||
T: Default + Copy + Eq, |
||||
P: Ord, |
||||
F: Fn(OctreeNode, T) -> Option<P>, |
||||
{ |
||||
fn new( |
||||
octree: &'a ChunkedOctree<T>, |
||||
from: impl IntoIterator<Item = &'a ChunkPos>, |
||||
priority_fn: F, |
||||
) -> Self { |
||||
let mut result = Self { |
||||
octree, |
||||
checked: Default::default(), |
||||
queue: Default::default(), |
||||
priority_fn, |
||||
}; |
||||
for pos in from { |
||||
let node_pos: ZOrderIndex = (*pos).try_into().unwrap(); |
||||
result.search_region(node_pos >> octree.depth); |
||||
} |
||||
result |
||||
} |
||||
|
||||
fn search_region(&mut self, region_pos: ZOrderIndex) { |
||||
if self.checked.insert(region_pos) { |
||||
self.push_item((self.octree.depth, region_pos).into()); |
||||
} |
||||
} |
||||
|
||||
fn push_item(&mut self, node: OctreeNode) { |
||||
let value = self.octree.get(node); |
||||
if let Some(priority) = (self.priority_fn)(node, value) { |
||||
self.queue.push(PriorityItem { |
||||
priority, |
||||
node, |
||||
value, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl<'a, T, P, F> Iterator for OctreeIterator<'a, T, P, F> |
||||
where |
||||
T: Default + Copy + Eq, |
||||
P: Ord, |
||||
F: Fn(OctreeNode, T) -> Option<P>, |
||||
{ |
||||
type Item = (ChunkPos, T, P); |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
loop { |
||||
let PriorityItem { |
||||
priority, |
||||
node, |
||||
value, |
||||
} = self.queue.pop()?; |
||||
|
||||
if node.level == self.octree.depth { |
||||
// If the current highest priority item is a region, see if its
|
||||
// neighboring regions are candidates to consider as well.
|
||||
// NOTE: Faulty `priority_fn` could cause this to loop forever.
|
||||
|
||||
// Add all the region's neighbors to the priority queue.
|
||||
// NOTE: `search_region` skips any already added regions.
|
||||
for neighbor in node.neighbors() { |
||||
self.search_region(neighbor.pos); |
||||
} |
||||
} |
||||
|
||||
if let Some(children) = node.children() { |
||||
// If current highest priority node has children,
|
||||
// add them to the priority queue and continue.
|
||||
for child in children { |
||||
self.push_item(child); |
||||
} |
||||
} else { |
||||
// If there are no children, then we're at `ChunkPos` level.
|
||||
// Return this chunk as the highest priority item found.
|
||||
let chunk_pos = node.pos.into(); |
||||
return Some((chunk_pos, value, priority)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] |
||||
pub struct OctreeNode { |
||||
pub level: usize, |
||||
pub pos: ZOrderIndex, |
||||
} |
||||
|
||||
impl OctreeNode { |
||||
pub fn new(level: usize, pos: ZOrderIndex) -> Self { |
||||
Self { level, pos } |
||||
} |
||||
|
||||
pub fn parent(&self) -> Self { |
||||
Self::new(self.level + 1, self.pos >> 1) |
||||
} |
||||
|
||||
pub fn children(&self) -> Option<[Self; 8]> { |
||||
(self.level > 0).then(|| from_fn(|i| self.child_unchecked(i as u64))) |
||||
} |
||||
|
||||
pub fn neighbors(&self) -> [Self; 26] { |
||||
let (pos_x, pos_y, pos_z) = self.pos.into(); |
||||
let mut result = [OctreeNode::default(); 26]; |
||||
let mut index = 0; |
||||
for x in -1..1 { |
||||
for y in -1..1 { |
||||
for z in -1..1 { |
||||
if x != 0 && y != 0 && z != 0 { |
||||
let pos = zorder(pos_x + x, pos_y + y, pos_z + z); |
||||
result[index] = OctreeNode::new(self.level, pos); |
||||
index += 1; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
result |
||||
} |
||||
|
||||
pub fn region(&self) -> ChunkRegion { |
||||
let min = (self.pos << self.level).into(); |
||||
let max = min + IVec3::splat((1 << self.level) - 1); |
||||
ChunkRegion::new_unchecked(min, max) |
||||
} |
||||
|
||||
fn index(&self, depth: usize) -> usize { |
||||
let local_pos = self.pos & !(!0 << ((depth - self.level) * 3)); |
||||
START_INDEX_LOOKUP[depth - self.level] + local_pos.raw() as usize |
||||
} |
||||
|
||||
fn children_index(&self, depth: usize) -> Option<usize> { |
||||
(self.level > 0).then(|| self.child_unchecked(0).index(depth)) |
||||
} |
||||
|
||||
/// Returns the child of this node with the specified index (within `0..8`).
|
||||
/// No safety checks are done to ensure that `level` or `index` are valid.
|
||||
fn child_unchecked(&self, index: u64) -> Self { |
||||
Self::new(self.level - 1, (self.pos << 1) | index) |
||||
} |
||||
} |
||||
|
||||
impl Into<OctreeNode> for (usize, ZOrderIndex) { |
||||
fn into(self) -> OctreeNode { |
||||
OctreeNode::new(self.0, self.1) |
||||
} |
||||
} |
||||
|
||||
impl TryInto<OctreeNode> for ChunkPos { |
||||
type Error = (); |
||||
fn try_into(self) -> Result<OctreeNode, ()> { |
||||
Ok(OctreeNode::new(0, self.try_into()?)) |
||||
} |
||||
} |
||||
|
||||
impl TryInto<OctreeNode> for (i32, i32, i32) { |
||||
type Error = (); |
||||
fn try_into(self) -> Result<OctreeNode, ()> { |
||||
Into::<ChunkPos>::into(self).try_into() |
||||
} |
||||
} |
||||
|
||||
struct PriorityItem<T: Default + Copy + Eq, P: Ord> { |
||||
priority: P, |
||||
node: OctreeNode, |
||||
value: T, |
||||
} |
||||
|
||||
impl<T: Default + Copy + Eq, P: Ord> Eq for PriorityItem<T, P> {} |
||||
|
||||
impl<T: Default + Copy + Eq, P: Ord> PartialEq for PriorityItem<T, P> { |
||||
fn eq(&self, other: &Self) -> bool { |
||||
self.priority.eq(&other.priority) |
||||
} |
||||
} |
||||
|
||||
impl<T: Default + Copy + Eq, P: Ord> Ord for PriorityItem<T, P> { |
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
||||
self.priority.cmp(&other.priority) |
||||
} |
||||
} |
||||
|
||||
impl<T: Default + Copy + Eq, P: Ord> PartialOrd for PriorityItem<T, P> { |
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { |
||||
self.priority.partial_cmp(&other.priority) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn update() { |
||||
let mut octree = ChunkedOctree::<bool>::new(3); |
||||
|
||||
assert_eq!(false, octree.get((0, 0, 0))); |
||||
assert_eq!(false, octree.get((1, 1, 1))); |
||||
assert_eq!(false, octree.get((-1, -1, -1))); |
||||
|
||||
octree.update((0, 0, 0).into(), |_, _, parent| *parent = true); |
||||
assert_eq!(true, octree.get((0, 0, 0))); |
||||
|
||||
assert_eq!(true, octree.get((0, zorder(0, 0, 0)))); |
||||
assert_eq!(true, octree.get((1, zorder(0, 0, 0)))); |
||||
assert_eq!(true, octree.get((2, zorder(0, 0, 0)))); |
||||
assert_eq!(true, octree.get((3, zorder(0, 0, 0)))); |
||||
|
||||
assert_eq!(false, octree.get((0, zorder(1, 1, 1)))); |
||||
assert_eq!(false, octree.get((1, zorder(2, 2, 2)))); |
||||
assert_eq!(false, octree.get((2, zorder(4, 4, 4)))); |
||||
assert_eq!(false, octree.get((3, zorder(8, 8, 8)))); |
||||
|
||||
assert_eq!(false, octree.get((0, zorder(-1, -1, -1)))); |
||||
assert_eq!(false, octree.get((1, zorder(-1, -1, -1)))); |
||||
assert_eq!(false, octree.get((2, zorder(-1, -1, -1)))); |
||||
assert_eq!(false, octree.get((3, zorder(-1, -1, -1)))); |
||||
|
||||
octree.update(ChunkPos::new(-12, -17, -42), |_, _, parent| *parent = true); |
||||
assert_eq!(true, octree.get((-12, -17, -42))); |
||||
|
||||
assert_eq!(true, octree.get((0, zorder(-12, -17, -42)))); |
||||
assert_eq!(true, octree.get((1, zorder(-6, -9, -21)))); |
||||
assert_eq!(true, octree.get((2, zorder(-3, -5, -11)))); |
||||
assert_eq!(true, octree.get((3, zorder(-2, -3, -6)))); |
||||
} |
||||
|
||||
#[test] |
||||
#[rustfmt::skip] |
||||
fn find() { |
||||
let mut octree = ChunkedOctree::<bool>::new(3); |
||||
octree.update(( 2, 3, 4).into(), |_, _, parent| *parent = true); |
||||
octree.update((10, 10, 10).into(), |_, _, parent| *parent = true); |
||||
octree.update(( 8, 8, 8).into(), |_, _, parent| *parent = true); |
||||
octree.update(( 0, 16, -1).into(), |_, _, parent| *parent = true); |
||||
octree.update(( 9, 9, 9).into(), |_, _, parent| *parent = true); |
||||
|
||||
let from = ChunkPos::new(8, 8, 8); |
||||
let mut iterator = octree.find([&from], |node, value| { |
||||
value.then(|| -node.region().distance_to_squared(from)) |
||||
}); |
||||
|
||||
assert_eq!(Some((( 8, 8, 8).into(), true, -(0*0 + 0*0 + 0*0))), iterator.next()); |
||||
assert_eq!(Some((( 9, 9, 9).into(), true, -(1*1 + 1*1 + 1*1))), iterator.next()); |
||||
assert_eq!(Some(((10, 10, 10).into(), true, -(2*2 + 2*2 + 2*2))), iterator.next()); |
||||
assert_eq!(Some((( 2, 3, 4).into(), true, -(6*6 + 5*5 + 4*4))), iterator.next()); |
||||
assert_eq!(Some((( 0, 16, -1).into(), true, -(8*8 + 8*8 + 9*9))), iterator.next()); |
||||
assert_eq!(None, iterator.next()); |
||||
} |
||||
|
||||
#[test] |
||||
fn node_region() { |
||||
let chunk = |a: (i32, i32, i32), b: (i32, i32, i32)| -> ChunkRegion { |
||||
ChunkRegion::new(a.into(), b.into()) |
||||
}; |
||||
let node = |level: usize, pos: (i32, i32, i32)| -> ChunkRegion { |
||||
OctreeNode::new(level, pos.try_into().unwrap()).region() |
||||
}; |
||||
|
||||
assert_eq!(chunk((0, 0, 0), (0, 0, 0)), node(0, (0, 0, 0))); |
||||
assert_eq!(chunk((0, 0, 0), (1, 1, 1)), node(1, (0, 0, 0))); |
||||
assert_eq!(chunk((0, 0, 0), (3, 3, 3)), node(2, (0, 0, 0))); |
||||
assert_eq!(chunk((0, 0, 0), (7, 7, 7)), node(3, (0, 0, 0))); |
||||
|
||||
assert_eq!(chunk((-1, -1, -1), (-1, -1, -1)), node(0, (-1, -1, -1))); |
||||
assert_eq!(chunk((-2, -2, -2), (-1, -1, -1)), node(1, (-1, -1, -1))); |
||||
assert_eq!(chunk((-4, -4, -4), (-1, -1, -1)), node(2, (-1, -1, -1))); |
||||
assert_eq!(chunk((-8, -8, -8), (-1, -1, -1)), node(3, (-1, -1, -1))); |
||||
|
||||
assert_eq!(chunk((2, -3, -4), (2, -3, -4)), node(0, (2, -3, -4))); |
||||
assert_eq!(chunk((4, -6, -8), (5, -5, -7)), node(1, (2, -3, -4))); |
||||
assert_eq!(chunk((8, -12, -16), (11, -9, -13)), node(2, (2, -3, -4))); |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
mod bloxel_array; |
||||
mod bloxel_view; |
||||
mod chunked_octree; |
||||
mod palette_bloxel_storage; |
||||
mod palette_storage; |
||||
|
||||
pub use bloxel_array::*; |
||||
pub use bloxel_view::*; |
||||
pub use chunked_octree::*; |
||||
pub use palette_bloxel_storage::*; |
@ -0,0 +1,63 @@ |
||||
use bevy::math::{IVec3, UVec3}; |
||||
|
||||
use super::{ |
||||
bloxel_view::{ivec3_to_index, BloxelView, BloxelViewMut}, |
||||
palette_storage::PaletteStorage, |
||||
}; |
||||
|
||||
pub struct PaletteBloxelStorage<T: Copy + Eq> { |
||||
size: UVec3, |
||||
data: PaletteStorage<T>, |
||||
} |
||||
|
||||
impl<T: Copy + Eq> PaletteBloxelStorage<T> { |
||||
pub fn new(size: UVec3, fill: T) -> Self { |
||||
assert!(size.x > 0 && size.y > 0 && size.z > 0); |
||||
let len = (size.x * size.y * size.z).try_into().unwrap(); |
||||
let data = PaletteStorage::new(len, fill); |
||||
Self { size, data } |
||||
} |
||||
|
||||
pub fn new_with_default(size: UVec3) -> Self |
||||
where |
||||
T: Default, |
||||
{ |
||||
Self::new(size, Default::default()) |
||||
} |
||||
|
||||
pub fn get_used_count(&self, value: T) -> usize { |
||||
self.data.get_used_count(value) |
||||
} |
||||
|
||||
fn get_index(&self, pos: impl Into<IVec3>) -> usize { |
||||
let pos = pos.into(); |
||||
assert!(self.contains(pos)); |
||||
ivec3_to_index(pos, self.size) |
||||
} |
||||
} |
||||
|
||||
impl<T: Copy + Eq> BloxelView<T> for PaletteBloxelStorage<T> { |
||||
fn size(&self) -> UVec3 { |
||||
self.size |
||||
} |
||||
|
||||
fn contains(&self, pos: impl Into<IVec3>) -> bool { |
||||
let pos = pos.into(); |
||||
let size = self.size.as_ivec3(); |
||||
(pos.x >= 0 && pos.x < size.x) |
||||
&& (pos.y >= 0 && pos.y < size.y) |
||||
&& (pos.z >= 0 && pos.z < size.z) |
||||
} |
||||
|
||||
fn get(&self, pos: impl Into<IVec3>) -> T { |
||||
let index = self.get_index(pos); |
||||
self.data.get(index) |
||||
} |
||||
} |
||||
|
||||
impl<T: Copy + Eq> BloxelViewMut<T> for PaletteBloxelStorage<T> { |
||||
fn set(&mut self, pos: impl Into<IVec3>, value: T) { |
||||
let index = self.get_index(pos); |
||||
self.data.set(index, value); |
||||
} |
||||
} |
@ -0,0 +1,213 @@ |
||||
use bitvec::prelude::*; |
||||
|
||||
pub struct PaletteStorage<T: Copy + Eq> { |
||||
len: usize, |
||||
bits: usize, |
||||
data: BitVec, |
||||
entries: Vec<PaletteEntry<T>>, |
||||
} |
||||
|
||||
impl<T: Copy + Eq> PaletteStorage<T> { |
||||
pub fn new(len: usize, fill: T) -> Self { |
||||
Self { |
||||
len, |
||||
bits: 0, |
||||
data: bitvec![], |
||||
entries: vec![PaletteEntry { |
||||
value: Some(fill), |
||||
used: len, |
||||
}], |
||||
} |
||||
} |
||||
|
||||
pub fn new_with_default(len: usize) -> Self |
||||
where |
||||
T: Default, |
||||
{ |
||||
Self::new(len, Default::default()) |
||||
} |
||||
|
||||
/// Gets the length of this `PaletteStorage`. That is, how many
|
||||
/// elements can be accessed via the `get` and `set` functions.
|
||||
pub fn len(&self) -> usize { |
||||
self.len |
||||
} |
||||
|
||||
/// Gets the number of bits each element takes up when stored.
|
||||
///
|
||||
/// More bits means more palette entries can be stored, so more unique
|
||||
/// values can be stored in this `PaletteStorage`, at the cost of more
|
||||
/// memory. This will automatically increase as necessary.
|
||||
pub fn bits(&self) -> usize { |
||||
self.bits |
||||
} |
||||
|
||||
/// Gets the number of times the specific value occurs in this `PaletteStorage`.
|
||||
pub fn get_used_count(&self, value: T) -> usize { |
||||
self.find_existing_entry(value).map(|e| e.used).unwrap_or(0) |
||||
} |
||||
|
||||
pub fn get(&self, index: usize) -> T { |
||||
let palette_index = self.get_palette_index(index); |
||||
let entry = &self.entries[palette_index]; |
||||
entry.value.expect("invalid palette entry") |
||||
} |
||||
|
||||
pub fn set(&mut self, index: usize, value: T) -> T { |
||||
let prev_palette_index = self.get_palette_index(index); |
||||
let prev_entry = &mut self.entries[prev_palette_index]; |
||||
let prev_value = prev_entry.value.expect("invalid palette entry"); |
||||
|
||||
// If entry found at `index` already has this value, return early.
|
||||
if prev_value == value { |
||||
return value; |
||||
} |
||||
|
||||
// Reduce the number of times the previously used palette for this
|
||||
// `index` is in use. This potentially allows this entry to be reused.
|
||||
prev_entry.used -= 1; |
||||
|
||||
// Find a palette entry for this value. Resizes palette if necessary.
|
||||
let (new_palette_index, new_entry) = self.find_or_create_entry(value); |
||||
// increase the number of times it's in use
|
||||
new_entry.used += 1; |
||||
|
||||
// Update the palette index in the actual `data` BitVec.
|
||||
self.set_palette_index(index, new_palette_index); |
||||
|
||||
prev_value |
||||
} |
||||
|
||||
/// Gets the bit range into `data` for the specified index.
|
||||
fn bit_range(&self, index: usize) -> std::ops::Range<usize> { |
||||
(index * self.bits)..((index + 1) * self.bits) |
||||
} |
||||
|
||||
/// Looks up the palette index (into `entries`) at the specified index.
|
||||
fn get_palette_index(&self, index: usize) -> usize { |
||||
if self.bits > 0 { |
||||
let bit_range = self.bit_range(index); |
||||
self.data[bit_range].load() |
||||
} else { |
||||
0 |
||||
} |
||||
} |
||||
|
||||
fn set_palette_index(&mut self, index: usize, value: usize) { |
||||
let bit_range = self.bit_range(index); |
||||
self.data[bit_range].store(value) |
||||
} |
||||
|
||||
fn find_existing_entry(&self, value: T) -> Option<&PaletteEntry<T>> { |
||||
self.entries.iter().find(|e| e.value == Some(value)) |
||||
} |
||||
|
||||
fn find_or_create_entry(&mut self, value: T) -> (usize, &mut PaletteEntry<T>) { |
||||
match self.find_entry_index(value) { |
||||
// The palette entry already exists, so just return it.
|
||||
FindResult::Existing(index) => { |
||||
let entry = &mut self.entries[index]; |
||||
(index, entry) |
||||
} |
||||
// The entry didn't exist, but we found one we can reuse.
|
||||
FindResult::Uninitialized(index) | FindResult::Unused(index) => { |
||||
let entry = &mut self.entries[index]; |
||||
entry.value = Some(value); |
||||
(index, entry) |
||||
} |
||||
// Everything in the palette is already in use. RESIZE!
|
||||
FindResult::None => { |
||||
let index = self.entries.len(); |
||||
self.resize_palette(self.bits + 1); |
||||
let entry = &mut self.entries[index]; |
||||
entry.value = Some(value); |
||||
(index, entry) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn find_entry_index(&self, value: T) -> FindResult { |
||||
let mut result = FindResult::None; |
||||
for (index, entry) in self.entries.iter().enumerate() { |
||||
match entry { |
||||
// If the specified value is found in the palette, return it immediately.
|
||||
PaletteEntry { value: v, .. } if *v == Some(value) => { |
||||
return FindResult::Existing(index) |
||||
} |
||||
// Store the first uninitialized entry in case we don't find the specified value.
|
||||
PaletteEntry { value: None, .. } => match result { |
||||
FindResult::Existing(_) => unreachable!(), |
||||
FindResult::Uninitialized(_) => {} |
||||
_ => result = FindResult::Uninitialized(index), |
||||
}, |
||||
// Otherwise, pick the first initialized entry that's currently used.
|
||||
PaletteEntry { used: 0, .. } => match result { |
||||
FindResult::Existing(_) => unreachable!(), |
||||
FindResult::Uninitialized(_) | FindResult::Unused(_) => {} |
||||
FindResult::None => result = FindResult::Unused(index), |
||||
}, |
||||
_ => {} |
||||
} |
||||
} |
||||
result |
||||
} |
||||
|
||||
fn resize_palette(&mut self, new_bits: usize) { |
||||
if new_bits == self.bits { |
||||
return; |
||||
} |
||||
|
||||
// TODO: Revisit this to see if we can optimize it.
|
||||
// Create a new data BitVec and copy the bits from the old one over.
|
||||
let mut new_data = BitVec::with_capacity(self.len * new_bits); |
||||
if new_bits == 0 { |
||||
// Nothing to do if we have nothing to copy to.
|
||||
} else if self.bits == 0 { |
||||
// No data to copy from, so just fill with zeroes.
|
||||
new_data.resize(self.len * new_bits, false); |
||||
} else if new_bits > self.bits { |
||||
let additional_bits = new_bits - self.bits; |
||||
for chunk in self.data.chunks_exact(self.bits) { |
||||
new_data.extend(chunk); |
||||
for _ in 0..additional_bits { |
||||
new_data.push(false); |
||||
} |
||||
} |
||||
} else { |
||||
for chunk in self.data.chunks_exact(self.bits) { |
||||
new_data.extend(&chunk[0..new_bits]); |
||||
} |
||||
} |
||||
self.data = new_data; |
||||
self.bits = new_bits; |
||||
|
||||
// Resize the palette itself.
|
||||
let num_entries = 2usize.pow(new_bits as u32); |
||||
self.entries.resize_with(num_entries, Default::default); |
||||
} |
||||
} |
||||
|
||||
enum FindResult { |
||||
Existing(usize), |
||||
Uninitialized(usize), |
||||
Unused(usize), |
||||
None, |
||||
} |
||||
|
||||
// #[derive(Default)]
|
||||
struct PaletteEntry<T> { |
||||
value: Option<T>, |
||||
used: usize, |
||||
} |
||||
|
||||
// NOTE: For some weird reason, deriving `Default` doesn't quite do what we want
|
||||
// when later calling `Vec::resize_with`, so we're manually implementing
|
||||
// the trait instead. [insert confused noises here]
|
||||
impl<T> Default for PaletteEntry<T> { |
||||
fn default() -> Self { |
||||
Self { |
||||
value: None, |
||||
used: 0, |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,91 @@ |
||||
use bevy::prelude::*; |
||||
use rand::prelude::*; |
||||
|
||||
use crate::{ |
||||
bloxel::{prelude::*, storage::ChunkedOctree}, |
||||
camera_controller::ControlledCamera, |
||||
TerrainBlocks, |
||||
}; |
||||
|
||||
pub fn create_chunks_around_camera( |
||||
mut commands: Commands, |
||||
mut octree: Local<GeneratedChunks>, |
||||
camera: Single<&Transform, With<ControlledCamera>>, |
||||
chunk_map: Single<Entity, With<ChunkMap>>, |
||||
) { |
||||
let block_pos = camera.translation.as_ivec3().into(); |
||||
let (from, _) = ChunkPos::from_block_pos(block_pos); |
||||
let mut iterator = octree.find([&from], |node, generated| { |
||||
if generated != Generated::All { |
||||
let distance = node.region().distance_to_squared(from); |
||||
if distance <= 6 * 6 { |
||||
return Some(-distance); |
||||
} |
||||
} |
||||
None |
||||
}); |
||||
|
||||
if let Some((chunk_pos, _, _)) = iterator.next() { |
||||
commands.entity(*chunk_map).with_child((Chunk, chunk_pos)); |
||||
octree.update(chunk_pos, |_node, children, parent| { |
||||
let children = children.map(|a| a.as_slice()).unwrap_or_default(); |
||||
*parent = if children.iter().all(|c| *c == Generated::All) { |
||||
Generated::All |
||||
} else { |
||||
Generated::Some |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
pub fn generate_terrain( |
||||
mut commands: Commands, |
||||
terrain_blocks: Option<Res<TerrainBlocks>>, |
||||
chunks_without_data: Query<(Entity, &ChunkPos), (With<Chunk>, Without<ChunkData>)>, |
||||
) { |
||||
let Some(terrain) = terrain_blocks else { |
||||
return; |
||||
}; |
||||
|
||||
let mut rng = rand::rng(); |
||||
let random_blocks = [terrain.grass, terrain.dirt, terrain.rock, terrain.sand]; |
||||
|
||||
for (entity, chunk_pos) in chunks_without_data.iter() { |
||||
let mut data = ChunkData::new(terrain.air); |
||||
let size = data.size().as_ivec3(); |
||||
for z in 0..size.z { |
||||
for y in 0..size.y { |
||||
for x in 0..size.x { |
||||
let relative = IVec3::new(x, y, z); |
||||
let block_pos = chunk_pos.to_block_pos(relative); |
||||
let chance = (-block_pos.y as f64 / 32.).clamp(0.001, 1.); |
||||
if rng.random_bool(chance) { |
||||
let block = *random_blocks.choose(&mut rng).unwrap(); |
||||
data.set(relative, block); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
commands.entity(entity).insert(data); |
||||
} |
||||
} |
||||
|
||||
#[derive(Deref, DerefMut)] |
||||
pub struct GeneratedChunks { |
||||
octree: ChunkedOctree<Generated>, |
||||
} |
||||
|
||||
impl Default for GeneratedChunks { |
||||
fn default() -> Self { |
||||
let octree = ChunkedOctree::new(5); |
||||
Self { octree } |
||||
} |
||||
} |
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] |
||||
pub enum Generated { |
||||
#[default] |
||||
None, |
||||
Some, |
||||
All, |
||||
} |
@ -0,0 +1,110 @@ |
||||
use bevy::{input::mouse::AccumulatedMouseMotion, prelude::*, window::CursorGrabMode}; |
||||
|
||||
const MOVEMENT_SPEED: f32 = 0.15; |
||||
const MOUSE_SENSITIVITY: Vec2 = Vec2::new(0.002, 0.002); |
||||
|
||||
pub struct CameraControllerPlugin; |
||||
|
||||
/// Marks an entity as being the camera controlled by this plugin.
|
||||
#[derive(Component)] |
||||
pub struct ControlledCamera; |
||||
|
||||
impl Plugin for CameraControllerPlugin { |
||||
fn build(&self, app: &mut App) { |
||||
app.add_systems( |
||||
Update, |
||||
( |
||||
capture_mouse, |
||||
camera_mouse_rotation, |
||||
camera_keyboard_translation, |
||||
) |
||||
.chain(), |
||||
); |
||||
} |
||||
} |
||||
|
||||
fn capture_mouse( |
||||
mut window: Single<&mut Window>, |
||||
mouse: Res<ButtonInput<MouseButton>>, |
||||
key: Res<ButtonInput<KeyCode>>, |
||||
) { |
||||
if mouse.just_pressed(MouseButton::Left) { |
||||
window.cursor_options.visible = false; |
||||
window.cursor_options.grab_mode = CursorGrabMode::Locked; |
||||
} |
||||
|
||||
if key.just_pressed(KeyCode::Escape) { |
||||
window.cursor_options.visible = true; |
||||
window.cursor_options.grab_mode = CursorGrabMode::None; |
||||
} |
||||
} |
||||
|
||||
fn camera_mouse_rotation( |
||||
window: Single<&Window>, |
||||
mut camera: Query<&mut Transform, With<ControlledCamera>>, |
||||
mouse_motion: Res<AccumulatedMouseMotion>, |
||||
) { |
||||
// Mouse must be grabbed by the window for this system to run.
|
||||
if window.cursor_options.grab_mode != CursorGrabMode::Locked { |
||||
return; |
||||
} |
||||
|
||||
// We also need to have exactly one camera with the `ControlledCamera` component.
|
||||
let Ok(mut transform) = camera.get_single_mut() else { |
||||
return; |
||||
}; |
||||
|
||||
let delta = mouse_motion.delta; |
||||
if delta != Vec2::ZERO { |
||||
let delta_yaw = -delta.x * MOUSE_SENSITIVITY.x; |
||||
let delta_pitch = -delta.y * MOUSE_SENSITIVITY.y; |
||||
|
||||
let rot_yaw = Quat::from_axis_angle(Vec3::Y, delta_yaw); |
||||
let rot_pitch = Quat::from_axis_angle(Vec3::X, delta_pitch); |
||||
transform.rotation = rot_yaw * transform.rotation * rot_pitch; |
||||
} |
||||
} |
||||
|
||||
fn camera_keyboard_translation( |
||||
window: Single<&Window>, |
||||
mut camera: Query<&mut Transform, With<ControlledCamera>>, |
||||
key: Res<ButtonInput<KeyCode>>, |
||||
) { |
||||
// Mouse must be grabbed by the window for this system to run.
|
||||
if window.cursor_options.grab_mode != CursorGrabMode::Locked { |
||||
return; |
||||
} |
||||
|
||||
// We also need to have exactly one camera with the `ControlledCamera` component.
|
||||
let Ok(mut transform) = camera.get_single_mut() else { |
||||
return; |
||||
}; |
||||
|
||||
let mut input = Vec2::ZERO; |
||||
if key.pressed(KeyCode::KeyD) { |
||||
input.x += 1.; |
||||
} |
||||
if key.pressed(KeyCode::KeyA) { |
||||
input.x -= 1.; |
||||
} |
||||
if key.pressed(KeyCode::KeyW) { |
||||
input.y += 1.; |
||||
} |
||||
if key.pressed(KeyCode::KeyS) { |
||||
input.y -= 1.; |
||||
} |
||||
|
||||
let mut movement = input * MOVEMENT_SPEED; |
||||
if key.pressed(KeyCode::ShiftLeft) { |
||||
movement *= 4.; |
||||
} |
||||
|
||||
if movement.x != 0. { |
||||
let translation = transform.right() * movement.x; |
||||
transform.translation += translation; |
||||
} |
||||
if movement.y != 0. { |
||||
let translation = transform.forward() * movement.y; |
||||
transform.translation += translation; |
||||
} |
||||
} |
@ -0,0 +1,145 @@ |
||||
use bevy::{asset::LoadedFolder, prelude::*}; |
||||
|
||||
mod bloxel; |
||||
mod camera_controller; |
||||
|
||||
use bloxel::block::BlockTexture; |
||||
use bloxel::prelude::*; |
||||
use camera_controller::{CameraControllerPlugin, ControlledCamera}; |
||||
|
||||
#[derive(Resource)] |
||||
pub struct TerrainTextures(Handle<LoadedFolder>); |
||||
|
||||
#[derive(Resource)] |
||||
pub struct TerrainAtlas { |
||||
pub layout: Handle<TextureAtlasLayout>, |
||||
pub sources: TextureAtlasSources, |
||||
pub texture: Handle<Image>, |
||||
} |
||||
|
||||
#[derive(Resource)] |
||||
pub struct TerrainMaterial(Handle<StandardMaterial>); |
||||
|
||||
#[derive(Resource)] |
||||
pub struct TerrainBlocks { |
||||
air: Entity, |
||||
grass: Entity, |
||||
dirt: Entity, |
||||
rock: Entity, |
||||
sand: Entity, |
||||
} |
||||
|
||||
fn main() { |
||||
App::new() |
||||
.add_plugins(( |
||||
DefaultPlugins.set(ImagePlugin::default_nearest()), |
||||
CameraControllerPlugin, |
||||
BloxelPlugin, |
||||
)) |
||||
.add_systems( |
||||
Startup, |
||||
( |
||||
setup_sunlight, |
||||
setup_camera, |
||||
setup_terrain_blocks, |
||||
load_terrain_textures, |
||||
generate_sample_chunks, |
||||
), |
||||
) |
||||
.add_systems(Update, check_terrain_textures) |
||||
.run(); |
||||
} |
||||
|
||||
fn setup_terrain_blocks(mut commands: Commands, asset_server: Res<AssetServer>) { |
||||
let air = commands.spawn((Block, Name::new("air"))).id(); |
||||
|
||||
let mut block = |name: &'static str, texture: &str| -> Entity { |
||||
let path = format!("terrain/{texture}.png"); |
||||
let texture = BlockTexture(asset_server.load(path)); |
||||
commands.spawn((Block, Name::new(name), texture)).id() |
||||
}; |
||||
|
||||
let blocks = TerrainBlocks { |
||||
air, |
||||
grass: block("grass", "grass_top"), |
||||
dirt: block("dirt", "dirt"), |
||||
rock: block("rock", "rock"), |
||||
sand: block("sand", "sand"), |
||||
}; |
||||
|
||||
commands.insert_resource(blocks); |
||||
} |
||||
|
||||
fn load_terrain_textures(mut commands: Commands, asset_server: Res<AssetServer>) { |
||||
commands.insert_resource(TerrainTextures(asset_server.load_folder("terrain"))); |
||||
} |
||||
|
||||
/// Waits for all textures in `assets/terrain/` to be loaded, then builds an atlas
|
||||
/// from those along with the [`TerrainMaterial`] and [`TerrainAtlas`] resources.
|
||||
fn check_terrain_textures( |
||||
mut commands: Commands, |
||||
mut events: EventReader<AssetEvent<LoadedFolder>>, |
||||
terrain_textures: Res<TerrainTextures>, |
||||
loaded_folders: Res<Assets<LoadedFolder>>, |
||||
mut textures: ResMut<Assets<Image>>, |
||||
mut materials: ResMut<Assets<StandardMaterial>>, |
||||
mut atlas_layouts: ResMut<Assets<TextureAtlasLayout>>, |
||||
) { |
||||
for event in events.read() { |
||||
if event.is_loaded_with_dependencies(&terrain_textures.0) { |
||||
let mut atlas_builder = TextureAtlasBuilder::default(); |
||||
|
||||
let folder = loaded_folders.get(&terrain_textures.0).unwrap(); |
||||
for handle in folder.handles.iter() { |
||||
let id = handle.id().typed_unchecked::<Image>(); |
||||
let Some(texture) = textures.get(id) else { |
||||
warn!( |
||||
"{:?} did not resolve to an `Image` asset.", |
||||
handle.path().unwrap() |
||||
); |
||||
continue; |
||||
}; |
||||
atlas_builder.add_texture(Some(id), texture); |
||||
} |
||||
|
||||
let (layout, sources, texture) = atlas_builder.build().unwrap(); |
||||
let layout = atlas_layouts.add(layout); |
||||
let texture = textures.add(texture); |
||||
let material = materials.add(texture.clone()); |
||||
|
||||
commands.insert_resource(TerrainMaterial(material)); |
||||
|
||||
commands.insert_resource(TerrainAtlas { |
||||
layout, |
||||
sources, |
||||
texture, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn generate_sample_chunks(mut commands: Commands) { |
||||
commands.spawn(ChunkMap::default()).with_children(|map| { |
||||
// map.spawn((Chunk, ChunkPos::new(0, 0, 0)));
|
||||
// map.spawn((Chunk, ChunkPos::new(1, 0, 0)));
|
||||
// map.spawn((Chunk, ChunkPos::new(-1, 0, 0)));
|
||||
}); |
||||
} |
||||
|
||||
fn setup_sunlight(mut commands: Commands) { |
||||
commands.spawn(( |
||||
DirectionalLight { |
||||
shadows_enabled: true, |
||||
..default() |
||||
}, |
||||
Transform::from_xyz(4., 8., 4.).looking_at(Vec3::ZERO, Vec3::Y), |
||||
)); |
||||
} |
||||
|
||||
fn setup_camera(mut commands: Commands) { |
||||
commands.spawn(( |
||||
ControlledCamera, |
||||
Camera3d::default(), |
||||
Transform::from_xyz(-24., 16., 16.).looking_at((0., 0., 0.).into(), Vec3::Y), |
||||
)); |
||||
} |