Three.jsのパフォーマンスTips
2018/03/14

はじめに


こんにちは。お弁当盛りつけ係のあんどうです。


先月のことですが、ウェブ/ゲーム開発者のJack RugileさんのTwitterでの「Three.jsに関するパフォーマンスや効率化のためのTipsや工夫をみんなでまとめよう!」という呼びかけに対して、なんとThree.jsの作者であるmr.doobさんが「すぐに思いつく10のこと」を直接回答してくれていました。

せっかくなので簡単に説明しながら紹介したいと思います。

その1



「できるだけGeometryMaterialを再利用する」

同じ形状やテクスチャのメッシュが複数ある場合に、それぞれのメッシュ用にGeometryMaterialをインスタンス化してしまうとその分メモリも食いますし、GLとのデータのやり取りや状態の変更が無駄に発生します。特にマテリアルについては

new THREE.Mesh(
  geom,
  new THREE.MeshPhoneMaterial({color:0xff0000})
);

みたいな感じで、ついメッシュ作成時に合わせて作成してしまうこともあるかと思いますが、パフォーマンスが気になるようならそういったことは避けましょう。

その2



「できるだけGeometryではなくBufferGeometryを使用する」

Three.jsでパフォーマンスを気にする場合の鉄則です。BufferGeometryを使用することでプロパティへのアクセスが若干面倒になりますが、そのかわりに内部的なGLとのデータのやり取りを大幅に減らすことができます。

その3



「できるだけMeshBasicMaterial(ライティングを考慮しないシェーディング)とMeshLambertMaterial(頂点ごとのシェーディング)を使用する」

テクスチャで雰囲気を出せてライトの影響を考える必要がなければMeshBasicMaterialを使いましょう。ライトの影響を考える必要があればMeshLambertMaterialが一番軽量です。マットな質感で問題なければMeshLambertMaterialを使いましょう。

その4



「シーン内のライトは少ないほどいい」

CGとは要するにライティングの計算です。少ないライトで済むならもちろん少なく済ませましょう。

その5



「できるだけシーンのライトをテクスチャに焼き込んでライトマップとして使用する」

その4と関係して、ではどうやってライトを減らすかという話ですが、可能なら事前に影を計算してテクスチャにし、そのテクスチャをマテリアルのライトマップに設定しましょう。

let lm = new THREE.TextureLoader().load('lm.png');
new THREE.MeshBasicMaterial({lightMap:lm});

その6



camera.nearcamera.farはできるだけシーンぎりぎりになるように維持する。1単位が1メートルであることも覚えておく」

カメラには描画対象の領域を設定できます。描画対象領域が広くなるということは、その分レンダリングの負荷が増えるということです。近平面と遠平面はデフォルトのままにせず、シーン内部に含まれるオブジェクトを考慮した値を設定しましょう。

その7



「できるだけ小さい2のべき乗になるようにテクスチャをリサイズする」

小さな領域に設定するテクスチャは小さくしましょう。不必要に巨大なテクスチャはファイルダウンロード時間の面でもメモリの使用量の面でもGPUへの転送という面でも、全方位的に無駄です。またThree.jsでは任意のサイズの画像をテクスチャとして使用できますが、2のべき乗がもっともメモリ効率がよく利用にあたっての制限もないので、面倒臭がらずに事前にそのようにリサイズしておきましょう。

その8



「透明度を設定する場合はできるだけmaterial.alphaTest = 0.5を使用する」

alphaTestを設定すると、不透明度の値がそれ以下の場合は描画されなくなります。デフォルト値は0ですが、この値を0.5にすることで不透明度の低い要素が描画対象から外されます。0.5の理由はよく分かりません。1/2だから?株式会社カブクではこの疑問に答えられるメンバーを募集しています。

その9



light.shadow.mapSizeを増やす代わりに、scene.add(new THREE.CameraHelper(light.shadow.camera))を使用してシャドウマップを小さくする」

影の計算は負荷の大きな処理です。THREE.CameraHelperを使用するとカメラの視錐台を画面に表示できますが、ここにlight.shadow.cameraを渡すことで影を計算する領域を表示できます。うまく利用して計算対象の領域をできる限り小さくしましょう。

その10



「できるだけantialias: trueの代わりにFXAAShaderを使用する」

WebGLRendererには描画をアンチエイリアシングするためのantialiasプロパティがありますが、それよりもFXAAShaderを使用したほうが負荷が少ないようです。FXAAShaderの使用例は以下にあります。


まとめ


ということで、最後にもう一度10個のTipsをまとめておきます。

  1. できるだけGeometryMaterialを再利用する
  2. できるだけGeometryではなくBufferGeometryを使用する
  3. できるだけMeshBasicMaterial(ライティングを考慮しないシェーディング)とMeshLambertMaterial(頂点ごとのシェーディング)を使用する
  4. シーン内のライトは少ないほどいい
  5. できるだけシーンのライトをテクスチャに焼き込んでライトマップとして使用する
  6. camera.nearcamera.farはできるだけシーンぎりぎりになるように維持する。1単位が1メートルであることも覚えておく
  7. できるだけ小さい2の累乗になるようにテクスチャをリサイズする
  8. 透明度を設定する場合はできるだけmaterial.alphaTest = 0.5を使用する
  9. light.shadow.mapSizeを増やす代わりに、scene.add(new THREE.CameraHelper(light.shadow.camera))を使用してシャドウマップを小さくする
  10. できるだけantialias: trueの代わりにFXAAShaderを使用する

WebGLガチ勢にはThree.jsは冗長な処理が多くてパフォーマンスが悪いと思われがちで、正直そこは完全に否定できるものでもありません。しかしThree.jsもその特性を理解して使用することで、多くの場合は必要十分なパフォーマンスを実現できるものと考えています。

株式会社カブクではThree.jsアプリのパフォーマンスを改善してくれるメンバーとWebGLガチ勢を募集中です。