To ray trace this data structure I decided to implement a dynamic step size ray marcher. What this means is that instead of calculating if a line intersects a triangle you shoot a ray in a certain direction and take steps until the ray collides with something. This can be done either with a fixed step size, or with a dynamic one where you jump different lengths depending on the environment. For an SVO ray marcher this is especially useful since you can jump further distances if you know the data is sparse, which the data structure allows for.
To begin the ray marching algorithm we first have to find what node holds the starting position of the camera. Therefore I began by defining the function for getting a node at a certain position. I found this youtube video that explains how you can very easily know what child index to traverse down by seeing each octant as a cube of size 1. Thus we get coordinates at each of the corners that consist of zeroes and ones (hint hint). By seeing this position as a binary number instead of a vector we can get the child index. Codewise this can be done quite simply by defining each bit as a boolean representing each axis, that says whether the position is higher than the center of the parent node. This essentially simulates the same logic of having a cube of size 1 but generalizes it for all node positions. I then implemented the below code on this logic.
float nodeSize = svoSize / (float)(1 << depth);
vec3 center = offset + vec3(nodeSize, nodeSize, nodeSize) * 0.5f;
ivec3 childPos;
childPos.x = pos.x >= center.x;
childPos.y = pos.y >= center.y;
childPos.z = pos.z >= center.z;
int childIndex = (childPos.x << 0) | (childPos.y << 1) | (childPos.z << 2);
First we just make sure that the node we are in is not a leaf, but if not we now know what node we are in, and can begin actually marching the ray forward. Firstly we have to calculate how far to step to reach into the next voxel boundary. To do this we snap the position in each axis to the closest voxel edge and save this as gridPos. This means that we either round the position up or down to the nearest grid, depending on if the direction of the ray is positive or negative for each axis. By elementwise subtracting our original position from this gridposition, and then elementwise dividing this new vector by the direction vector from the ray, we get a new vector that contains how many steps of d to take in each axis to reach that axis boundary. By taking the minimum value of the x, y and z of this vector we get the shortest step that will make us reach a new voxel.
Here I encountered quite a few issues with inaccuracies and artifacts, some of which I still haven’t solved that cause the engine to look a bit strange if you look closely.
However, I was able to implement some fixes that got rid of some of these inaccuracies.
The biggest fix was probably the face bias. By adding a small factor that stepped a bit backwards in comparison to the normal of the face of the octant you are passing through, it made sure the algorithm didn’t land on the boundary between two octants. This avoided some major artifacts that were very prevalent otherwise as can be seen in the image below.
I also added some epsilons and safety checks to different operations, to make sure that very very small numbers passed as zeroes for equals operations and that the code never divided by numbers very close to zeroes. This also helped fix some artifacts that I was not able to reproduce while writing this blog however.