Zylann / godot_voxel

Voxel module for Godot Engine
MIT License
2.49k stars 233 forks source link

Specialize terrain nodes #41

Open Zylann opened 4 years ago

Zylann commented 4 years ago

Currently, there are two terrain types:

Given the use cases for each type, I am tempted to change them:

This also has the advantage that it's easier to expose settings in the inspector, as they can be wildly different between blocky and smooth. One drawback is, VoxelLodTerrain currently doesn't support edition, mostly because I need to find a strategy to replicate edits on all LODs.


Edit: this may be part of a larger refactoring to come in the future. Main points being:

(How these requirments will be implemented may vary from the final design)

TokisanGames commented 4 years ago

Sounds like a good idea. They can have simpler, more focused code, and the LOD terrain is already 4-10x faster than the other.

Though, yes adding/removing terrain and infinite paging need to make their way into LOD before cutting smooth out of VoxelTerrain.

I don't really understand the code, but as a user it seems the map is built completely 8 times (or several times), one LOD at a time, starting at 0,0,0.

It would seem to me that one could edit the data source*, mark the block as dirty and the mesher would just remesh the data on whatever LOD is appropriate. No need to replicate the change to all LODs, instead LODs regenerate exactly as they are built now.

*How to "edit the data source?" I'm not sure how it's acquired, but...

If every time a block is remeshed, it's loaded from the stream, then perhaps there could be a stream-cacher which retains the edits and gives those if present, instead of querying the stream.

If the stream is not queried again and the data in VoxelBuffer is sufficient to remesh the block, then the voxelbuffer can be edited and the block marked dirty, forcing a remesh.

It seems to me that editing the mesh itself is the worst way to go, as we have to edit 8 different ones per block. You've already built algorithms that mesh 8 LODs based on source data. So it seems most efficient just to edit the data and remesh.

Also I think the mesher should mesh only one LOD per block. Say the first thing built is LOD0, 2 blocks around the player. Then LOD1, 3-4 blocks around. LOD2, 5-6 blocks around.... This would eliminate the current delay before LOD0 is built, and might speed up the whole process up to 8x more as only one LOD is built per block instead of 8.

What do you think about these ideas?

Zylann commented 4 years ago

I don't really understand the code, but as a user it seems the map is built completely 8 times (or several times), one LOD at a time, starting at 0,0,0.

This is due to the technique currently in use, which is a classic Octree that cannot subdivide a node if it does not fully have the data it needs. And conversely, 8 children cannot join if their parent data is unavailable. So in order to transition, it has to go through all intermediate layers. If it doesn't, you run the risk of opening a crack in the terrain (which happened a lot in my early experiments). Loading the world for the first time sounds like a bad case for this logic, but it makes sense when you move around (and in fact, that's what happens if you zoom very fast on Google Maps :D) I think it can be improved with a specialized implementation (current is generic), but I'm focused on saving at the moment. We could open an issue to discuss this.

It would seem to me that one could edit the data source*, mark the block as dirty and the mesher would just remesh the data on whatever LOD is appropriate. No need to replicate the change to all LODs, instead LODs regenerate exactly as they are built now.

The situation with LOD is that it currently works just like texture mipmaps, and loads them progressively. There really is as many sets of data as LODs, except each one spans a larger world distance. If you run an edit at a given LOD, data from other affected LODs must be updated too and then remeshed. Some of them will be already in memory, others won't. For those, I thought I could remember edits, and apply them as soon as blocks of that specific LOD are loaded (as if they were "part" of the generation of those never-explored LODs). Other strategies involve downscaling, which requires two levels to be loaded, and it works only downwards, so edition would only work within the constraints of LOD0 (so not suitable for landscaping in editor or large-scale explosions). Upscaling is even worse because a large-scale edit would not look as good from up close. I tried that in 2D and it even created seams. In any case, the more we can fit in memory, the faster this process will be, and that's where compression may help. So many things to do! We could open an issue about this as well.

I think I will likely do this refactoring on terrain nodes once I come up with something mergeable from the persistence branch.

afonsolage commented 4 years ago

Also, if I'm not mistaken, there are some extra work done by smooth terrain which isn't needed on blocky.

For instance, on this block of code, at voxel_terrain.cpp(763), both channels (type and sdf/isolevel) are copied into the buffer, which is gonna be sent to MeshUpdateProcessor, but the blocky mersher only uses type_channel, so the sdf/isolevel channel is useless here.

unsigned int channels_mask = (1 << VoxelBuffer::CHANNEL_TYPE) | (1 << VoxelBuffer::CHANNEL_ISOLEVEL);
_map->get_buffer_copy(_map->block_to_voxel(block_pos) - Vector3i(padding), **nbuffer, channels_mask);

By specializing VoxelTerrainBlocky it'll be able to copy only the needed channel.

Zylann commented 4 years ago

@afonsolage the copy is avoided by not having an SDF channel in the first place. If you don't want smooth terrain, there won't be one, so it won't be copied. By default, channels you don't touch will have a UNIFORM compression, which means their data will be defined by a single default value rather than a full memory block.

afonsolage commented 4 years ago

I see, indeed I misinterpreted the VoxelBuffer::fill_area function, which is called by VoxelMap::get_buffer_copy when get_block return a null block. After reading it more carefully and reading your explanation, it relays on default value and doesn't touch the channel. Amazing :+1:

Zylann commented 4 years ago

Another thing I've been thinking about, was to replace the terrain term with paged volume, as there could be fixed volume for non-paged voxel nodes... although it might sound less appealing.

TokisanGames commented 4 years ago

The end result is a mesh(instance), so maybe that is the final word.

Is paging something that can be turned on and off for a finite mesh, or will it be a separate object? Maybe the paged one inherits the non paged one.

Maybe VoxelBlockyMesh, VoxelSmoothMesh, and if needed Voxel(Blocky|Smooth)PagedMesh.

Zylann commented 4 years ago

Paging is not something that can easily be turned off, it's two entirely different systems. I would also not make paged inherit non-paged, but they might both have a common base.

Zylann commented 4 years ago

Update: don't wait too much for this one, I'm still figuring out design adjustments for those terrain nodes types, it may take a while. Some parts are embarrassingly similar between the two so they might at least have a common base. I'm also considering to have a concept of "voxel configuration" so that the same meshing stack can be used for different kinds of nodes (i.e paged terrain, fixed size debris, as some games may not necessarily have only one voxel node) and meshing can be centralized instead of being in every terrain node (VoxelServer might become a reality in the future), or at least introduce a job system because I'm worried about thread starvation.