🧱

Nomad Sculpt から出力した glTF を Three.js で表示するまでのサンプルコード

2024/08/15に公開

個人的備忘録です。表題についてのまとめになります。

成果物

Nomad Sculpt で作成した 3D モデルを Three.js を使って、Web ページ上で表示したときのサンプルページです。
> Nomad Sculpt x Three.js sample page

Nomad Sculpt x Three.js sample page 画面キャプチャ
Nomad Sculpt で作成した 3D モデルを Three.js を使って表示してみた

リポジトリは下記です。
> t-tonyo-maru/pub_web_nomad-sculpt-model_for-threejs

リポジトリには多くのファイルが格納されていますが、重要なファイルは src/main.ts のみです。


ちなみに、今回作成した 3D モデルですが、Jane artwork 氏の動画を参考にして作成してみました。とても良き動画です!
他にも多くの Nomad Sculpt の解説動画を投稿されているようですので、Nomad Sculpt デビューした人は、一度見てみてはいかがでしょうか?

https://youtu.be/qQl_mAO4gAU?si=1zp7xJKX14gCSDiI

アプリ・ライブラリのバージョン情報

アプリ・ライブラリ バージョン
Nomad Sculpt 1.90
--- ---
Node.js >=20
typescript ^5.2.2
vite ^5.2.0
@types/three ^0.167.1
three ^0.167.1
lil-gui ^0.19.2

Nomad Sculpt とは

iOS/Android で動作するスカルプティングアプリです。
ZBrush のスマホ/タブレットアプリ版みたいなモノですね。思った以上にサクサクと動作してくれます。

> Nomad Sculpt 公式サイト
> Nomad Sculpt | App Store
> Nomad Sculpt | Google Play

Nomad Sculpt で glTF ファイルを出力する方法

Nomad Sculpt で作成した 3D モデルを、Web 上で表示するには glTF 形式で出力する必要があります。
glTF ファイルの出力手順はとても簡単で、下記のとおりです!

  1. Nomad Sculpt の画面左上にあるフォルダマークを押下する
  2. 展開されるメニューを下へスクロールして、「書き出す」の項目まで移動する
  3. 「書き出す」直下の glTF にチェックを入れる
  4. 「glTF 2.0で書き出す」ボタンを押下する

Nomad Sculpt で glTF 形式で書き出す方法
キャプチャの赤枠内の「glTF 2.0で書き出す」ボタンを押下すれば glTF 形式で書き出せます

glTF ファイルを Three.js で表示するサンプルコード

試行錯誤の後が残っていますが、、見なかった事にしてください。。

src/main.ts
import './reset.css'
import './index.css'
import * as THREE from 'three'
import { GLTFLoader, type GLTF } from 'three/addons/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GUI } from 'lil-gui'

const VITE_GITHUB_PAGES_PATH = import.meta.env.BASE_URL || '/'

const WIDTH = 800
const HEIGHT = 600

// レンダラー
const renderer = new THREE.WebGLRenderer({
  antialias: true
})
renderer.setSize(WIDTH, HEIGHT)
renderer.shadowMap.enabled = true

// シーン
const scene = new THREE.Scene()
// カメラ
const camera = new THREE.PerspectiveCamera(45, WIDTH / HEIGHT, 1, 10000)
camera.position.set(10, 10, 10)
camera.lookAt(scene.position)

// 地面
const planeGeometry = new THREE.PlaneGeometry(10, 10)
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x213573 })
const plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.rotation.x = -Math.PI / 2
plane.receiveShadow = true
scene.add(plane)

// 平行光源
const directionalLight = new THREE.DirectionalLight(0xffffff)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)

// 環境光源
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0)
scene.add(ambientLight)

// スポットライト
const spotLight = new THREE.SpotLight(0xffffff, 24, 12, Math.PI / 4, 10, 0.5)
spotLight.position.set(0, 8, 0)
spotLight.castShadow = true
spotLight.shadow.mapSize.set(4096, 4096)
scene.add(spotLight)
const spotLightHepler = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHepler)

// 金属テクスチャ
const textureloader = new THREE.TextureLoader()
const metalTexture = textureloader.load(
  `${VITE_GITHUB_PAGES_PATH}/assets/models/metal_texture.jpg`
)

// gltf オブジェクト
let gltfObject: GLTF
let gltfObjectHelper: THREE.BoxHelper

// lil-gui
const gui = new GUI()
// …略…
gui
  .add({ gltfObjectAddMaterial: false }, 'gltfObjectAddMaterial')
  .onChange((value: boolean) => {
    if (!gltfObject) return

    gltfObject.scene.traverse((child) => { // …(5)
      if (child.name === 'Torus' && child instanceof THREE.Mesh) { // …(6)
        if (value) {
          child.material.map = metalTexture
          child.material.metalness = 0.75
          child.material.roughness = 0.1
        } else {
          child.material.map = null
          child.material.metalness = 0
          child.material.roughness = 1
        }
      }
    })
  })
// …略…

// OrbitControls
const orbitController = new OrbitControls(camera, renderer.domElement)
orbitController.maxPolarAngle = Math.PI * 0.5
orbitController.minDistance = 0.1
orbitController.maxDistance = 10000
orbitController.autoRotateSpeed = 1.0

// gltf ファイルの読み込み
const gltfLoader = new GLTFLoader() // …(1)
gltfLoader.load(`${VITE_GITHUB_PAGES_PATH}/assets/models/model.glb`, (data) => { // …(2)
  gltfObject = data

  // (3)
  // scene.children.map() ではなく scene.traverse() とするのが正しい様子 …(4)
  gltfObject.scene.traverse((child) => {
    // child.receiveShadow = true; // 不要っぽい
    child.castShadow = true
  })

  gltfObject.scene.scale.set(1, 1, 1)
  gltfObject.scene.position.set(0, 2.5, 0)
  scene.add(gltfObject.scene)

  gltfObjectHelper = new THREE.BoxHelper(gltfObject.scene, 0xffff00)
  scene.add(gltfObjectHelper)
})

const wrapper = document.querySelector<HTMLDivElement>('#app')!
wrapper.appendChild(renderer.domElement)

// 画面に表示+アニメーション
const ticker = () => {
  requestAnimationFrame(ticker)

  // gltfObject の回転
  const time = Date.now() * 0.001
  if (gltfObject) {
    gltfObject.scene.position.set(Math.cos(time) * 2, 2.5, Math.sin(time) * 2)
    gltfObject.scene.rotation.y += 0.01
  }

  // helper の更新
  spotLightHepler.update()
  if (gltfObjectHelper) gltfObjectHelper.update()

  orbitController.update()
  renderer.render(scene, camera)
}
ticker()

いくつかポイントをピックアップして解説します。
まず GLTFLoader を使って、glTF 用の loaderを用意します。(1)
loader が用意できたら、さっそく load() 関数を使って、glTF ファイルを読み込んでいきます。
第一引数には glTF ファイルへのパスを。第二引数には読み込み後に発火させるコールバック関数を指定します。(2)
コールバック関数では読み込んだ glTF オブジェクトに対して、諸々の設定をしています。
child.castShadow = true で glTF オブジェクトが影を落とせるよう設定したり…、
new THREE.BoxHelper() で glTF オブジェクトにヘルパーを追加したり…をやっています。

ちなみに、glTF オブジェクトのシーン内の各要素に対して、何らかの処理を行いたい場合は、scene.children.map() ではなく scene.traverse() とするのが正…らしいです。
(理由を調べてもイマイチよく分からずでした。。また機会があったら調べてみようと思います。)

また、lil-gui を使ってコントロールパネルから glTF オブジェクトのテクスチャを更新できるようにしています。(5)
「どの要素のテクスチャを更新するか」は、glTF オブジェクトの要素内の名称から判定しています。(6)
今回は Torus を対象にしています。指輪のリングに相当する部分ですね。

Nomad Sculpt のモデルのオブジェクト名は「シーン」から確認できます。
オブジェクト名を変更するには、「シーン」直下にあるモデル名の右端にある横三点リーダを押下して、「名前」を選択します。

Nomad Sculpt におけるシーン内のオブジェクトの名称
Nomad Sculpt のモデルのオブジェクト名は、「シーン」から変更できます



これら以外の点は、いつもの Three.js のコードと変わりありません。

まとめ

以上、「Nomad Sculpt から出力した glTF を Three.js で表示するまでのサンプルコード」でした。
やってみようと思ったときは、いろんなところでつまづくだろうな…と思っていましたが、案外簡単に表示まで行けてしまいました。
ドキュメントサイトと先人の情報サイトに感謝ですね!

では、また!

Discussion