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 glitch 在 setObject3D
之后设置线框
我将从不同的方法入手。创建盒子作为场景子项并根据模型 worldMatrix + 骨骼偏移管理它们的变换。管理(缩放 up/down、重新定位)和调试会更容易。
我想使用 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 glitch 在 setObject3D
我将从不同的方法入手。创建盒子作为场景子项并根据模型 worldMatrix + 骨骼偏移管理它们的变换。管理(缩放 up/down、重新定位)和调试会更容易。