10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++で作るゲームエンジン自作シリーズ

Part1 設計編 Part2 ウィンドウ Part3 OpenGL Part4 シェーダー Part5 テクスチャ Part6 3Dモデル Part7 当たり判定
- - - - - - 👈 Now

はじめに

これが最終回。

絵が出るだけじゃゲームにならない。当たり判定を実装して、ゲームエンジンを完成させる。

衝突検出の基本

バウンディングボリューム

複雑なメッシュ同士の衝突判定は重い。だから単純な形状で近似する。

┌─────────────┐
│  ┌───┐      │  ← AABB(軸平行境界ボックス)
│  │ ◆ │      │     複雑な形状を箱で囲む
│  └───┘      │
└─────────────┘

主なバウンディングボリューム:

形状 判定速度 フィット精度
AABB 最速 低い
OBB(向き付きボックス) 速い
速い 低い
カプセル 速い
凸包 遅い 高い
メッシュ 最遅 最高

ゲームではAABBがよく使われる。

AABB(Axis-Aligned Bounding Box)

定義

struct AABB {
    glm::vec3 min;  // 最小点
    glm::vec3 max;  // 最大点
};

「軸に平行なボックス」という意味。回転しない。

       max
    ┌─────┐
    │     │
    │     │
    └─────┘
  min

AABB同士の衝突判定

めちゃくちゃシンプル:

bool intersects(const AABB& a, const AABB& b) {
    return a.min.x <= b.max.x && a.max.x >= b.min.x &&
           a.min.y <= b.max.y && a.max.y >= b.min.y &&
           a.min.z <= b.max.z && a.max.z >= b.min.z;
}

全ての軸で重なっていれば衝突

2Dだとこう:

     A          B
  ┌─────┐   ┌─────┐
  │     │   │     │    ← X軸で重なってない → 衝突なし
  └─────┘   └─────┘

  ┌─────┐
  │  ┌──┼──┐
  │  │  │  │             ← X軸でもY軸でも重なってる → 衝突!
  └──┼──┘  │
     └─────┘

点の内包判定

bool contains(const AABB& box, glm::vec3 point) {
    return point.x >= box.min.x && point.x <= box.max.x &&
           point.y >= box.min.y && point.y <= box.max.y &&
           point.z >= box.min.z && point.z <= box.max.z;
}

球の衝突判定

球同士

struct Sphere {
    glm::vec3 center;
    float radius;
};

bool intersects(const Sphere& a, const Sphere& b) {
    float distance = glm::length(a.center - b.center);
    return distance <= (a.radius + b.radius);
}

中心間の距離が半径の和以下なら衝突

球とAABB

bool intersects(const Sphere& sphere, const AABB& box) {
    // 球の中心から最も近いAABB上の点を求める
    glm::vec3 closest;
    closest.x = std::max(box.min.x, std::min(sphere.center.x, box.max.x));
    closest.y = std::max(box.min.y, std::min(sphere.center.y, box.max.y));
    closest.z = std::max(box.min.z, std::min(sphere.center.z, box.max.z));
    
    // その点と球の中心の距離
    float distance = glm::length(closest - sphere.center);
    return distance <= sphere.radius;
}

レイキャスト

「この方向にビームを撃ったら何に当たるか」を調べる。

レイの定義

struct Ray {
    glm::vec3 origin;     // 始点
    glm::vec3 direction;  // 方向(正規化)
};

レイとAABBの交差(Slab法)

bool rayIntersectsAABB(const Ray& ray, const AABB& box, float& tMin, float& tMax) {
    tMin = 0.0f;
    tMax = std::numeric_limits<float>::max();
    
    for (int i = 0; i < 3; i++) {
        float minVal = box.min[i];
        float maxVal = box.max[i];
        float origin = ray.origin[i];
        float dir = ray.direction[i];
        
        if (std::abs(dir) < 1e-6f) {
            // レイが軸に平行
            if (origin < minVal || origin > maxVal) {
                return false;
            }
        } else {
            float t1 = (minVal - origin) / dir;
            float t2 = (maxVal - origin) / dir;
            
            if (t1 > t2) std::swap(t1, t2);
            
            tMin = std::max(tMin, t1);
            tMax = std::min(tMax, t2);
            
            if (tMin > tMax) {
                return false;
            }
        }
    }
    
    return true;
}

Slab法は、AABBを3つの「スラブ(板状の領域)」の交差として扱う。

衝突応答

衝突を検出するだけじゃ足りない。めり込みを解消する必要がある。

衝突情報の取得

struct CollisionInfo {
    bool collided;
    glm::vec3 normal;     // 衝突面の法線
    float penetration;    // めり込み量
};

CollisionInfo getCollisionInfo(const AABB& a, const AABB& b) {
    CollisionInfo info;
    
    if (!intersects(a, b)) {
        info.collided = false;
        return info;
    }
    
    info.collided = true;
    
    // 各軸でのオーバーラップ量を計算
    float overlapX = std::min(a.max.x - b.min.x, b.max.x - a.min.x);
    float overlapY = std::min(a.max.y - b.min.y, b.max.y - a.min.y);
    float overlapZ = std::min(a.max.z - b.min.z, b.max.z - a.min.z);
    
    // 最小のオーバーラップ軸を分離軸とする
    if (overlapX <= overlapY && overlapX <= overlapZ) {
        info.penetration = overlapX;
        info.normal = (a.center().x < b.center().x) 
            ? glm::vec3(-1, 0, 0) : glm::vec3(1, 0, 0);
    } else if (overlapY <= overlapZ) {
        info.penetration = overlapY;
        info.normal = (a.center().y < b.center().y) 
            ? glm::vec3(0, -1, 0) : glm::vec3(0, 1, 0);
    } else {
        info.penetration = overlapZ;
        info.normal = (a.center().z < b.center().z) 
            ? glm::vec3(0, 0, -1) : glm::vec3(0, 0, 1);
    }
    
    return info;
}

めり込み解消

void resolveCollision(GameObject& obj, const CollisionInfo& info) {
    if (!info.collided) return;
    
    // めり込み分だけ押し戻す
    obj.position += info.normal * info.penetration;
    
    // 衝突面に沿った速度成分だけ残す
    float normalVelocity = glm::dot(obj.velocity, info.normal);
    if (normalVelocity < 0) {
        obj.velocity -= info.normal * normalVelocity;
    }
}

実装例:完全なcollision.h

#pragma once

#include <glm/glm.hpp>
#include <algorithm>

struct AABB {
    glm::vec3 min;
    glm::vec3 max;
    
    AABB() : min(0.0f), max(0.0f) {}
    AABB(glm::vec3 min, glm::vec3 max) : min(min), max(max) {}
    
    static AABB fromCenterSize(glm::vec3 center, glm::vec3 size) {
        glm::vec3 halfSize = size * 0.5f;
        return AABB(center - halfSize, center + halfSize);
    }
    
    glm::vec3 center() const { return (min + max) * 0.5f; }
    glm::vec3 size() const { return max - min; }
    
    bool contains(glm::vec3 point) const {
        return point.x >= min.x && point.x <= max.x &&
               point.y >= min.y && point.y <= max.y &&
               point.z >= min.z && point.z <= max.z;
    }
    
    bool intersects(const AABB& other) const {
        return min.x <= other.max.x && max.x >= other.min.x &&
               min.y <= other.max.y && max.y >= other.min.y &&
               min.z <= other.max.z && max.z >= other.min.z;
    }
    
    AABB offset(glm::vec3 offset) const {
        return AABB(min + offset, max + offset);
    }
};

struct Sphere {
    glm::vec3 center;
    float radius;
    
    bool intersects(const Sphere& other) const {
        return glm::length(center - other.center) <= (radius + other.radius);
    }
    
    bool intersects(const AABB& box) const {
        glm::vec3 closest;
        closest.x = std::max(box.min.x, std::min(center.x, box.max.x));
        closest.y = std::max(box.min.y, std::min(center.y, box.max.y));
        closest.z = std::max(box.min.z, std::min(center.z, box.max.z));
        return glm::length(closest - center) <= radius;
    }
};

struct Ray {
    glm::vec3 origin;
    glm::vec3 direction;
    
    bool intersects(const AABB& box, float& t) const {
        float tMin = 0.0f, tMax = FLT_MAX;
        for (int i = 0; i < 3; i++) {
            if (std::abs(direction[i]) < 1e-6f) {
                if (origin[i] < box.min[i] || origin[i] > box.max[i]) return false;
            } else {
                float t1 = (box.min[i] - origin[i]) / direction[i];
                float t2 = (box.max[i] - origin[i]) / direction[i];
                if (t1 > t2) std::swap(t1, t2);
                tMin = std::max(tMin, t1);
                tMax = std::min(tMax, t2);
                if (tMin > tMax) return false;
            }
        }
        t = tMin;
        return true;
    }
};

ゲームでの使用例

class GameObject {
public:
    glm::vec3 position;
    glm::vec3 velocity;
    AABB boundingBox;
    
    void update(float deltaTime) {
        position += velocity * deltaTime;
        boundingBox = AABB::fromCenterSize(position, glm::vec3(1.0f));
    }
};

void gameLoop() {
    std::vector<GameObject> objects;
    
    // 衝突判定
    for (size_t i = 0; i < objects.size(); i++) {
        for (size_t j = i + 1; j < objects.size(); j++) {
            if (objects[i].boundingBox.intersects(objects[j].boundingBox)) {
                // 衝突した!
                auto info = getCollisionInfo(objects[i].boundingBox, 
                                            objects[j].boundingBox);
                resolveCollision(objects[i], info);
            }
        }
    }
}

最適化:空間分割

オブジェクトが100個あると、総当たりで 100×99/2 = 4950 回の判定が必要。

空間分割で減らせる:

グリッド分割

class SpatialGrid {
    std::unordered_map<int, std::vector<GameObject*>> cells;
    float cellSize;
    
    int hash(glm::vec3 pos) {
        int x = (int)(pos.x / cellSize);
        int y = (int)(pos.y / cellSize);
        int z = (int)(pos.z / cellSize);
        return x + y * 1000 + z * 1000000;
    }
    
public:
    void insert(GameObject* obj) {
        cells[hash(obj->position)].push_back(obj);
    }
    
    std::vector<GameObject*> getNearby(glm::vec3 pos) {
        // 周囲のセルのオブジェクトだけ返す
    }
};

他の手法

  • Octree: 八分木。3D向け
  • BVH: バウンディングボリューム階層。レイトレ向け
  • Sweep and Prune: 軸ソートして重なりを検出

デバッグ表示

当たり判定は見えないので、デバッグ時は可視化すると便利:

void renderAABB(const AABB& box, glm::vec3 color) {
    // ワイヤーフレームで描画
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // キューブを描画
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

まとめ:シリーズ完結!

7回に渡って、ゲームエンジンの基礎を作ってきた。

作ったもの

  1. Part1: 全体設計
  2. Part2: GLFWでウィンドウ
  3. Part3: OpenGLで三角形
  4. Part4: シェーダー(uniform、アニメーション)
  5. Part5: テクスチャ(stb_image、UV座標)
  6. Part6: 3Dモデル(OBJパーサー、Phongライティング)
  7. Part7: 当たり判定(AABB、球、レイキャスト)

次のステップ

これで基礎はできた。ここから先は:

  • Entity-Component-System(ECS)パターン
  • 物理エンジン(重力、反発、摩擦)
  • サウンド(OpenAL)
  • UI(ImGui)
  • シーン管理
  • アセット管理

自分だけのゲームエンジンを育てていこう。

おわりに

ゲームエンジンを作ると、ゲームの仕組みがわかる

UnityやUnrealを使うにしても、中身を理解してると強い。

最終的なコードは全部GitHubに上げてあるので、参考にどうぞ。

お疲れ様でした!

10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?