a-farme-球に文字test33
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 回転立方体と情報パネル</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御コンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
// コンポーネントのパラメータ定義
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
acceleration: { type: 'number', default: 10 }, // 加速の度合い
damping: { type: 'number', default: 8 }, // 自然減速の度合い (キー入力がない時)
brakingDeceleration: { type: 'number', default: 20 },// 逆キー入力時のブレーキの度合い
enabled: { type: 'boolean', default: true } // このコンポーネントが有効か
},
// コンポーネント初期化時の処理
init: function () {
this.keys = {}; // 押されているキー{ードのキーを記録するオブジェクト
this.thumbstickInput = { x: 0, y: 0 }; // VRコントローラのサムスティック入力値
// 各種ベクトルを事前に生成して再利用 (パフォー}ンスのため)
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.cameraEl = this.el.querySelector('[camera]'); // カメラエンティティへの参照
this.isReady = false; // カメラなどの準備ができたかどうかのフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls requires a child entity with the [camera] component.');
}
// --- VRコントローラーのセットアップ ---
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティ
if (this.leftHand) {
// 左手スティックの動きを監視
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("Left hand controller (leftHand) not found for thumbstick movement.");
}
// --- キー{ードイベントリスナーのセットアップ ---
this.onKeyDown = this.onKeyDown.bind(this); // thisの束縛
this.onKeyUp = this.onKeyUp.bind(this); // 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) {
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
}
},
// 左手コントローラーのサムスティックが動いた時の処理
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値を取得
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.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) { return; } // 念のため再度チェック
const el = this.el; // このコンポーネントがアタッチされているエンティティ (rig)
const data = this.data; // スキー}で定義されたパラメータ
const position = el.object3D.position; // rigの現在位置
const dt = timeDelta / 1000; // 経過時間 (秒)
// --- カメラの向きから前方・右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
// 前方 (カメラのローカル-Z軸がワールド前方に対応)
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize();
// 右方向 (カメラのローカルX軸がワールド右方向に対応、Y軸は無視して水平移動)
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); }
// VRサムスティック入力 (Y軸は反転していることが多いので-1を掛ける)
if (Math.abs(this.thumbstickInput.y) > 0.1) { // デッドゾーン
const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y);
this.moveDirection.add(forwardBackward);
}
if (Math.abs(this.thumbstickInput.x) > 0.1) { // デッドゾーン
const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.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) { // 内積が負なら逆方向への入力と判断 (閾値-0.1)
isOpposingInput = true;
}
}
if (isOpposingInput) { // 逆方向への入力の場合 (ブレーキ)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.brakingDeceleration; // ブレーキ用の減速係数を使用
} else { // 順方向または停止からの入力の場合 (加速)
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); // 目標速度を設定
lerpFactor = data.acceleration; // 加速用の係数を使用
}
} else { // 入力がない場合 (自然減速)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.damping; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在速度を目標速度に向けて滑らかに補間 (Lerp) ---
// dt を使ってフレームレート変動の影響を軽減する補間係数の計算
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);
}
// 現在速度に基づいてrigの位置を移動
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];
}
}
});
// --- ランダム回転コンポーネント (変更なし) ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor"
cursor="rayOrigin: mouse; fuse: false;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;" cursor="rayOrigin: entity; fuse: false;" line="color: white; opacity: 0.5; length: 3" >
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"></a-circle>
</a-entity>
<script>
// --- グローバル変数と初期設定 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor'); // }ウスカーソル要素を取得
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // パネル位置計算用
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
// --- グローバル変数ここまで ---
// --- 立方体(旧球体)の生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box');
const side = Math.random() * 10.0 + 0.5;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
const sphereIndex = i + 1;
cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject');
cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2);
cubeEl.setAttribute('random-rotate', {maxSpeed: 5});
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl);
}
// --- 立方体生成ここまで ---
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex];
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`;
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- パネル表示更新ここまで ---
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) {
event.stopPropagation(); // イベントのバブリングを停止
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; }
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
console.log("Data stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
const offsetDistance = cubeSide / 2 + 0.5; // 立方体の中心からのオフセット
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- 立方体クリック処理ここまで ---
// --- パネルの{タンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex);
});
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex;
console.log("Close button clicked, panel hidden.");
});
// --- パネル{タン処理ここまで ---
// --- VRモードによる}ウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () {
console.log("Entered VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); // VRモードでは}ウスカーソルを非表示
// 必要であれば、VRコントローラーのraycasterを有効にするなどの処理
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', true);
}
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたら}ウスカーソルを再表示
// VRコントローラーのraycasterを無効にするなど
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', false);
}
});
// --- }ウスカーソル制御ここまで ---
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
使用変数
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 回転立方体と情報パネル</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御コンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
// コンポーネントのパラメータ定義
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
acceleration: { type: 'number', default: 10 }, // 加速の度合い
damping: { type: 'number', default: 8 }, // 自然減速の度合い (キー入力がない時)
brakingDeceleration: { type: 'number', default: 20 },// 逆キー入力時のブレーキの度合い
enabled: { type: 'boolean', default: true } // このコンポーネントが有効か
},
// コンポーネント初期化時の処理
init: function () {
this.keys = {}; // 押されているキー{ードのキーを記録するオブジェクト
this.thumbstickInput = { x: 0, y: 0 }; // VRコントローラのサムスティック入力値
// 各種ベクトルを事前に生成して再利用 (パフォー}ンスのため)
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.cameraEl = this.el.querySelector('[camera]'); // カメラエンティティへの参照
this.isReady = false; // カメラなどの準備ができたかどうかのフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls requires a child entity with the [camera] component.');
}
// --- VRコントローラーのセットアップ ---
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティ
if (this.leftHand) {
// 左手スティックの動きを監視
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("Left hand controller (leftHand) not found for thumbstick movement.");
}
// --- キー{ードイベントリスナーのセットアップ ---
this.onKeyDown = this.onKeyDown.bind(this); // thisの束縛
this.onKeyUp = this.onKeyUp.bind(this); // 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) {
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
}
},
// 左手コントローラーのサムスティックが動いた時の処理
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値を取得
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.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) { return; } // 念のため再度チェック
const el = this.el; // このコンポーネントがアタッチされているエンティティ (rig)
const data = this.data; // スキー}で定義されたパラメータ
const position = el.object3D.position; // rigの現在位置
const dt = timeDelta / 1000; // 経過時間 (秒)
// --- カメラの向きから前方・右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
// 前方 (カメラのローカル-Z軸がワールド前方に対応)
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize();
// 右方向 (カメラのローカルX軸がワールド右方向に対応、Y軸は無視して水平移動)
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); }
// VRサムスティック入力 (Y軸は反転していることが多いので-1を掛ける)
if (Math.abs(this.thumbstickInput.y) > 0.1) { // デッドゾーン
const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y);
this.moveDirection.add(forwardBackward);
}
if (Math.abs(this.thumbstickInput.x) > 0.1) { // デッドゾーン
const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.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) { // 内積が負なら逆方向への入力と判断 (閾値-0.1)
isOpposingInput = true;
}
}
if (isOpposingInput) { // 逆方向への入力の場合 (ブレーキ)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.brakingDeceleration; // ブレーキ用の減速係数を使用
} else { // 順方向または停止からの入力の場合 (加速)
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); // 目標速度を設定
lerpFactor = data.acceleration; // 加速用の係数を使用
}
} else { // 入力がない場合 (自然減速)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.damping; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在速度を目標速度に向けて滑らかに補間 (Lerp) ---
// dt を使ってフレームレート変動の影響を軽減する補間係数の計算
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);
}
// 現在速度に基づいてrigの位置を移動
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];
}
}
});
// --- ランダム回転コンポーネント (変更なし) ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor"
cursor="rayOrigin: mouse; fuse: false;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;" cursor="rayOrigin: entity; fuse: false;" line="color: white; opacity: 0.5; length: 3" >
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"></a-circle>
</a-entity>
<script>
// --- グローバル変数と初期設定 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor'); // }ウスカーソル要素を取得
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // パネル位置計算用
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
// --- グローバル変数ここまで ---
// --- 立方体(旧球体)の生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box');
const side = Math.random() * 10.0 + 0.5;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
const sphereIndex = i + 1;
cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject');
cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2);
cubeEl.setAttribute('random-rotate', {maxSpeed: 5});
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl);
}
// --- 立方体生成ここまで ---
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex];
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`;
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- パネル表示更新ここまで ---
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) {
event.stopPropagation(); // イベントのバブリングを停止
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; }
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
console.log("Data stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
const offsetDistance = cubeSide / 2 + 0.5; // 立方体の中心からのオフセット
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- 立方体クリック処理ここまで ---
// --- パネルの{タンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex);
});
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex;
console.log("Close button clicked, panel hidden.");
});
// --- パネル{タン処理ここまで ---
// --- VRモードによる}ウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () {
console.log("Entered VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); // VRモードでは}ウスカーソルを非表示
// 必要であれば、VRコントローラーのraycasterを有効にするなどの処理
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', true);
}
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたら}ウスカーソルを再表示
// VRコントローラーのraycasterを無効にするなど
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', false);
}
});
// --- }ウスカーソル制御ここまで ---
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
使用変数
-------( Function ) | |
ameraWorldPosition | |
angle | |
argetWorldPosition | |
at | |
attribute | |
axis | |
background | |
begin | |
camera | |
cameraDirection | |
cameraEl | |
cameraRight | |
class | |
clickedCube | |
closeButtonEl | |
color | |
controls | |
cubeEl | |
cubeSide | |
currentPageIndex | |
currentVelocity | |
cursor | |
data | |
deltaPosition | |
desiredVelocity | |
direction | |
displayText | |
dotProduct | |
dt | |
dur | |
easing | |
el | |
eraWorldQuaternion | |
ffectiveLerpFactor | |
fill | |
finalDisplayText | |
forwardBackward | |
from | |
geometry | |
handleSphereClick -------( Function ) | |
height | |
i | |
id | |
index | |
infoPanelEl | |
isCurrentlyMoving | |
isInputActive | |
isOpposingInput | |
isReady | |
keys | |
leftHand | |
leftRight | |
lerpFactor | |
light | |
line | |
material | |
mouseCursorEl | |
moveDirection | |
nextButtonEl | |
numSpheres | |
offsetDistance | |
onKeyDown | |
onKeyUp | |
opacity | |
pageIndex | |
pageIndicator | |
PAGES | |
pageType | |
panelPosition | |
panelTextEl | |
position | |
prevButtonEl | |
radius | |
raycaster | |
rotation | |
RotationQuaternion | |
scale | |
sceneEl | |
shader | |
side | |
size | |
speed | |
speedRad | |
sphereIndex | |
spread | |
src | |
text | |
thumbstickInput | |
to | |
TOTAL_PAGES | |
ui | |
updatePanelDisplay -------( Function ) | |
visible | |
width | |
x | |
y | |
z | |
ZERO_VECTOR |