Minimalist multipurpose blocky voxel engine API design documentation. With blocky we mean Minecraft/Teardown style.
There are some good voxel engines out there, but those all seem to be very restrictive in terms of customization and come with large code bases that you need to understand and hack in order to add custom functionality. I want to propose a minimalistic and generalized API that a voxel engine could provide/expose for advanced voxel game/sandbox developers.
Ideally witten in C so can be used as header in HLSL/GLSL/C++/C and beyond (inspired by https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/PNanoVDB.h). Neverless some parts belong to CPU only, for othes it depends on the implementation. The tehnical imlementation details are out of the scope of this document.
Camera {
float3 position,
float3 rotation,
float viewDistance,
float fov,
int3 partitionCoords
}
viewDistance
engines uses this to calculate when to stream in/out partitions. If camera position + view distance is out of current partition (in any direction) and in next one, it should be streamed in. And via versa.fov
field of view.position
current position of camerarotation
current rotation of camera in euler anglespartitionCoords
partition coordinates where camera resides at this moment
Top view for illustration purposes only:
Partitions are cubic shaped grids that form up into a larger uniform grid and are streamed in and out on demand (think of Unreal World partitions, but 3D). A partition where the player resides and it's neigbour partitions make up the space that player should be able to observe and where voxel manipulations/events are possible. The goal is to always have 26 neighbour partitions around current one.
Partition {
int3 position
}
position
x, y, z coordinates of the partition.
Voxel {
// generic properties here - primitives only
float3 color,
float hardness
}
The data here is generic. Whatever is needed for certain purpose. The structure will be the same for all voxels and has do be declared before runtime. More properties, larger the map in MB. All possible varinations have to be registered (see in fallowing section "Registers").
Group {
// generic properties here - primitives only
float damage,
float3 velocity
}
The data here is generic. Whatever is needed for certain purpose. The structure will be the same for all groups and has do be declared before runtime. More properties, larger the map in MB.
VoxelAccessor {
int3 position,
int lod,
Voxel voxel
}
position
voxel index position in the gridlod
depth of voxelvoxel
structure described above
This is object you use for updating and querying voxel.
VoxelsSelector {
VoxelAccessor[] getAll(),
VoxelAccessor[] slice(uint from, uint to),
VoxelAccessor[] splice(uint from, uint to, VoxelAccessor va = null),
}
getAll()
getter for all voxel accessorsslice(uint from, uint to)
take a piece of voxel accessorssplice(uint from, uint to, VoxelAccessor va = null)
insert, replace or remove voxel accessor(s) // concept from JavaScript splice
This is object is used to point to multiple voxels.
GroupAccessor {
float3 position,
float3 rotation,
int voxelCount,
Group group
}
position
current position of grouprotation
current rotation of group in euler anglesvoxelCount
total voxel count in groupgroup
structure described above
This is object you use for updating and querying group.
Collider {
impactPoint,
voxelAccessor
}
Object holding info about collision.
impactPoint
local hit pointvoxelAccessor
voxel colliding
Hit {
float3 localPointOnVoxel
VoxelAccessor voxel
float length
}
localPointOnVoxel
local hit pointvoxel
voxel being hitlength
ray total length
Engine can make impressive optimziations if it knows all possible variations of the voxel generic data. There for we have function:
registerVoxelProperties<T = propery of Voxel>(keyof T propery, valueof T[] values)
propery
generic property likecolor
values
generic property value like{ float3(1,0,0), float3(1,1,0) }
NOTE: This is not optional.
changeSettings(int gridPartitionSize, int minVoxelSize, int maxTreeDepth, Enum accelerationStructure = null)
This should be called somwhere in game initialization phase to provide these important settings to the engine.
gridPartitionSize
is the size of a single partitionminVoxelSize
how small is the smallest voxel. This will have major impact on performance and visual appealmaxTreeDepth
how many lod levels each partition grid tree (acceleration structure) will haveaccelerationStructure
engine can provide multiple acceleration structures, like octees, brickmaps, sparse brick sets, etc. If you change this after saving a map, you wont be able, most likely to load it, unless there is some centralized storage format.
camera = createCamera(float viewDistance, float fov)
Defining the camera.
viewDistance
description in defintionsfov
description in defintions
transformCamera(Camera camera, float3 position, float3 rotation)
Transforming the camera. This can trigger streaming of partition(s) dependent on position and view distance.
camera
camera objectposition
desired position of camerarotation
desired rotation of camera in euler angles
onGridChunkStream(Partition partition)
Event fired whenever engine needs to stream (in/out) a partition. Can be used of procedural generation or other purposes.
partition
description in defintions
Partition[] partitions = getAllActivePartitions(int3 coords = null)
Get all stramed in or streaming in paritions at this moment.
coords
if provided, will return 1 respestive partition or null if its not active
selectPartition(int3 coords)
We select a one of the Streamed in paritions, to work with. All the things below will take place in this selected partition and its local coordinates. It should throw error, if parition is not active streamed in.
coords
partition global 3d coordinates
VoxelsSelector voxelsSel = setVoxels(int3[] gridCellCoords, Voxel voxel)
Spawns block voxel with all its properties in selected area, all of it. Returns those new voxels.
gridCellCoords
grid cell coordinates where to spawn voxelvoxel
description in defintions
VoxelsSelector voxelsSel = selectVoxelInRectArea(int3 start, int3 end)
Get all voxels in rectangular area.
start
star position in grid, or bottom left cornerend
end position in grid, or top right corner
GroupAccessor group = groupVoxels(VoxelsSelector voxelsSel)
Registers a voxel group. This very usefull to represent sort of a Entity, like a rock, that is composed from multiple voxels.
Group can never be larger than one parition.
GroupAccessor group = getVoxelGroup(VoxelAccessor voxel)
Check/get if voxel is in a group.
VoxelsSelector voxelsSel = getGroupVoxels(GroupAccessor group)
To get all voxels in a group. Voxels belonging to a partition that is not yet streamed in wont be returned. This can be checked by 'GroupAccessor.voxelCount'
transform(GroupAccessor group, float3 position, float3 rotation)
Transform group by given position and rotation.
group
voxel group accessorposition
desired position for grouprotation
desired rotation for group in euler angles
changeProperty<T = propery of Voxel>(VoxelAccessor voxel, keyof T property, valueof T value)
changeProperty<T = propery of Voxel>(VoxelsSelector voxelSel, keyof T property, valueof T value)
changeProperty<T = propery of Group>(GroupAccessor group, keyof T property, valueof T value)
Change properties of an existing voxel or group of voxels
voxels/voxelSel/group
voxel accessor, voxel selection or voxel group accessorproperty
generic propertyvalue
generic property value
onCollision(Collider c1, Collider c2){ // Colliders for both voxels, description is in defintions
float3 impactPoint1 = c1.impactPoint
VoxelAccessor va2 = c2.voxelAccessor
// code the collision reaction your self, dependent on properties etc.
}
Event fired whenever 2 voxels collide.
Hit hit = rayCast(float3 origin, float3 direction, float3 lenngth = null, uint lod = null)
Raycast voxels in streamed in partitions. Must be as efficient as possible, because will be probably used for rendering.
origin
origin of the raydirection
direction to shoot the raylenngth
optional length for ray, by default its view distancelodLevel
optional level of detail, by default is accepts leaf voxels. More coarse voxels from tree stracture can be fetched by providing depth level here. Its properties are interpolated from child voxels.
save(str file, int3 partitionCoords)
Store parition.
file
file destinationpartitionCoords
partition coords to store
load(str file, int3 partitionCoords)
Manually load some parition.
file
file destinationpartitionCoords
partition coords to load
Engine takes care of acceleration structure creation and modification, compression, partition stream in/out, raycast by lod, etc. Also speculatively deactivates and activates colliders depenging on actions in the parition.
Programmer uses all of this to create own voxel game logic and experience. No Biased opinions on how physics should work, what renderer to use (taster/raytrace/pathtrace) and so on.
No. Not at all. You can use the Raycast for such purposes, but the rest is for you to implement.
No. Not at all. Only collisions, but the rest is for you to implement.
We can fast access voxels that make up some kind of a entity, like a door, rock, etc.
As you can see lots of voxe engines (Teardown, The Sandbox, Atomontage, etc.) are already doing this. Thencial details are not part of this document.
Think of Minecraft, Teardown, etc. Blocks.
It is essential for procedural generation. And also for coordination system. Each partition has local one. If we use global coordinates, the numbers can get insanely large to impossible.