You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在C++中为自研OpenGL 3D游戏实现可调用的Ray Casting类

如何在C++中为自研OpenGL 3D游戏实现可调用的Ray Casting类

很高兴看到你从零开始撸OpenGL 3D游戏!射线检测(Ray Casting)类绝对是碰撞检测、射击瞄准、距离测算这类功能的核心组件,我来给你一步步拆解怎么实现一个灵活、可随时调用的版本。

一、先明确射线的核心结构

射线本质上就是「起点 + 归一化方向向量」,为了和OpenGL坐标系统无缝兼容,我推荐用glm库的向量类型(如果还没引入glm,强烈建议加它,专门处理图形学数学计算,省超多重复造轮子的活)。当然你也可以替换成自己实现的向量类。

#include <glm/glm.hpp>
#include <glm/gtx/intersect.hpp> // 可选,glm内置了相交检测函数,能大幅简化代码

// 射线基础结构体
struct Ray {
    glm::vec3 origin;    // 射线起点(比如相机位置、枪口位置)
    glm::vec3 direction; // 射线方向向量(必须归一化!否则距离计算会完全错误)

    // 构造函数,自动归一化方向向量
    Ray(const glm::vec3& origin, const glm::vec3& direction)
        : origin(origin), direction(glm::normalize(direction)) {}
};

二、封装RayCaster类:把检测逻辑打包成可调用工具

我们要做的是一个轻量的工具类,让你在游戏循环、射击触发、鼠标拾取等场景随时调用。核心是定义「检测结果结构体」和「不同几何体的检测方法」。

1. 定义命中结果结构体

先把你需要的命中信息打包成一个结构体,方便后续逻辑处理:

#include <optional> // C++17及以上支持,用来表示「可能无结果」的场景

// 射线命中结果
struct RayHitResult {
    bool hit = false;               // 是否命中物体
    glm::vec3 hitPoint;             // 命中点的世界坐标
    float distance = 0.0f;          // 射线起点到命中点的直线距离
    void* hitObject = nullptr;      // 指向命中物体的指针(可选,用来区分不同物体,比如扣血)
    glm::vec3 normal;               // 命中表面的法向量(可选,用于光影、子弹反弹效果)
};

2. RayCaster类核心实现

这里做成静态工具类(不需要维护状态,随时调用),如果你的场景需要缓存碰撞体数据,也可以改成实例类:

#include <vector>
#include <limits>
#include <iostream>

class RayCaster {
public:
    // 检测射线与AABB包围盒的相交(游戏里最常用的碰撞体,简单高效)
    static RayHitResult intersectAABB(const Ray& ray, const glm::vec3& aabbMin, const glm::vec3& aabbMax, void* hitObject = nullptr) {
        RayHitResult result;

        float tMin, tMax;
        // 用glm内置函数计算射线与AABB的相交区间
        if (glm::intersectRayAABB(ray.origin, ray.direction, aabbMin, aabbMax, tMin, tMax)) {
            // 只保留射线前进方向(t>0)的命中结果
            if (tMin < 0.0f) tMin = tMax;
            if (tMin < 0.0f) return result; // 命中点在射线起点后方,忽略

            result.hit = true;
            result.distance = tMin;
            result.hitPoint = ray.origin + ray.direction * tMin;
            result.hitObject = hitObject;

            // 计算命中面的法向量(可选逻辑)
            glm::vec3 boxCenter = (aabbMin + aabbMax) * 0.5f;
            glm::vec3 hitOffset = result.hitPoint - boxCenter;
            glm::vec3 halfExtents = (aabbMax - aabbMin) * 0.5f;

            float maxAxis = std::max({std::abs(hitOffset.x), std::abs(hitOffset.y), std::abs(hitOffset.z)});
            if (maxAxis == std::abs(hitOffset.x)) result.normal = glm::vec3(hitOffset.x > 0 ? 1 : -1, 0, 0);
            else if (maxAxis == std::abs(hitOffset.y)) result.normal = glm::vec3(0, hitOffset.y > 0 ? 1 : -1, 0);
            else result.normal = glm::vec3(0, 0, hitOffset.z > 0 ? 1 : -1);
        }

        return result;
    }

    // 检测射线与球体的相交(比如子弹、圆形障碍物)
    static RayHitResult intersectSphere(const Ray& ray, const glm::vec3& sphereCenter, float sphereRadius, void* hitObject = nullptr) {
        RayHitResult result;

        float t;
        // glm内置函数,参数里的半径是平方值,避免开方运算
        if (glm::intersectRaySphere(ray.origin, ray.direction, sphereCenter, sphereRadius * sphereRadius, t)) {
            if (t < 0.0f) return result;

            result.hit = true;
            result.distance = t;
            result.hitPoint = ray.origin + ray.direction * t;
            result.hitObject = hitObject;
            result.normal = glm::normalize(result.hitPoint - sphereCenter);
        }

        return result;
    }

    // 批量检测场景中所有物体,返回最近的命中结果
    static RayHitResult castRayAgainstScene(const Ray& ray, const std::vector<class GameObject*>& sceneObjects) {
        RayHitResult closestHit;
        float closestDist = std::numeric_limits<float>::max();

        for (auto* obj : sceneObjects) {
            // 假设你的GameObject类有getAABBMin()和getAABBMax()方法
            auto hit = intersectAABB(ray, obj->getAABBMin(), obj->getAABBMax(), obj);
            if (hit.hit && hit.distance < closestDist) {
                closestDist = hit.distance;
                closestHit = hit;
            }
        }

        return closestHit;
    }
};

三、游戏中的实际调用示例

1. 射击功能调用

比如玩家按下射击键时,从相机位置向视线方向发射射线:

// 假设你有场景物体列表
std::vector<GameObject*> sceneObjects;

// 射击触发函数
void onShoot(const glm::vec3& cameraPos, const glm::vec3& cameraForward) {
    // 创建射击射线:起点是相机位置,方向是相机前向
    Ray shootRay(cameraPos, cameraForward);

    // 检测与场景中所有物体的相交
    auto hitResult = RayCaster::castRayAgainstScene(shootRay, sceneObjects);

    if (hitResult.hit) {
        // 命中逻辑:扣血、播放特效等
        std::cout << "命中物体!距离:" << hitResult.distance << std::endl;
        GameObject* hitObj = static_cast<GameObject*>(hitResult.hitObject);
        hitObj->takeDamage(10); // 假设你的物体有受伤害方法
    } else {
        std::cout << "未命中任何物体" << std::endl;
    }
}

2. 鼠标拾取功能调用

把屏幕坐标转成世界射线,实现点击选中物体的功能:

// 屏幕坐标转世界空间射线
Ray screenToWorldRay(int mouseX, int mouseY, int screenWidth, int screenHeight, const glm::mat4& viewMatrix, const glm::mat4& projMatrix) {
    // 1. 屏幕坐标转NDC(标准化设备坐标,范围-1到1)
    float x = (2.0f * mouseX) / screenWidth - 1.0f;
    float y = 1.0f - (2.0f * mouseY) / screenHeight; // OpenGL Y轴是从下到上,需要翻转
    glm::vec4 rayNDC = glm::vec4(x, y, -1.0f, 1.0f); // Z=-1对应近平面

    // 2. 投影矩阵逆矩阵转视图空间射线
    glm::mat4 invProj = glm::inverse(projMatrix);
    glm::vec4 rayView = invProj * rayNDC;
    rayView = glm::vec4(rayView.x, rayView.y, -1.0f, 0.0f); // 方向向量,W设为0

    // 3. 视图矩阵逆矩阵转世界空间射线
    glm::mat4 invView = glm::inverse(viewMatrix);
    glm::vec4 rayWorld = invView * rayView;
    glm::vec3 rayDir = glm::normalize(glm::vec3(rayWorld));

    // 射线起点是相机的世界位置(视图矩阵逆矩阵的平移分量)
    glm::vec3 rayOrigin = glm::vec3(invView[3]);

    return Ray(rayOrigin, rayDir);
}

// 鼠标点击拾取
void onMouseClick(int mouseX, int mouseY, int screenWidth, int screenHeight, const glm::mat4& view, const glm::mat4& proj) {
    Ray pickRay = screenToWorldRay(mouseX, mouseY, screenWidth, screenHeight, view, proj);
    auto hitResult = RayCaster::castRayAgainstScene(pickRay, sceneObjects);

    if (hitResult.hit) {
        // 选中物体,比如高亮显示
        static_cast<GameObject*>(hitResult.hitObject)->setHighlighted(true);
    }
}

四、关键优化与注意事项

  • 强制归一化方向:射线方向向量必须归一化,不然距离计算会完全偏离预期,我在Ray构造函数里已经加了自动归一化,别随意去掉。
  • 分层检测策略:复杂模型可以先做AABB粗检测,过滤掉不可能命中的物体,再对候选物体做三角面细检测,避免直接遍历所有三角面(性能会暴跌)。
  • 避免检测自身:射击时记得在castRayAgainstScene里过滤掉玩家自己的模型,不然会出现“刚开枪就命中自己”的bug。
  • 性能优化:如果场景物体数量极多,可以把碰撞体按空间划分(比如八叉树、网格划分),减少每次检测的物体数量,避免主线程卡顿。

火山引擎 最新活动