Blenderで作成した3Dモデルを、Three.jsで表示した際の「色味が違う…」の解決方法

Blenderで作成した3Dモデルを、Three.jsで表示した際の「色味が違う…」の解決方法

ぼこ

ぼこ

こんにちは。フロントエンドエンジニアになりたてのぼこです。

最近プライベートでBlenderを始めまして、作った3DモデルをWeb上で扱う練習をしたりしています。

ブラウザ上で3Dモデルを表示するためには、まず作成した3Dモデルのデータをgltf(glb)という形式で書き出し、それをWebGL(Three.js)で読み込んでcanvasに表示することになると思いますが、やってみると「あれ、Blenderで作ったときとなんか色味が違う……」っていう現象に陥ることが少なくないように思います。

せっかくBlenderでいい感じに調整したのに、いざThree.jsに持ってきたときに表示が変だと悲しいですよね。

今回はその問題を解決するための備忘録的な記事を書こうと思います。

使用ツールのバージョン
Blender: 2.93.0
Three.js: r135

とりあえず結論

Three.jsに持ってきたときの色味の違いは、THREE.WebGLRendererの設定が関係していることが多い気がします。以下に、とりあえず設定しておくと良さげなプロパティをまとめておきますので、お急ぎの方はこちらを試してみてください。
※あくまで僕の場合これで解決した、というものであり、この設定があらゆるケースにおける正解というわけではありませんのでご注意ください

// THREE.WebGLRenderer を作成する際に以下を追記
renderer.physicallyCorrectLights = true;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;

次のセクションから、実際にBlenderで簡単な3Dモデルを作成し、Three.jsで表示するまでの手順を書いていきます。(BlenderやThree.jsの基本的な使い方についての説明は割愛します、ご了承ください)

Blenderでの作業

シーンの作成

こんな感じのシーンを作成します。角を少し丸くしただけの立方体と床だけの超手抜きモデリングです。今回は色味の検証なので許されたい。

ここでの注意点は、シェーディング方法を「スムーズシェード」にしている点です。シェーディングというのは3Dオブジェクトの表面に陰影をつけることを指し、「フラットシェード」と「スムーズシェード」があります。

フラットシェード

ポリゴンをそのまま描画するので、カクカクして見える。Blenderのデフォルトはこっち。

スムーズシェード

疑似的に陰影を計算することで、なめらかに見える。Three.jsのデフォルトはこっち。

 

Blenderでスムーズシェードを適用するには、オブジェクトモードでオブジェクトを右クリックし、「スムーズシェード」を選択します。

 

// フラットシェードにする場合。読み込んだ3Dモデルのmeshに対して追記
mesh.material.flatShading = true;

もしフラットシェードのままモデルを表示したい場合、Three.js側で設定をする必要があるので注意です。

書き出し

作成したシーンを書き出します。今回はBlender側で調整したライトをそのまま使うため、立方体、床、ライトを同時に選択して書き出します。

ライトを一緒に書き出す場合、書き出しのオプションで「Punctualライト」にチェックを入れておく必要があるので注意です。

Three.jsでの作業

とりあえず読み込んで表示してみる

特に何も考えず、3Dモデルを読み込んで表示した結果がこちら。

いや、まじで色全然違う。

初めて3Dモデルを表示したときびっくりしませんでした? 僕はしました。

明るさの設定

とりあえず表示してみると、「めっちゃ暗い」か「めっちゃ明るい」のどっちかな気がします。僕の場合は床が白くなってしまっているので、一緒に書き出したライトが強すぎるようです。

でもBlenderで見たらいい感じだったのに何故? となりますが、下記の設定を追記します。こちらは物理的に正しい照明モードを使用する設定で、デフォルトではfalseになっています。

renderer.physicallyCorrectLights = true;

詳しい原理はよくわかっていませんが、これをtrueにするとBlenderで設定したライトの値をそのまま使うことができる印象があります。

 

照明モードを変更した結果こんな感じになりました。白飛びはなくなりましたが、逆に若干暗くなりましたね。

次に環境光を設置してみます。

const light = new THREE.AmbientLight(0xffffff, 1.0);
scene.add(light);

Blenderではデフォルトで環境が設置してあるので、Three.jsでも同じように置かないと暗くなってしまうと思います。Blender上での環境光は、右側の地球儀みたいなアイコンから確認できるので、一旦その通りに入れてみるのがいいかもしれません。

 

環境光を追加してこんな感じになりました。明るさは程よくなった気がしますが、色味が全然違うんですよね……。

レンダラーにさらに設定を追記

renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;

上記の設定を追記してみます。

レンダラーの outputEncodingtoneMapping の設定を変更しました。ちなみに他の設定もいろいろ試してみたんですが、割と似たような色味になる設定が他にもあり、この設定が適しているかどうかは正直わかりません。

outputEncoding に関しては似たような色空間の THREE.GammaEncoding が使われることも多いようです。色空間って難しい。 toneMapping のほうも、他にも色々な設定があり、それぞれ微妙に色味が変わってくるようです。とにかくデフォルト設定から変えてみることが大事な気がします。

 

上記2つの設定を追加して、こんな感じになりました。これでだいぶ色味が近づいたんじゃないでしょうか。自作したモデルを表示するだけでも結構気にしないといけないことが多いんですね。

余談ですが、上記の2つの設定はreact-three-fiber(以下r3f)という、ReactでThree.jsを扱うためのライブラリの実装を参考にしました。

ある日、r3fで自作モデルを表示してみたら、なんの設定もなくBlender通りの色味で表示できたので、気になって調べてみたところ、この設定に辿り着いたという経緯でした。

影について

色味はだいぶ近づけることができましたが、一つ決定的な違いがあります。そう、影の有無ですね。表面の陰影ではなく、物体が物体に影を落とす表現です。

 

renderer.shadowMap.enabled = true;

影を落とす処理は重くなりがちなので、Three.jsではデフォルトでoffになっています。なのでまずは、レンダラーの設定で影の計算を有効にします。

次に、シーン内のモデルとライトに対して、影を「落とす(castShadow)」、または「受ける(receiveShadow)」設定を有効にします。シーン内において、ライトは影を「落とす」側です。対してそれ以外の3Dオブジェクトは影を 「受ける」側、かつ他のオブジェクトに対して自身の影を「落とす」こともあります。

 

loader.load('読み込むgltfファイルへのパス', (data) => {
    const model = data.scene;
    model.traverse((obj) => {
      if (obj.type === 'Mesh') {
        obj.receiveShadow = true;
        obj.castShadow = true;
      } else if (obj.type === 'DirectionalLight') {
        obj.castShadow = true;
      }
    });

    stage.scene.add(gltf);
});

上記の影の設定をします。

このように、モデルを読み込む際にtraverseメソッドを使ってgltfに含まれる全ての3Dオブジェクトに対して処理を書くことができます。forEachみたいな感じです。

2022/07/04 追記

上記のコードでは、オブジェクトタイプがMeshであるもの全てに receiveShadow および castShadow の設定をしていますが、床となるメッシュに関しては、castShadow の設定は不要です(床はどこにも影を落とさないので)。不要どころか、余計な計算が発生して描画が荒くなってしまう原因になり得るので、床の castShadow はオフにします。

 

model.traverse((obj) => {
  if (obj.type === 'Mesh') {
    obj.receiveShadow = true;

    if (obj.name !== 'Floor') {
      obj.castShadow = true;
    }
  } else if (obj.type === 'DirectionalLight') {
    obj.castShadow = true;
  }
});

このように、床以外の場合のみ、castShadow をtrueにします。(「Floor」という名前はBlender側で決めた名前になります)

 

これで、オブジェクトに対して影を落とすことができます。ただ、そのままだと影の範囲が小さい(もしくは影がない)、影が荒いことがあります。

 

// 先ほどのtraverseメソッド内で、DirectionalLightに対して設定を追記する
- 省略 -
else if (obj.type === 'DirectionalLight') {
	const shadowSize = 20;
    obj.shadow.camera.right = shadowSize;
    obj.shadow.camera.left = -shadowSize;
    obj.shadow.camera.top = -shadowSize;
    obj.shadow.camera.bottom = shadowSize;
}

影が小さい、もしくは見えない場合、影を落とす範囲を広げることで解決する場合があります。

 

// 先ほどのtraverseメソッド内で、DirectionalLightに対して設定する
- 省略 -
else if (obj.type === 'DirectionalLight') {
	obj.shadow.mapSize.set(2048, 2048);
}

影が荒い場合、影の解像度を大きくすることで改善することがありますが、パフォーマンスとトレードオフなので無闇に上げすぎるのは良くなさそうです。値は2の冪乗で指定します。

 

影がそれなりに綺麗に描画できました(影が有効なエリアが若干暗くなると思いますので、THREE.AmbientLightを調整して明るくしています)。

そうです、「それなりに」なんですよね。
このようにThree.jsで動的に影をつける場合、どうしても近くで見るとあまり綺麗じゃなかったり、負荷の関係で妥協せざるを得なかったりします。

例えばこれに対して、あらかじめオブジェクトのテクスチャに影を焼き込むテクスチャベイクと呼ばれる方法があります。Blender上で計算された影をテクスチャにするので、表示が綺麗だったり計算の負荷がかからないといったメリットがありますが、影の位置が固定になるので自由度が低いとも言えます。

おわりに

今回はBlenderで作成したモデルをThree.jsに読み込む際に、気にしておくと良さそうな設定をまとめてみました。個人的には、知らなくて結構苦戦した部分なので、少しでも同じような問題に悩む方の助けになれば幸いです。

LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。

Webサイト制作の実績・料金を見る

この記事のシェア数

新卒で入社しました。気持ちの良い動きやプログラムが生み出すグラフィックに興味があります。

このメンバーの記事をもっと読む