/* * Created by Brett Terpstra 6920201 on 17/10/22. * Copyright (c) 2022 Brett Terpstra. All Rights Reserved. */ #ifndef STEP_2_BVH_H #define STEP_2_BVH_H #include "engine/util/std.h" #include "engine/types.h" #include #ifdef COMPILE_GUI #include #include #endif #include // A currently pure header implementation of a BVH. TODO: make source file. // this is also for testing and might not make it into the step 2. namespace Raytracing { #ifdef COMPILE_GUI extern std::shared_ptr aabbVAO; extern int count; extern int selected; #endif struct BVHObject { Object* ptr = nullptr; AABB aabb; }; struct BVHPartitionedSpace { std::vector left; std::vector right; }; struct BVHNode { private: static Raytracing::Mat4x4 getTransform(const AABB& _aabb) { Raytracing::Mat4x4 transform{}; auto center = _aabb.getCenter(); transform.translate(center); auto xRadius = _aabb.getXRadius(center) * 2; auto yRadius = _aabb.getYRadius(center) * 2; auto zRadius = _aabb.getZRadius(center) * 2; transform.scale(float(xRadius), float(yRadius), float(zRadius)); return transform; } public: struct BVHHitData { BVHNode* ptr; AABBHitData data; bool hit = false; }; std::vector objs; AABB aabb; BVHNode* left; BVHNode* right; int index; int hit = 0; BVHNode(std::vector objs, AABB aabb, BVHNode* left, BVHNode* right): objs(std::move(objs)), aabb(std::move(aabb)), left(left), right(right) { index = count++; } BVHHitData doesRayIntersect(const Ray& r, PRECISION_TYPE min, PRECISION_TYPE max){ auto ourHitData = aabb.intersects(r, min, max); if (!ourHitData.hit) return {this, ourHitData, false}; this->hit = 2; BVHHitData leftHit{}; leftHit.hit = false; BVHHitData rightHit{}; rightHit.hit = false; if (left != nullptr) leftHit = left->doesRayIntersect(r, min, ourHitData.tMax); if (right != nullptr) rightHit = right->doesRayIntersect(r, min, leftHit.hit ? leftHit.data.tMin : ourHitData.tMax); //tlog << "On the left we " << (leftHit.hit ? "hit" : "didn't hit") << ". with tmax " << leftHit.data.tMax << " and tmin " << leftHit.data.tMin << "\n"; //tlog << "On the right we " << (rightHit.hit ? "hit" : "didn't hit") << ". with tmax " << rightHit.data.tMax << " and tmin " << rightHit.data.tMin << "\n"; if (leftHit.hit && (leftHit.data.tMax < rightHit.data.tMax || !rightHit.hit)) return leftHit; else if (rightHit.hit && (rightHit.data.tMax < leftHit.data.tMax || !leftHit.hit)) return rightHit; //tlog << index << "I " << leftHit.hit << " ? " << rightHit.hit << " " << left << " " << right << " : " << objs.size() << " is empty? " << objs.empty() << "\n" ; this->hit = !objs.empty(); if (objs.empty()){ //tlog << "we hit an empty box " << index << "\n"; return {this, ourHitData, false}; } //tlog << "We hit a box with objects " << objs.size() << " ! " << index << "\n"; return {this, ourHitData, true}; } #ifdef COMPILE_GUI void draw(Shader& worldShader) { worldShader.setVec3("color", {1.0, 1.0, 1.0}); aabbVAO->bind(); if (selected == index) { if (selected == index && ImGui::BeginListBox("", ImVec2(250, 350))) { std::stringstream strs; strs << aabb; ImGui::Text("%s", strs.str().c_str()); for (const auto& item: objs) { auto pos = item.ptr->getPosition(); std::stringstream stm; stm << item.aabb; ImGui::Text("%s,\n\t%s", (std::to_string(pos.x()) + " " + std::to_string(pos.y()) + " " + std::to_string(pos.z())).c_str(), stm.str().c_str()); } ImGui::EndListBox(); } for (const auto& obj: objs) { auto transform = getTransform(obj.aabb); worldShader.setMatrix("transform", transform); aabbVAO->draw(worldShader); } auto transform = getTransform(aabb); worldShader.setMatrix("transform", transform); aabbVAO->draw(worldShader); /*auto splitAABBs = aabb.splitByLongestAxis(); transform = getTransform(splitAABBs.second); worldShader.setMatrix("transform", transform); aabbVAO->draw(worldShader); transform = getTransform(splitAABBs.first); worldShader.setMatrix("transform", transform); aabbVAO->draw(worldShader);*/ } if (hit){ if (hit == 1) worldShader.setVec3("color", {0.0, 0.0, 1.0}); else if (hit == 2) worldShader.setVec3("color", {0.0, 1.0, 0.0}); else worldShader.setVec3("color", {1.0, 0.5, 0.5}); auto transform = getTransform(aabb); worldShader.setMatrix("transform", transform); aabbVAO->draw(worldShader); } } void gui() const { int c1 = -1; int c2 = -1; if (left != nullptr) c1 = left->index; if (right != nullptr) c2 = right->index; std::string t; if (c1 == -1 && c2 == -1) t = " LEAF"; else t = " L: " + std::to_string(c1) + " R: " + std::to_string(c2); if (ImGui::Selectable(("S: " + std::to_string(objs.size()) + " I: " + std::to_string(index) + t).c_str(), selected == index)) selected = index; } #endif ~BVHNode() { delete (left); delete (right); } }; class BVHTree { private: BVHNode* root = nullptr; // splits the objs in the vector based on the provided AABBs static BVHPartitionedSpace partition(const std::pair& aabbs, const std::vector& objs) { BVHPartitionedSpace space; for (const auto& obj: objs) { // if this object doesn't have an AABB, we cannot use a BVH on it. If this ever fails we have a problem with the implementation. RTAssert(!obj.aabb.isEmpty()); if (aabbs.first.intersects(obj.aabb)) space.left.push_back(obj); else if (aabbs.second.intersects(obj.aabb)) space.right.push_back(obj); } return space; } static bool vectorEquals(const BVHPartitionedSpace& oldSpace, const BVHPartitionedSpace& newSpace){ if (oldSpace.left.size() != newSpace.left.size() || oldSpace.right.size() != newSpace.right.size()) return false; for (int i = 0; i < oldSpace.left.size(); i++){ if (oldSpace.left[i].aabb != newSpace.left[i].aabb) return false; } for (int i = 0; i < oldSpace.right.size(); i++){ if (oldSpace.right[i].aabb != newSpace.right[i].aabb) return false; } return true; } BVHNode* addObjectsRecur(const std::vector& objects, const BVHPartitionedSpace& prevSpace) { // create a volume for the entire world. // yes, we could use a recursion provided AABB, but that wouldn't be minimum, only half. this ensures that we have a minimum AABB. AABB world; for (const auto& obj: objects) world = world.expand(obj.aabb); // then split and partition the world auto splitAABBs = world.splitByLongestAxis(); auto partitionedObjs = partition(splitAABBs, objects); if (vectorEquals(prevSpace, partitionedObjs)){ splitAABBs = world.splitAlongAxis(); partitionedObjs = partition(splitAABBs, objects); } if ((objects.size() <= 1 && !objects.empty())) { return new BVHNode(objects, world, nullptr, nullptr); } else if (objects.empty()) // should never reach here!! return nullptr; BVHNode* left = nullptr; BVHNode* right = nullptr; // don't try to explore nodes which don't have anything in them. if (!partitionedObjs.left.empty()) left = addObjectsRecur(partitionedObjs.left, partitionedObjs); if (!partitionedObjs.right.empty()) right = addObjectsRecur(partitionedObjs.right, partitionedObjs); if (left == nullptr && right == nullptr) return new BVHNode(objects, world, left, right); else return new BVHNode({}, world, left, right); } #ifdef COMPILE_GUI void drawNodesRecur(Shader& worldShader, BVHNode* node) { node->draw(worldShader); if (node->left != nullptr) drawNodesRecur(worldShader, node->left); if (node->right != nullptr) drawNodesRecur(worldShader, node->right); } void guiNodesRecur(BVHNode* node) { node->gui(); if (node->left != nullptr) guiNodesRecur(node->left); if (node->right != nullptr) guiNodesRecur(node->right); } #endif void reset(BVHNode* node){ if (node == nullptr) return; node->hit = false; reset(node->left); reset(node->right); } public: std::vector noAABBObjects; explicit BVHTree(const std::vector& objectsInWorld) { addObjects(objectsInWorld); #ifdef COMPILE_GUI auto aabbVertexData = Shapes::cubeVertexBuilder{}; if (aabbVAO == nullptr) aabbVAO = std::make_shared(aabbVertexData.cubeVerticesRaw, aabbVertexData.cubeUVs); #endif } void addObjects(const std::vector& objects) { if (root != nullptr) throw std::runtime_error("BVHTree already exists. What are you trying to do?"); // move all the object's aabb's into world position std::vector objs; for (auto* obj: objects) { // we don't want to store all the AABBs which don't exist: ie spheres if (obj->getAABB().isEmpty()) { noAABBObjects.push_back(obj); continue; } BVHObject bvhObject; // returns a copy of the AABB object and assigns it in to the tree storage object bvhObject.aabb = obj->getAABB().translate(obj->getPosition()); // which means we don't have to do memory management, since we are using the pointer without ownership or coping now. bvhObject.ptr = obj; objs.push_back(bvhObject); } root = addObjectsRecur(objs, {}); } std::vector rayIntersect(const Ray& ray, PRECISION_TYPE min, PRECISION_TYPE max) { RTAssert(root != nullptr); auto results = root->doesRayIntersect(ray, min, max); RTAssert(results.ptr != nullptr); if (results.hit) return results.ptr->objs; else return {}; } void resetNodes(){ reset(root); } #ifdef COMPILE_GUI // renders all the debug VAOs on screen. void render(Shader& worldShader) { ImGui::Begin(("BVH Data "), nullptr, ImGuiWindowFlags_NoCollapse); worldShader.use(); worldShader.setInt("useWhite", 1); worldShader.setVec3("color", {1.0, 1.0, 1.0}); { ImGui::BeginChild("left pane", ImVec2(180, 0), true); guiNodesRecur(root); ImGui::EndChild(); } ImGui::SameLine(); { ImGui::BeginGroup(); ImGui::BeginChild("item view", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), true, ImGuiWindowFlags_AlwaysAutoResize); // Leave room for 1 line below us drawNodesRecur(worldShader, root); ImGui::EndChild(); ImGui::EndGroup(); } worldShader.setInt("useWhite", 0); ImGui::End(); } #endif ~BVHTree() { delete (root); } }; } #endif //STEP_2_BVH_H