Initial commit (for lizzie <3)

main
copygirl 1 week ago
commit 625348981d
  1. 2
      .gitignore
  2. 48
      .vscode/launch.json
  3. 5125
      Cargo.lock
  4. 19
      Cargo.toml
  5. BIN
      assets/terrain/clay.png
  6. BIN
      assets/terrain/clay_bricks.png
  7. BIN
      assets/terrain/dirt.png
  8. BIN
      assets/terrain/grass_side.png
  9. BIN
      assets/terrain/grass_tall.png
  10. BIN
      assets/terrain/grass_top.png
  11. BIN
      assets/terrain/gravel.png
  12. BIN
      assets/terrain/leaves.png
  13. BIN
      assets/terrain/planks.png
  14. BIN
      assets/terrain/rock.png
  15. BIN
      assets/terrain/sand.png
  16. BIN
      assets/terrain/stone_bricks.png
  17. BIN
      assets/terrain/trunk.png
  18. BIN
      assets/terrain/trunk_inner.png
  19. 7
      src/bloxel/block.rs
  20. 26
      src/bloxel/chunk.rs
  21. 5
      src/bloxel/generic_math/mod.rs
  22. 212
      src/bloxel/generic_math/pos.rs
  23. 175
      src/bloxel/generic_math/region.rs
  24. 129
      src/bloxel/math/block_pos.rs
  25. 87
      src/bloxel/math/block_region.rs
  26. 158
      src/bloxel/math/chunk_pos.rs
  27. 94
      src/bloxel/math/chunk_region.rs
  28. 11
      src/bloxel/math/mod.rs
  29. 211
      src/bloxel/math/z_order_index.rs
  30. 127
      src/bloxel/mesh/mesh_generator.rs
  31. 46
      src/bloxel/mesh/mod.rs
  32. 78
      src/bloxel/mod.rs
  33. 74
      src/bloxel/storage/bloxel_array.rs
  34. 28
      src/bloxel/storage/bloxel_view.rs
  35. 375
      src/bloxel/storage/chunked_octree.rs
  36. 10
      src/bloxel/storage/mod.rs
  37. 63
      src/bloxel/storage/palette_bloxel_storage.rs
  38. 213
      src/bloxel/storage/palette_storage.rs
  39. 91
      src/bloxel/worldgen/mod.rs
  40. 110
      src/camera_controller.rs
  41. 145
      src/main.rs

2
.gitignore vendored

@ -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}",
}
]
}

5125
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

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),
));
}
Loading…
Cancel
Save