Three.js -3

ブラウザリサイズ時の描画範囲変更の追従とアニメーション

これまでThree.jsをつかって単一フレームに描画していたのを、固定間隔で再レンダリングしアニメーション化する。
また、ブラウザのサイズ変更を検知し、レンダリングパラメータを更新することでブラウザサイズに依存しない描画を実現する。

index.html

今回もbodyに何も要素を書かないindex.htmlを作成する。
canvasなどはデフォルトでbodyのmargin属性(デフォルト値5px)の影響を受けるため、左と上に空白がある。
この空白はJavaScript内で操作して0にする。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <title>Three.js My Tutorial 3</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width,user-scalable=0" />

        <meta http-equiv="Pragma" content="no-cache" />
        <meta http-equiv="Cache-Control" content="no-cache" />
        <meta http-equiv="Expires" content="0" />

        <script charset="utf-8" src="lib_ext/three.min.js"></script>
        <script charset="utf-8" src="lib_int/tutorial3.js"></script>
    </head>

    <body>
    </body>
</html>

tutorial3.js

ドキュメントがロード完了したときに実行されるonLoaded関数、およびブラウザのサイズが変更されたときに実行されるonResized関数を定義し、それぞれの関数をイベントリスナに登録する。

var renderer = null;
var scene = null;
var camera = null;


var fpsRender = 30; // 30FPSで描画
var queueRender = null;
var msRenderWait = 1000 / fpsRender;


var onLoaded = function() {

    var vectorZero = new THREE.Vector3(0, 0, 0);
    var vectorUnitX = new THREE.Vector3(1, 0, 0);
    var vectorUnitY = new THREE.Vector3(0, 1, 0);
    var vectorUnitZ = new THREE.Vector3(0, 0, 1);

    var vectorForward = vectorUnitZ;
    var vectorLeft = vectorUnitX;
    var vectorUp = vectorUnitY;


    var THREE_CreateAxesMesh = function() {
        var axes = new THREE.Object3D();

        var axisXBody = new THREE.Mesh(
            new THREE.CylinderGeometry(0.01, 0.05, 1, 4),
            new THREE.MeshBasicMaterial({color: 0xff0000})
        );
        axisXBody.translateX(0.5);
        axisXBody.quaternion.setFromAxisAngle(vectorUnitZ, -Math.PI / 2);
        axes.add(axisXBody);

        var axisXHead = new THREE.Mesh(
            new THREE.CylinderGeometry(0.01, 0.05, 0.125, 4),
            new THREE.MeshBasicMaterial({color: 0xff0000})
        );
        axisXHead.translateX(0.9375);
        axisXHead.quaternion.setFromAxisAngle(vectorUnitZ, -Math.PI / 2);
        axes.add(axisXHead);
    
        var axisYBody = new THREE.Mesh(
            new THREE.CylinderGeometry(0.01, 0.05, 1, 4),
            new THREE.MeshBasicMaterial({color: 0x00ff00})
        );
        axisYBody.translateY(0.5);
        axes.add(axisYBody);
    
        var axisYHead = new THREE.Mesh(
            new THREE.CylinderGeometry(0.01, 0.05, 0.125, 4),
            new THREE.MeshBasicMaterial({color: 0x00ff00})
        );
        axisYHead.translateY(0.9375);
        axes.add(axisYHead);
    
        var axisZBody = new THREE.Mesh(
            new THREE.CylinderGeometry(0.01, 0.05, 1, 4),
            new THREE.MeshBasicMaterial({color: 0x0000ff})
        );
        axisZBody.translateZ(0.5);
        axisZBody.quaternion.setFromAxisAngle(vectorUnitX, Math.PI / 2);
        axes.add(axisZBody);

        var axisZHead = new THREE.Mesh(
            new THREE.CylinderGeometry(0.01, 0.05, 0.125, 4),
            new THREE.MeshBasicMaterial({color: 0x0000ff})
        );
        axisZHead.translateZ(0.9375);
        axisZHead.quaternion.setFromAxisAngle(vectorUnitX, Math.PI / 2);
        axes.add(axisZHead);

        return axes;
    };

    // 各種設定値
    var pixWidthScreen = window.innerWidth;
    var pixHeightScreen = window.innerHeight;

    // レンダラーの初期化
    // = WebGLの有効化+HTMLへ要素の追加
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(pixWidthScreen, pixHeightScreen);
    document.body.style.marginTop = "0px";
    document.body.style.marginLeft = "0px";
    document.body.appendChild(renderer.domElement);

    // シーンの初期化
    scene = new THREE.Scene();
    scene.position = vectorZero;

    // カメラの初期化
    camera = new THREE.PerspectiveCamera(130, pixWidthScreen / pixHeightScreen, 0.25, 128); // 透視投影の場合
    //var camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.25, 128); // 平行投影の場合
    camera.visible = false;
    camera.position.set(1, 0.5, 0.5);
    camera.lookAt(vectorZero);
    scene.add(camera);

    // シーンオブジェクト1 光源の初期化
    var light = new THREE.PointLight(0xffffff, 1, 16); // 白色光の点光源
    light.position.set(1, 1, 1);
    scene.add(light);

    // シーンオブジェクト2 座標軸の初期化
    var axes = THREE_CreateAxesMesh();
    scene.add(axes);

    // シーンオブジェクト3 箱の初期化
    var sphere = new THREE.Mesh(
        new THREE.BoxGeometry(0.5, 0.5, 0.5, 4, 4, 4),
        new THREE.MeshPhongMaterial({color: 0x00ffff})
    )
    sphere.position.set(0, 0, 0);
    scene.add(sphere);


    // 30FSP目標で繰り返し描画
    queueRender = window.setInterval(function() {
        var qCurrent = sphere.quaternion.clone();
        sphere.quaternion.setFromAxisAngle(vectorUp, Math.PI / 30);
        sphere.quaternion.multiply(qCurrent);

        renderer.render(scene, camera);
    }, msRenderWait);

    return;
};



var queueResize = null;
var msResizeWait = 500;
var onResized = function() {
    window.clearTimeout(queueResize);
    queueResize = window.setTimeout(function() {
        var pixWidthScreen = window.innerWidth;
        var pixHeightScreen = window.innerHeight;

        if(renderer != null) {
            renderer.setSize(pixWidthScreen, pixHeightScreen);
        }

        if(camera != null) {
            camera.aspect = pixWidthScreen / pixHeightScreen;
            camera.updateProjectionMatrix();
        }

        // アニメーションループを使わず1回のみ描画する場合には、リサイズ後も再描画させる必要がある
        //renderer.render(scene, camera);

        return;
    }, msResizeWait);

    return;
};



window.addEventListener("load", onLoaded, false);
window.addEventListener("resize", onResized, false);

onResizedとonLoaded、2つの関数からcamera, scene, renderオブジェクトを利用する必要があるため、これらを一旦グローバルオブジェクトとして定義する。

onResizedの中で、変更後の描画領域のサイズを取得し、WebGLのパラメータ変更を行う。
具体的には

  • WebGLRenderer.setSize()でWebGLの出力対象であるCanvasのサイズおよびViewportのサイズの指定
  • カメラオブジェクトのアスペクト比の修正

を行う。

アニメーションしていることをわかりやすくするため、今回は箱状のオブジェクトを作り、1ラジアン毎秒の回転を行わせる。

完成

tutorial3_980x380
980 x 380の場合。

tutorial3_480x320
480 x 320の場合。

いずれもカメラのアスペクト比が適切に修正され、オブジェクト自体のアスペクト比は一定になっていることがわかる。

メモ

連続リサイズ時のイベントの処理の間引き

参考:http://lab.informarc.co.jp/javascript/resize_queue.html
PCブラウザでリサイズしたときなどは、リサイズイベントが非常に多く発行されるが、これをいちいち処理していたらパフォーマンスの問題につながりかねない。
そのため更新命令が来たら一旦キューに入れ一定の時間待ち、その実行までの間に次の更新が来たら先のキューを無効化、新たに更新キューを作成するような仕組みにする。
(これにより、最短でもwaitResizeに指定したミリ秒だけ、反映が遅れることになる)

Three.jsにおけるダブルバッファリング

参考:https://github.com/mrdoob/three.js/issues/442
WebGLRenderの初期化パラメータにpreserveDrawingBuffer = trueを指定すればよい(デフォルトではfalse)。
ただし、一部不完全な実装の様子。

JavaScriptからbodyエレメントのマージンを0にする

参考:http://qiita.com/tyfkda/items/d1c650786bce6ff825b1
マージンを0にしないと、window.innerWidth, window.innerHeightをそのままviewportの幅高に指定できない(マージンを考慮する必要がでてまう)。

今回の為に突貫的にグローバル変数を増やしてしまい、コードとしてはあまりきれいとは言えない状況になってしまったので、この辺の修正を行う。
また、描画ループと更新ループを分割し、描画ループをブラウザの更新タイミングに、更新ループを固定FPSで行うよう、繰り返し処理の工夫を行う。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です