mirror of
https://github.com/eliasstepanik/voxel-simulation.git
synced 2026-01-27 13:38:39 +00:00
Add basic voxel texture atlas support
This commit is contained in:
parent
0b07669345
commit
1b4f070015
@ -9,6 +9,7 @@ use crate::plugins::environment::systems::voxels::queue_systems::{
|
|||||||
enqueue_visible_chunks, process_chunk_queue,
|
enqueue_visible_chunks, process_chunk_queue,
|
||||||
};
|
};
|
||||||
use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks;
|
use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks;
|
||||||
|
use crate::plugins::environment::systems::voxels::atlas::{VoxelTextureAtlas};
|
||||||
use crate::plugins::environment::systems::voxels::structure::{
|
use crate::plugins::environment::systems::voxels::structure::{
|
||||||
ChunkBudget, ChunkCullingCfg, ChunkQueue, MeshBufferPool, PrevCameraChunk, SparseVoxelOctree,
|
ChunkBudget, ChunkCullingCfg, ChunkQueue, MeshBufferPool, PrevCameraChunk, SparseVoxelOctree,
|
||||||
SpawnedChunks,
|
SpawnedChunks,
|
||||||
@ -22,6 +23,7 @@ impl Plugin for EnvironmentPlugin {
|
|||||||
app.add_systems(
|
app.add_systems(
|
||||||
Startup,
|
Startup,
|
||||||
(
|
(
|
||||||
|
setup_texture_atlas,
|
||||||
crate::plugins::environment::systems::camera_system::setup,
|
crate::plugins::environment::systems::camera_system::setup,
|
||||||
crate::plugins::environment::systems::environment_system::setup
|
crate::plugins::environment::systems::environment_system::setup
|
||||||
.after(crate::plugins::environment::systems::camera_system::setup),
|
.after(crate::plugins::environment::systems::camera_system::setup),
|
||||||
@ -89,3 +91,8 @@ fn should_draw_grid(octree_query: Query<&SparseVoxelOctree>) -> bool {
|
|||||||
};
|
};
|
||||||
octree.show_world_grid
|
octree.show_world_grid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_texture_atlas(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
|
||||||
|
let atlas = VoxelTextureAtlas::generate(&mut images);
|
||||||
|
commands.insert_resource(atlas);
|
||||||
|
}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ pub fn generate_voxel_sphere_parallel(
|
|||||||
center.y + iy as f32 * step,
|
center.y + iy as f32 * step,
|
||||||
center.z + iz as f32 * step,
|
center.z + iz as f32 * step,
|
||||||
);
|
);
|
||||||
(pos, Voxel { color })
|
(pos, Voxel { color, textures: [0; 6] })
|
||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -133,6 +133,7 @@ fn generate_voxel_sphere(
|
|||||||
// Insert the voxel
|
// Insert the voxel
|
||||||
let voxel = Voxel {
|
let voxel = Voxel {
|
||||||
color: voxel_color,
|
color: voxel_color,
|
||||||
|
textures: [0; 6],
|
||||||
};
|
};
|
||||||
octree.insert(position, voxel);
|
octree.insert(position, voxel);
|
||||||
}
|
}
|
||||||
@ -174,6 +175,7 @@ fn generate_voxel_rect(
|
|||||||
// Insert the voxel
|
// Insert the voxel
|
||||||
let voxel = Voxel {
|
let voxel = Voxel {
|
||||||
color: voxel_color,
|
color: voxel_color,
|
||||||
|
textures: [0; 6],
|
||||||
};
|
};
|
||||||
octree.insert(position, voxel);
|
octree.insert(position, voxel);
|
||||||
}
|
}
|
||||||
@ -209,6 +211,7 @@ fn generate_large_plane(
|
|||||||
// Insert the voxel
|
// Insert the voxel
|
||||||
let voxel = Voxel {
|
let voxel = Voxel {
|
||||||
color,
|
color,
|
||||||
|
textures: [0; 6],
|
||||||
};
|
};
|
||||||
octree.insert(position, voxel);
|
octree.insert(position, voxel);
|
||||||
}
|
}
|
||||||
@ -251,7 +254,7 @@ pub fn generate_solid_plane_with_noise(
|
|||||||
z * step,
|
z * step,
|
||||||
);
|
);
|
||||||
|
|
||||||
let voxel = Voxel { color };
|
let voxel = Voxel { color, textures: [0; 6] };
|
||||||
octree.insert(position, voxel);
|
octree.insert(position, voxel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
client/src/plugins/environment/systems/voxels/atlas.rs
Normal file
66
client/src/plugins/environment/systems/voxels/atlas.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::render::texture::{Extent3d, TextureDimension, TextureFormat};
|
||||||
|
|
||||||
|
/// Configuration and handle for the voxel texture atlas.
|
||||||
|
#[derive(Resource, Clone)]
|
||||||
|
pub struct VoxelTextureAtlas {
|
||||||
|
pub handle: Handle<Image>,
|
||||||
|
pub columns: usize,
|
||||||
|
pub rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VoxelTextureAtlas {
|
||||||
|
/// Create a simple procedural atlas with solid colors.
|
||||||
|
pub fn generate(images: &mut Assets<Image>) -> Self {
|
||||||
|
let tile_size = 16u32;
|
||||||
|
let columns = 2;
|
||||||
|
let rows = 3;
|
||||||
|
let width = tile_size * columns as u32;
|
||||||
|
let height = tile_size * rows as u32;
|
||||||
|
let mut data = vec![0u8; (width * height * 4) as usize];
|
||||||
|
let colors = [
|
||||||
|
[255, 0, 0, 255], // red
|
||||||
|
[0, 255, 0, 255], // green
|
||||||
|
[0, 0, 255, 255], // blue
|
||||||
|
[255, 255, 0, 255], // yellow
|
||||||
|
[255, 0, 255, 255], // magenta
|
||||||
|
[0, 255, 255, 255], // cyan
|
||||||
|
];
|
||||||
|
for (i, col) in colors.iter().enumerate() {
|
||||||
|
let cx = (i % columns) as u32 * tile_size;
|
||||||
|
let cy = (i / columns) as u32 * tile_size;
|
||||||
|
for y in 0..tile_size {
|
||||||
|
for x in 0..tile_size {
|
||||||
|
let idx = (((cy + y) * width + (cx + x)) * 4) as usize;
|
||||||
|
data[idx..idx + 4].copy_from_slice(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let image = Image::new_fill(
|
||||||
|
Extent3d { width, height, depth_or_array_layers: 1 },
|
||||||
|
TextureDimension::D2,
|
||||||
|
&data,
|
||||||
|
TextureFormat::Rgba8UnormSrgb,
|
||||||
|
);
|
||||||
|
let handle = images.add(image);
|
||||||
|
Self { handle, columns, rows }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute UV coordinates for the given atlas index.
|
||||||
|
pub fn uv_rect(&self, index: usize) -> [[f32; 2]; 4] {
|
||||||
|
let col = index % self.columns;
|
||||||
|
let row = index / self.columns;
|
||||||
|
let cols = self.columns as f32;
|
||||||
|
let rows = self.rows as f32;
|
||||||
|
let u0 = col as f32 / cols;
|
||||||
|
let v0 = row as f32 / rows;
|
||||||
|
let u1 = (col + 1) as f32 / cols;
|
||||||
|
let v1 = (row + 1) as f32 / rows;
|
||||||
|
[
|
||||||
|
[u0, v1],
|
||||||
|
[u1, v1],
|
||||||
|
[u1, v0],
|
||||||
|
[u0, v0],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::plugins::environment::systems::voxels::structure::*;
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::atlas::VoxelTextureAtlas;
|
||||||
use bevy::asset::RenderAssetUsages;
|
use bevy::asset::RenderAssetUsages;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::render::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
|
use bevy::render::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
|
||||||
@ -304,6 +305,7 @@ pub(crate) fn mesh_chunk(
|
|||||||
step: f32,
|
step: f32,
|
||||||
tree: &SparseVoxelOctree,
|
tree: &SparseVoxelOctree,
|
||||||
pool: &mut MeshBufferPool,
|
pool: &mut MeshBufferPool,
|
||||||
|
atlas: &VoxelTextureAtlas,
|
||||||
) -> Option<Mesh> {
|
) -> Option<Mesh> {
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
// Helpers
|
// Helpers
|
||||||
@ -313,15 +315,15 @@ pub(crate) fn mesh_chunk(
|
|||||||
const MASK_LEN: usize = N * N;
|
const MASK_LEN: usize = N * N;
|
||||||
|
|
||||||
// Safe voxel query that falls back to the octree for out‑of‑chunk requests.
|
// Safe voxel query that falls back to the octree for out‑of‑chunk requests.
|
||||||
let filled = |x: i32, y: i32, z: i32| -> bool {
|
let get_voxel = |x: i32, y: i32, z: i32| -> Option<Voxel> {
|
||||||
if (0..CHUNK_SIZE).contains(&x)
|
if (0..CHUNK_SIZE).contains(&x)
|
||||||
&& (0..CHUNK_SIZE).contains(&y)
|
&& (0..CHUNK_SIZE).contains(&y)
|
||||||
&& (0..CHUNK_SIZE).contains(&z)
|
&& (0..CHUNK_SIZE).contains(&z)
|
||||||
{
|
{
|
||||||
buffer[x as usize][y as usize][z as usize].is_some()
|
buffer[x as usize][y as usize][z as usize]
|
||||||
} else {
|
} else {
|
||||||
let world = origin + Vec3::new(x as f32 * step, y as f32 * step, z as f32 * step);
|
let world = origin + Vec3::new(x as f32 * step, y as f32 * step, z as f32 * step);
|
||||||
tree.get_voxel_at_world_coords(world).is_some()
|
tree.get_voxel_at_world_coords(world).copied()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -341,7 +343,7 @@ pub(crate) fn mesh_chunk(
|
|||||||
let uvs = &mut pool.uvs;
|
let uvs = &mut pool.uvs;
|
||||||
let indices = &mut pool.indices;
|
let indices = &mut pool.indices;
|
||||||
|
|
||||||
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| {
|
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3, tex_id: usize| {
|
||||||
let i0 = positions.len() as u32;
|
let i0 = positions.len() as u32;
|
||||||
positions.extend_from_slice(&[
|
positions.extend_from_slice(&[
|
||||||
(base).into(),
|
(base).into(),
|
||||||
@ -350,7 +352,8 @@ pub(crate) fn mesh_chunk(
|
|||||||
(base + v * size.y).into(),
|
(base + v * size.y).into(),
|
||||||
]);
|
]);
|
||||||
normals.extend_from_slice(&[[n.x, n.y, n.z]; 4]);
|
normals.extend_from_slice(&[[n.x, n.y, n.z]; 4]);
|
||||||
uvs.extend_from_slice(&[[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]]);
|
let uv_rect = atlas.uv_rect(tex_id);
|
||||||
|
uvs.extend_from_slice(&uv_rect);
|
||||||
|
|
||||||
if n.x + n.y + n.z >= 0.0 {
|
if n.x + n.y + n.z >= 0.0 {
|
||||||
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
|
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
|
||||||
@ -382,7 +385,7 @@ pub(crate) fn mesh_chunk(
|
|||||||
for slice in 0..=N {
|
for slice in 0..=N {
|
||||||
// Build the face mask for this slice using a fixed-size array to
|
// Build the face mask for this slice using a fixed-size array to
|
||||||
// avoid heap allocations.
|
// avoid heap allocations.
|
||||||
let mut mask = [false; MASK_LEN];
|
let mut mask = [None::<usize>; MASK_LEN];
|
||||||
let mut visited = [false; MASK_LEN];
|
let mut visited = [false; MASK_LEN];
|
||||||
let idx = |u: usize, v: usize| -> usize { u * N + v };
|
let idx = |u: usize, v: usize| -> usize { u * N + v };
|
||||||
|
|
||||||
@ -400,25 +403,38 @@ pub(crate) fn mesh_chunk(
|
|||||||
neighbor[u_axis] = u as i32;
|
neighbor[u_axis] = u as i32;
|
||||||
neighbor[v_axis] = v as i32;
|
neighbor[v_axis] = v as i32;
|
||||||
|
|
||||||
if filled(cell[0], cell[1], cell[2])
|
if let Some(vox) = get_voxel(cell[0], cell[1], cell[2]) {
|
||||||
&& !filled(neighbor[0], neighbor[1], neighbor[2])
|
if get_voxel(neighbor[0], neighbor[1], neighbor[2]).is_none() {
|
||||||
{
|
let face_idx = match (axis, dir) {
|
||||||
mask[idx(u, v)] = true;
|
(0, -1) => 0,
|
||||||
|
(0, 1) => 1,
|
||||||
|
(1, -1) => 2,
|
||||||
|
(1, 1) => 3,
|
||||||
|
(2, -1) => 4,
|
||||||
|
(2, 1) => 5,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
mask[idx(u, v)] = Some(vox.textures[face_idx]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Greedy merge the mask into maximal rectangles.
|
// Greedy merge the mask into maximal rectangles.
|
||||||
for u0 in 0..N {
|
for u0 in 0..N {
|
||||||
for v0 in 0..N {
|
for v0 in 0..N {
|
||||||
if !mask[idx(u0, v0)] || visited[idx(u0, v0)] {
|
if visited[idx(u0, v0)] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(tex_id) = mask[idx(u0, v0)] else { continue };
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the rectangle width.
|
// Determine the rectangle width.
|
||||||
let mut width = 1;
|
let mut width = 1;
|
||||||
while u0 + width < N
|
while u0 + width < N
|
||||||
&& mask[idx(u0 + width, v0)]
|
&& mask[idx(u0 + width, v0)] == Some(tex_id)
|
||||||
&& !visited[idx(u0 + width, v0)]
|
&& !visited[idx(u0 + width, v0)]
|
||||||
{
|
{
|
||||||
width += 1;
|
width += 1;
|
||||||
@ -428,7 +444,7 @@ pub(crate) fn mesh_chunk(
|
|||||||
let mut height = 1;
|
let mut height = 1;
|
||||||
'h: while v0 + height < N {
|
'h: while v0 + height < N {
|
||||||
for du in 0..width {
|
for du in 0..width {
|
||||||
if !mask[idx(u0 + du, v0 + height)]
|
if mask[idx(u0 + du, v0 + height)] != Some(tex_id)
|
||||||
|| visited[idx(u0 + du, v0 + height)]
|
|| visited[idx(u0 + du, v0 + height)]
|
||||||
{
|
{
|
||||||
break 'h;
|
break 'h;
|
||||||
@ -466,7 +482,7 @@ pub(crate) fn mesh_chunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let size = Vec2::new(width as f32 * step, height as f32 * step);
|
let size = Vec2::new(width as f32 * step, height as f32 * step);
|
||||||
push_quad(base, size, face_normal, u_vec, v_vec);
|
push_quad(base, size, face_normal, u_vec, v_vec, tex_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,3 +10,4 @@ mod meshing;
|
|||||||
pub mod meshing_gpu;
|
pub mod meshing_gpu;
|
||||||
pub mod queue_systems;
|
pub mod queue_systems;
|
||||||
pub mod render_chunks;
|
pub mod render_chunks;
|
||||||
|
pub mod atlas;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
|
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
|
||||||
use crate::plugins::environment::systems::voxels::structure::*;
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::atlas::VoxelTextureAtlas;
|
||||||
use bevy::pbr::wireframe::Wireframe;
|
use bevy::pbr::wireframe::Wireframe;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::render::mesh::Mesh;
|
use bevy::render::mesh::Mesh;
|
||||||
@ -25,6 +26,7 @@ pub fn rebuild_dirty_chunks(
|
|||||||
mut spawned: ResMut<SpawnedChunks>,
|
mut spawned: ResMut<SpawnedChunks>,
|
||||||
mut pool: ResMut<MeshBufferPool>,
|
mut pool: ResMut<MeshBufferPool>,
|
||||||
root: Res<RootGrid>,
|
root: Res<RootGrid>,
|
||||||
|
atlas: Res<VoxelTextureAtlas>,
|
||||||
) {
|
) {
|
||||||
// map ChunkKey → (entity, mesh-handle, material-handle)
|
// map ChunkKey → (entity, mesh-handle, material-handle)
|
||||||
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
|
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
|
||||||
@ -87,7 +89,7 @@ pub fn rebuild_dirty_chunks(
|
|||||||
for (key, buf, origin, step, lod) in bufs {
|
for (key, buf, origin, step, lod) in bufs {
|
||||||
if let Some((ent, mesh_h, _mat_h, _)) = existing.get(&key).cloned() {
|
if let Some((ent, mesh_h, _mat_h, _)) = existing.get(&key).cloned() {
|
||||||
// update mesh in-place; keeps old asset id
|
// update mesh in-place; keeps old asset id
|
||||||
match mesh_chunk(&buf, origin, step, &tree, &mut pool) {
|
match mesh_chunk(&buf, origin, step, &tree, &mut pool, &atlas) {
|
||||||
Some(new_mesh) => {
|
Some(new_mesh) => {
|
||||||
if let Some(mesh) = meshes.get_mut(&mesh_h) {
|
if let Some(mesh) = meshes.get_mut(&mesh_h) {
|
||||||
*mesh = new_mesh;
|
*mesh = new_mesh;
|
||||||
@ -100,10 +102,13 @@ pub fn rebuild_dirty_chunks(
|
|||||||
spawned.0.remove(&key);
|
spawned.0.remove(&key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(mesh) = mesh_chunk(&buf, origin, step, &tree, &mut pool) {
|
} else if let Some(mesh) = mesh_chunk(&buf, origin, step, &tree, &mut pool, &atlas) {
|
||||||
// spawn brand-new chunk only if mesh has faces
|
// spawn brand-new chunk only if mesh has faces
|
||||||
let mesh_h = meshes.add(mesh);
|
let mesh_h = meshes.add(mesh);
|
||||||
let mat_h = materials.add(StandardMaterial::default());
|
let mat_h = materials.add(StandardMaterial {
|
||||||
|
base_color_texture: Some(atlas.handle.clone()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
commands.entity(root.0).with_children(|p| {
|
commands.entity(root.0).with_children(|p| {
|
||||||
let e = p
|
let e = p
|
||||||
|
|||||||
@ -20,13 +20,26 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a single voxel with a color.
|
/// Represents a single voxel with a color.
|
||||||
#[derive(Debug, Clone, Copy, Component, PartialEq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Component, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Voxel {
|
pub struct Voxel {
|
||||||
#[serde(
|
#[serde(
|
||||||
serialize_with = "serialize_color",
|
serialize_with = "serialize_color",
|
||||||
deserialize_with = "deserialize_color"
|
deserialize_with = "deserialize_color"
|
||||||
)]
|
)]
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
|
/// Indexes into the texture atlas for the six faces in the order
|
||||||
|
/// left, right, bottom, top, back, front.
|
||||||
|
#[serde(default)]
|
||||||
|
pub textures: [usize; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Voxel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
color: Color::WHITE,
|
||||||
|
textures: [0; 6],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@ -77,8 +90,8 @@ impl OctreeNode {
|
|||||||
|
|
||||||
impl Voxel {
|
impl Voxel {
|
||||||
/// Creates a new empty octree node.
|
/// Creates a new empty octree node.
|
||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color, textures: [usize; 6]) -> Self {
|
||||||
Self { color }
|
Self { color, textures }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user