はじめに
WebGLは、ブラウザ上で3Dグラフィックスを表示するための技術です。今回はWebGLを使って、3D空間でカメラを操作して探索できるアプリケーションを作成しました。(基本的にChat GPTで調べています)
出来あがったものが こちら
できること
- 移動: WASDキーを使って現在向いている方向に移動。カメラ位置は常に床の上に保持、高さは一定。左下のスライダーでスピード変更可能。
- カメラ操作: クリックしながらマウスで操作。Qキーを押すとカメラの上下の向きが元に戻る。
使った技術とライブラリ
- WebGL: ブラウザで3Dコンテンツを表示するための技術
- Three.js: WebGLを簡単に扱うためのJavaScriptライブラリ
コードの概要
コード全文
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL 3D Object with Camera Control</title>
<style>
/* body と canvas のスタイルを簡潔にまとめる */
body { margin: 0; font-family: Arial, sans-serif; }
canvas { display: block; } /* Three.jsで描画を行うために自動的に生成される */
/* 共通のスタイルをまとめる */
.control-box {
position: absolute;
color: white;
font-size: 16px;
z-index: 1;
background-color: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
#instructions { top: 10px; left: 10px; }
#coordinates { bottom: 60px; left: 10px; }
#speedControl { bottom: 10px; left: 10px; }
input[type="range"] { width: 200px; }
</style>
</head>
<body>
<!-- 操作方法の表示 -->
<div id="instructions" class="control-box">
<p><strong>操作方法:</strong></p>
<ul>
<li>視点:マウス(クリック)</li>
<li>視点リセット:Q</li>
<li>移動:WASD</li>
</ul>
</div>
<!-- カメラ座標の表示 -->
<div id="coordinates" class="control-box">
<p>位置: X: 0, Y: 0, Z: 5</p>
</div>
<!-- スライダーの表示 -->
<div id="speedControl" class="control-box">
<label for="speedSlider">移動スピード:</label>
<input type="range" id="speedSlider" min="0.01" max="1" step="0.01" value="0.1">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// シーン、カメラ、レンダラーの初期化
const scene = new THREE.Scene();
// ↓ カメラの設定。 視野角, アスペクト比, 近距離クリッピング, 遠距離クリッピング(それより近い(遠い)ものは描画されない)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
// レンダラー(描画エンジン)のサイズをブラウザウィンドウの幅と高さにする
renderer.setSize(window.innerWidth, window.innerHeight);
// レンダラーをHTMLに追加
document.body.appendChild(renderer.domElement);
// 背景色の設定
scene.background = new THREE.Color(0x87CEEB); // 空色
// ライトの設定
const light = new THREE.DirectionalLight(0xffffff, 2); // 光の色, 光の強さ
light.position.set(10, 10, 10); // 光源の位置
scene.add(light); // 光源をシーンに追加
// 立方体1(光沢あり)
const cube1 = new THREE.Mesh(
new THREE.BoxGeometry(2, 1, 1),
new THREE.MeshStandardMaterial({
color: 0x00ff00, // 緑色
roughness: 0.2, // 少し光沢がある
metalness: 0.5 // 半金属
})
);
cube1.position.set(-2, 0.5, 0); // y座標を0.5に設定(地面から浮かせる)
scene.add(cube1);
// 立方体2(光沢なし)
const cube2 = new THREE.Mesh(
new THREE.BoxGeometry(1, 2, 1),
new THREE.MeshStandardMaterial({
color: 0xff0000, // 赤色
roughness: 1, // 光沢なし
metalness: 0 // 非金属
})
);
cube2.position.set(0, 0.5, 0); // y座標を0.5に設定
scene.add(cube2);
// 立方体3(光沢強)
const cube3 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 2),
new THREE.MeshStandardMaterial({
color: 0x0000ff, // 青色
roughness: 0.05, // 非常に光沢が強い
metalness: 1 // 金属的な質感
})
);
cube3.position.set(2, 0.5, 0); // y座標を0.5に設定
scene.add(cube3);
// 立方体(半透明)
const cube4 = new THREE.Mesh(
new THREE.BoxGeometry(2, 2, 1),
new THREE.MeshStandardMaterial({
color: 0x00ff00,
opacity: 0.5, // 50%透明
transparent: true // 透明にするための設定
})
);
cube4.position.set(-3, 0.5, -4);
scene.add(cube4);
// 立方体(発光)
const cube5 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({
color: 0x00ff00,
emissive: 0x00ff00, // 緑色の発光を設定
emissiveIntensity: 5 // 発光の強さ
})
);
cube5.position.set(2, 2, -6); // y座標を0.5に設定
scene.add(cube5);
// 床の作成
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(500, 500), // 500 * 500 の平面
new THREE.MeshBasicMaterial({ color: 0x2e8b57, side: THREE.DoubleSide })); // 平面の両面を描画
floor.rotation.x = -Math.PI / 2; // 90度回転(そのままだと縦に描画される)
scene.add(floor);
// グリッド 全体のサイズ, 1マスの大きさ(分割数), グリッドの線の色
scene.add(new THREE.GridHelper(500, 50, 0xFFFFFF, 0xFFFFFF));
camera.position.z = 5;
// マウス操作に関するフラグと変数
let isMousePressed = false // マウスが押されているかどうか
let lastMouseX = 0, lastMouseY = 0; // マウスの最後の位置を記録(これで移動量を出す)
let rotationX = 0, rotationY = 0; // 回転かくどを管理
// 進行方向の管理(キーを押されたらtrue,離れたらfalseにする)
const moveDirection = { forward: false, backward: false, left: false, right: false };
let moveSpeed = 0.1; // 動く速さ(スライダーで調節される)
// スライダーの値を取得して移動速度を更新
document.getElementById('speedSlider').addEventListener('input', (event) => {
moveSpeed = parseFloat(event.target.value);
});
// マウス操作
// クリックしたとき(押し始め)
document.addEventListener('mousedown', (event) => {
isMousePressed = true; // マウスが置かれている状態を記録
lastMouseX = event.clientX; // マウスが押された時点でのx座標を記録
lastMouseY = event.clientY; // マウスが押された時点でのy座標を記録
});
// クリックが外れた時(押し終わり)
document.addEventListener('mouseup', () => {
isMousePressed = false;
});
// マウスを移動させたときに発火(if (isMousePressed)によってクリック中のみになる)
document.addEventListener('mousemove', (event) => {
if (isMousePressed) {
rotationX += (event.clientX - lastMouseX) * 0.005; // 現在-直前でマウスの移動量を計算
rotationY -= (event.clientY - lastMouseY) * 0.005; // 0.005の部分は回転感度の調整の係数
camera.rotation.set(rotationY, rotationX, 0); // x(上下),y(左右),z(傾き)
lastMouseX = event.clientX; // 現在のマウスの座標を記録
lastMouseY = event.clientY;
}
});
// キーボード操作
document.addEventListener('keydown', (event) => {
if (event.key === 'w') moveDirection.forward = true;
if (event.key === 's') moveDirection.backward = true;
if (event.key === 'a') moveDirection.left = true;
if (event.key === 'd') moveDirection.right = true;
if (event.key === 'q') { rotationY = 0; camera.rotation.x = 0; } // カメラリセット
});
document.addEventListener('keyup', (event) => {
if (event.key === 'w') moveDirection.forward = false;
if (event.key === 's') moveDirection.backward = false;
if (event.key === 'a') moveDirection.left = false;
if (event.key === 'd') moveDirection.right = false;
});
// アニメーション
function animate() {
requestAnimationFrame(animate); // 次のフレームをリクエスト
const direction = new THREE.Vector3();
camera.getWorldDirection(direction); // カメラが向いている方向を取得
direction.y = 0; // Y軸(上下)方向の移動を制限
direction.normalize(); // 正規化(ベクトルの長さを1にする)
// 前後移動
if (moveDirection.forward) camera.position.addScaledVector(direction, moveSpeed);
if (moveDirection.backward) camera.position.addScaledVector(direction, -moveSpeed);
const right = new THREE.Vector3();
camera.getWorldDirection(right); // カメラの向いている向きを取得
right.cross(new THREE.Vector3(0, 1, 0)); // カメラの右方向を計算
right.normalize();
// 左右移動
if (moveDirection.left) camera.position.addScaledVector(right, -moveSpeed);
if (moveDirection.right) camera.position.addScaledVector(right, moveSpeed);
camera.position.y = 1; // 高さ固定
// カメラ座標の表示更新
document.getElementById('coordinates').innerHTML = `<p>位置: X: ${camera.position.x.toFixed(2)}, Y: ${camera.position.y.toFixed(2)}, Z: ${camera.position.z.toFixed(2)}</p>`;
renderer.render(scene, camera);
}
// アニメーションの開始
animate();
// ウィンドウサイズ変更対応
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight; // アスペクト比の更新
camera.updateProjectionMatrix(); // 投影行列の更新(カメラの視覚的な変換を表現する行列)
renderer.setSize(window.innerWidth, window.innerHeight); // レンダラーサイズの更新
});
</script>
</body>
</html>
シーンとカメラのセットアップ
最初に、THREE.SceneとTHREE.PerspectiveCameraを使って3D空間を作成し、カメラを配置。
// シーン、カメラ、レンダラーの初期化
const scene = new THREE.Scene();
// ↓ カメラの設定。 視野角, アスペクト比, 近距離クリッピング, 遠距離クリッピング(それより近い(遠い)ものは描画されない)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
レンダラーの設定
次に、THREE.WebGLRendererを使って、ブラウザ上にシーンを描画。レンダラーは、シーンとカメラの情報を元にブラウザに3Dグラフィックを描画する役割を果たす。
const renderer = new THREE.WebGLRenderer();
// レンダラー(描画エンジン)のサイズをブラウザウィンドウの幅と高さにする
renderer.setSize(window.innerWidth, window.innerHeight);
// レンダラーをHTMLに追加
document.body.appendChild(renderer.domElement);
背景色の設定とライトの設定
光の当たっている面が明るく、当たっていない面が暗くなる。
// 背景色の設定
scene.background = new THREE.Color(0x87CEEB); // 空色
// ライトの設定
const light = new THREE.DirectionalLight(0xffffff, 2); // 光の色, 光の強さ
light.position.set(10, 10, 10); // 光源の位置
scene.add(light); // 光源をシーンに追加
立方体の作成
THREE.BoxGeometryを使って、いくつかの立方体を作成。半透明とか光沢とか発光とか。光沢についてはよくわからなかった。
立方体のコード
// 立方体1(光沢あり)
const cube1 = new THREE.Mesh(
new THREE.BoxGeometry(2, 1, 1),
new THREE.MeshStandardMaterial({
color: 0x00ff00, // 緑色
roughness: 0.2, // 少し光沢がある
metalness: 0.5 // 半金属
})
);
cube1.position.set(-2, 0.5, 0); // y座標を0.5に設定(地面から浮かせる)
scene.add(cube1);
// 立方体2(光沢なし)
const cube2 = new THREE.Mesh(
new THREE.BoxGeometry(1, 2, 1),
new THREE.MeshStandardMaterial({
color: 0xff0000, // 赤色
roughness: 1, // 光沢なし
metalness: 0 // 非金属
})
);
cube2.position.set(0, 0.5, 0); // y座標を0.5に設定
scene.add(cube2);
// 立方体3(光沢強)
const cube3 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 2),
new THREE.MeshStandardMaterial({
color: 0x0000ff, // 青色
roughness: 0.05, // 非常に光沢が強い
metalness: 1 // 金属的な質感
})
);
cube3.position.set(2, 0.5, 0); // y座標を0.5に設定
scene.add(cube3);
// 立方体(半透明)
const cube4 = new THREE.Mesh(
new THREE.BoxGeometry(2, 2, 1),
new THREE.MeshStandardMaterial({
color: 0x00ff00,
opacity: 0.5, // 50%透明
transparent: true // 透明にするための設定
})
);
cube4.position.set(-3, 0.5, -4);
scene.add(cube4);
// 立方体(発光)
const cube5 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({
color: 0x00ff00,
emissive: 0x00ff00, // 緑色の発光を設定
emissiveIntensity: 5 // 発光の強さ
})
);
cube5.position.set(2, 2, -6); // y座標を0.5に設定
scene.add(cube5);
床とグリッドの追加
床の作成。3D空間内での位置を確認しやすくするため、GridHelperを使ってグリッド模様を追加。回転させないと床にならないことに注意。
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(500, 500), // 500 * 500 の平面
new THREE.MeshBasicMaterial({ color: 0x2e8b57, side: THREE.DoubleSide })); // 平面の両面を描画
floor.rotation.x = -Math.PI / 2; // 90度回転(そのままだと縦に描画される)
scene.add(floor);
// グリッド 全体のサイズ, 1マスの大きさ(分割数), グリッドの線の色
scene.add(new THREE.GridHelper(500, 50, 0xFFFFFF, 0xFFFFFF));
カメラ操作
マウス操作でカメラを回転させたり、WASDキーで移動させたり、Qキーでカメラの向きをリセットできるようにした。
// マウス操作に関するフラグと変数
let isMousePressed = false // マウスが押されているかどうか
let lastMouseX = 0, lastMouseY = 0; // マウスの最後の位置を記録(これで移動量を出す)
let rotationX = 0, rotationY = 0; // 回転かくどを管理
// 進行方向の管理(キーを押されたらtrue,離れたらfalseにする)
const moveDirection = { forward: false, backward: false, left: false, right: false };
let moveSpeed = 0.1; // 動く速さ(スライダーで調節される)
移動速度の調節
移動速度をスライダーで変更できるようにした。これで世界の端までいける。
// スライダーの値を取得して移動速度を更新
document.getElementById('speedSlider').addEventListener('input', (event) => {
moveSpeed = parseFloat(event.target.value);
});
マウス操作(カメラ操作)
マウスでカメラ操作。クリックを管理して、クリック中のみ動くようにした。
// クリックしたとき(押し始め)
document.addEventListener('mousedown', (event) => {
isMousePressed = true; // マウスが置かれている状態を記録
lastMouseX = event.clientX; // マウスが押された時点でのx座標を記録
lastMouseY = event.clientY; // マウスが押された時点でのy座標を記録
});
// クリックが外れた時(押し終わり)
document.addEventListener('mouseup', () => {
isMousePressed = false;
});
// マウスを移動させたときに発火(if (isMousePressed)によってクリック中のみになる)
document.addEventListener('mousemove', (event) => {
if (isMousePressed) {
rotationX += (event.clientX - lastMouseX) * 0.005; // 現在-直前でマウスの移動量を計算
rotationY -= (event.clientY - lastMouseY) * 0.005; // 0.005の部分は回転感度の調整の係数
camera.rotation.set(rotationY, rotationX, 0); // x(上下),y(左右),z(傾き)
lastMouseX = event.clientX; // 現在のマウスの座標を記録
lastMouseY = event.clientY;
}
});
キーボード操作(移動)
wasdで移動。
// キーボード操作
document.addEventListener('keydown', (event) => {
if (event.key === 'w') moveDirection.forward = true;
if (event.key === 's') moveDirection.backward = true;
if (event.key === 'a') moveDirection.left = true;
if (event.key === 'd') moveDirection.right = true;
if (event.key === 'q') { rotationY = 0; camera.rotation.x = 0; } // カメラリセット
});
document.addEventListener('keyup', (event) => {
if (event.key === 'w') moveDirection.forward = false;
if (event.key === 's') moveDirection.backward = false;
if (event.key === 'a') moveDirection.left = false;
if (event.key === 'd') moveDirection.right = false;
});
アニメーション
画面の更新。
// アニメーション
function animate() {
requestAnimationFrame(animate); // 次のフレームをリクエスト
const direction = new THREE.Vector3();
camera.getWorldDirection(direction); // カメラが向いている方向を取得
direction.y = 0; // Y軸(上下)方向の移動を制限
direction.normalize(); // 正規化(ベクトルの長さを1にする)
// 前後移動
if (moveDirection.forward) camera.position.addScaledVector(direction, moveSpeed);
if (moveDirection.backward) camera.position.addScaledVector(direction, -moveSpeed);
const right = new THREE.Vector3();
camera.getWorldDirection(right); // カメラの向いている向きを取得
right.cross(new THREE.Vector3(0, 1, 0)); // カメラの右方向を計算
right.normalize();
// 左右移動
if (moveDirection.left) camera.position.addScaledVector(right, -moveSpeed);
if (moveDirection.right) camera.position.addScaledVector(right, moveSpeed);
camera.position.y = 1; // 高さ固定
// カメラ座標の表示更新
document.getElementById('coordinates').innerHTML = `<p>位置: X: ${camera.position.x.toFixed(2)}, Y: ${camera.position.y.toFixed(2)}, Z: ${camera.position.z.toFixed(2)}</p>`;
renderer.render(scene, camera);
}
// アニメーションの開始
animate();
画面サイズの変更への対応
ウィンドウのサイズが変わったときに、カメラのアスペクト比を更新。
// ウィンドウサイズ変更対応
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight; // アスペクト比の更新
camera.updateProjectionMatrix(); // 投影行列の更新(カメラの視覚的な変換を表現する行列)
renderer.setSize(window.innerWidth, window.innerHeight); // レンダラーサイズの更新
});
おわりに
WebGLとThree.jsを使うことで、ブラウザ上で簡単に3Dオブジェクトを描画し、インタラクティブなアプリケーションを作ることができました。
まだ単に空間を作って動けるようにしただけなので、他の3Dオブジェクトを追加したり、何かしらのアニメーションやエフェクトを加えたり、ちょっとしたミニゲームにしたり、いろいろやってみたいです。