godot_voxel
godot_voxel copied to clipboard
VoxelTerrain signals loaded/unloaded and entered/leave are not emitted symetrically
Describe the bug For both sets of signals independently, one would expect every loaded to have a matching unloaded, and every entered to have a matching leave. This is not the case. Sometimes no unloaded signal arrives, and often multiple arrive for the same block.
To Reproduce VoxelTerrainSignalsBug.zip Create a VoxelTerrain with a generator and mesher. Add a flying overhead camera with 2 VoxelViewer nodes spaced out a bit (for a wide screen) and a basic script to move the camera around. The signals emitted by VoxelTerrain for loaded blocks and meshes entered do not match when flying around. The attached project keeps track with a few dictionaries.
Expected behavior A block loaded signal should be emitted only once and have a single corresponding unloaded signal. Same for the mesh entered.
Environment
- Using development build of Godot 4.2 and godot_voxel. Last pulled early september.
** Notes ** This is truly an amazing plugin and my prototyping is going well, but I bumped into something which is likely a bug.
In my code (not in the example) I am also hooking into the generator to pre-compute data used for spawning, and I emit a signal for each generated full block (not the smaller edge blocks). These signals also do not match the block loaded / mesh entered signals.
When instancing units in the world, and especially unloading them, I want to use these loaded/entered signals. But they are not reliable, and when flying around with the camera, units remain because no unloaded signal was received.
If there is any further testing needed that I can do, let me know.
Does this happen if you use only one VoxelViewer?
Partly. It looks like I am getting matching signals for block loaded/unloaded with a single VoxelViewer! At all times there are 512 blocks loaded, which is correct for my setup. No blocks unloaded twice. I had not checked the total loaded blocks before, but with 2 VoxelViewers the total is incorrect, I see 488, 264, 456, the total is jumping around.
mesh_block_entered and _exited still has the problem when using one VoxelViewer. To be honest, I do not know what these signals stand for. But the number of entered mesh_blocks is not consistent, it tops at 64 but at times it is a bit lower. Also, exited is signaled multiple times for the same blocks (without a corresponding entered, so net negative).
The total blocks "loaded" for voxel data or mesh is not guaranteed to remain constant because it depends on multi-threaded logic. However I think there should not be duplicate or lack of matching signals for a given block. Although it might depend on the situation, for example if a block is seen by 2 viewers, then one viewer goes away, some signals won't fire because the block is still loaded due to the other viewer.
Note: to clarify one thing, block_loaded
and block_unloaded
are about voxel data. They can be emitted before the mesh even gets created, and are also emitted for viewers that dont require meshes. In this engine, mesh and voxel blocks are decoupled.
My particular problem is solved if I use just 1 VoxelViewer. Thanks a lot for the hint, I should have tested that myself. ;-)
I wanted to use 2 of them for my overhead (60deg) camera because the screen is not square but rectangular. But this could be premature optimization, loading the blocks is pretty fast and done in parallel so some redundant blocks might not be a big deal.
As for the situation you mention: yes I would expect the other viewer to suppress the unloaded signal when the first viewer goes out of range. But in my tests this is not the case, a single loaded signal is followed by multiple unloaded signals for the same block
So here is what's happening with mesh signals:
- Empty mesh blocks are created around the viewer and request their first update. A response will be received later with the result. No signals are emitted yet.
- When a mesh block receives an update (that includes the first time it gets meshed due to coming into range), if the result is an empty mesh,
on_mesh_block_exited
is emitted, even thoughon_mesh_block_entered
was never emitted. - When a mesh block receives an update, if the block had no mesh and the update comes with a non-empty mesh,
on_mesh_block_entered
is emitted - When a mesh block gets unloaded,
on_mesh_block_exited
is emitted, even if the block had no mesh.
It's a bit messy. This logic was originally ported from VoxelLodTerrain's instancer logic, which used "non-empty mesh blocks" rather than just "mesh blocks", but VoxelTerrain works a bit differently so there was some confusion. It clearly doesn't work if you expect matching calls, but it probably needs some refactoring to have more consistent behavior.
It seems you expected to use this signal to count the "first update" of mesh blocks? (When a mesh block receives its first update, and when a mesh block having received its first update gets removed; regardless of it being empty or not).
Alternatively, in VoxelLodTerrain, the original logic revolves around "non-empty mesh blocks" (When a mesh block becomes non-empty, and when a mesh block becomes empty).
Regarding the data blocks:
Found a bug where every data block leaving the area of a viewer will emit the block_unloaded
signal, not only when that block's reference count reaches zero (causing it to be effectively unloaded).
Awesome!
Considering your clarification about block_loaded
vs mesh_block_entered
: it seems the block signals are what I need to instance my simulated units, thanks. The simulation needs to run regardless of visuals. I think it is a good design decision to decouple the data from the visuals.
~~After more testing with my prototype itself (and not the minimal example) I found out that there is a second cause for my issues.~~ UPDATE: I had been mixing block loaded and mesh exited signals, my bad. Looks like it`s all good now.
I assumed every generate_block leads to a block_loaded emit of the terrain. This is not the case, and maybe by design?
Yes, you must be prepared for generator queries to get discarded. It normally doesn't happen often, but it can happen. A simple example is if your player moves fast enough or teleports quickly after entering the area of new chunk: those new chunks will be queued for generation, but by the time it comes back, the area can be out of range again, therefore it gets discarded. That can happens at several levels: while the request is queued (before the generator runs), and when the response comes back to the terrain (after the generator has run).
Pushed some fixes, with ea26244ef29b4243ab64fbd9231e7618e6fe1a4c your test project no longer prints errors.
Thanks for the warning/confirmation. I think I now have a reliable way of instancing units.
I have a custom generator script for the terrain. This currently forwards the block generation to a graph function because the visual graph editor is really useful for prototyping procedural generated worlds. Once a block is generated by the graph, I can analyze it and determine which are good spots for spawning. This computation is done on the generator worker thread(s) and thus does not lag the game. Results of this computation are attached as metadata to voxel 0,0,0 of the voxel block buffer. (Because the block metadata is not accessible later on).
Then, on the main thread, either the signal block_loaded
or mesh_block_entered
is used to actually instance my nodes. I am using block_loaded
because simulation of the units starts outside of view, and their AI can decide to walk towards the rendered area. Here I fetch the metadata of voxel 0,0,0 of the current block using the voxel tool, and use this metadata to do the spawning (everything is precomputed).
The block_unloaded
signal is used to despawn units, but here I use my own data structure that stores unit positions (as they may have moved) and only use the position provided by the block_unloaded
signal, no voxels nor metadata.