JavaScript
three.js
Blender

Blender から GLTF を出し、 Three.js で使うまでの覚書 (モデルメッシュ編)

Blenderは、GLTF(2.0)を出せます。

Three.jsは、GTLF(2.0)を、読めます。

以上。めでたしめでたし。

色々な情報が、ここで止まっています。

んで?その読み込んだブツは「使いものになるのかい?」

「3Dモデルを表示して終わり」なら上記でいいでしょうが、いわばインタラクティブなコンテンツというか、モデルを入力や状況によってああだこうだ動かしたい場合、上記情報を鵜呑みに「出来るんだぁ!」なんて思い込むと、痛い目を見ます。っていうか見ました。

Three.jsで「使うまで」ということで、Blednerでこうしたものは、Three.jsでこう行って、ここの設定に入る。だからこうすれば使える。逆に、こう入ってくるからそのままでは使えない。けど、こうすれば使える、というものを、自分なりにまとめました。

*この文では、「Blenderでのモデルの作り方」とか「Three.jsの使い方」とかは全くやりません。いじょ。


最初に結論


Blender側


  • 「属性」欄は全部チェック入れろ。入って困るような数値はほぼない。

  • 日本語版使ってる場合は、「アーマチュア」とか「平面」とかの全角文字も、そのまま出力される。怖いようなら英語版にしろ。まぁ大抵そのままで問題はない(少なくともThree.jsでは)

  • モディファイヤ使ってるようなら全部外した(メッシュに適用させた&ボーンなどの名前も確定させた)出力用の別ファイルを作って、そっちでやったほうが幸せ。

  • モデル(メッシュ)のみ出力する場合は、Export Selectino only は使っても良し。ただ、↓のことでチェック外し忘れとかで無駄なストレスを感じないために、外しておこう。

  • というか出力用ファイルは、出力のたびに作ったほうがいい。JS側で苦労するより出力を直したほうが楽な場合が多い。

  • アニメーション(アクション)は、1モデルにつき1つしか出力されない。1モデルに複数アニメをセットする方法はあるから諦めるな。

  • アニメーションを出す場合、GLTF出力設定の Export Selectino only は、忘れろ。チェック入ってたら今すぐ外せ。ロクなことにならん。

  • 非表示のファイルがそのまま出力されて容量が増える?当然だ。だから出力専用ファイルを作って、そっちで無駄なメッシュ&オブジェクトは消そう。


JS(three.js)側


  • 来るのは「シーン」であることを忘れるな。モデルやアニメだけ使いたい場合は、ちょっと一工夫必要になる。

  • アニメーションを組み合わせて使うような場合や、1モデルに複数アニメーションをさせたい場合は、がんばればなんとかなるから、頑張れ。

  • Blender側でマテリアルを分けたものは、Three.js側では「メッシュが分かれる」ことに留意すべし。アニメーションをさせたい場合は特に注意。

以下駄文の長文。


やりたかったこと

:複数モデルを用意。このモデルは、モデルデータのみ。アニメーションは含まない。

:アニメーションだけのファイルを読み込み、プログラム側で読み込んだモデルに対してアニメーションをセットする。

:別途、モデルには頂点モーフも用意した(表情とかね)。

:それをJSでThree.js使ってブラウザで使いたい。


Blender側での構成

まず当然ですが、BlenderのGLTFエクスポーターが必要になります。頑張ってセットしましょう。


  • 素体モデルの編集用ファイル


    • これには、モデル(頂点&マテリアル)のほか、ボーンデータ(もちろんウェイト)も入っている。



  • 素体を動かすアニメーションのファイル


    • 素体モデルをリンクとして読み込み、アニメーションだけさせたファイル。

    • アクションとして、1ファイル内に複数アクションを設定した。



  • 顔用モデル


    • これには、「頂点モーフ(シェイプ)」をセットした。




マテリアルについて

 ひとまず「GLTF出力用の、PBRマテリアルをやれば間違いはない」です。が、PBRマテリアルについては未検証(プログラム側で)なので、あんまり鵜呑みにしないでください。

以下、順番がいろいろ前後しますが、用途関係なしに汎用的に使えそうな情報から。


モデル情報の出され方

モデルの出力には、Blenderの出力で「Export GLTF」を選択します。GLTFとGLBがありますが、GLBで出すほうがいいと思います(軽いしファイルは1つにまとまってるし)

なお、下記は全て、Blenderでの設定を「こうした」場合です。

export_set.png


その1・最も単純なオブジェクト構成の場合(オブジェクト1つ、マテリアル1つ、ボーン&アーマチュアなし)

 GLTFをThree.jsで、GLTF Loaderを使って読んだ場合、まずは「Scene」の形式で入って来ます。そのため、モデルデータだけ欲しい場合は、こうなります。

const loader = new THREE.GLTFLoader();
loader.load('gltfObject.glb', (data) => {

const gltf = data;
const object = gltf.scene;
scene.add(object);
});

この場合ですが、上記の「 object 」には、 THREE.mesh の形式で入ってきます。そのため、scene にそのまま add すれば、なんの問題もなく表示してくれます。

ここまではいいですね。サンプル通りです。


その2・マテリアルを複数セットしたモデルを出力する場合

この場合は、上記のソースコードで読んだ場合、構成がこうなります

const object は、

THREE.Group
- Mesh (with マテリアル1)
- Mesh (with マテリアル2)

マテリアル毎にメッシュが分かれます。 THREE.Group 形式のまま THREE.Scene に突っ込んでも表示されるので、何もしない場合はこれでも問題ないように思えます。

ただ、マテリアルに対して何かしたい(シェーダー自力で書きたいマン)となった場合、「どれが自分がいじりたいマテリアル(が、入っているMeshか)」を、判別しなくてはなりません。

その場合は、親切なことに Mesh.material.name に、Blender側でマテリアルにセットした名前が入っているので、それを探すことになります。

こんなメソッドを用意するといいでしょう。


/// 指定したマテリアル名がセットされている[ mesh ] を返します
function getMatbyName(_o, _name){
let refO = null;

if(_o.material && _o.material.name.indexOf(_name) > -1 ){
return _o;
}

if(_o.materials){
for(let i =0; i < _o.materials.length; i++){
refO = getMatbyName(_o.materials[i], _name );
if(refO) {return refO;}
}
}

for(let i =0; i < _o.children.length; i++){
refO = getMatbyName(_o.children[i], _name );
if(refO) {return refO; }
}

return refO;
}

const targetMesh = getMatbyName(object, "mat1");


その3・頂点モーフ(シェイプ)を使う場合

頂点モーフ(シェイプ・・・以下モーフだけ)は、思いのほかすんなり出力してくれます・・いや嘘です。すんなりじゃないかも。

モーフは、 THree.Mesh の中に、morphTargetDictionary および morphTargetInfluences の配列の中に入ってきて、どちらかでもいいので、その配列の値を 0 ~ 1.0 で変化させれば、モーフィングしてくれます。

ただ、この Mesh の中、というのが厄介であり、上記のとおり、 Mesh がどこに入っているかは、マテリアルが複数かどうかで分岐&分かれているメッシュ全部に対してモーフ値をセットしないと狙った変化はしてくれないので、取り扱いがやや面倒です。

特に、名前で管理をしたいところですが、名前は morphTargetDictionary のキーとなっており、配列アクセスまでが一工夫です。


// Blender側で、 smile と名前を付けたシェイプキーを適用したい場合
// まず、[Group] から [mesh]を探す必要があります。
function getMeshByName(_o, _name ){
let refO = null;

if(_o.type.toLowerCase().indexOf("mesh") > -1 && _o.name.indexOf(_name) > -1 ){
return _o;
}

for(let i =0; i < _o.children.length; i++){
refO = getMeshByName(_o.children[i], _name );
if(refO) {break;}
}
return refO;
}

const faceObj = getMeshByName(object, "face");

// モーフキーの名前で列挙
const mopthKeys = Object.keys(faceObj.morphTargetDictionary);
let smileKeyIndex = 0;

for(let i =0; i < mopthKeys.length;i++){
if(mopthKeys[i] == "smile"){
smileKeyIndex = i;
break;
}
}

// モーフを適用
for(let i =0; i < object.childlen.length;i++){
if(object.childlen[i].morphTargetInfluences && object.childlen[i].morphTargetInfluences.length > 0){
object.childlen[i].morphTargetInfluences[smileKeyIndex] = 1.0;
}
}

誰だよ「すんなり」とか言った奴。

思いのほか長くなった&情報量が多くなったので、アニメーションは後編の アニメーション編(仮) で…