Optimize octree further by taking out children pointers. Nodes are only 5 bytes now.
This commit is contained in:
parent
0ee054dc3c
commit
98299f2b3c
@ -9,11 +9,19 @@
|
||||
template <class T>
|
||||
class LodOctree {
|
||||
public:
|
||||
static const unsigned int NO_CHILDREN = -1;
|
||||
static const unsigned int ROOT_INDEX = -1;
|
||||
static const unsigned int MAX_LOD = 32;
|
||||
|
||||
struct Node {
|
||||
Node *children[8];
|
||||
// Index to first child node within the node pool.
|
||||
// The 7 next indexes are the other children.
|
||||
// If the node isn't subdivided, it is set to NO_CHILDREN.
|
||||
// Could have used a pointer but an index is enough, occupies half memory and is immune to realloc
|
||||
unsigned int first_child;
|
||||
|
||||
// Whatever data to associate to the node, when it's a leaf.
|
||||
// It needs to be booleanizable, where `true` means presence of data, and `false` means no data.
|
||||
// Typically a pointer, but can be a straight boolean too.
|
||||
T block;
|
||||
|
||||
// Node positions are calculated on the fly to save memory,
|
||||
@ -25,18 +33,56 @@ public:
|
||||
}
|
||||
|
||||
inline bool has_children() const {
|
||||
return children[0] != nullptr;
|
||||
return first_child != NO_CHILDREN;
|
||||
}
|
||||
|
||||
inline void init() {
|
||||
block = T();
|
||||
children[0] = nullptr;
|
||||
// for (int i = 0; i < 8; ++i) {
|
||||
// children[i] = nullptr;
|
||||
// }
|
||||
first_child = NO_CHILDREN;
|
||||
}
|
||||
};
|
||||
|
||||
// This pool treats nodes as packs of 8 so they can be addressed by only knowing the first child
|
||||
class NodePool {
|
||||
public:
|
||||
// Warning: the returned pointer may be invalidated later by `allocate_children`. Use with care.
|
||||
inline Node *get_node(unsigned int i) {
|
||||
CRASH_COND(i >= _nodes.size());
|
||||
return &_nodes[i];
|
||||
}
|
||||
|
||||
unsigned int allocate_children() {
|
||||
if (_free_indexes.size() == 0) {
|
||||
unsigned int i0 = _nodes.size();
|
||||
_nodes.resize(i0 + 8);
|
||||
return i0;
|
||||
} else {
|
||||
unsigned int i0 = _free_indexes[_free_indexes.size() - 1];
|
||||
_free_indexes.pop_back();
|
||||
return i0;
|
||||
}
|
||||
}
|
||||
|
||||
// Warning: this is not recursive. Use it properly.
|
||||
void recycle_children(unsigned int i0) {
|
||||
|
||||
// Debug check, there is no use case in recycling a node which is not a first child
|
||||
CRASH_COND(i0 % 8 != 0);
|
||||
|
||||
for (unsigned int i = 0; i < 8; ++i) {
|
||||
_nodes[i0 + i].init();
|
||||
}
|
||||
|
||||
_free_indexes.push_back(i0);
|
||||
}
|
||||
|
||||
private:
|
||||
// TODO If this grows too much, mayyybe could implement a paged vector to fight fragmentation.
|
||||
// If we do so, that may also solve pointer invalidation since pages would remain stable
|
||||
std::vector<Node> _nodes;
|
||||
std::vector<unsigned int> _free_indexes;
|
||||
};
|
||||
|
||||
struct NoDestroyAction {
|
||||
inline void operator()(Node *node, Vector3i node_pos, int lod) {}
|
||||
};
|
||||
@ -59,7 +105,7 @@ public:
|
||||
|
||||
template <typename A>
|
||||
void create_from_lod_count(int base_size, unsigned int lod_count, A &destroy_action) {
|
||||
ERR_FAIL_COND(lod_count > 32);
|
||||
ERR_FAIL_COND(lod_count > MAX_LOD);
|
||||
clear(destroy_action);
|
||||
_base_size = base_size;
|
||||
_max_depth = lod_count - 1;
|
||||
@ -99,7 +145,7 @@ public:
|
||||
void update(Vector3 view_pos, A &create_action, B &destroy_action) {
|
||||
|
||||
if (_root.block || _root.has_children()) {
|
||||
update(&_root, Vector3i(), _max_depth, view_pos, create_action, destroy_action);
|
||||
update(ROOT_INDEX, Vector3i(), _max_depth, view_pos, create_action, destroy_action);
|
||||
|
||||
} else {
|
||||
// Treat the root in a slightly different way the first time.
|
||||
@ -109,10 +155,10 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
template <typename A>
|
||||
void foreach_node(A &action) {
|
||||
action(action, &_root, _max_depth);
|
||||
}
|
||||
// template <typename A>
|
||||
// void foreach_node(A &action) {
|
||||
// action(action, &_root, _max_depth);
|
||||
// }
|
||||
|
||||
static inline Vector3i get_child_position(Vector3i parent_position, int i) {
|
||||
return Vector3i(
|
||||
@ -122,37 +168,51 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename A>
|
||||
void foreach_node(A action, Node *node, int lod) {
|
||||
action(node, lod);
|
||||
if (node->has_children()) {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
foreach_node(action, node->children[i], lod - 1);
|
||||
}
|
||||
// template <typename A>
|
||||
// void foreach_node(A action, Node *node, int lod) {
|
||||
// action(node, lod);
|
||||
// if (node->has_children()) {
|
||||
// for (int i = 0; i < 8; ++i) {
|
||||
// foreach_node(action, node->children[i], lod - 1);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
inline Node *get_node(unsigned int index) {
|
||||
if (index == ROOT_INDEX) {
|
||||
return &_root;
|
||||
} else {
|
||||
return _pool.get_node(index);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename A, typename B>
|
||||
void update(Node *node, Vector3i node_pos, int lod, Vector3 view_pos, A &create_action, B &destroy_action) {
|
||||
void update(unsigned int node_index, Vector3i node_pos, int lod, Vector3 view_pos, A &create_action, B &destroy_action) {
|
||||
// This function should be called regularly over frames.
|
||||
|
||||
int lod_factor = get_lod_factor(lod);
|
||||
int chunk_size = _base_size * lod_factor;
|
||||
Vector3 world_center = static_cast<real_t>(chunk_size) * (node_pos.to_vec3() + Vector3(0.5, 0.5, 0.5));
|
||||
float split_distance = chunk_size * _split_scale;
|
||||
Node *node = get_node(node_index);
|
||||
|
||||
if (!node->has_children()) {
|
||||
|
||||
// If it's not the last LOD, if close enough and custom conditions get fulfilled
|
||||
if (lod > 0 && world_center.distance_to(view_pos) < split_distance && create_action.can_do_children(node, node_pos, lod - 1)) {
|
||||
// Split
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
|
||||
Node *child = _pool.create();
|
||||
unsigned int first_child = _pool.allocate_children();
|
||||
// Get node again because `allocate_children` may invalidate the pointer
|
||||
node = get_node(node_index);
|
||||
node->first_child = first_child;
|
||||
|
||||
for (unsigned int i = 0; i < 8; ++i) {
|
||||
|
||||
Node *child = _pool.get_node(first_child + i);
|
||||
|
||||
child->block = create_action(child, get_child_position(node_pos, i), lod - 1);
|
||||
|
||||
node->children[i] = child;
|
||||
// If the node needs to split more, we'll ask more recycling at the next frame...
|
||||
// That means the initialization of the game should do some warm up and fetch all leaves,
|
||||
// otherwise it's gonna be rough
|
||||
@ -167,25 +227,29 @@ private:
|
||||
} else {
|
||||
|
||||
bool has_split_child = false;
|
||||
unsigned int first_child = node->first_child;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Node *child = node->children[i];
|
||||
update(child, get_child_position(node_pos, i), lod - 1, view_pos, create_action, destroy_action);
|
||||
has_split_child |= child->has_children();
|
||||
for (unsigned int i = 0; i < 8; ++i) {
|
||||
unsigned int child_index = first_child + i;
|
||||
update(child_index, get_child_position(node_pos, i), lod - 1, view_pos, create_action, destroy_action);
|
||||
has_split_child |= _pool.get_node(child_index)->has_children();
|
||||
}
|
||||
|
||||
// Get node again because `update` may invalidate the pointer
|
||||
node = get_node(node_index);
|
||||
|
||||
if (!has_split_child && world_center.distance_to(view_pos) > split_distance && destroy_action.can_do(node, node_pos, lod)) {
|
||||
// Join
|
||||
if (node->has_children()) {
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Node *child = node->children[i];
|
||||
for (unsigned int i = 0; i < 8; ++i) {
|
||||
Node *child = _pool.get_node(first_child + i);
|
||||
destroy_action(child, get_child_position(node_pos, i), lod - 1);
|
||||
child->block = T();
|
||||
_pool.recycle(child);
|
||||
}
|
||||
|
||||
node->children[0] = nullptr;
|
||||
_pool.recycle_children(first_child);
|
||||
node->first_child = NO_CHILDREN;
|
||||
|
||||
// If this is true, means the parent wasn't properly split.
|
||||
// When subdividing a node, that node's block must be destroyed as it is replaced by its children.
|
||||
@ -199,22 +263,23 @@ private:
|
||||
|
||||
template <typename A>
|
||||
void join_all_recursively(Node *node, Vector3i node_pos, int lod, A &destroy_action) {
|
||||
// We can use pointers here because we won't allocate new nodes,
|
||||
// and won't shrink the node pool either
|
||||
|
||||
if (node->has_children()) {
|
||||
Node **children = node->children;
|
||||
unsigned int first_child = node->first_child;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Node *child = children[i];
|
||||
for (unsigned int i = 0; i < 8; ++i) {
|
||||
Node *child = _pool.get_node(first_child + i);
|
||||
join_all_recursively(child, get_child_position(node_pos, i), lod - 1, destroy_action);
|
||||
_pool.recycle(child);
|
||||
children[0] = nullptr;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (node->block) {
|
||||
destroy_action(node, node_pos, lod);
|
||||
node->block = T();
|
||||
}
|
||||
_pool.recycle_children(first_child);
|
||||
node->first_child = NO_CHILDREN;
|
||||
|
||||
} else if (node->block) {
|
||||
destroy_action(node, node_pos, lod);
|
||||
node->block = T();
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,7 +288,7 @@ private:
|
||||
float _base_size = 16;
|
||||
float _split_scale = 2.0;
|
||||
// TODO May be worth making this pool external for sharing purpose
|
||||
ObjectPool<Node> _pool;
|
||||
NodePool _pool;
|
||||
};
|
||||
|
||||
// Notes:
|
||||
|
@ -664,8 +664,7 @@ void VoxelLodTerrain::_process() {
|
||||
void operator()(LodOctree<bool>::Node *node, Vector3i node_pos, unsigned int lod_index) {
|
||||
Lod &lod = self->_lods[lod_index];
|
||||
|
||||
Vector3i bpos = node_pos;
|
||||
bpos += block_offset_lod0 >> lod_index;
|
||||
Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
||||
|
||||
VoxelBlock *block = lod.map->get_block(bpos);
|
||||
if (block) {
|
||||
@ -787,8 +786,7 @@ void VoxelLodTerrain::_process() {
|
||||
|
||||
Lod &lod = self->_lods[lod_index];
|
||||
|
||||
Vector3i bpos = node_pos;
|
||||
bpos += block_offset_lod0 >> lod_index;
|
||||
Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
||||
|
||||
VoxelBlock *block = lod.map->get_block(bpos);
|
||||
|
||||
@ -808,8 +806,7 @@ void VoxelLodTerrain::_process() {
|
||||
bool can_do(LodOctree<bool>::Node *node, Vector3i node_pos, unsigned int parent_lod_index) {
|
||||
|
||||
// Can only unsubdivide if the parent mesh is ready
|
||||
Vector3i bpos = node_pos;
|
||||
bpos += block_offset_lod0 >> parent_lod_index;
|
||||
Vector3i bpos = node_pos + (block_offset_lod0 >> parent_lod_index);
|
||||
|
||||
bool can = self->check_block_loaded_and_updated(bpos, parent_lod_index);
|
||||
|
||||
@ -824,8 +821,7 @@ void VoxelLodTerrain::_process() {
|
||||
|
||||
Lod &lod = self->_lods[lod_index];
|
||||
|
||||
Vector3i bpos = node_pos;
|
||||
bpos += block_offset_lod0 >> lod_index;
|
||||
Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
||||
|
||||
VoxelBlock *block = lod.map->get_block(bpos);
|
||||
if (block) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user