AFrame:如何使 raycaster 与 object3D 子对象一起工作?

AFrame: how to make the raycaster work with an object3D child objects?

我想使用 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 请注意,它对鸟来说如预期的那样工作,但对兽人来说表现得很奇怪。此外,边界框与碰撞形状框本身不匹配。这也是我不清楚的地方。

Also the bounding box doesn't match the collision-shape box itself.

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

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

The problem is that for some models it works, but for some of them it catches intersections outside the collision shape.

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

Here's a glitchsetObject3D

之后设置线框

我将从不同的方法入手。创建盒子作为场景子项并根据模型 worldMatrix + 骨骼偏移管理它们的变换。管理(缩放 up/down、重新定位)和调试会更容易。