Newer
Older
Kusanagi-system-M / test / OCSystem.js
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);
});