如何在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。 - 性能优化:如果场景物体数量极多,可以把碰撞体按空间划分(比如八叉树、网格划分),减少每次检测的物体数量,避免主线程卡顿。




