Introduction
I’m creating a Voxel-based tech demo and was hoping to leverage DOTS for easy multi-threaded operations across many “VoxelChunks”. I’ve done some preliminary research and read through many posts here on the forums to get an idea for what I can and cannot do, but I find I still have a few questions regarding what is possible and if my current thought process actually makes sense. I’m hoping that those who are wiser than me can help point me in the right direction.
The Tech Demo
Ships in a multiplayer setting shooting each other, with Finite Liquid simulation. Each ship needs to be independent of the other in regards to position and rotation, so many possible Voxel solutions (Minecraft Worlds) I have found here on the forums don’t apply. Each “Voxel” must be a ulong (64 bits). If needs be, it could be split into 2 separate “grids” of an uint (32 bits). Either way, 64 bits total is a must. Ships would have an upper limit of 2,097,152 (128^3) Voxels with an expectation that most would be far smaller. I’d like to support Ships to be up to 16,777,216 (256^3) Voxels, but that’s likely too large for what I am attempting. Each Voxel will effectively be it’s own “model”; this will not be using Marching Cubes.
All Voxels would need to be iterated over fairly often for Liquid simulation, perhaps once every 3 seconds (faster if I can manage) and need to access it’s immediate neighbor Voxels. This is on top of any immediate changes that must be made (Voxels destroyed/created/changed). It needs to be fast enough to handle ~32 Ships. These are the constraints, and here is my potential implementation:
VoxelChunks as an Entity
Near as I can find, placing Voxels into “VoxelChunks” is the best way to go. Trouble is at the size of 64 bits, I can only have a VoxelChunk size of 8x8x8, which equals to 4kb. Adding some additional information into the VoxelChunk guarantees a max of only 3 VoxelChunks per ECS Chunk (each ECS Chunk is 16kb in size).
Components:
- ushort ShipID: this would be a SharedComponentData indicating which Ship this VoxelChunk belongs to.
- DynamicBuffer Voxels: all Voxels in an 8x8x8 matrix as a 1d array in Morton order. Each Voxel would just be a component of a single ulong (64 bits). Capacity fixed at 4,096 (8^3).
- DynamicBuffer VoxelChunkNeighbor: each neighbor of the VoxelChunk, including diagonals so systems can do neighbor VoxelChunk checks. Capacity fixed at 26.
- uint Position: the VoxelChunk’s position (index) in a NativeHashMap grid in the Ship. (some bit shifting here to store each axis as 10 bits within the uint)
- Entity MeshChunk: the Entity that will render this VoxelChunk (among others). Stored here so that when this VoxelChunk becomes dirty, it can flag its specific MeshChunk to update.
MeshChunks as an Entity
These would store the Vertex data for a small grid of VoxelChunks. Either 2x2x2 (16^3 Voxels) or 4x4x4 (32^3 Voxels)
The Ship as an Entity
The ship simply contains various data that shouldn’t be too relevant to the problem at hand. All the VoxelChunks would be stored as a NativeHashMap<index, VoxelChunk> in a sparse configuration.
Concerns:
- Am I trying too hard to cram VoxelChunks into normal ECS Chunks? Holding only 3 Entities per ECS Chunk seems inefficient to me, but I can’t really reduce the VoxelChunk size any further. As it stands for a 128^3 Ship with a 8^3 VoxelChunk, I would be looking at 4,096 VoxelChunks spread into 1,365 ECS Chunks. By having such large Entities am I losing any potential benefit from DOTS? Would it be better to handle the ship as a regular class and just try to use the Job system alone for performance increase?
- Is splitting the Vertex data into a separate Entity problematic? To my knowledge, neither needs to know about the other except for when the MeshChunk must be rebuilt
- If separating the MeshChunks into a separate Entity is okay, would it make sense to cut the Voxel data in half to only 32 bits, and put the other 32 bits that rarely change and are only used for rendering into a separate Entity as well? That’d mean each ship would have a separate “grid” for VoxelChunk, MeshChunks, and “RenderChunks” (containing the other half of the Voxel information to be used for MeshChunk).
- Because the VoxelChunks are not stored in sequence, I am worried about too much processor time being spent for each VoxelChunk trying to look up it’s neighbor. Should they be stored in a more standard linear way?
- Are DynamicBuffers the best option for VoxelChunk when I know that Voxels and VoxelChunkNeighbor are fixed in capacity and will never change it’s size? Is there a more efficient way to handle this?
- Will I encounter difficulties trying to access neighbor VoxelChunks and their DynamicBuffer of Voxels when I need to check nearby Voxels at the edge of the VoxelChunk “grid”?
Conclusion
I could just write up the whole thing in a far more OOP way and avoid some of the hassle with operating in the bounds of DOTS. However, the whole point of this Tech Demo is to see if it can be done in DOTS, and if DOTS is competitive with the standard OOP way of handling this problem. I’ve seen this issue (both Voxels, and Meshes) talked about several times here on the forums, but I haven’t found clear answers for my use case here. I believe that DOTS should be able to compete with OOP in regards to supporting dense Voxels in a multi-world(Ship) setting, but my understanding of DOTS as a whole is more limited than others here on the forums.