Edited at

Three.jsでWebGLを触ってみる

More than 5 years have passed since last update.

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の配列