@ -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), |
||||||
|
)); |
||||||
|
} |