import * as THREE from 'three'; import { GLTFLoader } from 'GLTFLoader'; import { DRACOLoader } from 'DRACOLoader'; import { PointerLockControls } from 'PointerLockControls'; const isMobile = /Mobi|Android/i.test(navigator.userAgent); // シーン設定 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 1.6, 3); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0xcccccc); document.body.appendChild(renderer.domElement); const ambientLight = new THREE.AmbientLight(0xffffff, 1); scene.add(ambientLight); // PC用コントロール(PointerLockControls) const controls = new PointerLockControls(camera, document.body); const blocker = document.getElementById('blocker'); const instructions = document.getElementById('instructions'); instructions.addEventListener('click', () => { controls.lock(); }); controls.addEventListener('lock', () => { instructions.style.display = 'none'; blocker.style.display = 'none'; }); controls.addEventListener('unlock', () => { blocker.style.display = 'block'; instructions.style.display = 'flex'; }); let moveForward = false; let moveBackward = false; let moveLeft = false; let moveRight = false; let moveUp = false; // 上昇フラグ let moveDown = false; // 下降フラグ let canJump = false; const velocity = new THREE.Vector3(); const direction = new THREE.Vector3(); const collisionObjects = []; const onKeyDown = function (event) { switch (event.code) { case 'KeyW': moveForward = true; break; case 'KeyA': moveLeft = true; break; case 'KeyS': moveBackward = true; break; case 'KeyD': moveRight = true; break; case 'KeyR': // Rキーで上昇 moveUp = true; break; case 'KeyF': // Fキーで下降 moveDown = true; break; case 'Space': if (canJump === true) velocity.y += 20; canJump = false; break; case '.': if (pdfDoc) { currentPage = (currentPage % pdfDoc.numPages) + 1; renderPage(currentPage); } break; case ',': if (pdfDoc) { currentPage = (currentPage - 2 + pdfDoc.numPages) % pdfDoc.numPages + 1; renderPage(currentPage); } break; } }; const onKeyUp = function (event) { switch (event.code) { case 'KeyW': moveForward = false; break; case 'KeyA': moveLeft = false; break; case 'KeyS': moveBackward = false; break; case 'KeyD': moveRight = false; break; case 'KeyR': // Rキーを離したら上昇フラグをfalseに moveUp = false; break; case 'KeyF': // Fキーを離したら下降フラグをfalseに moveDown = false; break; } }; if (!isMobile) { document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); blocker.style.display = 'block'; } else { document.getElementById('joystickZone').style.display = 'block'; document.getElementById('controlPadRight').style.display = 'flex'; blocker.style.display = 'none'; instructions.style.display = 'none'; } // モデル読み込み const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('https://unpkg.com/three@0.157.0/examples/jsm/libs/draco/'); loader.setDRACOLoader(dracoLoader); loader.load('models/koeki3D/圧縮された公益大3Dその2.glb', function(gltf) { gltf.scene.position.y = 0; scene.add(gltf.scene); gltf.scene.traverse((child) => { if (child.isMesh) { collisionObjects.push(child); } }); console.log('モデル読み込み完了。'); animate(); }, undefined, function(error) { console.error('モデル読み込みエラー:', error); const errorMessage = document.createElement('div'); errorMessage.textContent = 'モデルの読み込みに失敗しました。'; errorMessage.style.position = 'absolute'; errorMessage.style.color = 'white'; errorMessage.style.top = '10px'; errorMessage.style.left = '10px'; document.body.appendChild(errorMessage); }); // 画像とPDF表示 const textureLoader = new THREE.TextureLoader(); const imageTexture = textureLoader.load('./OCpdfQR.png'); const imageMaterial = new THREE.MeshBasicMaterial({ map: imageTexture, transparent: true }); const imagePlane = new THREE.Mesh(new THREE.PlaneGeometry(1.44, 0.81), imageMaterial); imagePlane.position.set(-102.17, -6.62, 2.94); imagePlane.rotation.y = Math.PI / 2; scene.add(imagePlane); let pdfDoc = null; let currentPage = 1; let pdfMesh; const loadPDF = async (url) => { pdfDoc = await pdfjsLib.getDocument(url).promise; renderPage(currentPage); }; const renderPage = async (pageNum) => { const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: 2.0 }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: context, viewport }).promise; const pdfTexture = new THREE.CanvasTexture(canvas); if (!pdfMesh) { const material = new THREE.MeshBasicMaterial({ map: pdfTexture }); pdfMesh = new THREE.Mesh(new THREE.PlaneGeometry(1.44, 0.81), material); pdfMesh.position.set(-102.23, -6.6, 4.67); pdfMesh.rotation.y = Math.PI / 2; scene.add(pdfMesh); } else { pdfMesh.material.map = pdfTexture; pdfMesh.material.needsUpdate = true; } }; loadPDF('./koekiOC.pdf'); // モバイル用ジョイスティック let joystickManager; let moveDirection = { x: 0, y: 0, z: 0 }; const moveSpeedMobile = 7; if (isMobile) { joystickManager = nipplejs.create({ zone: document.getElementById('joystickZone'), mode: 'static', position: { left: '50%', top: '50%' }, color: 'white', size: 150 }); joystickManager.on('move', (evt, data) => { if (data.direction) { const angle = data.angle.radian; const force = Math.min(data.force, 1); const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize(); const right = new THREE.Vector3(); right.crossVectors(forward, camera.up).normalize(); moveDirection.x = (Math.sin(angle) * forward.x + Math.cos(angle) * right.x) * force; moveDirection.z = (Math.sin(angle) * forward.z + Math.cos(angle) * right.z) * force; } }); joystickManager.on('end', () => { moveDirection.x = 0; moveDirection.z = 0; }); const moveUpBtn = document.getElementById('moveUp'); const moveDownBtn = document.getElementById('moveDown'); moveUpBtn.addEventListener('touchstart', () => moveDirection.y = 1); moveUpBtn.addEventListener('touchend', () => moveDirection.y = 0); moveDownBtn.addEventListener('touchstart', () => moveDirection.y = -1); moveDownBtn.addEventListener('touchend', () => moveDirection.y = 0); const controlPadRight = document.getElementById('controlPadRight'); controlPadRight.addEventListener('contextmenu', (event) => event.preventDefault()); controlPadRight.addEventListener('touchstart', (event) => event.preventDefault(), { passive: false }); // 画面の右半分でのタッチ視点変更 let touchstartX = 0; let touchstartY = 0; const lookSpeedMobile = 0.002; renderer.domElement.addEventListener('touchstart', (event) => { const touch = event.changedTouches[0]; if (touch.clientX > window.innerWidth / 2) { touchstartX = touch.clientX; touchstartY = touch.clientY; } }, false); renderer.domElement.addEventListener('touchmove', (event) => { const touch = event.changedTouches[0]; if (touch.clientX > window.innerWidth / 2) { const deltaX = touch.clientX - touchstartX; const deltaY = touch.clientY - touchstartY; // Y軸回転(左右) camera.rotation.y -= deltaX * lookSpeedMobile; // X軸回転(上下) // クランプ処理で上下の角度を制限 let newRotationX = camera.rotation.x - deltaY * lookSpeedMobile; newRotationX = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, newRotationX)); camera.rotation.x = newRotationX; touchstartX = touch.clientX; touchstartY = touch.clientY; } }, false); } // アニメーションループ const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); const moveSpeed = 400.0; const verticalSpeed = 200.0; // 上下移動の速度 if (!isMobile && controls.isLocked) { velocity.x -= velocity.x * 10.0 * delta; velocity.z -= velocity.z * 10.0 * delta; // Y軸方向の減速処理 velocity.y -= velocity.y * 10.0 * delta; direction.z = Number(moveForward) - Number(moveBackward); direction.x = Number(moveRight) - Number(moveLeft); direction.normalize(); if (moveForward || moveBackward) velocity.z -= direction.z * moveSpeed * delta; if (moveLeft || moveRight) velocity.x -= direction.x * moveSpeed * delta; // 上下移動 if (moveUp) velocity.y += verticalSpeed * delta; if (moveDown) velocity.y -= verticalSpeed * delta; controls.moveRight(-velocity.x * delta); controls.moveForward(-velocity.z * delta); camera.position.y += velocity.y * delta; // カメラのY座標が1.6より下にならないように制限 if (camera.position.y < 1.6) { velocity.y = 0; camera.position.y = 1.6; canJump = true; } // 地面からのジャンプと、R/Fによる自由飛行を両立させる場合は // Spaceキーを押した時のみ重力を無効化するロジックが必要になります。 // 今回はシンプルに重力を無効化しています。 } else if (isMobile) { // ジョイスティックによる移動 camera.position.x += moveDirection.x * moveSpeedMobile * delta; camera.position.y += moveDirection.y * moveSpeedMobile * delta; camera.position.z += moveDirection.z * moveSpeedMobile * delta; } renderer.render(scene, camera); } // ウィンドウリサイズ window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });