CocosCreator 3D交互进阶:射线投射实现精准物体选中

一、射线投射技术基础解析

1.1 射线投射的数学本质

射线投射(Ray Casting)本质是基于空间几何的碰撞检测算法,通过从观察点发射一条无限延伸的射线,检测与场景中物体的交点。在3D空间中,射线可表示为参数方程:
P(t) = O + t * D
其中O为起点(通常是摄像机位置),D为归一化方向向量,t为距离参数。当t>0且最小时的交点即为最近的命中点。

1.2 CocosCreator中的实现架构

CocosCreator 3.x版本通过PhysicsSystemRay类封装了射线投射功能。核心流程分为三步:

  1. 射线生成:通过摄像机坐标和屏幕触摸点计算世界空间射线
  2. 物理查询:调用物理引擎进行碰撞检测
  3. 结果处理:解析命中信息并触发交互逻辑

物理系统支持两种查询模式:

  • 射线投射查询(Raycast):返回第一个命中的物体
  • 球形投射查询(SphereCast):适用于需要碰撞体积的场景

二、核心实现步骤详解

2.1 射线生成与转换

  1. // 获取主摄像机节点
  2. const camera = find('MainCamera')?.getComponent(Camera)!;
  3. // 将屏幕坐标转换为世界空间射线
  4. function screenToWorldRay(screenPos: Vec2): Ray {
  5. // 1. 将屏幕坐标归一化到[-1,1]范围
  6. const ndc = new Vec3(
  7. screenPos.x / canvas.width * 2 - 1,
  8. screenPos.y / canvas.height * 2 - 1,
  9. 1
  10. );
  11. // 2. 通过摄像机反投影计算射线方向
  12. const ray = new Ray();
  13. camera.screenPointToRay(ndc, ray);
  14. return ray;
  15. }

关键点说明:

  • 需处理不同分辨率下的坐标归一化
  • 深度值(Z坐标)影响射线长度,通常设为1获取从摄像机出发的标准射线

2.2 物理查询实现

  1. import { physics } from 'cc';
  2. function checkRaycast(ray: Ray): boolean {
  3. const result = new physics.RaycastResult();
  4. const hit = physics.raycast(
  5. ray.origin,
  6. ray.direction,
  7. 1000, // 最大检测距离
  8. 0x01, // 碰撞组(需与目标物体匹配)
  9. result
  10. );
  11. if (hit) {
  12. console.log(`命中物体: ${result.collider.node.name}`);
  13. // 处理选中逻辑...
  14. return true;
  15. }
  16. return false;
  17. }

参数配置要点:

  • 最大距离:根据场景规模合理设置,避免无效计算
  • 碰撞组:通过physics.CollisionGroup控制检测范围
  • 结果对象:包含碰撞点、法线、碰撞体等关键信息

2.3 触摸事件集成

  1. // 在Canvas节点上添加触摸事件
  2. canvas.node.on(Node.EventType.TOUCH_START, (event: EventTouch) => {
  3. const pos = event.getUILocation();
  4. const ray = screenToWorldRay(new Vec2(pos.x, pos.y));
  5. checkRaycast(ray);
  6. });

移动端适配建议:

  • 使用getUILocation()而非getLocation()确保坐标系统一致
  • 添加触摸区域判断,避免UI元素干扰

三、性能优化策略

3.1 分层检测技术

通过physics.Layer实现选择性检测:

  1. // 定义可选中物体层
  2. const SELECTABLE_LAYER = 1 << 0;
  3. // 在物体上设置碰撞层
  4. this.node.group = SELECTABLE_LAYER;
  5. // 查询时指定层
  6. physics.raycast(ray.origin, ray.direction, 1000, SELECTABLE_LAYER, result);

效果:

  • 减少不必要的碰撞检测
  • 提升复杂场景下的帧率稳定性

3.2 空间分区优化

对于大规模场景:

  1. 使用QuadTreeOctree进行空间划分
  2. 先进行粗粒度区域检测,再执行精确射线投射
  3. 典型实现:

    1. class SpacePartitioner {
    2. private trees: Map<number, QuadTree>;
    3. constructor() {
    4. this.trees = new Map();
    5. }
    6. query(bounds: Rect, callback: (node: Node) => void) {
    7. // 实现空间分区查询逻辑...
    8. }
    9. }

3.3 批处理检测

针对多物体同时检测场景:

  1. function batchRaycast(rays: Ray[]): RaycastResult[] {
  2. return rays.map(ray => {
  3. const result = new physics.RaycastResult();
  4. physics.raycast(ray.origin, ray.direction, 1000, 0x01, result);
  5. return result;
  6. });
  7. }

适用场景:

  • VR/AR中的多指触控
  • 策略游戏中的范围选择

四、高级应用场景

4.1 穿透检测实现

通过修改射线参数实现穿透效果:

  1. function penetratingRaycast(ray: Ray, maxHits: number): RaycastResult[] {
  2. const results: RaycastResult[] = [];
  3. let currentRay = ray.clone();
  4. for (let i = 0; i < maxHits; i++) {
  5. const result = new physics.RaycastResult();
  6. const hit = physics.raycast(
  7. currentRay.origin,
  8. currentRay.direction,
  9. 1000,
  10. 0x01,
  11. result
  12. );
  13. if (!hit) break;
  14. results.push(result);
  15. // 将射线起点移动到碰撞点后方继续检测
  16. currentRay.origin.add(currentRay.direction.multiplyScalar(result.distance + 0.01));
  17. }
  18. return results;
  19. }

4.2 预测性射线投射

结合物体运动预测的改进方案:

  1. function predictiveRaycast(
  2. ray: Ray,
  3. target: Node,
  4. deltaTime: number
  5. ): RaycastResult | null {
  6. // 1. 获取目标当前速度
  7. const rb = target.getComponent(RigidBody);
  8. const velocity = rb?.linearVelocity || Vec3.ZERO;
  9. // 2. 预测未来位置
  10. const futurePos = target.worldPosition.add(velocity.multiplyScalar(deltaTime));
  11. // 3. 构建预测射线
  12. const predictiveRay = new Ray(
  13. ray.origin,
  14. futurePos.subtract(ray.origin).normalize()
  15. );
  16. // 4. 执行检测
  17. const result = new physics.RaycastResult();
  18. return physics.raycast(predictiveRay.origin, predictiveRay.direction, 1000, 0x01, result)
  19. ? result
  20. : null;
  21. }

五、常见问题解决方案

5.1 检测失效排查

  1. 摄像机投影矩阵问题

    • 检查Camera组件的projection属性是否为PERSPECTIVE
    • 验证nearClipfarClip值是否合理
  2. 碰撞体配置错误

    • 确保目标物体有Collider组件
    • 检查isTrigger属性是否符合需求
  3. 坐标系不匹配

    • 统一使用世界坐标进行计算
    • 避免混合使用局部坐标和世界坐标

5.2 移动端适配建议

  1. 触摸精度优化

    1. // 增加触摸容错区域
    2. const touchThreshold = 10; // 像素
    3. function isNearPreviousTouch(pos: Vec2, prevPos: Vec2): boolean {
    4. return pos.distanceTo(prevPos) < touchThreshold;
    5. }
  2. 多指触控处理

    1. canvas.node.on(Node.EventType.TOUCH_MOVE, (event: EventTouch) => {
    2. const touches = event.getTouches();
    3. if (touches.length > 1) {
    4. // 处理双指缩放等手势
    5. }
    6. });

六、最佳实践总结

  1. 分层架构设计

    • 将射线检测逻辑封装为独立模块
    • 使用事件系统解耦检测与响应
  2. 性能监控

    1. // 在关键位置添加性能标记
    2. if (CC_DEBUG) {
    3. console.time('raycast');
    4. // 执行检测...
    5. console.timeEnd('raycast');
    6. }
  3. 资源管理

    • 及时销毁不再需要的RaycastResult对象
    • 对静态场景预计算检测数据
  4. 跨平台兼容

    • 统一输入处理接口
    • 针对不同设备调整检测参数

通过系统掌握射线投射技术,开发者能够高效实现3D场景中的物体交互,为游戏和仿真应用构建更自然的操作体验。建议结合具体项目需求,在基础实现上逐步添加优化层,达到性能与效果的平衡。