<!DOCTYPE html>
<html>
<head>
<title>Three.js VRM Scene</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
body { margin: 0; }
canvas { display: block; }
</style>
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.159.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.159.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="module">
// --- 1. 基本的な部品をインポート ---
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// three-vrmはThree.jsに依存するため、ここでインポートします
import { VRMLoaderPlugin, VRMUtils } from 'https://unpkg.com/@pixiv/three-vrm@2.0.7/lib/three-vrm.module.js';
// --- 2. シーン、カメラ、レンダラーの準備 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xECECEC);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 5); // A-Frameの初期位置に合わせる
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- 3. ライト(照明)の準備 ---
const ambientLight = new THREE.AmbientLight(0x888888);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.0);
directionalLight.position.set(-1, 1.5, 1);
directionalLight.castShadow = true;
scene.add(directionalLight);
// --- 4. 地面の作成 ---
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x7BC8A4 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// --- 5. 球のランダム配置 ---
const numberOfSpheres = 20;
const groundSize = 100;
const spawnArea = groundSize / 2 * 0.9;
for (let i = 0; i < numberOfSpheres; i++) {
const radius = Math.random() * 1.5 + 0.2;
const sphereGeometry = new THREE.SphereGeometry(radius, 32, 32);
const color = new THREE.Color(`hsl(${Math.random() * 360}, 70%, 50%)`);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: color });
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.x = (Math.random() - 0.5) * 2 * spawnArea;
sphere.position.y = radius; // 地面に接するように
sphere.position.z = (Math.random() - 0.5) * 2 * spawnArea;
sphere.castShadow = true;
scene.add(sphere);
}
// --- 6. VRMの読み込み ---
let currentVrm = undefined;
const loader = new GLTFLoader();
loader.crossOrigin = 'anonymous';
loader.register((parser) => new VRMLoaderPlugin(parser));
loader.load(
'./vrm/tesA1_V0a.vrm',
(gltf) => {
const vrm = gltf.userData.vrm;
VRMUtils.removeUnnecessaryJoints(gltf.scene);
vrm.scene.position.set(0, 0, -2); // VRMモデルの位置
vrm.scene.scale.set(1.2, 1.2, 1.2); // VRMモデルのサイズ
vrm.scene.rotation.y = Math.PI; // VRMモデルの向き
scene.add(vrm.scene);
currentVrm = vrm;
},
(progress) => console.log('Loading model...', 100.0 * (progress.loaded / progress.total).toFixed(2), '%'),
(error) => console.error(error)
);
// --- 7. 移動と視点操作のコントロール ---
const keys = {};
document.addEventListener('keydown', (event) => keys[event.code] = true);
document.addEventListener('keyup', (event) => keys[event.code] = false);
let isPointerLocked = false;
renderer.domElement.addEventListener('click', () => {
if (!isPointerLocked) {
renderer.domElement.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
isPointerLocked = document.pointerLockElement === renderer.domElement;
});
document.addEventListener('mousemove', (event) => {
if (isPointerLocked) {
camera.rotation.y -= event.movementX * 0.002;
camera.rotation.x -= event.movementY * 0.002;
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x));
}
});
const clock = new THREE.Clock();
const movementSpeed = 8.0;
// --- 8. アニメーションループ(全ての更新処理) ---
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
// 移動処理
const moveDirection = new THREE.Vector3();
if (keys['KeyW'] || keys['ArrowUp']) moveDirection.z -= 1;
if (keys['KeyS'] || keys['ArrowDown']) moveDirection.z += 1;
if (keys['KeyA'] || keys['ArrowLeft']) moveDirection.x -= 1;
if (keys['KeyD'] || keys['ArrowRight']) moveDirection.x += 1;
if (moveDirection.lengthSq() > 0) {
moveDirection.normalize().applyQuaternion(camera.quaternion);
camera.position.addScaledVector(moveDirection, movementSpeed * deltaTime);
}
// VRMの更新
if (currentVrm) {
currentVrm.update(deltaTime);
}
// レンダリング
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
使用変数
ambientLight | |
animate -------( Function ) | |
aspect | |
background | |
camera | |
castShadow | |
charset | |
clock | |
color | |
content | |
crossOrigin | |
currentVrm | |
deltaTime | |
directionalLight | |
enabled | |
ground | |
groundGeometry | |
groundMaterial | |
groundSize | |
i | |
isPointerLocked | |
keys | |
loader | |
moveDirection | |
movementSpeed | |
name | |
numberOfSpheres | |
pointerLockElement | |
radius | |
receiveShadow | |
renderer | |
scalable | |
scale | |
scene | |
spawnArea | |
sphere | |
sphereGeometry | |
sphereMaterial | |
src | |
type | |
vrm | |
width | |
x | |
y | |
z |