Review context can be found here: 32x32x32 Units Octree With 15-Bit Voxels: Second Edition
The above question and the self answer include information such as the definition of the Octree
and Node8
data structures, MAX_LEVEL
, DATA_MASK
, and FLAG_MASK
macros, and an overall description of this system. They only include an octree_get()
function to query a node at a given location and octree_set()
function to set a node in a given location.
The only difference is that the base
field of Octree
is now in the beginning and not the end. But just for good measure I will include them here also:
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <stdio.h>
#define FLAG_MASK 0x8000
#define DATA_MASK 0x7FFF
#define MAX_LEVEL 5
#define node_to_set(ptr) ((Node8 *)((uintptr_t)(ptr)&-sizeof(Node8)))
typedef union Node2 {
uint16_t x[2];
uint32_t z;
} Node2;
typedef union Node4 {
uint16_t x[4];
Node2 z[2];
uint64_t y;
} Node4;
typedef union Node8 {
uint16_t x[8];
Node2 z[4];
Node4 y[2];
} Node8;
typedef union Ptr {
union Ptr *p;
uint64_t u;
} Ptr;
typedef struct Octree {
uint16_t base, data_size, set_size;
uint8_t data_alloc, set_alloc;
Ptr data;
Node8 set[];
} Octree;
The reason for the difference is so then an Octree
pointer and the pointer to the base
are the same and can be treated interchangeably. No need to initialize a new array with addresses of the base
fields of the Octree
pointers in octree_next
because then octree_next
can be used directly for this purpose.
I have created new code loosely based on the Iter()
function in my even older question: 32x32x32 units octree, that supports 15-bit data units
I have taken this a step further. Instead of simply descending each branch and printing out the value of each node, the descent process involves recording all the neighbors. The code should print not only the value of each node (3 times), but also the value of the node immediately to the East of (positive X-axis), to the North of (positive Z-axis), and above (positive Y-axis) then current node.
I also included the xzy
array to keep track of the spatial coordinates of the current node. More specifically, of the Southwest bottom corner of the node, where the North/East/upwards extent of the node is simply 32>>level
. This is somewhat confusing, because in my previous question with the octree_get
/octree_set()
functions, level == 0
represents the bottom/most subdivided layer of the tree, and not the top/least subdivided layers.
Note: next
in identifiers in the code refers to 'neighboring.' They have similar meanings and the former is easier to type. The function itself is called octree_render()
because iterating the structure while keeping track of the neighboring nodes is the first step in actually rendering the voxels onscreen, because this is needed to find all the exposed voxel faces in the region. I am not at that point yet but I would like to make sure I have a solid foundation before continuing.
The first iteration runs unconditionally to make sure that the root node is processed. Then, if no descent has occurred (level
is still 0), the loop is broken. When the loop is ascended (once index
reaches zero and all descents have either not existed or already been handled), the level
is checked before continuing, to avoid revisiting the same nodes infinity times.
void octree_render(const Octree *const octree, const Octree *const octree_next[const static 3]) {
const uint16_t **next_ptr = (const uint16_t **)octree_next, *next_stack[MAX_LEVEL][3];
const Node8 *set_ptr = (const Node8 *)octree, *set_stack[MAX_LEVEL];
unsigned level = 0, index = 0, index_stack[MAX_LEVEL], xzy[3] = {0};
for (;;) {
for (;;) {
const unsigned node = set_ptr->x[index];
if (node&FLAG_MASK && level < MAX_LEVEL) {
set_stack[level] = set_ptr;
set_ptr += node&DATA_MASK;
index_stack[level] = index;
index = 7;
memcpy(next_stack+(level++), next_ptr, sizeof(*next_stack));
goto update;
}
for (unsigned dim = 2;; --dim) {
printf("%d %d %d\n", node, *next_ptr[dim], xzy[dim]);
if (!dim)
break;
}
if (!index)
break;
--index;
update:
for (uint_fast8_t dim = (index&1)<<(index>>1&1);; --dim) {
// The weird expression that dim is initialized to makes sure that only the values that have to be updated are updated
// The X values have to be updated twice as often as the Z values, which have to be updated twice as often as the Y values
xzy[dim] ^= 32>>level;
const unsigned bit = 1<<dim;
const uint16_t *const next_dim = next_stack[level-1][dim];
next_ptr[dim] = index&bit ?
*next_dim&0x8000 // The neighboring node is in the neighboring set
? // The next set has to be descended
(node_to_set(next_dim)+(*next_dim&0x7FFF))->x+(index&~bit)
: next_dim
: set_ptr->x+(index|bit); // The neighboring node is in the current set
if (!dim)
break;
}
}
done:
if (!level)
break;
if (!(index = index_stack[--level]))
goto done;
--index;
set_ptr = set_stack[level];
memcpy(next_ptr, next_stack+level, sizeof(*next_stack));
goto update;
}
}
I have tested the function and it gives expected results for several test cases including this one:
Octree *octree = memset(malloc(sizeof(Octree)), 0, sizeof(Octree));
octree = octree_set(octree, 1, 1, 0, 0);
octree = octree_set(octree, 2, 2, 0, 0);
Octree *blank = &(Octree){0};
octree_render(octree, (const Octree *const []){blank, blank, blank});
free(octree);
Some concerns:
Spaghetti code: I find my code difficult to follow due to the nested loops and
goto
statements, but this is the only way I figured out how to achieve the logic that does not involve any redundant tests. Is there a clearer way to express the loop logic? Ifgoto
is still needed that is OK but its current state seems unnecessarily complex.How can I avoid having to modify
octree_next
(as pointed to bynext_ptr
) and how can I avoid having to usememcpy
to copy data to and fromnext_ptr
andnext_stack
? There should be a way to changenext_ptr
to reference the top ofnext_stack
somehow but I could not get that to work out. (The initial value it points to might (?) not be needed after the first iteration).
octree_render()
did not use any casts. It is going to take some time to get through that to recommend alternatives. Unnecessary cast are indicator, though not a certain proof, of weak/high maintenance code. \$\endgroup\$&octree->base
would be better than(const uint16_t *)octree
. \$\endgroup\$