<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Physics Sample V4 (VR Hands)</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://binzume.github.io/aframe-xylayout/dist/xylayout-all.min.js"></script>
<script src="//cdn.rawgit.com/donmccurdy/aframe-physics-system/v4.0.1/dist/aframe-physics-system.min.js"></script>
<script src="https://binzume.github.io/aframe-vrm/dist/aframe-vrm.js"></script>
</head>
<body>
<a-scene background="color: #25a" physics="debug: true; gravity: -9.8;">
<a-light type="ambient" color="#888"></a-light>
<a-light type="point" intensity="0.5" position="2 4 4"></a-light>
<a-entity id="camera-rig" camera-control>
<a-camera position="0 1.6 0"></a-camera>
<a-entity hand-controller></a-entity>
<a-entity id="leftHand" laser-controls="hand: left"></a-entity>
<a-entity id="rightHand" laser-controls="hand: right"></a-entity>
</a-entity>
<a-box id="table"
position="0 0.5 -1"
width="2" height="0.2" depth="1.5"
color="gray"
static-body="shape: box;">
</a-box>
<a-box id="falling-box"
position="0.2 3 -1"
rotation="10 20 30"
width="0.3" height="0.3" depth="0.3"
color="green"
dynamic-body="shape: box; mass: 2;">
</a-box>
<a-box id="ground"
position="0 -0.05 0"
width="20" height="0.1" depth="20"
color="#484848"
static-body="shape: box;">
</a-box>
</a-scene>
<script>
'use strict';
AFRAME.registerComponent('camera-control', {
schema: {
homePosition: { type: 'vec3', default: { x: 0, y: 1.6, z: 4 } },
vrHomePosition: { type: 'vec3', default: { x: 0, y: 0, z: 0 } }
},
init() {
this.dragging = false;
this.el.sceneEl.addEventListener('exit-vr', ev => this.resetPosition());
this.el.sceneEl.addEventListener('enter-vr', ev => this.resetPosition());
this.resetPosition();
let cursorEl = document.getElementById('mouse-cursor');
let canvasEl = this.el.sceneEl.canvas;
let dragX = 0, dragY = 0;
let lookAt = new THREE.Vector3(0, 1, 0); // 視点の中心を少し上に
let rotation = new THREE.Euler(-0.2, 0, 0, 'YXZ'); // 少し下を向くように初期設定
let distance = 3;
let updateCamera = () => {
// VRモード中はマウス操作を無効化
if (this.el.sceneEl.is('vr-mode')) {
this.el.object3D.position.set(0,0,0);
this.el.object3D.rotation.set(0,0,0);
return;
}
let cameraObj = this.el.object3D;
let cameraRot = new THREE.Quaternion().setFromEuler(rotation);
let cameraVec = new THREE.Vector3(0, 0, 1).applyQuaternion(cameraRot).multiplyScalar(distance);
let cameraPos = lookAt.clone().add(cameraVec);
cameraObj.position.copy(cameraPos);
cameraObj.quaternion.copy(cameraRot);
};
updateCamera(); // 初期視点を設定
this.onMouseMove = (ev) => {
let speedFactor = 0.005;
// 中ボタン(ホイール)ドラッグで視点中心が移動
if (ev.buttons & 4) {
let v = new THREE.Vector3(dragX - ev.offsetX, -(dragY - ev.offsetY), 0).applyQuaternion(this.el.object3D.quaternion);
lookAt.add(v.multiplyScalar(speedFactor * distance));
}
// 左ボタンドラッグで回転
else if (ev.buttons & 1) {
rotation.x -= (dragY - ev.offsetY) * speedFactor;
rotation.y -= (dragX - ev.offsetX) * speedFactor;
rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, rotation.x)); // 上下角制限
}
updateCamera();
dragX = ev.offsetX;
dragY = ev.offsetY;
};
canvasEl.addEventListener('mousedown', (ev) => {
if (!this.dragging && ev.target === canvasEl) {
this.dragging = true;
dragX = ev.offsetX;
dragY = ev.offsetY;
canvasEl.addEventListener('mousemove', this.onMouseMove);
}
});
canvasEl.addEventListener('mouseup', (ev) => {
this.dragging = false;
canvasEl.removeEventListener('mousemove', this.onMouseMove);
});
canvasEl.addEventListener('wheel', ev => {
if (this.el.sceneEl.is('vr-mode')) return;
let speedFactor = 0.001;
distance = Math.max(0.5, distance + ev.deltaY * speedFactor);
updateCamera();
});
},
resetPosition() {
if (this.el.sceneEl.is('vr-mode')) {
this.el.setAttribute('position', this.data.vrHomePosition);
} else {
// PCモードではカメラリグの位置は動かさない(中のカメラの位置を制御するため)
}
}
});
AFRAME.registerComponent('pose-editor-window', {
schema: {
vrm: { type: 'selector', default: '[vrm]' },
},
init() {
let listEl = this.el.querySelector('[name=item-list]');
if (!listEl) return;
let list = this.list = listEl.components.xylist;
let self = this;
list.setAdapter({
create() {
let el = document.createElement('a-plane');
el.setAttribute('width', 3);
el.setAttribute('height', 0.48);
el.setAttribute('color', 'black');
el.setAttribute('xyrect', {});
let sliderEl = document.createElement('a-xyrange');
sliderEl.setAttribute('width', 1.5);
sliderEl.setAttribute('position', { x: 0.8, y: 0, z: 0.05 });
sliderEl.addEventListener('change', (ev) => {
self.vrm.setBlendShapeWeight(el.getAttribute('xylabel').value, ev.detail.value * 0.01);
});
el.appendChild(sliderEl);
return el;
},
bind(position, el, contents) {
el.setAttribute('xylabel', { value: contents[position], wrapCount: 16, renderingMode: 'canvas' });
el.querySelector('a-xyrange').value = self.vrm.getBlendShapeWeight(contents[position]) * 100;
}
});
this.el.querySelector('[name=reset-all-morph]').addEventListener('click', (ev) => {
self.vrm.resetBlendShape();
this.list.setContents(this.blendShapeNames);
});
this.onModelLoaded = (ev) => this.updateAvatar(ev.detail.avatar);
},
update() {
this.remove();
this.vrmEl = this.data.vrm;
if (!this.vrmEl) return;
this.vrmEl.addEventListener('model-loaded', this.onModelLoaded);
if (this.vrmEl.components.vrm.avatar) {
this.updateAvatar(this.vrmEl.components.vrm.avatar);
}
},
updateAvatar(avatar) {
this.vrm = avatar;
this.blendShapeNames = Object.keys(avatar.blendShapes);
this.list.setContents(this.blendShapeNames);
},
remove() {
if (this.vrmEl) {
this.vrmEl.removeEventListener('model-loaded', this.onModelLoaded);
}
}
});
AFRAME.registerComponent('hand-controller', {
schema: {
color: { default: '#00ff00' }
},
init() {
this.hands = {};
this.physics = null;
this._tmpQ0 = new THREE.Quaternion();
this._tmpV0 = new THREE.Vector3();
this._tmpV1 = new THREE.Vector3();
if (this.el.sceneEl.systems.webxr) {
this.el.sceneEl.setAttribute('webxr', 'optionalFeatures:bounded-floor,hand-tracking');
let hand0 = this.el.sceneEl.renderer.xr.getHand(0);
hand0.addEventListener('connected', ev => this._handConnected(hand0, ev, 'leftHand'));
hand0.addEventListener('disconnected', ev => this._handDisconnected(hand0, ev, 'leftHand'));
let hand1 = this.el.sceneEl.renderer.xr.getHand(1);
hand1.addEventListener('connected', ev => this._handConnected(hand1, ev, 'rightHand'));
hand1.addEventListener('disconnected', ev => this._handDisconnected(hand1, ev, 'rightHand'));
}
},
tick() {
let hands = Object.values(this.hands);
if (hands.length == 0) {
this.pause();
}
hands.forEach(hand => {
hand.binds.forEach(([node, obj, body]) => {
if (body) {
body.position.copy(node.getWorldPosition(this._tmpV0));
body.quaternion.copy(node.getWorldQuaternion(this._tmpQ0));
}
});
});
},
remove() {
let names = Object.keys(this.hands);
names.forEach(name => {
this.el.removeObject3D(name);
});
},
_handConnected(hand, ev, name) {
if (!ev.data.hand || this.hands[name]) {
return;
}
if (globalThis.CANNON && this.el.sceneEl.systems.physics && this.el.sceneEl.systems.physics.driver) {
this.physics = { driver: this.el.sceneEl.systems.physics.driver };
}
console.log("Hand connected:", name, ev);
let geometry = new THREE.BoxGeometry(1, 1, 1);
let material = new THREE.MeshBasicMaterial({ color: new THREE.Color(this.data.color) });
material.transparent = true;
material.opacity = 0.4;
this.el.setObject3D(name, hand);
let handData = { hand: hand, name: name, binds: [] };
this.hands[name] = handData;
for (let joint of hand.joints) {
let cube = new THREE.Mesh(geometry, material);
let scale = Math.min(joint.jointRadius || 0.015, 0.05);
cube.scale.set(scale, scale, scale);
joint.add(cube);
let body = null;
if (this.physics) {
body = new CANNON.Body({
mass: 0, // 手は物理演算の影響を受けず、他のものを押すだけ (kinematic)
collisionFilterGroup: 4,
collisionFilterMask: -1 // 全てのグループと衝突
});
body.addShape(new CANNON.Sphere(scale * 0.5));
this.physics.driver.addBody(body);
}
handData.binds.push([joint, cube, body]);
}
this.play();
// 元のコントローラーモデルを非表示にする
let controllerEl = document.getElementById(name);
if(controllerEl) {
controllerEl.setAttribute('visible', false);
}
},
_handDisconnected(hand, ev, name) {
console.log("Hand disconnected:", name);
if (this.hands[name]) {
this.hands[name].binds.forEach(([node, obj, body]) => {
obj.parent.remove(obj);
if (body) {
this.physics.driver.removeBody(body);
}
});
this.el.removeObject3D(name);
delete this.hands[name];
}
// コントローラーモデルを再表示
let controllerEl = document.getElementById(name);
if(controllerEl) {
controllerEl.setAttribute('visible', true);
}
}
});
AFRAME.registerComponent('draggable-body', {
// ... (変更なし)
});
window.addEventListener('DOMContentLoaded', (ev) => {
// ... (変更なし)
}, { once: true });
</script>
</body>
</html>
使用変数
background | |
blendShapeNames | |
body | |
cameraObj | |
cameraPos | |
cameraRot | |
cameraVec | |
canvasEl | |
charset | |
color | |
content | |
controllerEl | |
controls | |
cube | |
cursorEl | |
depth | |
distance | |
dragging | |
dragX | |
dragY | |
el | |
equiv | |
ev | |
geometry | |
hand0 | |
hand1 | |
hand | |
handData | |
hands | |
height | |
id | |
intensity | |
length | |
list | |
listEl | |
lookAt | |
material | |
name | |
names | |
onModelLoaded | |
onMouseMove | |
opacity | |
physics | |
position | |
rotation | |
scale | |
self | |
sliderEl | |
speedFactor | |
src | |
target | |
transparent | |
type | |
updateCamera | |
v | |
value | |
vrm | |
vrmEl | |
width | |
x | |
_tmpQ0 | |
_tmpV0 | |
_tmpV1 |