0
1

【Three.js】1つのページに複数のThree.jsシーンを作成する

Posted at

Three.jsでHTMLのbody内にシーンを追加する場合、new THREE.Scene();で新たにシーンを作成しますが、1つのbody内に複数のシーンを作成して、スクロールなどで切り替えたい、というようなケースもあります。その場合、HTMLで要素にdata属性を指定して複数のシーンを描画する、という方法を紹介します。

デモはこちら

data属性とは

HTML5では、<img src="photo.jpg" width="500" height="300">srcwidth,heightのような、標準で定められている「属性」の他に、独自の属性を開発者が付加できるdata属性というものがあります。これは、接頭辞にdata-をつけて、開発者が自由に名前をつけることができます。

<!-- div要素に`data-hoge`属性を追加 -->
<div data-hoge="piyo" id="fuga"> </div>

ここで指定したdata-hoge属性は、JavaScriptで以下のようにして取得することができます。

let elem = document.getElementById("fuga");
console.log(elem.dataset.hoge);
// piyo と表示される

このdata属性を利用して、1つのbody要素の中に複数のThree.jsシーンを追加していきます。

参考:データ属性の使用 - ウェブ開発を学ぶ | MDN

HTMLにシーンの数だけ要素を作成

まず、作成したいシーンの数だけHTML上に要素を作成します。
今回は、box,corn,octahedronの3つのメッシュをそれぞれ1つ描画する3つのシーンを作成します。
それぞれの要素に、data-scene="box"のようにdata属性を追加しておきます。

index.html
<!--boxを描画するシーン-->
<div data-scene="box">
    <div class="wrap">
        <h2>Box</h2>
    </div>
</div>

<!--cornを描画するシーン-->
<div data-scene="corn">
    <div class="wrap">
        <h2>Corn</h2>
    </div>
</div>

<!--octahedronを描画するシーン-->
<div data-scene="Octahedron">
    <div class="wrap">
        <h2>Octahedron</h2>
    </div>
</div>

JavaScriptで配列として各シーンを定義

1つだけのシーンを作成

次に、JavaScriptでThree.jsのシーンを定義していきます。
まずは、body要素に1つのシーンだけを作成し、Boxを1個だけ描画してみます。

function init(){
    //レンダラー
    const renderer = new THREE.WebGLRenderer({
        antialias:true,
    });
    renderer.setSize(window.innerWidth, window.innerHeight);

    //data-scene属性を持つ要素にレンダラーを追加
    const elem = document.querySelector("[data-scene]");
    elem.appendChild(renderer.domElement);

    //シーン
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x333333);

    //カメラ
    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 0, 10);
    camera.lookAt(0,0,0);
    scene.add(camera);

    //ライティング
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    camera.add(directionalLight);

    //Boxを1つだけシーンに追加
    const geometry = new THREE.BoxGeometry(2,2,2);
    const material = new THREE.MeshPhongMaterial({
        color: 0xff0000,
    });
    let mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);

    //アニメーション用の時間
    let time = new THREE.Clock();

    render();

    //毎フレーム実行されるレンダー関数
    function render(){
        requestAnimationFrame(render);
        renderer.render(scene, camera);
        mesh.rotation.x += Math.cos(time.getElapsedTime()) * 0.01;
        mesh.rotation.y += Math.sin(time.getElapsedTime()) * 0.01;
    }
}

//エントリーポイント
window.addEventListener("DOMContentLoaded", init);

Three.jsの基本的なコードですね。
ここから、複数のシーンを追加していきます。

複数のシーンを作成

data属性を利用して複数のシーンを描画する手順は、大まかに以下の流れになります。

  1. シーンをレンダリングする1つのレンダラーを用意する
  2. シーンを作成する関数makeScene()を作成する
  3. 各シーンをオブジェクトの配列として定義し、makeScene()を実行する
  4. HTMLからdata-scene属性を持つ全ての要素を取得し、それぞれに各シーンを割り当てて行く
  5. ブラウザの表示領域に入ったシーンだけを、レンダラーを使ってレンダリングする

まず、レンダラーを1つだけ用意します。

const renderer = new THREE.WebGLRenderer({
    antialias:true,
});

次に、シーンを作成し戻り値としてsceneとcameraを返す関数makeScene()を定義します。
1つだけのシーンを描画する場合、sceneとcameraは1つずつあれば事足りますが、
今回は複数のシーンを描画するので、関数として定義しておいて、各シーン内の作成時に関数として利用できるようにしておきます。

function makeScene(){
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 1000);
    camera.position.set(0, 0, 10);
    camera.lookAt(0,0,0);
    scene.add(camera);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    camera.add(directionalLight);

    return {scene, camera};
}

次に、個別のシーンをオブジェクトの配列sceneInitFunctionsByNameとして定義します。
この中で、先程作成したmakeScene()関数を呼び出してシーンを作成します。

//各シーンを定義
const sceneInitFunctionsByName = {
    //boxを描画するシーン
    "box":() => {
        const {scene, camera} = makeScene();
        scene.background = new THREE.Color(0x333333);
        const geometry = new THREE.BoxGeometry(2,2,2);
        const material = new THREE.MeshPhongMaterial({
            color: 0xff0000,
        });
        const mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);
        return (time, rect) => {
            mesh.rotation.x = time;
            mesh.rotation.y = time;
            camera.aspect = rect.width / rect.height;
            camera.updateProjectionMatrix();
            renderer.render(scene, camera);
        };
    },
    //cornを描画するシーン
    "corn":() => {
        const {scene, camera} = makeScene();
        scene.background = new THREE.Color(0x111111);
        const geometry = new THREE.ConeGeometry(1, 3, 8);;
        const material = new THREE.MeshPhongMaterial({
            color: 0x00ff00,
        });
        const mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);
        return (time, rect) => {
            mesh.rotation.x = time;
            mesh.rotation.z = time;
            camera.aspect = rect.width / rect.height;
            camera.updateProjectionMatrix();
            renderer.render(scene, camera);
        }
    },
    //octahedronを描画するシーン
    "octahedron":() => {
        const {scene, camera} = makeScene();
        scene.background = new THREE.Color(0x222222);
        const geometry = new THREE.OctahedronGeometry(2, 0);;
        const material = new THREE.MeshPhongMaterial({
            color: 0x0000ff,
        });
        const mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);
        return (time, rect) => {
            mesh.rotation.x = time;
            mesh.rotation.z = time;
            camera.aspect = rect.width / rect.height;
            camera.updateProjectionMatrix();
            renderer.render(scene, camera);
        }
    }
}

次に、body要素からdata-scene属性を持つすべての要素を探してきて、同じ値を持つシーンを割り当てます。

//全てのシーンを管理する配列
const sceneElements = [];

//配列にシーンを追加する関数
function addScene(elem, fn) {
  const ctx = document.createElement("canvas").getContext("2d");
  elem.appendChild(ctx.canvas);
  sceneElements.push({elem, ctx, fn});
}

//HTMLから全てのdata-sceneを探してきて、要素の数だけシーンを描画
document.querySelectorAll("[data-scene]").forEach((elem) => {
    //data-scene属性の値から、シーンの名前を取得する(box, corn, octahedron)
    const sceneName = elem.dataset.scene;
    //data-scene属性の値と同じ名前を持つシーンをsceneInitFunctionに代入する
    const sceneInitFunction = sceneInitFunctionsByName[sceneName];
    //data-scene属性を持つ要素内に、同じ名前のシーンを作成する
    const sceneRenderFunction = sceneInitFunction(elem);
    addScene(elem, sceneRenderFunction);
});

最後に、ブラウザの表示領域内(ViewPort)に入っているdata-scene属性を持つ要素のシーンだけを描画するように、render()関数を定義します。

//毎フレーム実行されるレンダー関数
function render(time){
    time *= 0.001;
    for (const {elem, fn, ctx} of sceneElements) {
        //ブラウザで表示される領域(ViewPort)を取得
        let rect = elem.getBoundingClientRect();
        const {left, right, top, bottom, width, height} = rect;
        const rendererCanvas = renderer.domElement;

        // ViewPort内に収まっていない場合は、isOffscreenフラグを立てる
        const isOffscreen =
            bottom < 0 ||
            top > window.innerHeight ||
            right < 0 ||
            left > window.innerWidth;

        // isOfscreenフラグがない = ViewPort内に表示されていれば、シーンをレンダリングする
        if (!isOffscreen) {
            if (rendererCanvas.width < width || rendererCanvas.height < height) {
                renderer.setSize(width, height, false);
            }

            if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
                ctx.canvas.width = width;
                ctx.canvas.height = height;
            }

            renderer.setScissor(0, 0, width, height);
            renderer.setViewport(0, 0, width, height);

            fn(time, rect);

            ctx.globalCompositeOperation = 'copy';
            ctx.drawImage(
                rendererCanvas,
                0, rendererCanvas.height - height, width, height,
                0, 0, width, height);
                }
            }
        requestAnimationFrame(render);
}

これで、ページの読み込み時にrender関数を呼び出せば、JavaScriptは完成です。

window.addEventListener("DOMContentLoaded", render);

最後に、作成したcanvas要素がフルスクリーンで表示されるように調整して終了です。

CSSでスクロール時の挙動を調整

*{
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    list-style: none;
}

canvas{
    width: 100%;
    height: 100%;
}
*[data-scene]{
    width: 100%;
    height: 100vh;
    position: relative;
    z-index: 1;

    .wrap{
        position: absolute;
        top: 0;
        right: 1%;
        left: 1%;
        max-width: 700px;
        height: 100%; //これ大事
        margin: 0 auto;
        padding: 2.0rem;
        overflow-y: scroll;
        &::-webkit-scrollbar{
            display: none;
        }
        color: #ccc;
        h3{
            margin-top: 60px;
        }
    }
}

これで、1つのbody内に複数のシーンを描画できました。
ここからパララックスや慣性スクロールなどのインタラクションをつけていくと、Three.jsを利用したリッチなページを作成できるのではないかと思います!

全体のコードおよびデモはこちら

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