0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebGLで遊んでみた! 3D空間でカメラを動かして立方体を操るWebアプリの作成

Last updated at Posted at 2025-01-24

はじめに

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オブジェクトを追加したり、何かしらのアニメーションやエフェクトを加えたり、ちょっとしたミニゲームにしたり、いろいろやってみたいです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?