基於KD樹、包圍盒與RayCast(射線投射)實現物體拾取的示例程式碼框架

MarsCactus發表於2024-11-10

以下是一個基於KD樹、包圍盒與RayCast(射線投射)實現物體拾取的示例程式碼框架及相關解釋。這個示例假設是在一個三維空間場景下進行操作,主要目的是透過從指定視點發出射線,利用KD樹對場景中的物體包圍盒進行組織和快速搜尋,來判斷射線與哪個物體相交,從而實現物體的拾取。

#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>

// 定義三維點結構體
struct Point3D {
    double x;
    double y;
    double z;

    Point3D(double _x = 0, double _y = 0, double _z = 0) : x(_x), y(_y), z(_z) {}
};

// 定義包圍盒結構體
struct BoundingBox {
    Point3D min;
    Point3D max;

    BoundingBox(const Point3D& _min = Point3D(), const Point3D& _max = Point3D()) : min(_min), max(_max) {}

    // 判斷點是否在包圍盒內
    bool contains(const Point3D& p) const {
        return p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y && p.z >= min.z && p.z <= max.z;
    }

    // 判斷射線是否與包圍盒相交
    bool intersectsRay(const Point3D& rayOrigin, const Point3D& rayDirection) const {
        double tmin, tmax, tymin, tymax, tzmin, tzmax;

        tmin = (min.x - rayOrigin.x) / rayDirection.x;
        tmax = (max.x - rayOrigin.x) / rayDirection.x;
        if (tmin > tmax) std::swap(tmin, tmax);

        tymin = (min.y - rayOrigin.y) / rayDirection.y;
        tymax = (max.y - rayOrigin.y) / rayDirection.y;
        if (tymin > tymax) std::swap(tymin, tymax);

        if ((tmin > tymax) || (tymin > tmax)) return false;

        if (tymin > tmin) tmin = tymin;
        if (tymax < tmax) tmax = tymax;

        tzmin = (min.z - rayOrigin.z) / rayDirection.z;
        tzmax = (max.z - rayOrigin.z) / rayDirection.z;
        if (tzmin > tzmax) std::swap(tzmin, tzmax);

        if ((tmin > tzmax) || (tzmin > tmax)) return false;

        return true;
    }
};

// KD樹節點結構體
struct KDNode {
    BoundingBox boundingBox;
    KDNode* left;
    KDNode* right;
    std::vector<int> objectIndices; // 儲存該節點所對應包圍盒包含的物體索引

    KDNode(const BoundingBox& bb) : boundingBox(bb), left(nullptr), right(nullptr) {}
};

// 計算兩點之間的歐幾里得距離(三維空間)
double distance(const Point3D& p1, const Point3D& p2) {
    return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) + (p1.z - p2.z) * (p1.z - p2.z));
}

// 構建KD樹,以包圍盒為基礎進行劃分
KDNode* buildKDTree(const std::vector<BoundingBox>& boundingBoxes, const std::vector<int>& objectIndices, int depth) {
    if (boundingBoxes.empty()) {
        return nullptr;
    }

    int k = 3; // 三維空間
    int axis = depth % k;

    // 根據當前劃分維度對包圍盒進行排序
    std::vector<BoundingBox> sortedBoundingBoxes = boundingBoxes;
    if (axis == 0) {
        std::sort(sortedBoundingBoxes.begin(), sortedBoundingBoxes.end(), [](const BoundingBox& bb1, const BoundingBox& bb2) {
            return bb1.min.x < bb2.min.x;
        });
    } else if (axis == 1) {
        std::sort(sortedBoundingBoxes.begin(), sortedBoundingBoxes.end(), [](const BoundingBox& bb1, const BoundingBox& bb2) {
            return bb1.min.y < bb2.min.y;
        });
    } else {
        std::sort(sortedBoundingBoxes.begin(), sortedBoundingBoxes.end(), [](const BoundingBox& bb1, const BoundingBox& bb2) {
            return bb1.min.z < bb2.min.z;
        });
    }

    int medianIndex = sortedBoundingBoxes.size() / 2;
    KDNode* node = new KDNode(sortedBoundingBoxes[medianIndex]);

    std::vector<BoundingBox> leftBoundingBoxes(sortedBoundingBoxes.begin(), sortedBoundingBoxes.begin() + medianIndex);
    std::vector<BoundingBox> rightBoundingBoxes(sortedBoundingBoxes.begin() + medianIndex + 1, sortedBoundingBoxes.end());

    std::vector<int> leftObjectIndices;
    std::vector<int> rightObjectIndices;

    for (int i : objectIndices) {
        if (leftBoundingBoxes.back().contains(boundingBoxes[i])) {
            leftObjectIndices.push_back(i);
        } else if (rightBoundingBoxes.front().contains(boundingBoxes[i])) {
            rightObjectIndices.push_back(i);
        }
    }

    node->left = buildKDTree(leftBoundingBoxes, leftObjectIndices, depth + 1);
    node->right = buildKDTree(rightBoundingBoxes, rightObjectIndices, depth + 1);

    node->objectIndices = objectIndices;

    return node;
}

// 執行射線投射,透過KD樹進行加速搜尋以找到相交的物體
int rayCastPickup(KDNode* root, const Point3D& rayOrigin, const Point3D& rayDirection) {
    if (root == nullptr) {
        return -1; // -1表示未找到相交物體
    }

    int k = 3;
    int axis = depth % k;

    KDNode* nextBranch = nullptr;
    KDNode* otherBranch = nullptr;

    if ((axis == 0 && rayOrigin.x < root->boundingBox.min.x) || (axis == 1 && rayOrigin.y < root->boundingBox.min.y) ||
        (axis == 2 && rayOrigin.z < root->boundingBox.min.z)) {
        nextBranch = root->left;
        otherBranch = root->right;
    } else {
        nextBranch = root->right;
        otherBranch = root->left;
    }

    int candidateIndex = rayCastPickup(nextBranch, rayOrigin, rayDirection);

    if (candidateIndex!= -1) {
        return candidateIndex;
    }

    if (root->boundingBox.intersectsRay(rayOrigin, rayDirection)) {
        // 遍歷該節點所包含的物體索引,進一步檢查物體與射線是否真正相交
        for (int i : root->objectIndices) {
            // 這裡假設可以透過物體索引獲取到物體的詳細資訊,如包圍盒等,並進行更精確的相交檢測
            // 如果檢測到相交,則返回該物體索引
            // 此處省略具體的物體相交檢測程式碼,可根據實際情況補充
            if (true) {
                return i;
            }
        }
    }

    if (root->boundingBox.intersectsRay(rayOrigin, rayDirection)) {
        candidateIndex = rayCastPickup(otherBranch, rayOrigin, rayDirection);
        if (candidateIndex!= -1) {
            return candidateIndex;
        }
    }

    return -1;
}

int main() {
    // 示例資料,實際應用中這些資料需要根據具體場景生成
    std::vector<BoundingBox> boundingBoxes;
    std::vector<int> objectIndices;

    // 假設這裡生成了一些包圍盒和對應的物體索引資料
    boundingBoxes.push_back(BoundingBox(Point3D(1, 1, 1), Point3D(3, 3, 3)));
    objectIndices.push_back(0);
    boundingBoxes.push_back(BoundingBox(Point3D(4, 4, 4), Point3D(6, 6, 6)));
    objectIndices.push_back(1);

    KDNode* root = buildKDTree(boundingBoxes, objectIndices, 0);

    Point3D rayOrigin(0, 0, 0);
    Point3D rayDirection(1, 1, 1);

    int pickedObjectIndex = rayCastPickup(root, rayOrigin, rayDirection);

    if (pickedObjectIndex!= -1) {
        std::cout << "拾取到物體,索引為: " << pickedObjectIndex << std::endl;
    } else {
        std::cout << "未拾取到物體" << std::endl;
    }

    return 0;
}

以下是對上述程式碼的詳細解釋:

1. 資料結構定義

  • Point3D:用於表示三維空間中的點,包含x、y、z三個座標分量。
  • BoundingBox:定義包圍盒結構體,包含最小點(min)和最大點(max)兩個成員,用於界定一個三維空間區域。同時提供了判斷點是否在包圍盒內以及射線是否與包圍盒相交的函式。
  • KDNode:KD樹的節點結構體,包含一個包圍盒(boundingBox)用於劃分空間,左右子節點指標(left、right)以及一個向量(objectIndices)用於儲存該節點所對應包圍盒包含的物體索引。

2. 構建KD樹

  • buildKDTree函式以包圍盒為基礎構建KD樹。首先根據當前劃分維度(x、y、z軸迴圈)對包圍盒進行排序,然後選取中位數對應的包圍盒作為當前節點的包圍盒。接著將物體索引根據其對應的包圍盒所屬的左右子樹進行劃分,分別構建左右子樹。

3. 射線投射與拾取

  • rayCastPickup函式執行射線投射以實現物體拾取。從KD樹的根節點開始,根據射線的起始點(rayOrigin)與當前節點包圍盒的關係確定下一步搜尋的子樹分支(nextBranch),並先在該分支中進行搜尋。如果在該分支中未找到相交物體,則檢查當前節點的包圍盒是否與射線相交。如果相交,則遍歷該節點所包含的物體索引,進一步檢查物體與射線是否真正相交(此處省略了具體的物體相交檢測程式碼,實際應用中需要根據物體的具體情況進行詳細檢測)。如果在當前節點及其子樹中都未找到相交物體,則再檢查另一個分支(otherBranch)。

4. 主函式

  • main函式中,首先生成了一些示例的包圍盒和對應的物體索引資料,然後構建KD樹。接著定義了射線的起始點和方向,透過呼叫rayCastPickup函式執行射線投射並獲取拾取到的物體索引,最後根據結果輸出相應的資訊。

請注意,上述程式碼只是一個簡化的示例框架,在實際應用中,需要根據具體場景對以下方面進行完善:

  • 物體相交檢測:在rayCastPickup函式中,當檢查節點所包含的物體與射線是否真正相交時,需要根據物體的具體情況(如物體的幾何形狀、表面特性等)進行詳細的檢測,這裡只是簡單假設可以透過物體索引獲取到物體的詳細資訊並進行檢測。
  • 資料生成:在main函式中,示例資料只是簡單給出了幾個包圍盒和物體索引,實際應用中需要根據具體場景準確生成這些資料,例如從三維模型檔案中讀取物體資訊並計算其包圍盒和對應索引等。
  • 記憶體管理:程式碼中動態分配了KD樹節點等記憶體,在實際應用中需要注意記憶體的釋放,避免記憶體洩漏等問題。

相關文章