// PlaneTree is an octree-like data structure. // // THE COORDINATE SPACE // // The tree stores objects which have (x,y,z) coordinates where each of x,y,z is // an unsigned 16-bit integer. The API uses floating-point coordinates, but // these are converted to uint16_t for use in the tree. // // In an octree, each node has a 2x2x2 array of children (total: 8 children). In // this tree, however, each node has a 4x4x4 array of children (total: 64 // children). // // The depth of the tree is fixed. It always has one leaf level (level 0), plus // 8 internal levels (levels 1-8). The root of the tree is level 8. Tangibles // are always are always stored in the leaf-level. Since there are 8 // subdivisions, at 4x4x4 per subdivision, that divides the space into 65536 x // 65536 x 65536. Hence the 16-bit integer coordinates. // // Here are the sizes of the levels: // // * Level 8 (internal) is 1 cubed. // * Level 7 (internal) is 4 cubed. // * Level 6 (internal) is 16 cubed. // * Level 5 (internal) is 64 cubed. // * Level 4 (internal) is 256 cubed. // * Level 3 (internal) is 1024 cubed. // * Level 2 (internal) is 4096 cubed. // * Level 1 (internal) is 16385 cubed. // * Level 0 (leaf) is 65536 cubed. // // NODE IDS AND THE NODE TABLES // // Every tree node has a unique "NodeID", which consists of it's level and the // lowest (x,y,z) coordinate in its region. Every tree node is stored in a hash // table that maps NodeID to the data for that tree node. // // Tree nodes don't contain any child pointers or parent pointers. Instead, you // can calculate the NodeIDs of the parent and children from the NodeID of a // node. So, you can just look up parents and children in the hash table. Even // though PlaneTree really is a tree, it doesn't use pointers at all. // // There are two separate hash tables - one for storing internal nodes, and one // for storing leaf nodes. The only data stored in an internal node is a // bitvector indicating which of its 64 children are nonempty. The only data // stored in a leaf node is a list of PlaneItems. The list of PlaneItems is // stored as an intrusive doubly-linked circular list. // // THE PLANETREE BOUNDING BOX, OUTLIERS, AND ADAPTIVE RESIZING // // The planetree as a whole has a bounding box. For now, the bounding box is // fixed at (-65536, -65536, -65536) to (65536, 65536, 65536). That makes the // size of the leaf cells two meters. However, it is my plan to eventually have // the bounding box selected adaptively. // // "Outliers" are objects that are outside the PlaneTree's bounding box. When an // object is an outlier, its (X,Y,Z) coordinates are clamped to the tree's // bounding box for purposes of storing it in the tree. This puts all the // outliers into the cells on the edge of the tree. When we need to scan a // region that includes areas outside the tree's bounding box, we scan the edge // cells. // // The tree keeps a count of the number of outliers. The intended use for these // outlier counts is to implement adaptive logic that says "are there too many // outliers? If so, expand the tree's bounding box." But we haven't written the // adaptive algorithm yet, so the outlier counts don't serve any purpose yet. // // We probably *don't* want to capture 100% of the outliers. It seems likely // that the programmer will deliberately put a few objects way off in the // distance. For example, you might imagine somebody creating a "LoginManager" // object and putting it way off at (1000000, 0, 0) in order to get it out of // the way. If we were to expand the bounding box to include such objects, we // would make the tree's bounding box unreasonably large. I have an idea for a // heuristic: expand the bounding box until it covers 90% of the tangibles, then // expand it by 25% more, then stop. I have another idea for a different // heuristic: keep expanding the bounding box as long as making a small // increment will capture significantly more tangibles, then stop. I don't know // if either of those heuristics will work well. // // Any adaptive algorithm we create must take another factor into account: we // don't want the cell size to be too small. Overly small cell sizes create lots // of extra work when an object moves around. So sometimes, it's advantageous // to deliberately expand the bounding box way beyond what is actually needed to // capture the outliers, just to make the cell size large enough. Doing this // isn't wasteful: octrees are very good at handling big empty spaces inside the // bounding box. As long as the cell size is large enough, but not too large, // and there aren't too many outliers, everything will work well. Unfortunately, // I haven't devised a good heuristic to decide when the cell size is "right." // // Another thought I have, periodically, is that writing a lot of logic just to // choose a cell size seems like a lot of unnecessary work: we could just // configure the cell size for the game in the script. // // BYPASSING THE TREE // // In many cases, it's possible to jump directly to a leaf node and start // processing. For example, let's say you want to move a tangible. You know // its current position, therefore, you know exactly the NodeID of the leaf node // where it is stored. There is no need to walk the tree to get there: you can // just access it directly using the hash table. // #include "luastack.hpp" #include "util.hpp" #include "planemap.hpp" #include #include using NodeID = uint64_t; using ChildBits = uint64_t; using IdVector = util::IdVector; // These need to be static floats to encourage gcc to generate // efficient code in NodeInfo. static float k_lo = 0.0f; static float k_hi = 65535.0f; static constexpr ChildBits child_bit(int i) { return (uint64_t(1) << i); } static constexpr ChildBits child_bit(int x, int y, int z) { return child_bit((x << 4) | (y << 2) | z); } static constexpr uint8_t node_get_level(NodeID node) { return node >> 48; } static constexpr uint16_t node_get_x(NodeID node) { return uint16_t(node >> 32); } static constexpr uint16_t node_get_y(NodeID node) { return uint16_t(node >> 16); } static constexpr uint16_t node_get_z(NodeID node) { return uint16_t(node >> 0); } static constexpr NodeID node_lxyz(uint8_t level, uint16_t x, uint16_t y, uint16_t z) { return (NodeID(level) << 48) | (NodeID(x) << 32) | (NodeID(y) << 16) | (NodeID(z) << 0); } static constexpr NodeID node_parent(NodeID node) { uint8_t level = node_get_level(node); return ((node & node_lxyz(0, 0xFFFC, 0xFFFC, 0xFFFC)) >> 2) | node_lxyz(level + 1, 0, 0, 0); } static constexpr uint8_t node_childindex(NodeID node) { uint64_t masked = node & node_lxyz(0, 0x0003, 0x0003, 0x0003); return (masked >> 0) | (masked >> 14) | (masked >> 28); } static constexpr ChildBits node_childbit(NodeID node) { return child_bit(node_childindex(node)); } static constexpr NodeID node_child(NodeID node, uint16_t x, uint16_t y, uint16_t z) { int level = node_get_level(node); return ((node & node_lxyz(0, 0xFFFF, 0xFFFF, 0xFFFF)) << 2) | node_lxyz(level - 1, x, y, z); } static constexpr NodeID node_nthchild(NodeID node, int i) { return node_child(node, (i >> 4) & 3, (i >> 2) & 3, (i >> 0) & 3); } static void print_node_id(NodeID node, std::ostream *os) { int level = node_get_level(node); if (level >= 8) { (*os) << "L" << level << ":root"; return; } uint16_t x = node_get_x(node); uint16_t y = node_get_y(node); uint16_t z = node_get_z(node); auto fmt = util::hex.width(4 - (level >> 1)).fill('0'); (*os) << "L" << level << ":" << fmt.val(x) << "," << fmt.val(y) << "," << fmt.val(z); } enum BBoxCheck { INSIDE_BBOX, JUST_OUTSIDE_BBOX, WAY_OUTSIDE_BBOX }; struct NodeInfo { NodeID node; BBoxCheck bbcheck; NodeInfo(float scale, float x, float y, float z) { float sx = (x * scale); float sy = (y * scale); float sz = (z * scale); float dist = std::max({std::abs(sx), std::abs(sy), std::abs(sz)}); if (dist >= 32768.0f) { bbcheck = (dist > (32768.0f + 8192.0f)) ? WAY_OUTSIDE_BBOX : JUST_OUTSIDE_BBOX; float clampx = std::min(k_hi, std::max(k_lo, sx + 32768.0f)); float clampy = std::min(k_hi, std::max(k_lo, sy + 32768.0f)); float clampz = std::min(k_hi, std::max(k_lo, sz + 32768.0f)); node = node_lxyz(0, clampx, clampy, clampz); } else { node = node_lxyz(0, sx + 32768.0f, sy + 32768.0f, sz + 32768.0f); bbcheck = INSIDE_BBOX; } } }; template struct TreeLevel { constexpr static int child() { return LEVEL - 1; } }; template <> struct TreeLevel<0> { constexpr static int child() { return 0; } }; // Class PlaneTree. Everything here is 'public', but this class // is only visible inside this one C++ file. class PlaneTree : public eng::opnew { public: void set_radius(float r); using NodeID = uint64_t; using ChildBits = uint64_t; // The PlaneMap that this tree is a part of. PlaneMap *planemap_; // The name of this plane. std::string plane_; // Internal nodes in the tree just have bits indicating // which children exist. eng::bytell_hash_map internal_nodes_; // Leaf nodes in the tree contain a doubly-linked // intrusive ring. eng::bytell_hash_map leaf_nodes_; // The radius of the bounding box. double radius_; // A conversion factor to convert float coordinates to // integral coordinates. Equal to 32k / radius. float scale_; // total number of items in the planetree. int total_count_; // total number of outliers in the planetree. Outliers are // classified as just outside or way outside the bbox. int just_outside_bbox_; int way_outside_bbox_; public: // The following state is initialized whenever we do a scan. // It is only relevant during the scan. const PlaneScan *scan_config_; IdVector *scan_result_; util::XYZ scan_lo_; util::XYZ scan_hi_; util::XYZ scan_invradius_; int scan_bbxlo_[9], scan_bbxhi_[9]; int scan_bbylo_[9], scan_bbyhi_[9]; int scan_bbzlo_[9], scan_bbzhi_[9]; public: ChildBits get_internal_node(NodeID id) const { auto iter = internal_nodes_.find(id); if (iter == internal_nodes_.end()) return 0; return iter->second; } PlaneItem *get_leaf_node(NodeID id) const { auto iter = leaf_nodes_.find(id); if (iter == leaf_nodes_.end()) return 0; return iter->second; } // Untrack all planeitems. This is for unit testing and for destructors. We // don't use PlaneItem::untrack, because that would create problems with // removing items from a list while iterating over that list. void untrack_all() { for (auto &l : leaf_nodes_) { PlaneItem *first = l.second; PlaneItem *pi = first; assert(pi != nullptr); while (true) { PlaneItem *next = pi->next_; pi->tree_= nullptr; pi = next; if (pi == first) break; } } leaf_nodes_.clear(); internal_nodes_.clear(); } // This just sets the radius. // It verifies that the tree is empty first. void set_radius_of_empty_tree(float r) { assert(total_count_ == 0); radius_ = r; scale_ = 32768.0 / r; } // Get a PlaneTree by plane name. static PlaneTree *get(PlaneMap *pmap, const eng::string &plane) { std::unique_ptr &result = pmap->planes_[plane]; if (result == nullptr) { result.reset(new PlaneTree(pmap, plane)); } return result.get(); } // Remove an item from the specified leaf node. // Returns true if we removed the last item from the leaf. bool remove_planeitem_from_leaf(NodeID node, PlaneItem *item) { if (item->next_ == item) { leaf_nodes_.erase(node); // Note: these next two assignments are only needed for sanity checking. item->next_ = nullptr; item->prev_ = nullptr; return true; } else { item->prev_->next_ = item->next_; item->next_->prev_ = item->prev_; // Note: these next two assignments are only needed for sanity checking. item->next_ = nullptr; item->prev_ = nullptr; return false; } } // Insert an item into the specified leaf node. // Returns true if we inserted the first item into the leaf. bool insert_planeitem_into_leaf(NodeID node, PlaneItem *item) { PlaneItem *&newcell = leaf_nodes_[node]; if (newcell == nullptr) { newcell = item; item->next_ = item; item->prev_ = item; return true; } else { PlaneItem *next = newcell; PlaneItem *prev = newcell->prev_; item->next_ = next; item->prev_ = prev; prev->next_ = item; next->prev_ = item; return false; } } // Update the parent to reflect the fact that the child was added. // This will propagage all the way up the tree. void insert_child_into_childbits(NodeID child) { uint8_t childlevel = node_get_level(child); uint8_t parentlevel = childlevel + 1; NodeID parent = node_parent(child); ChildBits &inode = internal_nodes_[parent]; bool waszero = (inode == 0); inode |= node_childbit(child); if (waszero) { if (parentlevel < 8) insert_child_into_childbits(parent); } } // Update the parent to reflect the fact that the child was removed. // This will propagage all the way up the tree. void remove_child_from_childbits(NodeID child) { uint8_t childlevel = node_get_level(child); uint8_t parentlevel = childlevel + 1; NodeID parent = node_parent(child); auto iter = internal_nodes_.find(parent); assert(iter != internal_nodes_.end()); iter->second &= (~node_childbit(child)); if (iter->second == 0) { internal_nodes_.erase(iter); if (parentlevel < 8) remove_child_from_childbits(parent); } } // Update the counters to reflect the removal of one item from the tree. void decrement_planeitem_counters(BBoxCheck bbcheck) { total_count_ -= 1; if (bbcheck == JUST_OUTSIDE_BBOX) just_outside_bbox_ -= 1; if (bbcheck == WAY_OUTSIDE_BBOX) way_outside_bbox_ -= 1; } // Update the counters to reflect the insertion of one item into the tree. void increment_planeitem_counters(BBoxCheck bbcheck) { total_count_ += 1; if (bbcheck == JUST_OUTSIDE_BBOX) just_outside_bbox_ += 1; if (bbcheck == WAY_OUTSIDE_BBOX) way_outside_bbox_ += 1; } // Remove a planeitem from whatever tree it is in, preserving // all invariants. The planeitem ends up being an untracked PlaneItem. static void remove_planeitem(PlaneItem *item) { PlaneTree *tree = item->tree_; assert(tree != nullptr); NodeInfo info(tree->scale_, item->x_, item->y_, item->z_); tree->decrement_planeitem_counters(info.bbcheck); if (tree->remove_planeitem_from_leaf(info.node, item)) { tree->remove_child_from_childbits(info.node); } item->tree_ = nullptr; } // Insert a planeitem into whatever tree is specified, preserving // all invariants. The planeitem must be an untracked PlaneItem. void insert_planeitem(PlaneItem *item) { PlaneTree *tree = this; assert(item->tree_ == nullptr); NodeInfo info(tree->scale_, item->x_, item->y_, item->z_); tree->increment_planeitem_counters(info.bbcheck); if (tree->insert_planeitem_into_leaf(info.node, item)) { tree->insert_child_into_childbits(info.node); } item->tree_ = tree; item->plane_ = tree->plane_; } void print_indented_internal_node(NodeID node, ChildBits cb, std::ostream *os) { int level = node_get_level(node); int indent = 8 - level; (*os) << "|"; for (int i = 0; i < indent; i++) (*os) << " "; print_node_id(node, os); if ((cb == 0) && (level != 8)) { (*os) << " (invalid empty node)"; } } void print_indented_leaf_node(NodeID node, PlaneItem *first, std::ostream *os) { (*os) << "| "; print_node_id(node, os); (*os) << " "; IdVector ids; collect_planeitem_ids(first, &ids); std::sort(ids.begin(), ids.end()); util::print_id_vector(ids, os); } void print_tree_r(NodeID node, std::ostream *os) { int level = node_get_level(node); if (level == 0) { print_indented_leaf_node(node, get_leaf_node(node), os); } else { ChildBits cb = get_internal_node(node); if ((level & 1) == 0) { print_indented_internal_node(node, cb, os); } for (int i = 0; i < 64; i++) { if (cb & child_bit(i)) { NodeID child = node_nthchild(node, i); assert(node_childindex(child) == i); print_tree_r(child, os); } } } } // The final filtering step sometimes uses the inverse of the // radius. In the case that the radius is 0, we want to use a huge // number for the inverse radius, but not infinity, because using infinity // would result in the final filtering step calculating (inf*0). // In the case that the radius is infinite, we want to use zero for the // inverse radius. static float inverse_radius(float f) { if (f == 0) return std::numeric_limits::max(); if (std::isinf(f)) return 0; return 1.0f / f; } // Given a PlaneScan, calculate the search bboxes, // and all the other related configuration data. void calculate_search_bboxes(const PlaneScan &sc) { scan_config_ = ≻ scan_lo_ = sc.center_ - sc.radius_; scan_hi_ = sc.center_ + sc.radius_; scan_invradius_.x = inverse_radius(sc.radius_.x); scan_invradius_.y = inverse_radius(sc.radius_.y); scan_invradius_.z = inverse_radius(sc.radius_.z); // Convert the scan's bounding box to integral coordinates. NodeInfo bblo(scale_, scan_lo_.x, scan_lo_.y, scan_lo_.z); NodeInfo bbhi(scale_, scan_hi_.x, scan_hi_.y, scan_hi_.z); // Calculate the bounding box at each level of the tree. NodeID ibblo = bblo.node; NodeID ibbhi = bbhi.node; for (int i = 0; i <= 8; i++) { scan_bbxlo_[i] = node_get_x(ibblo); scan_bbylo_[i] = node_get_y(ibblo); scan_bbzlo_[i] = node_get_z(ibblo); scan_bbxhi_[i] = node_get_x(ibbhi); scan_bbyhi_[i] = node_get_y(ibbhi); scan_bbzhi_[i] = node_get_z(ibbhi); ibblo = node_parent(ibblo); ibbhi = node_parent(ibbhi); } } // Calculate the size of the search bboxes. int64_t scan_xsize(int i) const { return 1 + scan_bbxhi_[i] - scan_bbxlo_[i]; } int64_t scan_ysize(int i) const { return 1 + scan_bbyhi_[i] - scan_bbylo_[i]; } int64_t scan_zsize(int i) const { return 1 + scan_bbzhi_[i] - scan_bbzlo_[i]; } eng::string search_bboxes_debug_string(const PlaneScan &scan) { calculate_search_bboxes(scan); eng::ostringstream oss; for (int i = 8; i >= 0; i -= 2) { auto fmt = util::hex.width((8 - i) / 2); oss << "|Level " << i << " "; oss << fmt.val(scan_bbxlo_[i]) << "," << fmt.val(scan_bbylo_[i]) << "," << fmt.val(scan_bbzlo_[i]) << " - " << fmt.val(scan_bbxhi_[i]) << "," << fmt.val(scan_bbyhi_[i]) << "," << fmt.val(scan_bbzhi_[i]); } return oss.str(); } static inline void scan_push_id(int64_t id, int64_t near, IdVector *result) { if (id != near) { result->push_back(id); } } void scan_planeitem(PlaneItem *pi) { switch (scan_config_->shape_) { case PlaneScan::BOX: { if ((pi->x() >= scan_lo_.x) && (pi->x() <= scan_hi_.x) && (pi->y() >= scan_lo_.y) && (pi->y() <= scan_hi_.y) && (pi->z() >= scan_lo_.z) && (pi->z() <= scan_hi_.z)) { scan_push_id(pi->id(), scan_config_->near_, scan_result_); } break; } case PlaneScan::SPHERE: { float dx = (pi->x() - scan_config_->center_.x) * scan_invradius_.x; float dy = (pi->y() - scan_config_->center_.y) * scan_invradius_.y; float dz = (pi->z() - scan_config_->center_.z) * scan_invradius_.z; if (dx*dx + dy*dy + dz*dz <= 1.0) { scan_push_id(pi->id(), scan_config_->near_, scan_result_); } break; } case PlaneScan::CYLINDER: { if ((pi->z() >= scan_lo_.z) && (pi->z() <= scan_hi_.z)) { float dx = (pi->x() - scan_config_->center_.x) * scan_invradius_.x; float dy = (pi->y() - scan_config_->center_.y) * scan_invradius_.y; if (dx*dx + dy*dy <= 1.0) { scan_push_id(pi->id(), scan_config_->near_, scan_result_); } } break; } } } // Recursive part of the planetree scan. // Note: template expansion terminates because // TreeLevel<0>::child returns zero again. template void scan_node(NodeID node, std::ostream *debug) { if (LEVEL == 0) { auto iter = leaf_nodes_.find(node); assert (iter != leaf_nodes_.end()); PlaneItem *first = iter->second; assert(first != nullptr); if (debug != nullptr) { print_indented_leaf_node(node, first, debug); } PlaneItem *pi = first; while (true) { PlaneItem *next = pi->next_; scan_planeitem(pi); if (next == first) break; pi = next; } } else { constexpr int CHILDLEVEL = TreeLevel::child(); NodeID firstchild = node_nthchild(node, 0); int xlo = std::max(0, scan_bbxlo_[CHILDLEVEL] - int(node_get_x(firstchild))); int ylo = std::max(0, scan_bbylo_[CHILDLEVEL] - int(node_get_y(firstchild))); int zlo = std::max(0, scan_bbzlo_[CHILDLEVEL] - int(node_get_z(firstchild))); int xhi = std::min(3, scan_bbxhi_[CHILDLEVEL] - int(node_get_x(firstchild))); int yhi = std::min(3, scan_bbyhi_[CHILDLEVEL] - int(node_get_y(firstchild))); int zhi = std::min(3, scan_bbzhi_[CHILDLEVEL] - int(node_get_z(firstchild))); auto iter = internal_nodes_.find(node); assert (iter != internal_nodes_.end()); ChildBits cb = iter->second; assert (cb != 0); if (debug != nullptr) { print_indented_internal_node(node, cb, debug); } for (int x = xlo; x <= xhi; x++) { for (int y = ylo; y <= yhi; y++) { for (int z = zlo; z <= zhi; z++) { if (cb & child_bit(x, y, z)) { NodeID child = node_child(node, x, y, z); scan_node(child, debug); } } } } } } // Scan a planetree. void scan(const PlaneScan &sc, IdVector *result, std::ostream *debug) { calculate_search_bboxes(sc); scan_result_ = result; // We must only call 'scan_node' on nodes that actually exist. // So we check if the tree is empty, and if so, we don't scan the // root node. if (!internal_nodes_.empty()) { NodeID root = node_lxyz(8, 0, 0, 0); scan_node<8>(root, debug); } } eng::string scan_steps_debug_string(const PlaneScan &sc) { eng::ostringstream oss; IdVector result; scan(sc, &result, &oss); oss << "|Result: "; std::sort(result.begin(), result.end()); util::print_id_vector(result, &oss); return oss.str(); } void collect_planeitem_ids(PlaneItem *first, IdVector *ids) { if (first != nullptr) { PlaneItem *pi = first; while (true) { PlaneItem *next = pi->next_; ids->push_back(pi->id()); if (next == first) break; pi = next; } } } eng::string tree_debug_string() { eng::ostringstream oss; print_tree_r(node_lxyz(8,0,0,0), &oss); return oss.str(); } eng::string outliers_debug_string() { eng::ostringstream oss; oss << "total:" << total_count_ << " justout:" << just_outside_bbox_ << " wayout:" << way_outside_bbox_; return oss.str(); } // Construct a PlaneTree. PlaneTree(PlaneMap *pmap, std::string_view plane) { planemap_ = pmap; plane_ = plane; total_count_ = 0; just_outside_bbox_ = 0; way_outside_bbox_ = 0; set_radius_of_empty_tree(pmap->default_radius_); } // Destructor: the PlaneTree doesn't own the PlaneItems, so it doesn't // delete them, but it needs to untrack all the PlaneItems. ~PlaneTree() { untrack_all(); } }; void PlaneItem::track(PlaneMap *pmap) { // If we're already in a PlaneMap, and it's not the // PlaneMap we want to be in, remove from the old PlaneMap. if ((tree_ != nullptr) && (tree_->planemap_ != pmap)) { PlaneTree::remove_planeitem(this); } // If we're supposed to be in a PlaneMap, and we're not // already in the PlaneMap, insert it. if ((tree_ == nullptr) && (pmap != nullptr)) { PlaneTree::get(pmap, plane_)->insert_planeitem(this); } } void PlaneItem::set_pos(const eng::string &plane, float x, float y, float z) { // If we're not in a PlaneMap, nothing to do but set the variables. if (tree_ == nullptr) { plane_ = plane; x_ = x; y_ = y; z_ = z; return; } // When moving within a plane (not warping), use set_xyz, which is faster. if (plane_ == plane) { set_xyz(x, y, z); return; } // We're warping from one plane to another. That means we're removing // ourself from one planetree and inserting ourself into a different one. PlaneTree *newtree = PlaneTree::get(tree_->planemap_, plane); PlaneTree::remove_planeitem(this); x_ = x; y_ = y; z_ = z; newtree->insert_planeitem(this); } void PlaneItem::set_xyz(float x, float y, float z) { // If we're not in a PlaneMap, nothing to do but set the variables. if (tree_ == nullptr) { x_ = x; y_ = y; z_ = z; return; } // We could implement this function using 'PlaneTree::remove_planeitem' // and 'PlaneTree::insert_planeitem', which would be less code, but it // would also be slower. NodeInfo old_cell(tree_->scale_, x_, y_, z_); NodeInfo new_cell(tree_->scale_, x, y, z); // Update the variables. x_ = x; y_ = y; z_ = z; // Update the outliers counters (unlikely). if (old_cell.bbcheck != new_cell.bbcheck) { tree_->decrement_planeitem_counters(old_cell.bbcheck); tree_->increment_planeitem_counters(new_cell.bbcheck); } // If we have changed cells, update the tree. // We have to remove the child from the old leaf before inserting // it into the new leaf, because the 'next' and 'prev' pointers are // intrusive and we need them to be unused to do the insert. // However, inserting the child into the childbits first is faster // and poses no problems. if (new_cell.node != old_cell.node) { bool leaf_removed = tree_->remove_planeitem_from_leaf(old_cell.node, this); bool leaf_created = tree_->insert_planeitem_into_leaf(new_cell.node, this); if (leaf_created) tree_->insert_child_into_childbits(new_cell.node); if (leaf_removed) tree_->remove_child_from_childbits(old_cell.node); } } PlaneItem::PlaneItem() { id_ = 0; tree_ = nullptr; next_ = nullptr; prev_ = nullptr; x_ = y_ = z_ = 0.0; } PlaneItem::~PlaneItem() { untrack(); } PlaneMap::PlaneMap() : default_radius_(32768.0) {} PlaneMap::~PlaneMap() {} void PlaneMap::scan(const PlaneScan &sc, util::IdVector *into) const { into->clear(); if (sc.near_ != 0) { if (sc.include_near_) { into->push_back(sc.near_); } } if (sc.omit_nowhere_ && (sc.plane_ == "nowhere")) { return; } auto piter = planes_.find(std::string_view(sc.plane_)); if (piter != planes_.end()) { const std::unique_ptr &tree = piter->second; tree->scan(sc, into, nullptr); } if (sc.sorted_) { std::sort(into->begin(), into->end()); } } eng::string PlaneMap::tree_debug_string(const eng::string &plane) { return PlaneTree::get(this, plane)->tree_debug_string(); } eng::string PlaneMap::outliers_debug_string(const eng::string &plane) { return PlaneTree::get(this, plane)->outliers_debug_string(); } eng::string PlaneMap::search_bboxes_debug_string(const PlaneScan &scan) { return PlaneTree::get(this, eng::string(scan.plane_))->search_bboxes_debug_string(scan); } eng::string PlaneMap::scan_steps_debug_string(const PlaneScan &scan) { return PlaneTree::get(this, eng::string(scan.plane_))->scan_steps_debug_string(scan); } void PlaneMap::untrack_all() { for (const auto &pair : planes_) { pair.second->untrack_all(); } } void PlaneScan::configure(LuaKeywordParser &kp) { lua_State *L = kp.state(); LuaVar val, vx, vy, vz; LuaExtStack LS(L, val, vx, vy, vz); bool have_plane = false; bool have_center = false; bool have_radius = false; bool have_shape = false; bool have_near = false; if (kp.parse(val, "plane")) { plane_ = LS.ckstring(val, "plane"); have_plane = true; } if (kp.parse(val, "center")) { util::DXYZ xyz = LS.ckxyz(val, "center"); center_ = xyz; have_center = true; } if (kp.parse(val, "radius")) { auto simple = LS.trynumber(val); if (simple) { radius_ = *simple; have_radius = true; } else { auto full = LS.tryxyz(val); if (full) { radius_ = *full; have_radius = true; } } if (!have_radius) { luaL_error(L, "scan configuration: 'radius' must be a vector or number"); } } if (kp.parse(val, "shape")) { eng::string shape = LS.ckstring(val, "shape"); if (shape == "box") { shape_ = BOX; } else if (shape == "sphere") { shape_ = SPHERE; } else if (shape == "cylinder") { shape_ = CYLINDER; } else { luaL_error(L, "scan configuration: unknown shape %s", shape.c_str()); } have_shape = true; } if (kp.parse(val, "near")) { int64_t id = LS.tanid(val); if (id == 0) { luaL_error(L, "scan configuration: 'near' must be a tangible"); } if (have_center) { luaL_error(L, "scan configuration: specified both 'center' and 'near'"); } if (have_plane) { luaL_error(L, "scan configuration: specified both 'plane' and 'near'"); } near_ = id; have_center = true; have_plane = true; have_near = true; } if (kp.parse(val, "include")) { if (!have_near) { luaL_error(L, "scan configuration: 'include' specified without 'near'"); } include_near_ = LS.ckboolean(val, "include"); } if (kp.parse(val, "wholeplane")) { if (have_plane || have_center || have_radius || have_shape) { luaL_error(L, "scan configuration: do not specify plane, center, shape, or radius with 'wholeplane'"); } plane_ = LS.ckstring(val, "wholeplane"); set_whole_plane(); have_plane = true; have_center = true; have_radius = true; have_shape = true; } if (!have_plane) { luaL_error(L, "scan configuration: did not specify plane"); } if (!have_radius) { luaL_error(L, "scan configuration: did not specify radius"); } if (!have_center) { luaL_error(L, "scan configuration: did not specify center"); } if (!have_shape) { shape_ = SPHERE; // default value. } } eng::string PlaneScan::debug_string() const { eng::ostringstream oss; oss << "plane:" << plane_ << " center:" << center_ << " radius:" << radius_; if (shape_ == BOX) oss << " shape:box"; else if (shape_ == SPHERE) oss << " shape:sphere"; else if (shape_ == CYLINDER) oss << " shape:cylinder"; else oss << " shape:unknown"; if (near_ != 0) { oss << " near:" << near_ << " include:" << include_near_; } if (omit_nowhere_) { oss << " omit_nowhere:true"; } if (!sorted_) { oss << " sorted:false"; } return oss.str(); } // The default radius is set such that float coordinates map directly to // integer coordinates, with an offset of 0x8000. This makes unit testing // a lot easier. // LuaDefine(unittests_planemap, "", "some unit tests") { PlaneMap pm; PlaneItem pi123, pi456; PlaneScan scan; pi123.set_id(123); pi456.set_id(456); // Test that PlaneItems can be manipulated when they're not // yet tracking a PlaneMap. pi123.set_pos("p", 0x38, 0x16, 0x87); LuaAssert(L, pi123.plane() == "p"); LuaAssert(L, pi123.x() == 0x38); LuaAssert(L, pi123.y() == 0x16); LuaAssert(L, pi123.z() == 0x87); // TESTS OF TREE MANIPULATION FOLLOW. // Test track. pi123.set_pos("p", 0x38, 0x16, 0x87); pi123.track(&pm); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" "| L2:803,801,808" "| L0:8038,8016,8087 123"); // Test untrack. pi123.untrack(); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root"); // Track two items at a time, not in the same cell. pi456.set_pos("p", 0x12, 0x17, 0xAC); pi123.track(&pm); pi456.track(&pm); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" "| L2:801,801,80a" "| L0:8012,8017,80ac 456" "| L2:803,801,808" "| L0:8038,8016,8087 123"); // Move one of the items into the same cell as the other. pi456.set_xyz(0x38, 0x16, 0x87); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" "| L2:803,801,808" "| L0:8038,8016,8087 123,456"); // Move item 456 back out of the cell. pi456.set_xyz(0x27, 0x11, 0x31); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" "| L2:802,801,803" "| L0:8027,8011,8031 456" "| L2:803,801,808" "| L0:8038,8016,8087 123"); // Move item 123 to follow 456. pi123.set_xyz(0x27, 0x11, 0x31); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,8,8" "| L4:80,80,80" "| L2:802,801,803" "| L0:8027,8011,8031 123,456"); // TESTS OF OUTLIER CLAMPING FOLLOW. // Move item 456 close to, but not quite on the positive edge. pi123.untrack(); pi456.set_xyz(0x23, 0x7FFE, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" "| L2:802,fff,802" "| L0:8023,fffe,8027 456"); // Move item 456 so that it's on the positive edge, but not an outlier. pi456.set_xyz(0x23, 0x7FFF, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" "| L2:802,fff,802" "| L0:8023,ffff,8027 456"); // Move item 456 so that it's even closer to the positive edge, but not an outlier. pi456.set_xyz(0x23, 0x7FFF + 0.99, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" "| L2:802,fff,802" "| L0:8023,ffff,8027 456"); // Move item 456 so that it's just barely a positive outlier. pi456.set_xyz(0x23, 0x8000, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" "| L2:802,fff,802" "| L0:8023,ffff,8027 456"); // Move item 456 so that it's considerably past the positive edge. pi456.set_xyz(0x23, 0x8048, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" "| L2:802,fff,802" "| L0:8023,ffff,8027 456"); // Move item 456 so that it's way past the positive edge. pi456.set_xyz(0x23, 0x83748, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:1"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,f,8" "| L4:80,ff,80" "| L2:802,fff,802" "| L0:8023,ffff,8027 456"); // Move item 456 close to, but not quite on the negative edge. pi456.set_xyz(0x23, -0x7fff, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" "| L2:802,000,802" "| L0:8023,0001,8027 456"); // Move item 456 so that it's on the negative edge, but not an outlier. pi456.set_xyz(0x23, -0x7fff - 0.5, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" "| L2:802,000,802" "| L0:8023,0000,8027 456"); // Move item 456 so that it's just barely a negative outlier. pi456.set_xyz(0x23, -0x8000, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" "| L2:802,000,802" "| L0:8023,0000,8027 456"); // Move item 456 so that it's significantly past the negative edge. pi456.set_xyz(0x23, -0x8048, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:1 wayout:0"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" "| L2:802,000,802" "| L0:8023,0000,8027 456"); // Move item 456 so that it's way past the negative edge. pi456.set_xyz(0x23, -0x83048, 0x27); LuaAssertStrEq(L, pm.outliers_debug_string("p"), "total:1 justout:0 wayout:1"); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:8,0,8" "| L4:80,00,80" "| L2:802,000,802" "| L0:8023,0000,8027 456"); // Test the calculation of search bboxes. // The two corners are deliberately not in low-high order. scan.clear(); scan.set_plane("p"); scan.set_center_and_radius(util::XYZ(0x23, 0x97, 0x103), 2.0f); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" "|Level 4 80,80,81 - 80,80,81" "|Level 2 802,809,810 - 802,809,810" "|Level 0 8021,8095,8101 - 8025,8099,8105"); // TESTS OF SCANNING // Store a single object in the map to scan. pm.untrack_all(); pi123.set_pos("p", 0x12, 0x34, 0x45); pi123.track(&pm); // Set up a scan with a radius of zero, centered // right on the one object. Check the bboxes to make // sure they only include the one cell. scan.clear(); scan.set_plane("p"); scan.set_center_and_radius(util::XYZ(0x12, 0x34, 0x45), 0.0); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" "|Level 4 80,80,80 - 80,80,80" "|Level 2 801,803,804 - 801,803,804" "|Level 0 8012,8034,8045 - 8012,8034,8045"); // Run the scan with radius zero. It should find the one object. LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" "| L5:20,20,20" "| L4:80,80,80" "| L3:200,200,201" "| L2:801,803,804" "| L1:2004,200d,2011" "| L0:8012,8034,8045 123" "|Result: 123"); // Bump the scan over a half-unit. It should have the same // bboxes as before, since a half-unit isn't enough to shift // from one cell to the next. scan.clear(); scan.set_plane("p"); scan.set_center_and_radius(util::XYZ(0x12 + 0.5, 0x34, 0x45), 0.0); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" "|Level 4 80,80,80 - 80,80,80" "|Level 2 801,803,804 - 801,803,804" "|Level 0 8012,8034,8045 - 8012,8034,8045"); // Run the scan with radius zero, but one half-step away // from the object. It should encounter the cell containing the // one object, but the object should get removed in the final // filtering step. LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" "| L5:20,20,20" "| L4:80,80,80" "| L3:200,200,201" "| L2:801,803,804" "| L1:2004,200d,2011" "| L0:8012,8034,8045 123" "|Result: "); // Next, expand the scan radius to huge. Examine the bboxes // to make sure they cover the entire PlaneTree. scan.clear(); scan.set_plane("p"); scan.set_center_and_radius(util::XYZ(0x12, 0x34, 0x45), 100000.0); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 0,0,0 - f,f,f" "|Level 4 00,00,00 - ff,ff,ff" "|Level 2 000,000,000 - fff,fff,fff" "|Level 0 0000,0000,0000 - ffff,ffff,ffff"); // Walk the tree using the expansive search bboxes. It should // find the one object, and it should still only traverse the same // cells, because those are the only cells that exist. LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" "| L5:20,20,20" "| L4:80,80,80" "| L3:200,200,201" "| L2:801,803,804" "| L1:2004,200d,2011" "| L0:8012,8034,8045 123" "|Result: 123"); // Add another object to the tree. Then, scan again // using the expansive search. It should find both objects. pi456.set_pos("p", 0x14, 0x35, 0x30); pi456.track(&pm); LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" "| L5:20,20,20" "| L4:80,80,80" "| L3:200,200,200" "| L2:801,803,803" "| L1:2005,200d,200c" "| L0:8014,8035,8030 456" "| L3:200,200,201" "| L2:801,803,804" "| L1:2004,200d,2011" "| L0:8012,8034,8045 123" "|Result: 123,456"); // We're going to test that sphere of radius 0.0 works. This is an important // test because the sphere calculation involves calculating the inverse of // the radius, which has to be special-cased when radius is zero. We need // special-case code for this. In this test case, we set up a scan with two // objects in the same cell, just a smidge apart. Use a sphere scan with // radius zero, centered right on object 123 (but missing object 456). pm.untrack_all(); pi123.set_pos("p", 0x12 + 0.1, 0x34, 0x45); pi456.set_pos("p", 0x12 + 0.2, 0x34, 0x45); pi123.track(&pm); pi456.track(&pm); scan.clear(); scan.set_plane("p"); scan.set_shape(PlaneScan::SPHERE); scan.set_center_and_radius(util::XYZ(0x12 + 0.1, 0x34, 0x45), 0.0); LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" "| L5:20,20,20" "| L4:80,80,80" "| L3:200,200,201" "| L2:801,803,804" "| L1:2004,200d,2011" "| L0:8012,8034,8045 123,456" "|Result: 123"); // We're going to test that 'whole plane' searches work. // These use an infinite radius. pm.untrack_all(); pi123.set_pos("p", 0x12, 0x34, 0x45); pi123.track(&pm); scan.clear(); scan.set_plane("p"); scan.set_whole_plane(); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 0,0,0 - f,f,f" "|Level 4 00,00,00 - ff,ff,ff" "|Level 2 000,000,000 - fff,fff,fff" "|Level 0 0000,0000,0000 - ffff,ffff,ffff"); LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" "| L6:8,8,8" "| L5:20,20,20" "| L4:80,80,80" "| L3:200,200,201" "| L2:801,803,804" "| L1:2004,200d,2011" "| L0:8012,8034,8045 123" "|Result: 123"); // Set up a tree with a single object that's outside // the tree's bounding box (an outlier). Print the tree // to verify that the object ended up on the edge. pm.untrack_all(); pi123.set_pos("p", 0x100000, 0x16, 0x23); pi123.track(&pm); LuaAssertStrEq(L, pm.tree_debug_string("p"), "|L8:root" "| L6:f,8,8" "| L4:ff,80,80" "| L2:fff,801,802" "| L0:ffff,8016,8023 123"); // Now set up a scan around the target outlier. Print out // the scan bboxes. It should be scanning the edge cell that // contains the outlier. It also contains a few other cells // because the radius is nonzero. scan.clear(); scan.set_plane("p"); scan.set_center_and_radius(util::XYZ(0x100000, 0x16, 0x23), 0.2); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 f,8,8 - f,8,8" "|Level 4 ff,80,80 - ff,80,80" "|Level 2 fff,801,802 - fff,801,802" "|Level 0 ffff,8015,8022 - ffff,8016,8023"); // Confirm that the scan finds the outlier. LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:3,2,2" "| L6:f,8,8" "| L5:3f,20,20" "| L4:ff,80,80" "| L3:3ff,200,200" "| L2:fff,801,802" "| L1:3fff,2005,2008" "| L0:ffff,8016,8023 123" "|Result: 123"); return 0; }