Skip to content

Commit 5fa308a

Browse files
authored
Merge branch 'main' into wasm-support
2 parents 1746873 + 493d132 commit 5fa308a

10 files changed

Lines changed: 201 additions & 33 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.15.2
4+
5+
- Fix: `set_voxel` now queues loaded neighboring chunks for remeshing when a changed voxel is part of their padded chunk data. This keeps meshes correct across chunk boundaries without requiring user code to manually mark adjacent chunks.
6+
37
## 0.15.1
48

59
- Fix: `set_voxel` no longer triggers a chunk remesh when the written value is identical to the current value. Previously, writing the same voxel value every frame would cause perpetual remesh cancellation — the in-progress async meshing task was dropped each frame before completion, preventing the chunk from ever rendering.

Cargo.lock

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "bevy_voxel_world"
33
description = "A voxel world plugin for Bevy"
4-
version = "0.15.1"
4+
version = "0.15.2"
55
edition = "2021"
66
authors = ["Joacim Magnusson <joacim@isogram.se>"]
77
license = "MIT OR Apache-2.0"
@@ -28,7 +28,6 @@ path = "examples/noise_terrain.rs"
2828
[[example]]
2929
name = "noise_terrain_lod"
3030
path = "examples/noise_terrain_lod.rs"
31-
required-features = ["noise"]
3231

3332
[[example]]
3433
name = "custom_meshing"
@@ -56,7 +55,7 @@ bevy = { version = "0.18", features = [
5655
bevy_shader = "0.18"
5756
block-mesh = "0.2.0"
5857
futures-lite = "2.6.1"
59-
hashbrown = "0.16.1"
58+
hashbrown = "0.17.0"
6059
ndshape = "0.3.0"
6160
rand = "0.9.2"
6261
weak-table = { version = "0.3.2", features = ["ahash"] }

examples/custom_meshing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl VoxelWorldConfig for MainWorld {
9797
let mut tex_coords = Vec::with_capacity(num_vertices);
9898
let mut material_types = Vec::with_capacity(num_vertices);
9999

100-
for (group, face) in buffer.quads.groups.into_iter().zip(faces.into_iter()) {
100+
for (group, face) in buffer.quads.groups.into_iter().zip(faces) {
101101
for quad in group.into_iter() {
102102
let _normal = IVec3::from([
103103
face.signed_normal().x,

examples/multiple_noise_terrain.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ fn setup(mut commands: Commands) {
9090
});
9191
}
9292

93+
#[allow(clippy::type_complexity)]
9394
fn get_voxel_fn() -> Box<
9495
dyn FnMut(IVec3, Option<WorldVoxel<BlockTexture>>) -> WorldVoxel<BlockTexture>
9596
+ Send

examples/noise_terrain_lod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ fn setup(mut commands: Commands, mut fonts: ResMut<Assets<Font>>) {
202202
));
203203

204204
// Ambient light, same color as sun
205-
commands.insert_resource(AmbientLight {
205+
commands.insert_resource(GlobalAmbientLight {
206206
color: Color::srgb(0.98, 0.95, 0.82),
207207
brightness: 100.0,
208208
affects_lightmapped_meshes: true,

src/meshing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ fn mesh_from_quads_for_shape<I: PartialEq + Copy>(
114114

115115
let voxel_size = voxel_size_from_shape(shape);
116116

117-
for (group, face) in quads.groups.into_iter().zip(faces.into_iter()) {
117+
for (group, face) in quads.groups.into_iter().zip(faces) {
118118
for quad in group.into_iter() {
119119
let quad = Into::<block_mesh::geometry::UnorientedQuad>::into(quad);
120120
let normal = IVec3::from([

src/test.rs

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use bevy::mesh::VertexAttributeValues;
22
use bevy::prelude::*;
3-
use std::sync::Arc;
3+
use std::collections::HashSet;
4+
use std::sync::{Arc, Mutex};
45

5-
use crate::chunk_map::ChunkMapUpdateBuffer;
6+
use crate::chunk_map::{ChunkMapInsertBuffer, ChunkMapUpdateBuffer};
67
use crate::configuration::VoxelWorldConfig;
78
use crate::mesh_cache::MeshCacheInsertBuffer;
89
use crate::meshing::generate_chunk_mesh_for_shape;
910
use crate::prelude::*;
1011
use crate::voxel_traversal::voxel_line_traversal;
1112
use crate::{
12-
chunk::{ChunkData, ChunkTask, FillType, CHUNK_SIZE_F, PADDED_CHUNK_SIZE},
13+
chunk::{
14+
Chunk, ChunkData, ChunkTask, FillType, CHUNK_SIZE_F, CHUNK_SIZE_I,
15+
PADDED_CHUNK_SIZE,
16+
},
1317
prelude::VoxelWorldCamera,
1418
voxel_world::*,
1519
voxel_world_internal::ModifiedVoxels,
@@ -208,6 +212,126 @@ fn chunk_will_update_event() {
208212
app.update();
209213
}
210214

215+
#[test]
216+
fn affected_chunk_positions_include_padding_neighbors() {
217+
let affected: HashSet<_> = get_affected_chunk_positions(IVec3::new(0, 0, 0))
218+
.into_iter()
219+
.collect();
220+
let expected = HashSet::from([
221+
IVec3::new(0, 0, 0),
222+
IVec3::new(0, 0, -1),
223+
IVec3::new(0, -1, 0),
224+
IVec3::new(0, -1, -1),
225+
IVec3::new(-1, 0, 0),
226+
IVec3::new(-1, 0, -1),
227+
IVec3::new(-1, -1, 0),
228+
IVec3::new(-1, -1, -1),
229+
]);
230+
231+
assert_eq!(affected, expected);
232+
233+
assert_eq!(
234+
get_affected_chunk_positions(IVec3::new(1, 1, 1)),
235+
vec![IVec3::new(0, 0, 0)]
236+
);
237+
238+
let affected: HashSet<_> =
239+
get_affected_chunk_positions(IVec3::new(CHUNK_SIZE_I - 1, 1, 1))
240+
.into_iter()
241+
.collect();
242+
let expected = HashSet::from([IVec3::new(0, 0, 0), IVec3::new(1, 0, 0)]);
243+
244+
assert_eq!(affected, expected);
245+
}
246+
247+
#[test]
248+
fn set_voxel_on_chunk_boundary_marks_padding_neighbors_for_remesh() {
249+
let expected = HashSet::from([
250+
IVec3::new(0, 0, 0),
251+
IVec3::new(0, 0, -1),
252+
IVec3::new(0, -1, 0),
253+
IVec3::new(0, -1, -1),
254+
IVec3::new(-1, 0, 0),
255+
IVec3::new(-1, 0, -1),
256+
IVec3::new(-1, -1, 0),
257+
IVec3::new(-1, -1, -1),
258+
]);
259+
260+
let mut app = App::new();
261+
app.add_plugins((MinimalPlugins, VoxelWorldPlugin::<DefaultWorld>::minimal()));
262+
let camera_transform =
263+
Transform::from_xyz(10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y);
264+
app.world_mut().spawn((
265+
Camera::default(),
266+
Camera3d::default(),
267+
camera_transform,
268+
GlobalTransform::from(camera_transform),
269+
VoxelWorldCamera::<DefaultWorld>::default(),
270+
));
271+
app.update();
272+
273+
type Mat = <DefaultWorld as VoxelWorldConfig>::MaterialIndex;
274+
let mut chunks = Vec::new();
275+
for chunk_pos in expected.iter().copied() {
276+
let entity = app.world_mut().spawn_empty().id();
277+
let shape = UVec3::splat(PADDED_CHUNK_SIZE);
278+
app.world_mut()
279+
.entity_mut(entity)
280+
.insert(Chunk::<DefaultWorld>::new(
281+
chunk_pos, 0, entity, shape, shape,
282+
));
283+
chunks.push((chunk_pos, ChunkData::<Mat>::with_entity(entity)));
284+
}
285+
286+
app.world_mut()
287+
.resource_mut::<ChunkMapInsertBuffer<DefaultWorld, Mat>>()
288+
.extend(chunks);
289+
app.update();
290+
for _ in 0..100 {
291+
app.update();
292+
}
293+
294+
app.world_mut()
295+
.resource_mut::<Messages<ChunkWillRemesh<DefaultWorld>>>()
296+
.clear();
297+
298+
let remeshed_chunks = Arc::new(Mutex::new(Vec::new()));
299+
let remeshed_chunks_writer = remeshed_chunks.clone();
300+
app.add_systems(
301+
Update,
302+
move |mut events: MessageReader<ChunkWillRemesh<DefaultWorld>>| {
303+
remeshed_chunks_writer
304+
.lock()
305+
.unwrap()
306+
.extend(events.read().map(|event| event.chunk_key));
307+
},
308+
);
309+
310+
app.add_systems(
311+
Update,
312+
|mut voxel_world: VoxelWorld<DefaultWorld>, mut did_set: Local<bool>| {
313+
if *did_set {
314+
return;
315+
}
316+
317+
voxel_world.set_voxel(IVec3::new(0, 0, 0), WorldVoxel::Solid(1));
318+
*did_set = true;
319+
},
320+
);
321+
322+
app.update();
323+
app.update();
324+
app.update();
325+
326+
let remeshed_chunks: HashSet<_> =
327+
remeshed_chunks.lock().unwrap().iter().copied().collect();
328+
329+
assert!(
330+
expected.is_subset(&remeshed_chunks),
331+
"expected {expected:?} to be remeshed, got {remeshed_chunks:?}"
332+
);
333+
}
334+
211335
#[test]
212336
fn chunk_generate_reuses_previous_data_when_configured() {
213337
type Mat = <DefaultWorld as VoxelWorldConfig>::MaterialIndex;

src/voxel_world.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::sync::Arc;
88
use bevy::{ecs::system::SystemParam, math::bounding::RayCast3d, prelude::*};
99

1010
use crate::{
11-
chunk::{ChunkData, CHUNK_SIZE_F, CHUNK_SIZE_I},
11+
chunk::{ChunkData, CHUNK_SIZE_F, CHUNK_SIZE_I, CHUNK_SIZE_U},
1212
chunk_map::ChunkMap,
1313
configuration::VoxelWorldConfig,
1414
traversal_alg::voxel_line_traversal,
@@ -404,3 +404,38 @@ pub fn get_chunk_voxel_position(position: IVec3) -> (IVec3, UVec3) {
404404

405405
(chunk_position, voxel_position)
406406
}
407+
408+
/// Returns every chunk whose padded voxel data includes the given world-space voxel.
409+
pub(crate) fn get_affected_chunk_positions(position: IVec3) -> Vec<IVec3> {
410+
let (chunk_position, voxel_position) = get_chunk_voxel_position(position);
411+
412+
let axis_offsets = |component| {
413+
let mut offsets = [0, 0, 0];
414+
let mut len = 1;
415+
416+
if component == 1 {
417+
offsets[len] = -1;
418+
len += 1;
419+
}
420+
if component == CHUNK_SIZE_U {
421+
offsets[len] = 1;
422+
len += 1;
423+
}
424+
425+
(offsets, len)
426+
};
427+
428+
let (x_offsets, x_len) = axis_offsets(voxel_position.x);
429+
let (y_offsets, y_len) = axis_offsets(voxel_position.y);
430+
let (z_offsets, z_len) = axis_offsets(voxel_position.z);
431+
let mut affected_chunks = Vec::with_capacity(8);
432+
for x in x_offsets.iter().take(x_len).copied() {
433+
for y in y_offsets.iter().take(y_len).copied() {
434+
for z in z_offsets.iter().take(z_len).copied() {
435+
affected_chunks.push(chunk_position + IVec3::new(x, y, z));
436+
}
437+
}
438+
}
439+
440+
affected_chunks
441+
}

src/voxel_world_internal.rs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use crate::{
2525
voxel::WorldVoxel,
2626
voxel_material::LoadingTexture,
2727
voxel_world::{
28-
get_chunk_voxel_position, ChunkWillChangeLod, ChunkWillDespawn, ChunkWillRemesh,
29-
ChunkWillSpawn, ChunkWillUpdate, VoxelWorldCamera,
28+
get_affected_chunk_positions, ChunkWillChangeLod, ChunkWillDespawn,
29+
ChunkWillRemesh, ChunkWillSpawn, ChunkWillUpdate, VoxelWorldCamera,
3030
},
3131
};
3232

@@ -411,17 +411,13 @@ where
411411
) {
412412
let thread_pool = AsyncComputeTaskPool::get();
413413
let max_threads = configuration.max_active_chunk_threads();
414-
let mut active_threads = chunk_threads.iter().count();
414+
let remaining_threads = max_threads.saturating_sub(chunk_threads.iter().count());
415415

416-
if max_threads == 0 {
416+
if remaining_threads == 0 {
417417
return;
418418
}
419419

420-
for chunk in dirty_chunks.iter() {
421-
if active_threads >= max_threads {
422-
break;
423-
}
424-
420+
for chunk in dirty_chunks.iter().take(remaining_threads) {
425421
let previous_chunk_data = {
426422
let read_lock = chunk_map.get_read_lock();
427423
ChunkMap::<C, C::MaterialIndex>::get(&chunk.position, &read_lock)
@@ -492,8 +488,6 @@ where
492488
))
493489
.remove::<NeedsRemesh>();
494490

495-
active_threads += 1;
496-
497491
ev_chunk_will_remesh
498492
.write(ChunkWillRemesh::<C>::new(chunk.position, chunk.entity));
499493
}
@@ -621,17 +615,18 @@ where
621615
continue;
622616
}
623617

624-
let (chunk_pos, _vox_pos) = get_chunk_voxel_position(*position);
625618
modified_voxels.insert(*position, *voxel);
626619

627-
// Mark the chunk as needing remeshing or spawn a new chunk if it doesn't exist
628-
if let Some(chunk_data) =
629-
ChunkMap::<C, C::MaterialIndex>::get(&chunk_pos, &chunk_map_read_lock)
630-
{
631-
if let Ok(mut ent) = commands.get_entity(chunk_data.entity) {
632-
ent.try_insert(NeedsRemesh);
633-
ent.remove::<ChunkThread<C, C::MaterialIndex>>();
634-
updated_chunks.insert((chunk_data.entity, chunk_pos));
620+
for affected_chunk_pos in get_affected_chunk_positions(*position) {
621+
if let Some(chunk_data) = ChunkMap::<C, C::MaterialIndex>::get(
622+
&affected_chunk_pos,
623+
&chunk_map_read_lock,
624+
) {
625+
if let Ok(mut ent) = commands.get_entity(chunk_data.entity) {
626+
ent.try_insert(NeedsRemesh);
627+
ent.remove::<ChunkThread<C, C::MaterialIndex>>();
628+
updated_chunks.insert((chunk_data.entity, affected_chunk_pos));
629+
}
635630
}
636631
}
637632
}

0 commit comments

Comments
 (0)