<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VRM表示シーン(動的ロード版)</title>
<meta charset="utf-8">
<style>
body { margin: 0; }
.a-loader-title { color: #FAFAFA; }
</style>
</head>
<body>
<a-scene id="myScene" vr-mode-ui="enabled: true" background="color: #ECECEC">
<a-assets></a-assets>
<a-entity
id="vrm-target"
data-src="./vrm/tesA1_V0.vrm"
position="0 0 -2"
scale="1.2 1.2 1.2"
rotation="0 180 0">
</a-entity>
<a-entity id="rig" position="0 0 5">
<a-entity id="camera" camera="far: 20000;" position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"></a-entity>
</a-entity>
<a-entity id="leftHand"></a-entity>
<a-entity id="rightHand"></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.6; castShadow: true" position="-1 1.5 1"></a-entity>
<a-plane id="ground" position="0 0 0" rotation="-90 0 0" width="100" height="100" color="#7BC8A4" shadow="receive: true"></a-plane>
</a-scene>
<script>
// --- STEP 1: スクリプトを順番に読み込むためのローダー関数 ---
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => {
console.log(`Loaded: ${src}`);
resolve();
};
script.onerror = () => {
console.error(`Failed to load: ${src}`);
reject(new Error(`Script load error for ${src}`));
};
document.head.appendChild(script);
});
}
// --- STEP 2: 必要なライブラリを順番に読み込む ---
async function loadLibrariesAndStart() {
try {
console.log('Starting library loading...');
await loadScript("https://aframe.io/releases/1.7.0/aframe.min.js");
await loadScript("https://cdn.jsdelivr.net/npm/three@0.149.0/examples/js/loaders/GLTFLoader.js");
await loadScript("https://cdn.jsdelivr.net/npm/@pixiv/three-vrm@1.0.9/dist/three-vrm.js");
await loadScript("https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js");
await loadScript("https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js");
console.log('All libraries loaded successfully.');
// --- STEP 3: 全ライブラリ読み込み後に、A-Frameのコンポーネントを登録 ---
defineAFrameComponents();
// --- STEP 4: A-Frameのシーンが完全に準備できたらメインの処理を開始 ---
runMainLogic();
} catch (error) {
console.error("Could not initialize the application:", error);
alert("アプリケーションの初期化に失敗しました。コンソールを確認してください。");
}
}
// --- STEP 3 の詳細: A-Frameコンポーネントの定義 ---
function defineAFrameComponents() {
console.log('Defining A-Frame components...');
// プレイヤー移動制御用カスタムコンポーネント
AFRAME.registerComponent('camera-relative-controls', { schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true }, rotationSpeed: { type: 'number', default: 1.5 }, pitchLimit: { type: 'number', default: 85 }, verticalSpeed: { type: 'number', default: 30 } }, init: function () { this.keys = {}; this.leftThumbstickInput = { x: 0, y: 0 }; this.rightThumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.rigEl = this.el; this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } this.rightHand = document.getElementById('rightHand'); if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 右手コントローラー(#rightHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } catch(e){} } if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} } }, onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; }, onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.y = evt.detail.y; }, tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D || !this.rigEl || !this.rigEl.object3D) { return; } const data = this.data; const dt = timeDelta / 1000; if (this.rigEl.sceneEl.is('vr-mode')) { if (Math.abs(this.rightThumbstickInput.x) > 0.1) { const yawAngle = -this.rightThumbstickInput.x * data.rotationSpeed * dt; this.rigEl.object3D.rotation.y += yawAngle; } if (Math.abs(this.rightThumbstickInput.y) > 0.1) { const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt; this.rigEl.object3D.position.y -= verticalMovement; } } const position = this.rigEl.object3D.position; const cameraObject = this.cameraEl.object3D; cameraObject.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.leftThumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.leftThumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// VRMの物理演算や表情を更新するためだけのコンポーネント
AFRAME.registerComponent('vrm-updater', {
schema: { vrm: { default: null } },
tick: function(time, timeDelta) { if (this.data.vrm) { this.data.vrm.update(timeDelta / 1000); } }
});
console.log('All components defined.');
}
// --- STEP 4 の詳細: メインロジックの実行 ---
function runMainLogic() {
const sceneEl = document.querySelector('a-scene');
function onSceneLoaded() {
console.log('Scene loaded, executing main logic...');
// コンポーネントをシーン内のエンティティに適用
document.querySelector('#rig').setAttribute('camera-relative-controls', 'targetSpeed: 8; acceleration: 10; damping: 8;');
document.querySelector('#camera').setAttribute('look-controls','pointerLockEnabled: false; touchEnabled: false');
document.querySelector('#mouseCursor').setAttribute('raycaster','objects: a-sphere;');
document.querySelector('#leftHand').setAttribute('oculus-touch-controls','hand: left; model: true;');
document.querySelector('#rightHand').setAttribute('oculus-touch-controls','hand: right; model: true;');
document.querySelector('#rightHand').setAttribute('raycaster','objects: a-sphere;');
document.querySelector('#rightHand').setAttribute('laser-controls','hand: right; model: false; lineColor: white; lineOpacity: 0.75');
// 球を生成する
initializeSpheres();
// VRMを読み込む
loadVrm();
}
// シーンが既に読み込まれている場合と、これから読み込まれる場合の両方に対応
if (sceneEl.hasLoaded) {
onSceneLoaded();
} else {
sceneEl.addEventListener('loaded', onSceneLoaded, { once: true });
}
}
function initializeSpheres() {
console.log('Creating spheres...');
const sceneEl = document.querySelector('a-scene');
const numberOfSpheres = 20;
const groundSize = 100;
const spawnArea = groundSize / 2 * 0.9;
for (let i = 0; i < numberOfSpheres; i++) {
const sphereEl = document.createElement('a-sphere');
const radius = Math.random() * 1.5 + 0.2;
const x = (Math.random() - 0.5) * 2 * spawnArea;
const y = radius;
const z = (Math.random() - 0.5) * 2 * spawnArea;
const color = `hsl(${Math.random() * 360}, 70%, 50%)`;
sphereEl.setAttribute('position', {x: x, y: y, z: z});
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('shadow', 'cast: true');
sceneEl.appendChild(sphereEl);
}
console.log(`${numberOfSpheres} spheres created.`);
}
function loadVrm() {
console.log('Starting VRM loading process...');
const vrmEntity = document.querySelector('#vrm-target');
if (!vrmEntity) return;
const src = vrmEntity.dataset.src;
if (!src) return;
const gltfLoader = new THREE.GLTFLoader();
gltfLoader.register(parser => new THREE.VRM.VRMLoaderPlugin(parser));
gltfLoader.load(src,
(gltf) => {
const vrm = gltf.userData.vrm;
console.log('VRM model loaded successfully.');
vrmEntity.setObject3D('vrm', vrm.scene);
vrmEntity.setAttribute('vrm-updater', { vrm: vrm });
},
(progress) => console.log(`Loading model... ${(progress.loaded / progress.total * 100).toFixed(2)} %`),
(error) => console.error('An error happened during VRM loading:', error)
);
}
// --- アプリケーションを開始 ---
document.addEventListener('DOMContentLoaded', loadLibrariesAndStart);
</script>
</body>
</html>
使用変数
-------( Function ) | |
background | |
camera | |
cameraDirection | |
cameraEl | |
cameraObject | |
cameraRight | |
charset | |
color | |
currentVelocity | |
cursor | |
data | |
defineAFrameComponents -------( Function ) | |
deltaPosition | |
desiredVelocity | |
dotProduct | |
dt | |
eftThumbstickInput | |
eraWorldQuaternion | |
ffectiveLerpFactor | |
forwardBackward | |
geometry | |
ghtThumbstickInput | |
gltfLoader | |
groundSize | |
height | |
i | |
id | |
initializeSpheres -------( Function ) | |
isCurrentlyMoving | |
isInputActive | |
isOpposingInput | |
isReady | |
keys | |
leftHand | |
leftRight | |
lerpFactor | |
light | |
loadLibrariesAndStart -------( Function ) | |
loadScript -------( Function ) | |
loadVrm -------( Function ) | |
material | |
moveDirection | |
numberOfSpheres | |
onerror | |
onKeyDown | |
onKeyUp | |
onload | |
onSceneLoaded -------( Function ) | |
parser | |
position | |
radius | |
rigEl | |
rightHand | |
rotation | |
runMainLogic -------( Function ) | |
scale | |
sceneEl | |
script | |
shadow | |
spawnArea | |
sphereEl | |
src | |
time, timeDelta) { if -------( Function ) | |
ui | |
verticalMovement | |
vrm | |
vrmEntity | |
width | |
x | |
y | |
yawAngle | |
z | |
ZERO_VECTOR |