<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Physics Sample (Fixed)</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-camera position="0 1.6 4"></a-camera>
<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 3 -1"
width="0.5" height="0.5" depth="0.5"
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: 0, z: 4 } },
vrHomePosition: { type: 'vec3', default: { x: 0, y: 0, z: 0.5 } }
},
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, 0, 0);
let rotation = new THREE.Euler(0, 0, 0, 'YXZ');
let distance = lookAt.clone().sub(this.el.getAttribute('position')).length();
let updateCamera = () => {
if (this.el.sceneEl.is('vr-mode')) {
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(cameraObj.parent.worldToLocal(cameraPos));
cameraObj.quaternion.copy(cameraRot.multiply(cameraObj.parent.getWorldQuaternion(new THREE.Quaternion())));
};
this.onMouseMove = (ev) => {
let targetObj = this.el.object3D;
let speedFactor = 0.005;
if (ev.buttons & 6) {
let v = new THREE.Vector3(dragX - ev.offsetX, -(dragY - ev.offsetY), 0).applyQuaternion(targetObj.quaternion);
lookAt.add(v.multiplyScalar(speedFactor));
} else {
rotation.x += (dragY - ev.offsetY) * speedFactor;
rotation.y += (dragX - ev.offsetX) * speedFactor;
}
updateCamera();
dragX = ev.offsetX;
dragY = ev.offsetY;
};
canvasEl.addEventListener('mousedown', (ev) => {
if (!this.dragging && (!cursorEl || cursorEl.components.cursor.intersectedEl == null)) {
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 => {
let speedFactor = 0.005;
distance = Math.max(0.1, distance + ev.deltaY * speedFactor);
updateCamera();
});
},
resetPosition() {
let sky = this.el.sceneEl.querySelector('a-sky');
if (sky) {
sky.object3D.visible = !this.el.sceneEl.is('ar-mode');
}
if (this.el.sceneEl.is('vr-mode') || this.el.sceneEl.is('ar-mode')) {
this.el.setAttribute('position', this.data.vrHomePosition);
} else {
this.el.setAttribute('position', this.data.homePosition);
}
this.el.setAttribute('rotation', { x: 0, y: 0, z: 0 });
}
});
AFRAME.registerComponent('pose-editor-window', {
schema: {
vrm: { type: 'selector', default: '[vrm]' },
},
init() {
let listEl = this.el.querySelector('[name=item-list]');
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;
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));
}
});
hand.fingerLen = hand.fingers.map(n => {
let len = 0;
for (let i = n - 3; i < n; i++) {
len += this._tmpV0.copy(hand.hand.joints[i + 1].position).sub(hand.hand.joints[i].position).length();
}
return len;
});
let dd = hand.fingers.map((n, i) => { // 4
let tip = hand.hand.joints[n];
let root = hand.hand.joints[n - 3];
let tipPos = tip.getWorldPosition(this._tmpV0);
let rootPos = root.getWorldPosition(this._tmpV1);
if (hand.fingerLen[i] > 0.01) {
let r = tipPos.sub(rootPos).length() / hand.fingerLen[i];
if (r < 0.6 || r > 0.9) {
return r < 0.6;
}
}
return undefined;
});
hand.fingerState = dd;
let open = dd[0] == false && dd[1] == false && dd[2] == false && dd[3] == false && dd[4] == false;
if (!hand.open && open) {
let el = document.getElementById(hand.name);
if (el) {
el.setAttribute('raycaster', 'far', 0.04);
}
}
hand.open = open;
let pointing = dd[1] == false && dd[2] == true && dd[3] == true && dd[4] == true;
if (!hand.pointing && pointing) {
hand.pointing = pointing;
let el = document.getElementById(hand.name);
if (el) {
el.setAttribute('raycaster', 'far', Infinity);
}
}
hand.pointing = pointing;
});
},
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", hand, 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: [], fingers: [4, 9, 14, 19, 24] };
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,
collisionFilterGroup: 4,
collisionFilterMask: ~4
});
body.addShape(new CANNON.Sphere(scale * 0.5));
this.physics.driver.addBody(body);
}
handData.binds.push([joint, cube, body]);
}
this.play();
for (let controllerEl of this.el.sceneEl.querySelectorAll('[generic-tracked-controller-controls]')) {
controllerEl.setAttribute('generic-tracked-controller-controls', { defaultModel: false });
if (this.physics) {
controllerEl.removeAttribute('static-body');
}
console.log(controllerEl);
}
},
_handDisconnected(hand, ev, name) {
this.el.removeObject3D(name);
if (this.hands[name]) {
this.hands[name].binds.forEach(([node, obj, body]) => {
obj.parent.remove(obj);
if (body) {
this.physics.driver.removeBody(body);
}
});
delete this.hands[name];
}
}
});
AFRAME.registerComponent('draggable-body', {
dependencies: ['xy-drag-control'],
init() {
let el = this.el;
let dragging = false;
el.addEventListener('mousedown', ev => {
if (dragging) {
return;
}
let velocity = new THREE.Vector3(0, 0, 0);
let prevPos = el.object3D.position.clone();
let prevTime = el.sceneEl.time;
let timer = setInterval(() => {
let dt = el.sceneEl.time - prevTime;
if (dt > 0) {
velocity.copy(el.object3D.position).sub(prevPos).multiplyScalar(1000 / dt);
}
prevPos.copy(el.object3D.position);
prevTime = el.sceneEl.time;
}, 50);
// set mass = 0
let draggingObjectMass = el.body.mass;
dragging = true;
el.body.mass = 0;
el.addEventListener('mouseup', ev => {
dragging = false;
clearInterval(timer);
// restore mass
el.body.mass = draggingObjectMass;
el.body.velocity.copy(velocity);
}, { once: true });
});
}
});
window.addEventListener('DOMContentLoaded', (ev) => {
// このセクションは元のVRMデモのUIを初期化するためのものです。
// 対応するHTML要素がないため、コンソールでエラーが発生しますが、
// 物理デモ自体への影響はありません。
let vrmEl = document.getElementById('avatar');
let listEl = document.getElementById('model-list');
if (listEl && listEl.components.xylist) {
let models = [
{ name: 'AliciaSolid', src: 'assets/AliciaSolid/AliciaSolid.vrm' },
{ name: 'AliciaSolid_mmd', src: 'assets/AliciaSolid/AliciaSolid_mmd.vrm' },
{ name: '千駄ヶ谷 渋', src: 'assets/VRoid/8801565727279527051.vrm' },
{ name: '千駄ヶ谷 篠', src: 'assets/VRoid/4537789756845150029.vrm' },
{ name: '東北ずん子', src: 'assets/Zunko/zunko_vrm.vrm' }
];
let motions = [
'assets/bvhfiles/la_bvh_sample01.bvh',
'assets/bvhfiles/la_bvh_sample02.bvh',
'assets/bvhfiles/la_bvh_sample03.bvh'
];
let list = listEl.components.xylist;
list.setAdapter({
create(parent) {
let el = document.createElement('a-plane');
el.setAttribute('width', 3);
el.setAttribute('height', 0.45);
el.setAttribute('color', 'black');
el.setAttribute('xyrect', {});
return el;
},
bind(position, el, contents) {
el.setAttribute('xylabel', { value: contents[position].name, wrapCount: 16 });
}
});
list.setContents(models);
listEl.addEventListener('clickitem', (ev) => {
if (!vrmEl.hasAttribute('vrm-poser')) {
vrmEl.setAttribute('vrm-anim', { src: '' });
}
vrmEl.setAttribute('vrm', { src: models[ev.detail.index].src });
});
let files = motions.map(path => { let m = path.match(/([^\/]+)\.\w+$/); return m ? m[1] : path }).join(',');
document.getElementById('animation-select').setAttribute('values', files);
document.getElementById('animation-select').addEventListener('change', (ev) => {
vrmEl.setAttribute('vrm-anim', { src: motions[ev.detail.index], format: '' });
});
document.getElementById('skeleton-toggle').addEventListener('change', (ev) => {
if (ev.detail.value) {
vrmEl.setAttribute('vrm-skeleton', {});
} else {
vrmEl.removeAttribute('vrm-skeleton');
}
});
document.getElementById('blink-toggle').addEventListener('change', (ev) => {
vrmEl.setAttribute('vrm', 'blink', ev.detail.value);
});
document.getElementById('lookat-toggle').addEventListener('change', (ev) => {
vrmEl.setAttribute('vrm', 'lookAt', ev.detail.value ? 'a-camera' : null);
});
document.getElementById('first-person-toggle').addEventListener('change', (ev) => {
vrmEl.setAttribute('vrm', 'firstPerson', ev.detail.value ? 'a-camera' : null);
});
document.getElementById('physics-toggle').value = vrmEl.getAttribute('vrm').enablePhysics;
document.getElementById('physics-toggle').addEventListener('change', (ev) => {
vrmEl.setAttribute('vrm', 'enablePhysics', ev.detail.value);
});
document.getElementById('bone-toggle').addEventListener('change', (ev) => {
let containerEl = document.querySelector('#bone-buttons');
if (ev.detail.value) {
vrmEl.removeAttribute('vrm-anim');
vrmEl.setAttribute('vrm-poser', {});
containerEl.setAttribute('visible', true);
} else {
vrmEl.removeAttribute('vrm-poser');
containerEl.setAttribute('visible', false);
}
});
document.getElementById('bone-save-button').addEventListener('click', (ev) => {
if (vrmEl.hasAttribute('vrm-poser')) {
let poseJson = JSON.stringify(vrmEl.components['vrm-poser'].getPoseData(true));
localStorage.setItem('vrm-pose0', poseJson);
}
});
document.getElementById('bone-load-button').addEventListener('click', (ev) => {
if (vrmEl.hasAttribute('vrm-poser')) {
let poseJson = localStorage.getItem('vrm-pose0');
if (poseJson) {
vrmEl.components['vrm-poser'].setPoseData(JSON.parse(poseJson));
}
}
});
document.getElementById('stop-animation-button').addEventListener('click', (ev) => {
vrmEl.removeAttribute('vrm-anim');
vrmEl.components.vrm.avatar.restPose();
});
}
window.addEventListener('dragover', (ev) => {
ev.preventDefault();
});
window.addEventListener('drop', (ev) => {
ev.preventDefault();
for (let file of ev.dataTransfer.files) {
let namelc = file.name.toLowerCase();
if (namelc.endsWith('.vrm') || namelc.endsWith('.glb')) {
let url = URL.createObjectURL(file);
vrmEl.removeAttribute('vrm-poser');
vrmEl.setAttribute('vrm-anim', { src: '' });
vrmEl.setAttribute('vrm', { 'src': url });
models.push({ name: file.name, src: url });
list.setContents(models);
} else if (namelc.endsWith('.bvh') || namelc.endsWith('.vmd')) {
vrmEl.setAttribute('vrm-anim', { src: URL.createObjectURL(file), format: namelc.endsWith('.bvh') ? 'bvh' : '' });
}
}
});
}, { once: true });
</script>
</body>
</html>
使用変数
background | |
blendShapeNames | |
body | |
cameraObj | |
cameraPos | |
cameraRot | |
cameraVec | |
canvasEl | |
charset | |
color | |
containerEl | |
content | |
cube | |
cursorEl | |
dd | |
depth | |
distance | |
dragging | |
draggingObjectMass | |
dragX | |
dragY | |
dt | |
el | |
equiv | |
ev | |
files | |
fingerLen | |
fingerState | |
geometry | |
hand0 | |
hand1 | |
hand | |
handData | |
hands | |
height | |
i | |
id | |
intensity | |
intersectedEl | |
len | |
length | |
list | |
listEl | |
lookAt | |
m | |
mass | |
material | |
models | |
motions | |
n | |
name | |
namelc | |
names | |
onModelLoaded | |
onMouseMove | |
opacity | |
open | |
path | |
physics | |
pointing | |
poseJson | |
position | |
prevPos | |
prevTime | |
r | |
root | |
rootPos | |
rotation | |
scale | |
self | |
sky | |
sliderEl | |
speedFactor | |
src | |
targetObj | |
timer | |
tip | |
tipPos | |
transparent | |
type | |
updateCamera | |
url | |
v | |
value | |
velocity | |
visible | |
vrm | |
vrmEl | |
width | |
_tmpQ0 | |
_tmpV0 | |
_tmpV1 |