Three.jsでWebGLを触ってみる

  • 28
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

WebGLを手軽に使えるThree.jsを使って色々と作っていく過程で得たものを備忘録として残していく記事です。

自分が実際に触ったり、Three.jsに最初からついてるサンプルをいじりながら自分が理解したものなので、 中には誤解していたり勘違いしている情報もあるかも しれないので、参考にする場合は自己責任でお願いします;

まず手始めに、簡単なモデルデータの読み込み、morph animation、影の表示をやってみました。
実際に動いているものはjsdo.itにあげています。

モデルデータはBlenderに最初から用意されている猿?を使用しました。

Three.jsで影を表示する

Three.jsで影を表示するにはLightRendererMeshのそれぞれで影を有効にしないとなりません。

今回のサンプルでいうと以下のコード断片です。

Lightの設定

globalLight = new THREE.DirectionalLight( 0xefefff, 2 );
globalLight.position.set( 1, 1, 1 ).normalize();
globalLight.castShadow = true;
globalLight.shadowMapWidth = 2048;
globalLight.shadowMapHeight = 2048;

var d = 1000;
globalLight.shadowCameraLeft = -d;
globalLight.shadowCameraRight = d;
globalLight.shadowCameraTop = d;
globalLight.shadowCameraBottom = -d;
globalLight.shadowCameraNear = 1;
globalLight.shadowCameraFar = 1000;
globalLight.shadowCameraFov = 40;

//globalLight.shadowCameraVisible = false;

globalLight.shadowBias = 0.0001;
globalLight.shadowDarkness = 0.5;

scene.add(globalLight);

まず平行光源を作成します。(THREE.DirectionalLight)
ポイントは、shadowがついているプロパティです。
castShadowをtrueにすることでライトに対して影を生成させることを伝えます。
その他の設定は影がどう落ちるかの設定です。

shadowMap*は(おそらく)影の解像度です。
これを小さくすると影が若干ぼけたようになります。

shadowCamera*は、ライトから見た視点座標の設定でしょう。
(おそらくデプスバッファシャドウ?)
シャドウマッピングについてはこちらの記事(シャドウマッピング | wgld.org)が参考になります。
要は、ライトから見た視点から影の付き具合を計算する方法です。
設定項目については普通のカメラと似たようなものになります。

また、shadowCameraVisible = trueにすると、どこに影が落ちるのか視覚的に確認することができます。(主にデバッグ目的)
これをtrueにした状態で色々値をいじるとどう変化するかが分かるかと思います。

Meshの設定

mesh.castShadow = true;
mesh.receiveShadow = true;

Meshに対する設定は非常にシンプルです。
そのMeshが影を落とすのか(castShadow)、影を受けるのか(receiveShadow)の設定のみです。

ちなみに、castShadowとreceiveShadow双方をtrueにすると、自分自身にも影が落ちるようになります。(例えば手の影が自分の体に影を落とす、みたいな)

Rendererの設定

renderer.shadowMapEnabled = true;

rendererへの設定はさらにシンプルです。
shadowMapEnabled = trueを設定するだけです。

ただし、これを有効にした場合、少なくともひとつ以上のライトに対して適切にshadowの設定をしないと画面が真っ暗になってなにもレンダリングされない(というか、多分全部影になってる?)ので注意が必要です。

影を計算させる場合は、必ず上記3つにshadowの設定をしましょう。

モデルデータの読み込み

今回はTrhee.jsについてくるBlenderのexporterを使って、Blenderで作成したモデルをJSON形式に書き出し、それをJSONLoaderで読み込ませて使用しています。

var loader = new THREE.JSONLoader(true);
loader.load('models/monkey.js', createScene);

まずはJSONLoaderクラスを使って書きだしたJSONを読み込みます。(ファイルの拡張子は.js

function createScene(geometry, materials) {
    var material;

    if (materials) {
        for (var i = 0, l = materials.length; i < l; i++) {
            materials[i].morphTargets = true;
        }
        material = new THREE.MeshFaceMaterial(materials);
    }
    else {
        material = new THREE.MeshLambertMaterial( { color: 0x606060, morphTargets: true } )
    }

    mesh = new THREE.SkinnedMesh(geometry, material);
    mesh.scale.set(100, 100, 100);
    mesh.position.y += 40;
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    scene.add(mesh);
}

JSONLoaderで読み込んだデータをコールバックを使ってThree.jsで使える状態にします。

基本的にはLoaderクラス側で基本的なセットアップをしてくれるので、あとは使用するSceneに合わせて初期化します。

今回の例では全体的にモデルデータが小さいのでスケールを100倍し、さらに影の設定とpositionの設定を加えたあとSceneに追加しています。

3Dレンダリングの基本だと思いますが、シーンに描けるオブジェクトをMeshと呼び、MeshGeometry(形状)と、Material(質感)を持っています。

そのため、Loaderクラスは読み込んだモデルデータのGeometry(形状)と、モデルデータ内で定義されているMaterial(質感)を構築した上でそれらを引数としてコールバックが呼び出されます。

あとはそれを利用してMeshを生成し、シーンに追加すればシーンにBlenderなどで作成したモデルが表示される、というわけです。

Morph animation

今回はBlenderでちょっとしたアニメーションを作りそれを再生しています。
ここはまだ曖昧な部分でもあるので、こうしたら動いた、というような情報です。

まず、アニメーションを有効にするために以下の処理をします。

for (var i = 0, l = materials.length; i < l; i++) {
    materials[i].morphTargets = true;
}
material = new THREE.MeshFaceMaterial(materials);

JSONLoaderのコールバックで指定していた部分です。
materials[i].morphTargets = trueがアニメーションをONにしている部分です。
こうすることで、アニメーションが実行されるようになります。

今回はモーフアニメーションという、頂点位置を補完したものをアニメーションさせる、コマアニメ風のアニメーションです。
アニメーション部分は以下の指定で行います。

var duration = 1000;
var keyframes = 30;
var interpolation = duration / keyframes;
var lastKeyframe = 0;
var currentKeyframe = 0;
var time = Date.now() % duration;
var keyframe = Math.floor( time / interpolation );

if ( keyframe != currentKeyframe ) {
    mesh.morphTargetInfluences[ lastKeyframe ] = 0;
    mesh.morphTargetInfluences[ currentKeyframe ] = 1;
    mesh.morphTargetInfluences[ keyframe ] = 0;

    lastKeyframe = currentKeyframe;
    currentKeyframe = keyframe;
}

mesh.morphTargetInfluences[keyframe] = ( time % interpolation ) / interpolation;
mesh.morphTargetInfluences[lastKeyframe] = 1 - mesh.morphTargetInfluences[ keyframe ];

durationkeyframesなど、アニメーションの基本的な部分を設定します。
(今回は30フレームのアニメーションです)

重要な部分は、 mesh.morphTargetInfluences配列を適切にスイッチしていく という部分です。
ここが曖昧な部分なんですが、morphTargetInfluencesは作成したアニメーションのフレーム数と同じ数のインデックスがあります(今回だと30)。

値を設定している部分を見ると、01を切り替えていることから、ここで指定されたフレームに紐づく頂点位置を利用してアニメーションしているのでは、と思っています。(ここは引き続き調査予定)

別のアニメーション方法

今回はmorph animationでアニメーションを行いましたが、これは予めキーフレームごとの頂点位置を補完し、その補完された全頂点データを保存して頂点情報を切り替えながらアニメーションさせる、というものです。

そのため、複雑なモデルやアニメーション自体が長くなると当然、データ量が爆発的に増えていきます。
今回のサンプルのように簡単なアニメーションとかなら問題ないですが、普通はここまでシンプルなモデルはあまりないですよね。

そこで、これ以外のアニメーション方法として「ボーンアニメーション」というものがあります。
これは、モデルにボーン(骨)を設定し、そのボーンの動きだけを保存し、そのボーンの状態に応じて頂点位置を計算する、というものです。
逆にこちらは計算負荷が高くなってしまいますが、最初に読み込むデータ量が少なくなるのがメリットです。

こちらについてはまた別の機会に書きたいと思います。

Three.jsがデフォルトで提供している定数一覧

THREE.ShaderMaterialを使うと、Three.jsはデフォルトでいくつかの引数を渡してくれます。それが以下に示す変数群です。

For vertex shader

変数定義 説明
uniform mat4 modelMatrix モデル座標系からワールド座標系への変換行列
uniform mat4 viewMatrix ワールド座標系からビュー座標系への変換行列
uniform mat4 modelViewMatrix modelMatrixとviewMatrixを乗算したもの
uniform mat4 projectionMatrix 透視変換行列
uniform mat3 normalMatrix 法線をビュー座標系に変換する行列
uniform vec3 cameraPosition ワールド座標系でのカメラ座標
attribute vec3 position 頂点座標
attribute vec3 normal 頂点法線
attribute vec2 uv UV座標

For fragment shader

変数定義 説明
uniform mat4 viewMatrix ワールド座標系からビュー座標系への変換行列
uniform vec3 cameraPosition ワールド座標系でのカメラ座標

Three.jsのShaderMaterialで使う変数Type

type値 値のクラス
i 数値(int型)
f 数値(float型)
c THREE.Color
v2 THREE.Vector2
v3 THREE.Vector3
v4 THREE.Vector4
m4 THREE.Matrix4
t THREE.Texture
iv1 整数の配列
iv 整数の配列(長さは3の倍数)
fv1 浮動小数点値の配列
fv 浮動小数点値の配列(長さは3の倍数)
v2v THREE.Vector2の配列
v3v THREE.Vector3の配列
v4v THREE.Vector4の配列
m4v THREE.Matrix4の配列
tv THREE.Textureの配列