🚘

【Blender x Three.js】車の3DモデルViewer実装で試したこと

に公開

Webで車の3Dモデルをプレビューするページを実装する中で試したことをまとめていきます。

制作したもの

まだ演出やその他機能は制作中ですが、制作しているページは下記になります。

https://projects-car-show.vercel.app/?a4177b5cf72658

【実装内容】

  • Blenderで制作したモデルを Three.js で表示
  • 車・ワールドの色合いのカスタマイズ
  • カメラワーク、モデルの反射設定、その他環境設定、などをUIで操作

車以外のモデルは Blender で自作して、車のモデルはこちらからダウンロードして活用、それらを Three.js で読み込んで、UIで操作できるようにしています。

見栄え調整をしたこと

  1. 環境テクスチャ設定
  2. 鏡面反射の効果
  3. 光の拡散効果(Bloom)の適用

① 環境テクスチャ設定

環境テクスチャとは、3D空間全体を取り囲むように配置される画像やマップで、シーンに現実的なライティングや反射効果を与えるために使われます。

たとえば、スタジオ空間の環境テクスチャを使えば、実際にその空間にいるようなリアルな光の当たり方や映り込みを再現できます。

実際に比較してみたのでプレビューページで比べてみます。


スポットライトを1つだけ当てた見え方


環境テクスチャ実装をした見え方

現実なら周りの風景が映り込むので、環境テクスチャを付けることでそのリアルさを表現できます。また、反射させる環境テクスチャによって、見え方もかなり異なります。


青白い色合い


スタジオ部屋


夕焼けの色合い

車体色は全てシルバーですが、環境テクスチャの選択によって見え方はガラッと変わります。

ちなみに、Blender で 環境テクスチャファイルの HDRI は自作できるようです。Blender で実装したワールドを環境テクスチャとして貼れば、よりリアリティが出せるはず。

https://note.com/info_/n/nce33546f57ab

② 鏡面反射の効果

Blender だと、例えば「粗さ: 0」のマテリアルを設定すると周りの光を反射させてくれる仕組み(レイトレーシング)があるのですが、Three.js の通常環境のレンダリングだとその計算はしてくれません。


Blenderで反射させている様子

Three.js で鏡面反射の表現をするためには、2つほど選択肢がありました。

【選択肢①】Reflectorによる平面反射

平面ミラー反射を実装するための Three.js ヘルパークラスで、床や壁など 平らな面に反射を出したいとき主に使います。

https://sbcode.net/threejs/reflector/

反射面専用の仮想カメラを使って、反転した視点からの再描画結果をテクスチャとして貼る、という仕組み。

今回採用したのはこれで、Blender で作った円形のテーブルのサイズを計測して、 同じサイズのTHREE.CircleGeometry を作って Reflector として使っています。

export const cloneReflectorCircle = (
  name: string,
  model: GLTF,
  loadedAssets?: LoadedAssets,
): THREE.Group | null => {
  const originalMesh = model.scene.getObjectByName(name)

  if (originalMesh && originalMesh instanceof THREE.Mesh && loadedAssets) {

    // メッシュの半径を取得
    const box = new THREE.Box3().setFromObject(originalMesh)
    const size = new THREE.Vector3()
    box.getSize(size)
    const diameter = size.x
    const radius = diameter / 2

    // 円形のジオメトリを作成
    const geometry = new THREE.CircleGeometry(radius, 100)

    // 新しいメッシュとして作成
    const reflectorMesh = getReflector(geometry)

    // テクスチャを重ねる半透明 Plane
    const overlayMaterial = getReflectorOverlayMesh(loadedAssets)
    const overlayMesh = new THREE.Mesh(geometry, overlayMaterial)
    overlayMesh.position.z = 0.01

    // グループメッシュ
    const mesh = new THREE.Group()
    mesh.add(reflectorMesh, overlayMesh)

    // 位置・回転・スケールの調整
    mesh.position.copy(originalMesh.position)
    mesh.rotation.copy(originalMesh.rotation)
    mesh.scale.copy(originalMesh.scale)

    // オリジナルメッシュを非表示にする
    originalMesh.visible = false

    return mesh
  }

  return null
}

これで下記のような円形の平面メッシュに、鏡面反射効果をつけられます。

【選択肢②】CubeCamera + WebGLCubeRenderTarget によるキュービック反射

Reflector だと平面限定でしたが、湾曲したオブジェクトにも周囲の環境を写し込む鏡面反射を表現したいときに使えるのが、この手法。

  1. CubeCamera はシーンの中心点から 6方向(±X, ±Y, ±Z)にカメラを向けて描画 し、立方体テクスチャを生成
  2. それを 環境マップ(envMap)として反射マテリアルに適用
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {
  format: THREE.RGBAFormat,
  generateMipmaps: true,
  minFilter: THREE.LinearMipmapLinearFilter
});

const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
scene.add(cubeCamera);

const reflectiveMaterial = new THREE.MeshStandardMaterial({
  envMap: cubeRenderTarget.texture,
  metalness: 1,
  roughness: 0
});

const sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 32), reflectiveMaterial);
scene.add(sphere);

上記はサンプルのソースコードですが、この処理をマイフレーム実行するのは現実的じゃありません。通常の描画に+6回の描画が必要になるので、普通に重たくなります。

ハイポリモデルを扱う以上は、鏡面反射は平面を1つだけにしておくのがパフォーマンスに優しいと思います。

③ 光の拡散効果(Bloom)の適用

最後に光の拡散効果。Blender だと下記みたいに「放射」プロパティを設定すると表現できますが、Three.jsだとマテリアルの設定だけでは再現できません。

Three.js だと、ポストプロセスの「Unreal Bloom」を実装すると実現できます。

/**
 * ポストプロセッシング
 */
const composer = new EffectComposer(renderer)

composer.addPass(new RenderPass(scene, camera))

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight), // サイズ
  setupMember.postprocess.bloomPass.strength, // 強さ
  setupMember.postprocess.bloomPass.radius, // ブルームの半径
  setupMember.postprocess.bloomPass.threshold, // ブルームの強さ
)

composer.addPass(bloomPass)

// アニメーション内で composer をレンダリング
function animate() {
  composer.render()
}
renderer.setAnimationLoop(animate)

ちなみに、composer.addPass の流れは Blender でいうと「Compositing」タブのノードの要領で繋いでいて、重ねて他の効果もかけられます。

これで下記みたいな光の拡散効果を表現できます。

また、Bloomの光の色と強さはThree.js側でも操作できます。Blender で「放射」を設定しておくと、Three.js 側では mesh.material.emissiveIntensitymesh.material.emissive でアクセスできるようになるので、動的に色や強さも変更できます。

// model: GLTF
model.scene.traverse((child) => {
  if (child instanceof THREE.Mesh) {
    /**
     * マテリアル設定
     */
    if (child.material.name === 'Bloom_Common') {
      child.material.emissive = new THREE.Color(50, 50, 50) // カラー設定
      child.material.emissiveIntensity = 6 // 強さ設定
    }
  }
})

Blenderの見え方とThree.jsの見え方の比較

最後に、Blender側で自作したステージとThree.jsで実装したステージの見え方を比較してみます。


Blenderレンダリング画像


Three.js実装

BlenderのCyclesエンジンだと、壁や地面の反射もかなりリアルに描けますが、Three.jsだと鏡面反射の数の限界や、マテリアルの反射の仕方でやっぱり違いは出てしまいます。

とは言っても、マテリアルの質感の調整など色々試せばもう少しリアリティは出せるなとは感じていて、まだまだ検証中です。

Blender側での演出をどんなアプローチでThree.js側で実現するかが難しいところだな...と改めて思いました。

まとめ

Blender のレンダリングが強力で、Three.js で全て再現するのは難しく、どこまで近づけるか考える必要がありました。

Blenderで見栄えを考えていくなら、Three.js で何をどこまで表現できるか先に調査が必要だなと思いました。

また、鏡面反射等重たい処理を入れると、その分パフォーマンスも気にしないといけないので、解像度を調整したりしたのも追加で学びになりました。

パフォーマンス周りについては、別記事で下記にまとめているので良かったら見てみてください。

https://zenn.dev/fabrica/articles/91add47f1f72e0

Fabrica.テックブログ

Discussion