#include "test_octree.h" #include "../constants/cube_tables.h" #include "../terrain/variable_lod/lod_octree.h" #include "../util/math/conv.h" #include "../util/profiling_clock.h" #include "testing.h" #include #include #include #include namespace zylann::voxel::tests { void test_octree_update() { static const float lod_distance = 80; const float view_distance = 1024; static const int lod_count = 6; static const int block_size = 16; const Vector3 block_size_v(block_size, block_size, block_size); Vector3 viewer_pos = Vector3(100, 50, 200); const int octree_size = block_size << (lod_count - 1); // Testing as an octree forest, as it is the way they are used in VoxelLodTerrain std::map octrees; const Box3i viewer_box_voxels = Box3i::from_center_extents(math::floor_to_int(viewer_pos), Vector3iUtil::create(view_distance)); const Box3i viewer_box_octrees = viewer_box_voxels.downscaled(octree_size); viewer_box_octrees.for_each_cell([&octrees](Vector3i pos) { std::pair::iterator, bool> p = octrees.insert({ pos, LodOctree() }); ZN_ASSERT(p.second); LodOctree &octree = p.first->second; LodOctree::NoDestroyAction nda; octree.create(lod_count, nda); }); struct OctreeActions { int created_count = 0; int destroyed_count = 0; Vector3 viewer_pos_octree_space; float lod_distance_octree_space; void create_child(Vector3i node_pos, int lod_index, LodOctree::NodeData &data) { ++created_count; } void destroy_child(Vector3i node_pos, int lod_index) { ++destroyed_count; } void show_parent(Vector3i node_pos, int lod_index) {} void hide_parent(Vector3i node_pos, int lod_index) {} bool can_create_root(int lod_index) { return true; } bool can_split(Vector3i node_pos, int lod_index, LodOctree::NodeData &data) { return LodOctree::is_below_split_distance( node_pos, lod_index, viewer_pos_octree_space, lod_distance_octree_space); } bool can_join(Vector3i node_pos, int parent_lod_index) { return !LodOctree::is_below_split_distance( node_pos, parent_lod_index, viewer_pos_octree_space, lod_distance_octree_space); } }; int initial_block_count = 0; ProfilingClock profiling_clock; // Initial for (std::map::iterator it = octrees.begin(); it != octrees.end(); ++it) { LodOctree &octree = it->second; const Vector3i block_pos_maxlod = it->first; const Vector3i block_offset_lod0 = block_pos_maxlod << (lod_count - 1); const Vector3 relative_viewer_pos = viewer_pos - block_size_v * Vector3(block_offset_lod0); OctreeActions actions; actions.viewer_pos_octree_space = viewer_pos / block_size; actions.lod_distance_octree_space = lod_distance / block_size; octree.update(actions); initial_block_count += actions.created_count; ZYLANN_TEST_ASSERT(actions.destroyed_count == 0); } const int time_init = profiling_clock.restart(); int initial_block_count_rec = 0; for (std::map::iterator it = octrees.begin(); it != octrees.end(); ++it) { const LodOctree &octree = it->second; initial_block_count_rec += octree.get_node_count(); } print_line(String("Initial block count: {0}, time: {1} us").format(varray(initial_block_count, time_init))); ZYLANN_TEST_ASSERT(initial_block_count > 0); ZYLANN_TEST_ASSERT(initial_block_count == initial_block_count_rec); // Updates without moving int created_block_count = 0; int destroyed_block_count = 0; for (int i = 0; i < 10; ++i) { profiling_clock.restart(); created_block_count = 0; for (std::map::iterator it = octrees.begin(); it != octrees.end(); ++it) { LodOctree &octree = it->second; const Vector3i block_pos_maxlod = it->first; const Vector3i block_offset_lod0 = block_pos_maxlod << (lod_count - 1); const Vector3 relative_viewer_pos = viewer_pos - block_size_v * Vector3(block_offset_lod0); OctreeActions actions; actions.viewer_pos_octree_space = viewer_pos / block_size; actions.lod_distance_octree_space = lod_distance / block_size; octree.update(actions); created_block_count += actions.created_count; destroyed_block_count += actions.destroyed_count; } const int time_stay = profiling_clock.restart(); // Block count should not change ZYLANN_TEST_ASSERT(created_block_count == 0); ZYLANN_TEST_ASSERT(destroyed_block_count == 0); print_line(String("Stay time: {0} us").format(varray(time_stay))); } // Clearing int block_count = initial_block_count; for (std::map::iterator it = octrees.begin(); it != octrees.end(); ++it) { LodOctree &octree = it->second; struct DestroyAction { int destroyed_blocks = 0; inline void operator()(Vector3i node_pos, int lod) { ++destroyed_blocks; } }; DestroyAction da; octree.clear(da); block_count -= da.destroyed_blocks; } ZYLANN_TEST_ASSERT(block_count == 0); } void test_octree_find_in_box() { const int blocks_across = 32; const int block_size = 16; int lods = 0; { int diameter = blocks_across; while (diameter > 0) { diameter = diameter >> 1; lods += 1; } //print_line(String("Lod count: {0}").format(varray(lods))); } // Build a fully populated octree with all its leaves at LOD0 LodOctree octree; LodOctree::NoDestroyAction nda; octree.create(lods, nda); struct SubdivideActions { bool can_split(Vector3i node_pos, int lod_index, const LodOctree::NodeData &node_data) { return true; } void create_child(Vector3i pos, int lod_index, LodOctree::NodeData &node_data) { node_data.state = pos.x + pos.y + pos.z; } }; SubdivideActions sa; octree.subdivide(sa); std::unordered_map> expected_positions; const Box3i full_box(Vector3i(), Vector3i(blocks_across, blocks_across, blocks_across)); // Build expected result full_box.for_each_cell([full_box, &expected_positions](Vector3i pos) { Box3i area_box(pos - Vector3i(1, 1, 1), Vector3i(3, 3, 3)); area_box.clip(full_box); auto insert_result = expected_positions.insert(std::make_pair(pos, std::unordered_set())); ERR_FAIL_COND(insert_result.second == false); std::unordered_set &area_positions = insert_result.first->second; area_box.for_each_cell([&area_positions](Vector3i npos) { auto it = area_positions.insert(npos); ZYLANN_TEST_ASSERT(it.second == true); }); }); // Get octree results int checksum = 0; full_box.for_each_cell([&octree, &expected_positions, &checksum](Vector3i pos) { const Box3i area_box(pos - Vector3i(1, 1, 1), Vector3i(3, 3, 3)); auto it = expected_positions.find(pos); ZYLANN_TEST_ASSERT(it != expected_positions.end()); const std::unordered_set &expected_area_positions = it->second; std::unordered_set found_positions; octree.for_leaves_in_box(area_box, [&found_positions, &expected_area_positions, &checksum]( Vector3i node_pos, int lod, const LodOctree::NodeData &node_data) { auto insert_result = found_positions.insert(node_pos); // Must be one of the expected positions ZYLANN_TEST_ASSERT(expected_area_positions.find(node_pos) != expected_area_positions.end()); // Must not be a duplicate ZYLANN_TEST_ASSERT(insert_result.second == true); checksum += node_data.state; }); }); // Doing it again just to measure time { ProfilingClock profiling_clock; int checksum2 = 0; full_box.for_each_cell([&octree, &expected_positions, &checksum2](Vector3i pos) { const Box3i area_box(pos - Vector3i(1, 1, 1), Vector3i(3, 3, 3)); octree.for_leaves_in_box( area_box, [&checksum2](Vector3i node_pos, int lod, const LodOctree::NodeData &node_data) { checksum2 += node_data.state; }); }); ZYLANN_TEST_ASSERT(checksum2 == checksum); const int for_each_cell_time = profiling_clock.restart(); const float single_query_time = float(for_each_cell_time) / Vector3iUtil::get_volume(full_box.size); print_line(String("for_each_cell time with {0} lods: total {1} us, single query {2} us, checksum: {3}") .format(varray(lods, for_each_cell_time, single_query_time, checksum2))); } } } // namespace zylann::voxel::tests