AFrame:如何使光线投射器与 object3D 子对象一起工作?

Posted

技术标签:

【中文标题】AFrame:如何使光线投射器与 object3D 子对象一起工作?【英文标题】:AFrame: how to make the raycaster work with an object3D child objects? 【发布时间】:2022-01-20 04:37:54 【问题描述】:

我想使用 AFrame raycaster 组件来捕捉与对象的交集。我正在将我的自定义对象添加到 GLTF 模型中。我称它们为“碰撞形状”,它们被用来捕捉镀金模型和射弹之间的碰撞。用例:向敌人发射子弹。

问题在于,对于某些模型它可以工作,但对于其中一些模型,它会捕获碰撞形状之外的交叉点。

为了定位碰撞形状,我使用了碰撞对象应锚定到的骨骼名称。

我的代码如下(我删除了一些部分以使其更短):

<a-gltf-model src="#bird" 
                    position="2 -75 -300"
                    animation-mixer
                    scale="1 1 1"
                    
                    shape__Bone_38_08="bone: Bone_38_08; shape: box; halfExtents: 10 10 5"
                    shape__Bone_39_07="bone: Bone_39_07; shape: box; halfExtents: 15 10 10">
        
      </a-gltf-model>
      
      <a-gltf-model src="#orc" position="-2 0 -5" animation-mixer="clip: Orc.004" scale="2 2 2" rotation="0 180 0"
        shape__hair_1="bone: hair_1; shape: box; halfExtents: 0.05 0.075 0.05"
        shape__leg_L_1="bone: leg_L_1; shape: box; halfExtents: 0.05 0.125 0.05; offset: 0 -0.05 -0.1">

      </a-gltf-model>
      
      <a-entity camera look-controls position="0 1.6 0" wasd-controls>
        <a-cursor color="gray" raycaster="objects: [data-raycastable]" ></a-cursor>
      </a-entity>

组件:

AFRAME.registerComponent("shape", 
  schema: 
    bone:  default: "" ,
    shape:  default: "box", oneOf: ["box", "sphere", "cylinder"] ,
    offset:  type: "vec3", default:  x: 0, y: 0, z: 0  ,
    orientation:  type: "vec4", default:  x: 0, y: 0, z: 0, w: 1  ,
    // box
    halfExtents:  type: "vec3", default:  x: 0.5, y: 0.5, z: 0.5 , if:  shape: ["box"]  ,
    visible:  type: "boolean", default: true 
  ,
  multiple: true,
  init()
    const data = this.data;
    const self = this;
    const el = this.el;
    el.addEventListener("model-loaded", function modelReady() 
      el.removeEventListener("model-loaded", modelReady);

      const boneDummy = document.createElement("a-entity");
      self.setDummyShape(boneDummy, data);
      self.boneObj = self.getBone(el.object3D, data.bone);
      el.appendChild(boneDummy);
      self.boneDummy = boneDummy;
    );
  ,
  
  setDummyShape(dummy, data) 
    const shapeName = "collidable-shape";
    const config = 
      shapeName: data.bone,
      shape: data.shape,
      offset: data.offset,
      halfExtents: data.halfExtents
    ;

    dummy.setAttribute(shapeName, config);
  ,
  
  getBone(root, boneName) 
    let bone = root.getObjectByName(boneName);
    if (!bone) 
      root.traverse(node => 
        const n = node;
        if (n?.isBone && n.name.includes(boneName)) 
          bone = n;
        
      );
    

    return bone;
  ,
  
  inverseWorldMatrix: new THREE.Matrix4(),
  boneMatrix: new THREE.Matrix4(),

  tick() 
    const el = this.el;
    if (!el)  throw Error("AFRAME entity is undefined."); 
    if (!this.boneObj || !this.boneDummy) return;

    this.inverseWorldMatrix.copy(el.object3D.matrix).invert();

    this.boneMatrix.multiplyMatrices(this.inverseWorldMatrix, this.boneObj.matrixWorld);
    this.boneDummy.object3D.position.setFromMatrixPosition(this.boneMatrix);
  
)

AFRAME.registerComponent("collidable-shape", 
  schema: 
    shape:  default: "box", oneOf: ["box", "sphere", "cylinder"] ,
    offset:  type: "vec3", default:  x: 0, y: 0, z: 0  ,
    orientation:  type: "vec4", default:  x: 0, y: 0, z: 0, w: 1  ,

    // box
    halfExtents:  type: "vec3", default:  x: 0.5, y: 0.5, z: 0.5 , if:  shape: ["box"]  ,
    visible:  type: "boolean", default: true 
  ,

  collistionObject: null ,
  
  multiple:true,

  init() 
    const scene = this.el.sceneEl;
    if (!scene)  throw Error("AFRAME scene is undefined."); 

    if (scene.hasLoaded) 
      this.initShape();
     else 
      scene.addEventListener("loaded", this.initShape.bind(this));
    
  ,

  initShape() 
    const data = this.data;

    this.el.setAttribute("data-raycastable", "");
    this.el.addEventListener('mouseenter', evt => 
        console.log("mouse enter", data.shape);
        this.el.object3D.children[0].material.color.setHex(0x00ff00);
    );

    this.el.addEventListener('mouseleave', evt => 
        console.log("mouse leave", data.shape);
        this.el.object3D.children[0].material.color.setHex(0xff0000);
    );        

    const scale = new THREE.Vector3(1, 1, 1);
    this.el.object3D.getWorldScale(scale);
    let shape;
    let offset;
    let orientation;

    if (Object.prototype.hasOwnProperty.call(data, "offset")) 
      offset = new THREE.Vector3(
        data.offset.x * scale.x,
        data.offset.y * scale.y,
        data.offset.z * scale.z
      );
    

    if (Object.prototype.hasOwnProperty.call(data, "orientation")) 
      orientation = new THREE.Quaternion();
      orientation.copy(data.orientation);
    

    switch (data.shape) 
      case "box":
        shape = new THREE.BoxGeometry(
          data.halfExtents.x * 2 * scale.x,
          data.halfExtents.y * 2 * scale.y,
          data.halfExtents.z * 2 * scale.z
        );
        break;
    

    this._applyShape(shape, offset, data.visible);
  ,

  _applyShape(shape, offset, visible) 
    const material = new THREE.MeshBasicMaterial( color: 0xff0000, transparent: true, opacity: 0.3 );
    const wireframe = new THREE.LineSegments(
      new THREE.EdgesGeometry(shape),
      new THREE.LineBasicMaterial( color: 0xff0000, linewidth: 3 ));

    this.collistionObject = new THREE.Mesh(shape, material);
    this.collistionObject.add(wireframe);
    if (offset) 
      this.collistionObject.position.set(offset.x, offset.y, offset.z);
    
    this.collistionObject.visible = visible === true;
    this.el.setObject3D("mesh", this.collistionObject);
    
    const size = new THREE.Vector3();
    const box = new THREE.Box3().setFromObject(this.el.object3D);
    box.getSize(size);
    
    const bbox = new THREE.BoxGeometry(size.x, size.y, size.z);
    const bboxWireframe = new THREE.LineSegments(
      new THREE.EdgesGeometry(bbox),
      new THREE.LineBasicMaterial( color: 0x000000, linewidth: 10 ));
    
    this.el.object3D.add(bboxWireframe)
  
);

示例项目可以在这里找到:https://glitch.com/edit/#!/collisons-test 请注意,它对鸟的作用与预期一样,但对兽人来说表现得很奇怪。此外,边界框与碰撞形状框本身也不匹配。这一点我也不清楚。

【问题讨论】:

【参考方案1】:

边界框也与碰撞形状框本身不匹配。

边界框考虑了世界矩阵。当模型比例不同时,您可以看到它是如何变化的:

您还可以看到红色框也没有很好地缩放。我认为这里的大多数问题都是规模混淆的结果。

问题在于,对于某些模型它可以工作,但对于其中一些模型,它会捕获碰撞形状之外的交叉点。

在设置 object3D 之前添加线框会干扰 raycaster。不确定,但我猜这也是因为缩放问题。

Here's a glitch 在setObject3D 之后设置线框


我会从不同的方法开始。将盒子创建为场景子对象,并根据模型 worldMatrix + 骨骼偏移管理它们的变换。管理(放大/缩小、重新定位)和调试会更容易。

【讨论】:

非常感谢,伙计!像魅力一样工作。我会考虑按照您的建议将箱子移到现场。干杯!

以上是关于AFrame:如何使光线投射器与 object3D 子对象一起工作?的主要内容,如果未能解决你的问题,请参考以下文章

THREE.JS : 带有光线投射器和透视相机的点击事件

如何使用javascript从精灵表中缩放/拉伸/倾斜精灵?

GLSL 计算着色器闪烁块/正方形伪影

为什么在WebXR中错误地计算了光线投射方向?

aframe动态添加元素位置是错误的

三个js raycasting OBJ