three.js使いこなし 前編 three.jsの物理ベースレンダリング

2016年2月のthree.jsのアップデートに含まれていた「物理ベースレンダリング」について、これまでのマテリアルとの違いや、表示結果、利用方法などを紹介します。

発行

著者 小山田 晃浩 フロントエンド・エンジニア
three.js使いこなし シリーズの記事一覧

はじめに

three.js r74のアップデートが2016年2月にありました。このアップデートには「物理ベースレンダリング」が含まれています。今回は、three.jsにおける物理ベースレンダリングについて、これまでのマテリアルとの違いや、表示結果、利用方法などを紹介します。

このシリーズで紹介するサンプルは次のリポジトリにまとめてあります。併せて、参照してください。

three.js使いこなし

なお、本記事はthree.jsに少し触れたことがある人に向けた内容です。three.jsを含むWebGLの基本については、次のシリーズで解説していますので、併せて参照してください。

物理ベースレンダリングとは

three.jsのマテリアル(表面材質)は、これまで長くに渡りMeshPhongMaterial(フォン・シェーディング)とMeshLambertMaterial(ランバート・シェーディング)が中心となって活躍してきました。この2つのマテリアルは光の当たり方や影を表現できるものの、あくまでも「それっぽく見せる」ことができる技術であり、見栄えの向上のためには経験による数値の微調整や、作り込まれたディフューズテクスチャー(色テクスチャー)に頼るなどの必要がありました。

このデモの色として適用したディフューズテクスチャーは次の図のようになります。シェーダーは影をもちろん表現できますが、それでは表現しきれない部分に、あらかじめテクスチャー側で色を塗っておくわけです。

たとえば、服のしわや角、物体同士が近い場所などは影が付きやすい性質があります。これをオクルージョンといいます。そこにあらかじめ色を塗っておいたり、光が当たっていそうな部分は明るくしておいたりして、レンダリングしたときに雰囲気が出るようにしています。本来、影を付けるためのシェーダーの仕事ですが、人間が頑張って色を塗っておいているのです。

一方で、リアルタイムレンダリングの手法のひとつとして、物理ベースレンダリング(Physically Based Shading)が注目を集めています。2010年頃からディズニー映画(リンク先のPDFは7MB近くあるのでご注意ください)などで利用され、最近では3Dゲーム開発でも採用されるようにもなっています。

物理ベースレンダリングとは、光の反射や散乱など現実の物理現象を再現するレンダリング手法です。three.jsに物理ベースレンダリングが登場したことにより、つるつるであっても、ざらざらであっても、入力値を変更するだけでその違いを再現することができ、かつ、フォンシェーディングやランバートシェーディングよりも現実味のある表現が可能になりました。物理ベースレンダリングが適用されると、同一の指定であってもライティング環境が異なるシーンでは、指定に応じた適切なレンダリング結果を得ることができます。

デモは、上が金属、下が非金属。変化を見るために、11個の玉をループで自動で作る際に少しずつラフネスの値を上げています。一番左側の玉が0で、そのとなりは0.1、0.2……と0.1ずつ上げており、一番右側の玉は1に設定しました。数値を変えただけで、さまざまな材質の表現が可能になることがわかるでしょう。

「物理」と付くため難しく聞こえるかもしれませんが、用意された機能を利用するだけなら、とても簡単です!

一方で、物理ベースレンダリングでは、従来のレンダリング方式のように、影などが描き込まれたディフューズテクスチャーは正確な光の計算の邪魔になってしまいます。そのため、物理ベースレンダリングで色を表現するためには純粋に「色のみ」のディフューズテクスチャーを用意し、アルベド(Albedo、反射する色)の値として利用することになります。

three.jsを含むいくつかの物理ベースレンダリングではディフューズ(カラー)入力がアルベド(反射する色)として利用されます。これにより結果的にディフューズの色が物体の色のベースとなります。加えて、どれくらい鋭く(鈍く)光を反射するのかの値、金属であるのか否かなどによって物体の表示結果が決まります。

白い光のもとで赤く見える物体の例。アルベドは入射光に対して反射する光の割合であり、他のレンダリング手法におけるディフューズ(カラー)とは少し異なる。

three.jsの物理ベースレンダリング

物理ベースレンダリングは、UnityUnreal Engineでもすでに利用できましたが、three.jsでもr74からMeshStandardMaterialという名前で、ついに物理ベースのリアルタイムレンダリングが利用できるようになりました(r74で初めて追加されたばかりの機能であるため、今後のthree.jsのアップデートでコンストラクター名や利用できるプロパティ名が変更される可能性があります)。

three.jsの物理ベースレンダリングはInital PR for MeshStandardMaterial #7381というプルリクエストや#5847などのissueで論議を重ね、できるだけ多くのthree.jsユーザーが扱いやすく感じる名前としてMeshPhysicalMaterialではなく、MeshStandardMaterialが採用されました。また難しくなり過ぎないように、必要最低限の物理ベースレンダリングに関するプロパティを追加して実装されました。

MeshStandardMaterialで利用する主なプロパティは、次の通りです。

プロパティ名 効果
color 既存 アルベドとして利用される色
map 既存 アルベドとして利用されるディフューズテクスチャー
roughness 新設 表面の粗さ。ざらざらしていれば1に近づく
metalness 新設 金属(伝導体)/非金属(非伝導体)(多くの場合は0または1のどちらか)
envMap 既存 物体への映り込みに使われるキューブテクスチャー

MeshStandardMaterialを利用するための基本的なコードの書き方を見てみましょう。図のような映り込みがあるような金属を例とします。

// いつものようにシーン、カメラなどを用意していきます。
var width  = window.innerWidth,
    height = window.innerHeight;

var scene = new THREE.Scene();

var camera = new THREE.PerspectiveCamera( 40, width / height, 0.1, 100 );
camera.position.set( 0, 0, 3 );

// 通常、私達が使う画面では、色がsRGBに補正されています。
// 物理ベースレンダリングでは正確な色の計算を行うために
// リニア色空間での処理する必要があります。
// そのため、テクスチャーなど外からはいってくるRGBに対して
// 色の調整が必要になります。
// rendererの`gammaInput`、`gammaOutput`を
// `true`にすることでthree.jsが自動で色空間の調整を行ってくれます。
var renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( width, height );
renderer.setClearColor( 0xcccccc );
renderer.gammaInput = true;
renderer.gammaOutput = true;
document.body.appendChild( renderer.domElement );

scene.add( new THREE.HemisphereLight( 0x443333, 0x222233, 4 ) );

// 物理ベースレンダリングでは
// すべての物体は「光を反射」をします。
// 反射の材料となるキューブマップを用意しておきます。
var cubeTextureLoader = new THREE.CubeTextureLoader()
var textureCube = cubeTextureLoader.load( [
  './LancellottiChapel/posx.jpg',
  './LancellottiChapel/negx.jpg',
  './LancellottiChapel/posy.jpg',
  './LancellottiChapel/negy.jpg',
  './LancellottiChapel/posz.jpg',
  './LancellottiChapel/negz.jpg'
] );

// - アルベド(`color`)
// - ラフネス
// - メタリック
// - 環境マップ
// をオプションとして、マテリアルのインスタンスを用意します。
var material = new THREE.MeshStandardMaterial( {

    color: 0xFFC355,
    roughness: 0,
    metalness: 1,
    envMap: textureCube

} );

// これまでのthree.jsと同じように
// meshにmaterialを適用し、
// シーンに追加します
var sphere = new THREE.Mesh(
  new THREE.SphereGeometry( 0.4, 16, 16 ),
  material
);
scene.add( sphere );

// レンダリングします
( function renderLoop () {

  requestAnimationFrame( renderLoop );
  renderer.render( scene, camera );

} )();

どうでしょうか? three.jsをこれまで使ったことがあるのであれば、難しくはないでしょう。

この他にもMeshStandardMaterialには多数のプロパティがサポートされており、nomalMap(法線マップ)やaoMap(アンビエントオクルージョン)などを合わせて利用することによりさらに質感が向上します。

物理ベースレンダリングを利用する際には、現実世界での計測値が公開されているので参考になります。なお、roughnessはツールによって値の名称がsmoothnessやglossinessになることもあります。その場合、値の扱いも異なることがあるので、計測値の資料を参考にする際には適宜当てはめてみてください。

各マテリアルの計測値サンプル一例

Material アルベド
new THREE.Color( 0.560, 0.570, 0.580 )
new THREE.Color( 0.972, 0.960, 0.915 )
アルミ new THREE.Color( 0.913, 0.921, 0.925 )
new THREE.Color( 1.000, 0.766, 0.336 )
new THREE.Color( 0.955, 0.637, 0.538 )
クロミウム new THREE.Color( 0.550, 0.556, 0.554 )
ニッケル new THREE.Color( 0.660, 0.609, 0.526 )
チタン new THREE.Color( 0.542, 0.497, 0.449 )
コバルト new THREE.Color( 0.662, 0.655, 0.634 )
プラチナ new THREE.Color( 0.672, 0.637, 0.585 )

※測定値サンプルはPhysically Based Materials より

デモのマテリアルは、左から、鉄、銀、アルミ、金、銅、クロミウム、ニッケル、チタン、コバルト、プラチナです。

入力にテクスチャーを使う

MeshStandardMaterialroughnessmetalnessは、メッシュ全体に一様に適用される数値の代わりにグレースケールのテクスチャー画像による入力も受け付けています。その場合、roughnessMapmetalnessMapのプロパティを利用します。

左上:アルベド、右上:ラフネス、右下:メタルネス、左下:ノーマル

画像を一度ロードしてテクスチャーとしてキャッシュさせれば、「テクスチャーの色」に応じて、複雑なメッシュでも、部分ごとにさまざまな質感を適用することができます。これにより、「ざらざら」や「つるつる」が混ざり合った質感のオブジェクトであっても、1回のドローコールで表示することができます。

3Dモデリング時にテクスチャーを作り込む必要がありますが、パフォーマンス向上が必要な際には、ぜひ検討したいポイントとなるでしょう。

MeshStandardMaterialでサポートされているプロパティ

現時点でのMeshStandardMaterialでサポートされているプロパティをまとめると以下の通りです。

プロパティ名 値の型
color <hex>
roughness <float>
metalness <float>
opacity <float>
map new THREE.Texture( <Image> )
roughnessMap new THREE.Texture( <Image> )
metalnessMap new THREE.Texture( <Image> )
alphaMap new THREE.Texture( <Image> )
lightMap new THREE.Texture( <Image> )
lightMapIntensity <float>
aoMap new THREE.Texture( <Image> )
aoMapIntensity <float>
emissive <hex>
emissiveIntensity <float>
emissiveMap new THREE.Texture( <Image> )
bumpMap new THREE.Texture( <Image> )
bumpScale <float>
normalMap new THREE.Texture( <Image> )
normalScale <Vector2>
displacementMap new THREE.Texture( <Image> )
displacementScale <float>
displacementBias <float>
envMap new THREE.CubeTexture( [posx, negx, posy, negy, posz, negz] )
envMapIntensity <float>
refractionRatio <float>
shading THREE.SmoothShading
blending THREE.NormalBlending
depthTest <boolean>
depthWrite <boolean>
wireframe <boolean>
wireframeLinewidth <float>
vertexColors THREE.NoColors / THREE.VertexColors / THREE.FaceColors
skinning <boolean>
morphTargets <boolean>
morphNormals <boolean>
fog <boolean>
side THREE.FrontSide / THREE.BackSide / THREE.DoubleSide
transparent <boolean>
alphaTest <float>
visible <boolean>

まとめ

three.js r74から登場したMeshStandardMaterialによりマテリアルの選択肢が増え、よりリアルな表現を手軽に利用することができるようになりました。

とはいえ、MeshStandardMaterialにはフレネルやクリアコートなど、本格的な物理ベースレンダリングには少し足りない機能があります。

床に注目すると、視点が垂直に近いほど反射されているのがわかる。

車の表面はコーティングの層と塗装の層の性質の異なるレイヤーがあるため、2つの反射が組み合わさっている。

しかし、今後、より本格的な物理ベースレンダリングが利用できるMeshPhysicalMaterialが登場する可能性もあります。そのときに備える意味でも、MeshStandardMaterialに触れ、物理ベースレンダリングの基本を体感してみてはいかがでしょうか。