【JavaScript】three.jsで360°画像+GLTF+影を表示する

WebGLをベースとした3D表現のライブラリであるthree.jsは、ブラウザ上でリッチな3Dコンテンツを実現するための強力なツールとして注目されています。本記事では、three.jsを用いて360°背景画像を表示し、その上にGLTF形式の3Dモデルを配置、さらにリアルな影を付け加える手法をサンプルコードを通して詳しく解説していきます。
この記事は、three.jsに触れたことがある初級者や、さらに踏み込んで中級者レベルを目指す方向けの内容となっています。コードの各行がどのような役割を持っているのか、どういった考え方で実装されているのかを理解することで、あなた自身のプロジェクトに応用できる知識が得られるはずです。
以下、コードの全体像を再掲します。なお、このサンプルコードはHTML、CSS、JavaScriptを組み合わせたもので、ブラウザ上で動作することを前提としています。



<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>360度背景+GLTFモデル+影</title>
    <style>
        body { 
            margin: 0;
            overflow: hidden;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
</body>

<script type="importmap">
    {
        "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.141.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.141.0/examples/jsm/"
    }
}
</script>
<script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

    // === ① シーンの作成 ===
    const scene = new THREE.Scene();

    // === ② カメラの作成(適切な高さに設定) ===
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 0, 0.1); // カメラを適切な高さに配置
    // === ③ レンダラーの作成 ===
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true; // 影を有効化
    document.body.appendChild(renderer.domElement);

    // === ④ カメラコントロール ===
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.enablePan = false; // 平行移動(Shift+ドラッグ)を無効化
    controls.enableZoom = true;
    controls.maxDistance = 0.1; // 遠ざかれる距離の最大値

    // === ⑤ 360度背景画像を設定 ===
    const textureLoader = new THREE.TextureLoader();
    textureLoader.load('/img/scene01.jpg', (texture) => {
        texture.mapping = THREE.EquirectangularReflectionMapping;
        scene.background = texture;
        scene.environment = texture;
    });

    // === ⑥ 地面の作成(背景の地面の高さに合わせる) ===
    const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
    const groundMaterial = new THREE.ShadowMaterial({ opacity: 0.5 });
    groundMaterial.polygonOffset = true;
    groundMaterial.polygonOffsetFactor = -1;
    groundMaterial.polygonOffsetUnits = -4;
    const ground = new THREE.Mesh(groundGeometry, groundMaterial);
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -45; // 地面の高さを設定
    ground.receiveShadow = true;
    ground.renderOrder = 0;
    scene.add(ground);

    // === ⑦ 環境光・平行光を追加(影を出す) ===
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    // 影が出るための光源を追加
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    directionalLight.position.set(-100, 250, 200);
    directionalLight.castShadow = true;
    directionalLight.shadow.mapSize.width = 2048;
    directionalLight.shadow.mapSize.height = 2048;
    // 影の範囲を調整
    directionalLight.shadow.camera.left = -100;
    directionalLight.shadow.camera.right = 100;
    directionalLight.shadow.camera.top = 100;
    directionalLight.shadow.camera.bottom = -100;
    scene.add(directionalLight);
    // 光源の影キャストを有効化
    directionalLight.castShadow = true;

    // === ⑧ GLTFモデルの追加 ===
    const gltfLoader = new GLTFLoader();
    gltfLoader.load('/img/model03.gltf', (gltf) => {
        const model = gltf.scene;
        model.scale.set(50, 50, 50);
        model.position.set(0, -20 ,-75); // 地面上に配置

        // モデル内の全てのメッシュに対して影の設定を適用
        model.traverse((child) => {
            if (child.isMesh && child.material) {
                // まず通常の影キャスト設定
                child.castShadow = true;
                // マテリアルの alphaTest を設定(値は画像に合わせて調整)
                child.material.alphaTest = 0.1;
                child.material.transparent = true;
                child.material.needsUpdate = true;

                // カスタム深度マテリアルの設定(シャドウマッピング用)
                const customDepthMaterial = new THREE.MeshDepthMaterial({
                    depthPacking: THREE.RGBADepthPacking,
                    alphaTest: child.material.alphaTest,  // 同じ alphaTest 値を使う
                });
                customDepthMaterial.map = child.material.map;  // テクスチャをコピー
                child.customDepthMaterial = customDepthMaterial;
                // 必要に応じて、距離マテリアルも設定(高精度シャドウの場合)
                const customDistanceMaterial = new THREE.MeshDistanceMaterial({
                    alphaTest: child.material.alphaTest,
                });
                customDistanceMaterial.map = child.material.map;
                child.customDistanceMaterial = customDistanceMaterial;
            }
        });

        //model.castShadow = true;
        //model.receiveShadow = false;
        model.renderOrder = 1;
   
        scene.add(model);
    }, undefined, (error) => {
        console.error('GLTFモデルの読み込みエラー', error);
    });

    // === マウスホイールで背景と一緒に拡大縮小 ===
    window.addEventListener('wheel', (event) => {
        const zoomSpeed = 0.1;
        camera.fov += event.deltaY * zoomSpeed * 0.05; // FOVで拡大縮小
        camera.fov = Math.max(20, Math.min(120, camera.fov)); // FOVの範囲を制限
        camera.updateProjectionMatrix();
    });

    // === ⑨ アニメーションループ ===
    // アニメーション制御用のフラグ
    let isAnimating = true;

    // アニメーション関数
    function animate() {
        if (!isAnimating) return;  // 停止時はレンダリングしない
        requestAnimationFrame(animate);
        controls.update();
        renderer.render(scene, camera);
    }

    // 初期のアニメーションスタート
    animate();

    // === ⑩ ウィンドウリサイズ対応 ===
    window.addEventListener('resize', () => {
        renderer.setSize(window.innerWidth, window.innerHeight);
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
    });
</script>

</html>

シーンとカメラの設定

シーンの作成

まず、シーンはthree.jsにおけるすべての3Dオブジェクトが配置されるおおもとのコンテナです。

const scene = new THREE.Scene();

このコードにより、シーンオブジェクトが生成され、後にオブジェクトやライト、カメラなどを追加していくための基盤となります。

カメラの設定

次に、シーンを観察するためのカメラの設定です。

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 0.1);
  • パースペクティブカメラを使用しているため、遠近感があり、現実の視点に近い表示が可能です。
  • 75 は視野角(FOV: Field Of View)で、一般的な値として使われます。
  • アスペクト比はウィンドウのサイズに合わせて計算され、クリッピングの開始点と終了点がそれぞれ 0.11000 に設定されています。
  • camera.position.set(x, y, z); により、360°画像の中心点(原点座標)を指定します。ここでは X=0, Y= 0, Z= 0.1 に設定しています。
    カメラを原点 (0, 0, 0) に設定すると、OrbitControls のデフォルトのターゲットも (0, 0, 0) になっているため、カメラとターゲットが重なってしまいます。その結果、カメラとターゲットの間に距離がなく、オービット(回転)する際に有効な基準が存在しないため、操作が機能しなくなるために数字をずらしています。

このような設定により、シーン全体をバランスよく捉え、リアルな視点でのレンダリングが可能となります。

レンダラーと影の設定

WebGLレンダラーの作成

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
  • antialias オプションは、ジャギー(ギザギザ)の発生を抑え、滑らかな表示を実現するために有効化されています。
  • setSize によってレンダラーの描画領域がブラウザ全体に広がるように設定。
  • renderer.shadowMap.enabled = true; とすることで、シーン内でシャドウ(影)を有効にしています。
  • 最後に、レンダラーのDOM要素(canvas)がHTMLのbodyに追加され、ブラウザ上に表示されます。

この設定により、three.jsが持つ高度なレンダリング機能を最大限に活用でき、シャドウ表現を含むリッチな3Dコンテンツの表示が可能となります。

OrbitControlsによるカメラ操作

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enablePan = false;
controls.enableZoom = true;
controls.maxDistance = 0.1;
  • OrbitControls は、ユーザーがマウスでシーン内のオブジェクトを回転させたり、ズーム操作を行ったりできるようにするためのコントロールライブラリです。
  • enableDamping をtrueにすることで、マウス操作に対してスムーズな慣性運動が追加され、より自然なカメラ移動が実現されます。
  • enablePan をfalseに設定することで、Shiftキーを用いた平行移動を無効化し、シーンの中心を捉えた操作に限定しています。今回は、360°画像の中心からカメラは移動しません。
  • ズーム操作も有効にし、ユーザーがシーン内の詳細に近づけるように設定しています。
  • maxDistance = 0.1; を設定することで、ズームアウト時モデルの移動を防止します。0 に設定するとマウス操作でカメラの角度が変更できなくなります。

これらの設定は、ユーザーエクスペリエンスを向上させるために重要です。操作が直感的かつスムーズに行えることで、3D空間内の探索がより楽しくなります。

360°背景画像の読み込み

const textureLoader = new THREE.TextureLoader();
textureLoader.load('/img/scene01.jpg', (texture) => {
    texture.mapping = THREE.EquirectangularReflectionMapping;
    scene.background = texture;
    scene.environment = texture;
});
  • TextureLoader を使用して、360°背景として使用する画像ファイルを読み込みます。
  • 読み込んだテクスチャは、EquirectangularReflectionMapping を用いることで、全方位に広がる環境テクスチャとしてマッピングされます。
  • scene.background に設定することで、シーン全体の背景として適用され、360°の環境が実現されます。
  • また、scene.environment に同じテクスチャを設定することで、反射などのエフェクトに利用できる環境マップとなります。

360°背景は、ユーザーに臨場感溢れる体験を提供するための重要な要素です。特にVRコンテンツやインタラクティブなウェブサイトで活用され、シーン全体の没入感を大きく高めます。

地面の作成とシャドウマッピング

const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
const groundMaterial = new THREE.ShadowMaterial({ opacity: 0.5 });
groundMaterial.polygonOffset = true;
groundMaterial.polygonOffsetFactor = -1;
groundMaterial.polygonOffsetUnits = -4;
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -45;
ground.receiveShadow = true;
ground.renderOrder = 0;
scene.add(ground);
  • 地面は非常に大きな平面ジオメトリ(1000×1000)として作成され、シーン全体を覆うためのベースとなります。
  • ShadowMaterial を使用することで、地面が影を受け取ることに特化したマテリアルとなり、よりリアルなシャドウ表現が可能となります。
  • polygonOffset の設定は、ジオメトリ同士が重なったときにZファイティング(ちらつき現象)が起きるのを防止するための工夫です。
  • 地面の回転(rotation.x = -Math.PI / 2)によって水平面に設定され、position.y = -45 によりシーン内の高さが調整されます。360°画像の原点(0, 0, 0)より下に地面を設定します。
  • receiveShadow をtrueに設定することで、地面が他のオブジェクトから落ちる影を受け取るようになります。

このように、地面の設定はシーン全体の基盤として非常に重要であり、ライティングや影のリアルな表現を実現するための基本要素となっています。

光源の設定と影の生成

環境光(Ambient Light)

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
  • 環境光は、シーン全体に均一な明るさを付与するための光源です。
  • 色は白(0xffffff)で、強度は0.5に設定され、シーン内のすべてのオブジェクトに柔らかな明かりを与えます。

平行光(Directional Light)

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-100, 250, 200);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
scene.add(directionalLight);
directionalLight.castShadow = true;
  • 平行光は、太陽光のように一定方向から一様に照らす光源です。
  • このコードでは、平行光の位置を(-100, 250, 200)に配置し、シーン内のオブジェクトに明確な影を落とすように設定されています。
  • castShadow をtrueにすることで、この光源から発生する影が各オブジェクトに対して計算されます。
  • シャドウマップのサイズやカメラの視野(left, right, top, bottom)の設定により、影の精度と描画範囲を調整しています。

平行光によるライティングは、オブジェクトにシャドウを落とし、立体感や奥行きを強調するための重要な手法です。特に屋外シーンや、リアルな光と影の表現が必要な場合に効果を発揮します。
シャドウの精度は、directionalLight.shadow.mapSize やカメラの設定、各オブジェクトのcastShadowreceiveShadowの設定によって大きく変動します。パフォーマンスと品質のトレードオフを意識して設定値を調整します。

GLTFモデルの読み込みと影の設定

const gltfLoader = new GLTFLoader();
gltfLoader.load('/img/model03.gltf', (gltf) => {
    const model = gltf.scene;
    model.scale.set(50, 50, 50);
    model.position.set(0, -20 ,-50);

    model.traverse((child) => {
        if (child.isMesh && child.material) {
            child.castShadow = true;
            child.material.alphaTest = 0.1;
            child.material.transparent = true;
            child.material.needsUpdate = true;

            const customDepthMaterial = new THREE.MeshDepthMaterial({
                depthPacking: THREE.RGBADepthPacking,
                alphaTest: child.material.alphaTest,
            });
            customDepthMaterial.map = child.material.map;
            child.customDepthMaterial = customDepthMaterial;

            const customDistanceMaterial = new THREE.MeshDistanceMaterial({
                alphaTest: child.material.alphaTest,
            });
            customDistanceMaterial.map = child.material.map;
            child.customDistanceMaterial = customDistanceMaterial;
        }
    });

    model.renderOrder = 1;
    scene.add(model);
}, undefined, (error) => {
    console.error('GLTFモデルの読み込みエラー', error);
});
  • GLTFLoader を用いることで、GLTF形式の3Dモデルを非同期に読み込みます。
  • 読み込み完了後、取得したシーン(gltf.scene)を任意の位置・スケールに合わせて配置します。
  • model.traverse により、モデル内部のすべてのメッシュに対して、影のキャスト(castShadow)や、透明部分の正しい描画のためのalphaTestの設定、さらにカスタムの深度マテリアルや距離マテリアルを設定することで、シャドウマッピング時の問題を解決しています。
  • これらの工夫により、透過部分があるテクスチャでも正しい影が描画され、よりリアルな表現が可能となります。

GLTF形式は、軽量でありながらも多機能な3Dモデルデータを扱うことができるため、Webコンテンツにおいて非常に人気のあるフォーマットです。今回の実装例では、影の品質を向上させるための工夫も盛り込まれており、実務レベルでも応用可能な内容となっています。

その他の操作(ズームやリサイズ対応)

マウスホイールによるズーム

window.addEventListener('wheel', (event) => {
    const zoomSpeed = 0.1;
    camera.fov += event.deltaY * zoomSpeed * 0.05;
    camera.fov = Math.max(20, Math.min(120, camera.fov));
    camera.updateProjectionMatrix();
});
  • ユーザーがマウスホイールを操作すると、カメラの視野角(FOV)が調整されることで、シーン全体の拡大縮小が行われます。
  • FOVの範囲を20°~120°に制限することで、極端なズームインやズームアウトを防止しています。
  • updateProjectionMatrix を呼び出すことで、変更後のFOVが正しく反映されるようにしています。

ウィンドウリサイズ対応

window.addEventListener('resize', () => {
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
});
  • ブラウザウィンドウのサイズが変更された際、レンダラーのサイズやカメラのアスペクト比を再計算し、シーンが正しく表示されるように対応しています。
  • これにより、レスポンシブなデザインが実現され、様々なデバイスでの表示が保証されます。

アニメーションループ

let isAnimating = true;

function animate() {
    if (!isAnimating) return;
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}

animate();
  • アニメーションループは、requestAnimationFrame を用いて毎フレーム更新される関数です。
  • controls.update() により、OrbitControlsの操作がスムーズに反映され、ユーザーが操作するたびにシーンの描画が更新されます。
  • フラグ isAnimating を導入することで、必要に応じてアニメーションの停止も可能となっています。
  • ループ処理は、シーン内のすべての変化(カメラ操作、オブジェクトのアニメーションなど)を反映するために必須です。

おわりに

今回の記事では、three.jsを用いて360°背景、GLTFモデル、そして影を実装する手法について、コードの各パートを細かく解説しました。three.jsは非常に柔軟で強力なライブラリであり、今回ご紹介したサンプルコードをベースに、さらに複雑な3Dシーンやインタラクティブな体験を実現することが可能です。

初級者の方は、まず基本的なシーンの構築やカメラ、レンダラー、ライトの設定に慣れることから始めると良いでしょう。そして、中級者以上の方は、今回のサンプルコードに独自の工夫を加えたり、追加のエフェクトやインタラクションを実装して、より高度な3Dコンテンツの制作にチャレンジしてみてください。

three.jsのドキュメントやコミュニティも非常に充実しており、分からない点や困ったときには情報を探しやすい環境が整っています。自分のプロジェクトに合わせて自由にカスタマイズし、オリジナルの3D表現を追求してみてください。

本記事が、あなたのthree.jsを用いた3Dコンテンツ制作の一助となれば幸いです。今後も最新の技術や実装例を追いながら、ブラウザ上での3D表現の可能性を広げていきましょう。

PR広告



楽天ブックスでthree.jsの本を探す