Three.js中Raycaster检测不到模型反面(碰撞检测)

前序

最近一直在做碰撞检测的需求,在Raycaster的拾取上被硬控很长时间,在Three.js论坛和ChatGPT上也没找到解决的方法。昨天逛B站打发时间,竟阴差阳错找到一个更好的解决方法。

功能描述

这个功能类似游戏中的前后左右移动,但没有设置键盘事件(W/A/S/D移动),而是靠鼠标控制轨道来做碰撞。模型有三个房间,当相机进入到某个房间内,可以移动、旋转、缩放,一旦到达墙体时,就做相机悬停操作(也就是禁止相机超出房间),如果在悬停边缘 还进行前进操作,相机将顺着墙体移动。

踩坑指南

第一个思路是,给每个房间设置包围盒(详情请见),利用射线 检测相机与包围盒的关系。但后来行不通的原因是:三个房间中但凡有一个房间是不规则摆放(房间沿着Y轴旋转),那么这个房间的包围盒是不能够和这个模型匹配的。意思就是:如果模型Y轴旋转了30°,包围盒是不会随着模型变化的。直观了解一下吧,看图:

最终效果是这样的:黑色是房间模型,黄色是包围盒。所以这个方案pass

第二个思路是,参考官方的案例(games),官方案例的核心是胶囊体(Capsule.js)+八叉树(Octree.js)。但是这个方案对代码的相关结构有不小的改动,并且不是很符合我的需求。所以这个也pass

第三个思路是,利用射线 只需要检测房间的边缘模型(也就是4面墙体、房顶、地板)。不需要设置包围盒,也不需要使用第三方库。

一次次碰壁后,决定采用第三个方案。第三个方案检测准确率更高,毕竟检测的边缘体就是原模型。跟着第三个思路,继续往下走↓

实现思路

  1. 在模型加载完成时,将边缘模型找出来,存储到数组内。边缘模型?:就是一个房间的4个墙体、房顶和地板啦。
  2. 给相机设置四条射线(Raycaster),分别是相机正前方、相机正后方、相机左侧、相机右侧。
  3. 根据相机位置,在每次移动时,创建新的4条射线。
  4. 每次移动时,4条射线都将进行检测。
  5. 把4条射线检测到的第一项进行存储(Raycaster返回的数组是由近到远排列的),存储到一个新数组内。
  6. 对新数组进行sort排序,sort排序是再过滤一遍,目的就是将距离相机由近到远的模型排列组合。
  7. 控制相机悬停。

代码步骤

1. 统计需要检测的边缘模型

loader.load(
    'model.glb',
    function(gltf) {
      const sceneMarginBodys = []; // 边缘模型/墙体&房顶&地板

      const model = gltf.scene;
      
      // sceneMarginBodys:Array  需要统计的边缘模型名称
      for (let everyMargin = 0; everyMargin < sceneMarginBodys.length; everyMargin++) {
        gltf.scene.traverse(function (child) {
          if (child.isMesh){

            if ( sceneMarginBodys[everyMargin] == child.name ){
              child.material.side = THREE.DoubleSide; // *** 核心 ***
              sceneMarginBodys.push(child);
            }

          }
        })
      }

      Scene.add(model);

      if(callback) {
        callback(true, sceneMarginBodys);
      }
    }
  )

2. 为相机设置4条射线

// 获取相机4个方向的世界方向向量
var forwardDirection = camera.getWorldDirection(new THREE.Vector3());
var backwardDirection = forwardDirection.clone().negate();
var rightDirection = new THREE.Vector3(-forwardDirection.z, forwardDirection.y, forwardDirection.x).normalize();
var leftDirection = rightDirection.clone().negate();

3. 相机更新,射线更新

// 根据相机位置创建四条射线
var raycasterForward = createRaycaster(cp, forwardDirection);
var raycasterBackward = createRaycaster(cp, backwardDirection);
var raycasterRight = createRaycaster(cp, rightDirection);
var raycasterLeft = createRaycaster(cp, leftDirection);

4. 每次移动,都进行检测

// 检测四条射线是否与边界模型相交
var intersectsForward = raycasterForward.intersectObjects(marginBodys);
var intersectsBackward = raycasterBackward.intersectObjects(marginBodys);
var intersectsRight = raycasterRight.intersectObjects(marginBodys);
var intersectsLeft = raycasterLeft.intersectObjects(marginBodys);

5. 统计4条射线检测元素

var rc1 = intersectsForward.length > 0 ? intersectsForward[0] : null;
var rc2 = intersectsBackward.length > 0 ? intersectsBackward[0] : null;
var rc3 = intersectsRight.length > 0 ? intersectsRight[0] : null;
var rc4 = intersectsLeft.length > 0 ? intersectsLeft[0] : null;
var arr = [];
if(rc1) arr.push(rc1);
if(rc2) arr.push(rc2);
if(rc3) arr.push(rc3);
if(rc4) arr.push(rc4);

6. sort排序

获取到的每个元素内都有一个distance,根据这个字段进行排序,就可以拿到距离相机最近的模型

var rSorted = arr.sort((a, b) => { return a.distance - b.distance });

7. 相机悬停

提前设定一个最大检测长度规则,ThreeJs单位默认是米,所以设定到最小,我这里是0.1

if (rSorted.length > 0 && rSorted[0].distance < maxDistance) {
  var r = rSorted[0];
  var d = new THREE.Vector3().subVectors(cp, r.point).normalize();
  camera.position.addScaledVector(d, maxDistance - r.distance);
}

写到最后(本人遇到的问题)

*万恶的起源(重点):child.material.side = THREE.DoubleSide;

整体下来麻烦的点不是代码层面,而是Raycaster涉及到的知识。如果不使用上面代码,相机在模型正面可以正常的检测到墙体模型,但是在墙体另一面,就检测不到了~WTF!?

(PS:感兴趣的同学可以看看up主:老陈打码老师的课程,我是在这节课有的新思路,去看看。个人感觉这类知识点(BufferAttribute)可以了解一下,涉及到了优化GPU渲染方法)。

当然,在发现这个方法之前,原本打算使用一个笨方法的。就是请建模师 把边缘模型的法线翻转过来,这样就可以从墙体另一边检测到墙体了。但是这个方法会对模型结构产生影响,而且翻转后从正面就检测不到了,还有就是后续还需要建模师不断的bugfix,不太推荐(因为我们的建模师被折腾的准备骂娘了)。

使用上面的方法,可以更完美的解决问题,并且能节省更多的成本(老板龇着牙直夸你懂事)。OK,希望以后有遇到这个问题的同学,少走弯路 少踩坑

更新一下:

聊聊BufferAttribute。无论是ThreeJs还是webgl,所有几何体都是由三角面构成的,三角面的结构原理应该都是一样的,一个顶点由三个Float组成(逆时针) → 三个顶点构成一个三角面(逆时针)。聊到这就可以了~

有一个叫做“法线”的鬼东西,我也不清楚这个法线是存在顶点上还是三角面上。总之,构成三角面时,这个叫法线的现象就可以直观的看到了。正面可以正常看到,反面反而就看不到了?!如果想要反面正常 怎么办?那就改变顶点的组成顺序(顺时针),可这样,正面又看不到了。无论逆/顺时针,三角面永远都是一面可见。

抖一下包袱:使用THREE.DoubleSide,就可以双面可见。

聊到这里,对于法线的理解,应该有概念了:可见的面就是法线的朝向。单面可见的设计是为了节省GPU的工作量,可以亲手尝试一下~GPU是负责实际的图形计算和绘制。这也就说的通,为啥要设计成单面了,既要处理庞大的顶点数据,还要绘制图形。说到这还是离不开:性能,也就是在第一步代码里为什么要把边缘模型择出来了吧。其实不择出来,我电脑只要打开3D,也能嗡嗡响一天。目前我在各平台看webgl相关的讨论,大多离不开webGPU优化,所以... 还是有必要了解一下的

  • 36
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值