メインコンテンツまでスキップ

モデルの読み込み

シーンの構築ではすべてをプリミティブから作りました。実際のプロジェクトではモデルを読み込みます。このページはその周辺のワークフロー、つまりGLBをページに載せ、その中に実際に何が入っているかを知り、そして3Dツールを開くことなくそれを調整する方法を扱います。

このページ全体で使用するモデルはLionsharp Studios制作のPorsche 911 Carrera 4S(CC BY 4.0)で、Car Configuratorのサンプルで使われているのと同じアセットです。欠点も含めてごく普通のSketchfabのダウンロード品ですが、それこそが要点になります。

エクスポート時に気をつけること

glTFのどちらの形式でも動作します。.gltf(JSONで、テクスチャとジオメトリが別ファイル)と.glb(すべてが1つのバイナリファイル)です。Webでは.glbを推奨します。リクエストが1回で済み、相対パスが壊れることもありません。

エクスポート時に気にかける価値があるのは2つです。これらは後で使う語彙そのものになるためです。

  • ノード名。 <pc-node>はモデルの部分を名前で検索します。エクスポーターがObject_12を出力したなら、それを入力することになります。
  • マテリアル名。 マテリアルを差し替える際の指定手段であり、ノード名が意味を失っていても意味を保っていることが多くあります。

どちらも失敗しても致命的ではありません。後述のhierarchy()が実際に何が得られたかを教えてくれます。ただし、Blenderで名前を整える数分は、それ以上の時間を後で節約します。

読み込みとインスタンス化

読み込みには2つのタグが必要です。<pc-asset>がファイルを宣言し、<pc-model>がそれをシーンにインスタンス化します。

<pc-app>
<pc-asset id="car" type="container" src="assets/porsche-911-carrera-4s.glb"></pc-asset>
<pc-scene>
<pc-entity name="camera" position="3.4 1 3.8" rotation="-10 42 0">
<pc-camera clear-color="#dfe4ea"></pc-camera>
</pc-entity>
<pc-entity name="light" rotation="52 30 0">
<pc-light type="directional" cast-shadows></pc-light>
</pc-entity>
<pc-model asset="car"></pc-model>
</pc-scene>
</pc-app>

斜め前方から見たシルバーのPorsche 911

type="container"は重要です。GLBはコンテナアセットであり、メッシュ・マテリアル・テクスチャ・スキン・アニメーションをまとめて保持します。<pc-model>はそのコンテナから階層をインスタンス化します。別の型のアセットを指定すると、丁寧なメッセージではなくインスタンス化の時点で失敗するため、正しく指定する価値があります。それ以外の点で<pc-model><pc-entity>と同じように振る舞うため、positionrotationscaleを取り、別のエンティティの内側にネストすることもできます。

モデルはコンテンツがシーンに入った時点でready状態になり、loadを発生させます。読み込みが失敗した場合もcontentEntitynullのままreadyは確定し、errorイベントを発生させます。ファイルが届かない可能性がある場合はこちらをリッスンしてください。

document.querySelector('pc-model').addEventListener('error', (event) => {
console.warn(`the model did not load: ${event.message}`);
});
モデルの原点はアーティストが残したままです

モデルのピボットやスケールを正規化する仕組みはありません。この車の原点はボディの中央にあるため、ホイールはy=0よりにあり、原点に置いた地面プレーンをすり抜けて沈みます。<pc-model>positionを設定して持ち上げるか、地面を動かしてください。ただしこれは規約を前提にできるものではなく、アセットごとに対応することになります。

圧縮されたメッシュ

アセットサイトのモデルはほとんどがDraco圧縮されており、このモデルもそうです。DracoにはWebAssemblyデコーダーが必要で、<pc-app>の子として<pc-wasm>で宣言します。

<pc-wasm name="DracoDecoderModule"
glue="modules/draco/draco.wasm.js"
wasm="modules/draco/draco.wasm.wasm"
fallback="modules/draco/draco.js"></pc-wasm>

この3つのファイルはDracoデコーダーのビルド — glueスクリプト、.wasmバイナリ、そしてWebAssemblyのないブラウザ向けの純JavaScriptフォールバック — です。エンジンのnpmパッケージには含まれていないため、自分で配信します。Dracoのリリースから取得するか、Web Componentsのサンプルに同梱されている一式をコピーし、置いた場所を属性で指定してください。

<pc-app>は配下に宣言されたすべてのモジュールを待ってから起動するため、シーンが動き出す時点でデコーダーは配置済みです。これがないとDraco圧縮モデルの読み込みは失敗し、コンソールにその旨が出力されます。

圧縮テクスチャのトランスコードに使うBasisも同じ仕組みで供給します。KHR_texture_basisuを使うモデルにはこれが必要です。

読み込んだ内容を確認する

モデルファイルには厄介な真実があります。3Dツール上での名前が、エンジンに届く名前とは異なることが頻繁にあるのです。エクスポーターは名前を変更し、さらにエンジンのパーサーが階層を構築する際に、名前のないノードには名前を合成し、同名の兄弟には接尾辞を付けて区別します。

ですから推測しないでください。<pc-model>には実際に存在するとおりのツリーを報告するhierarchy()メソッドがあり、出力は1行で済みます。

import { whenReady } from '@playcanvas/web-components';

const model = await whenReady('pc-model');
console.log(String(model.hierarchy()));
Sketchfab_model
└─ Root
├─ window_rear
│ └─ window_rear_0 (render) {window}
├─ windshield
│ ├─ windshield_0 (render) {window}
│ └─ windshield_1 (render) {plastic}
├─ Plane.002
│ └─ Plane.002_0 (render) {paint}
├─ boot
│ └─ boot_0 (render) {full_black}
├─ underbody
│ └─ underbody_0 (render) {full_black}
├─ Cylinder.000
│ ├─ Cylinder.000_0 (render) {silver}
│ ├─ Cylinder.000_1 (render) {plastic}
│ ├─ Cylinder.000_2 (render) {rubber}
│ └─ Cylinder.000_3 (render) {Material.001}
├─ Plane
│ └─ Plane_0 (render) {Material}

├─ bumper_front.004
│ ├─ bumper_front.004_0 (render) {silver}
│ ├─ bumper_front.004_1 (render) {lights}
│ └─ bumper_front.004_2 (render) {plastic}

├─ boot.001
│ └─ boot.001_0 (render) {paint}

└─ boot.011
├─ boot.011_0 (render) {coat}
└─ boot.011_01 (render) {coat}

の箇所は省略しています(Rootは実際には32個の子を持ちます)が、それ以外はそのままの出力であり、以下のレシピで使うノードはすべてこの中に含まれています。

各行が1つのノードです。名前、括弧内の(render)やその他のコンポーネント、そして波括弧内にrenderコンポーネントのマテリアルが並びます。上の実際の出力を読むと、いくら推測しても分からなかったことがいくつも明らかになります。

  • ノード名には意味がありません。 boot.011Plane.002Cylinder.000 — これがエクスポートの結果です。一方でマテリアル名には意味があります。paintglassrubbersilverwindowlightsです。このモデルではマテリアルのほうが優れた指定手段であり、これはよくあることです。
  • renderコンポーネントは葉ノードにあります。 windshield自身はジオメトリを持たず、その子のwindshield_0が持っています。マテリアルを変更したい<pc-node>は、分かりやすそうな親ではなく、(render)マーカーが付いているノードにバインドする必要があります。
  • Cylinder.000はホイールです — リム、プラスチック、タイヤ、ブレーキに対応する4つの子ノードを持ちます。
  • boot.011_01はエンジンが改名したものです。 このGLBにはboot.011_0という名前の子が2つあり、同名の兄弟は階層構築時に接尾辞を付けて区別されます。

hierarchy()はプレーンなデータ — namepathindexcomponentsmaterialschildren — を返すため、読むだけでなく検索することもできます。フィールドの完全なリファレンスは階層の調査にあります。

// 'paint'マテリアルで塗られたジオメトリを持つすべてのノード
const painted = [];
const walk = (node) => {
if (node.materials.some(m => m.name === 'paint')) painted.push(node.name);
node.children.forEach(walk);
};
walk(model.hierarchy());
console.log(painted); // ['Plane.002_0', 'Plane.003_0', 'Plane.004_0', ...]

読み込んだ内容を調整する

<pc-node>は読み込まれた階層内のノードにバインドし、それに対するオーバーライドを宣言します。変更したい部分ごとに<pc-model>の内側にネストしてください。これは常に検索であって改名ではなく、省略した属性はモデルがオーサリングされたときの値を保持します。

部分を非表示にする

Sketchfabのモデルにはベイクされた影用のプレーンが同梱されていることが日常的にあり、このモデルにはさらにアーティストのウォーターマークがそこにベイクされています。どちらも属性1つで消えます。

<pc-model asset="car">
<pc-node name="Plane" enabled="false"></pc-node>
</pc-model>

ベイクされた地面プレーンとウォーターマークを取り除いた同じPorsche

enabled="false"はそのノードとその配下すべてを無効にします。ファイルを編集せずに不要なコンテンツを取り除く、宣言的な方法です。

部分を再配置する

<pc-node>positionrotationscaleは、オーサリングされたトランスフォームに加算されるのではなく置き換えます。

<pc-model asset="car">
<!-- 塗装されたボディパネルを1枚だけ持ち上げます。回転とスケールはエクスポート時のままです -->
<pc-node name="boot.001_0" position="0 0.4 0"></pc-node>
</pc-model>

思い描いた部分がどのノードなのかはhierarchy()に尋ねる問いです。このエクスポートでは、boot.001_0paintマテリアルを持つノードの1つに過ぎません。

これらは置き換えであるため、実行時に属性を削除する(または対応するJavaScriptプロパティにnullを代入する)とオーサリングされた値が戻ります。2つの状態を切り替えるのに便利です。

部分のマテリアルを差し替える

material-overridesはセレクターを<pc-material>のidにマッピングします。name:セレクターを与えると、そのノード上でその名前のマテリアルを持つすべてのメッシュインスタンスを差し替えます。

<pc-app>
<pc-material id="candy-red" name="Candy Red" diffuse="#c8102e" metalness="1" roughness="0.25"></pc-material>
<pc-scene>
<pc-model asset="car">
<pc-node name="Plane" enabled="false"></pc-node>
<pc-node name="Plane.002_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
<pc-node name="boot.001_0" material-overrides='{"name:paint": "candy-red"}'></pc-node>
</pc-model>
</pc-scene>
</pc-app>

キャンディレッドに塗り替えられたPorsche

その形に注目してください。塗装を持つノード1つごとに<pc-node>が1つ必要です。material-overridesはそれが置かれたノードのrenderコンポーネントに適用され、このモデルではpaintマテリアルが7つの異なるノードに散らばっています。つまり全塗装には7つのバインディングが必要です。リストが分かっていれば(上のhierarchy()が教えてくれました)これで問題ありませんし、idによって複数のノードが1つのマテリアル宣言を共有できます。

モデル全体を一度に処理したい場合や、仕上げの間をクロスフェードさせたい場合は、スクリプトの仕事になります。Car Configuratorのサンプルがそうしています。宣言的な方法は、事前に分かっている固定の部品セットのためのものです。

差し替え用の<pc-material>を後から識別したい場合はnameを設定してください。これはhierarchy()が報告するラベルであり、名前のないマテリアルはそこでUntitledと表示されます。マルチマテリアルメッシュ向けのindex:を含むセレクター文法の全体と、無効なルールがどう報告されるかはマテリアルのオーバーライドにあります。

部分に何かをアタッチする

<pc-node><pc-entity>の子を持つことができ、それらは作成されてバインドされたノードの下に親子付けされます。これにより、どのノードもトランスフォームを継承するアタッチポイントになります。

<pc-model asset="car">
<pc-node name="bumper_front.004">
<pc-entity position="0 0 0.3">
<pc-light type="spot" color="#fff6e0" intensity="12" outer-cone-angle="34"></pc-light>
</pc-entity>
</pc-node>
</pc-model>

子エンティティのトランスフォームはノードに対してローカルなので、その部分が動けば追従します。ライトはエンティティの負のY軸方向に照らすこと、そしてglTF階層の深くにあるノードは通常、継承された回転を持つことに注意してください。子のrotationは理屈で導くよりも、見た目に合わせて調整することになります。

部分にコンポーネントを与える

<pc-node><pc-entity>と同じコンポーネントタグを取り、バインドされたノードにそのコンポーネントを追加します。よくあるのは物理です。メッシュコライダーはそのノード自身のrenderコンポーネントから形状を取るため、リジッドボディとコライダーを組み合わせればエクスポートされたジオメトリが固体になります。

<pc-model asset="car">
<pc-node name="underbody_0">
<pc-rigid-body type="static"></pc-rigid-body>
<pc-collision type="mesh"></pc-collision>
</pc-node>
</pc-model>

物理にはDracoと同じ方法で宣言するAmmoモジュールが必要です。<pc-wasm>を参照してください。

部分をインタラクティブにする

ノードをバインドすることがそれをピック対象にするため、ポインターイベントはどの<pc-node>でも利用できます。これがモデルの一部だけをクリック可能にし、残りを反応しないままにする方法です。

<pc-model asset="car">
<pc-node name="boot.001_0" onpointerdown="this.setAttribute('position', '0 0.4 0')"></pc-node>
</pc-model>

インラインハンドラー内ではthis<pc-node>要素です。setAttributeを経由することでマークアップとシーンの内容が一致し続けます。対応するJavaScriptプロパティは型付けされており、positionrotationは文字列ではなくVec3を取るため、インラインハンドラーからは属性を、実際のスクリプトファイルからはプロパティを使うのが良いでしょう。

イベントとそのインライン属性形式は<pc-node>のリファレンスに一覧があります。

アニメーション

コンテナのアニメーションは、モデルの内側にネストした<pc-anim>が再生します。空のまま置けば、コンテナが持つすべてのアニメーションを、それぞれのトラック名を名前として割り当て、最初のものを再生します。「ファイルに入っていたものを再生する」がタグ1つで済みます。

<pc-asset id="robot" type="container" src="assets/walking-robot.glb"></pc-asset>
<pc-scene>
<pc-model name="robot" asset="robot">
<pc-anim></pc-anim>
</pc-model>
</pc-scene>

歩行アニメーションが再生され、歩を進めている途中のロボットキャラクター

ラッパーとなるエンティティは必要ありません。<pc-model>はそれ自体がエンティティのホストなので、その内側に置いたコンポーネントは<pc-entity>に取り付けるのと同じように、モデルに取り付けられます。つまりモデルは自身の名前・トランスフォーム・ポインタハンドラを持てて、コンテンツと並べて子エンティティをホストすることもできます。

どのアニメーションがエクスポートを通過したかを確認するには、コンポーネントに尋ねます。

const anim = await whenReady('pc-anim');
console.log(anim.clips); // ['Walk', 'Idle', 'Wave']

エクスポーターが付けた名前をそのまま使うのではなく、自分でクリップに名前を付けるには、クリップを宣言します。1クリップにつき1つの<pc-anim-clip>です。クリップごとの速度とループもここで設定し、他のファイルのクリップを混ぜるのもここです。

<pc-model name="robot" asset="robot">
<pc-anim clip="idle" transition-time="0.3">
<pc-anim-clip name="idle"></pc-anim-clip>
<pc-anim-clip name="walk" speed="1.2"></pc-anim-clip>
<pc-anim-clip name="wave" asset="wave-glb" loop="false"></pc-anim-clip>
</pc-anim>
</pc-model>

あとはclipを設定すればクリップが切り替わり、transition-timeにわたってクロスフェードします。

document.querySelector('pc-anim').setAttribute('clip', 'walk');

トラックは名前によってノードにバインドされます。別ファイルのクリップが、ノード名の一致するモデルしかアニメーションさせられないのはこのためであり、剛体パーツだけの単純な階層がスキン付きのスケルトンと同じようにアニメーションするのもこのためです。クロスフェード、一時停止、そしてエンジンがクリップの再生完了を通知しないことを含む全体像は、<pc-anim>を参照してください。

トラブルシューティング

何も表示されず、コンソールにDracoまたはBasisが出てくる。 モデルが圧縮されており、デコーダーモジュールがありません。圧縮されたメッシュを参照してください。

何も表示されず、警告もまったく出ない。 モデルのスケールと原点を確認してください。センチメートル単位でエクスポートされたモデルは100倍の大きさで届き、原点がジオメトリから遠く離れたモデルはカメラの視野の外に完全に収まってしまうことがあります。

<pc-node>が名前が曖昧だと警告する。 2つ以上のノードがその名前を共有しているため、要素は推測を拒否します。警告に候補が列挙されるので、indexで1つを選んでください。

<pc-node>が名前が何にも一致しないと警告する。 警告には見つかった中で最も近い名前が含まれており、通常はそれでタイプミスに気づけます。そうでない場合はhierarchy()を出力してください。求めている名前がエクスポート時に変更されているかもしれません。

material-overridesがノードにオーサリングされたrenderコンポーネントがないと言う。 ジオメトリを持つ葉ノードではなく、グループ化用のノードにバインドしています。hierarchy()の出力で(render)マーカーを探してください。

マテリアル名がUntitledまたはdefaultGlbMaterialと表示される。 これらは、名前のないglTFマテリアルと、マテリアルなしでエクスポートされたプリミティブに対するエンジンのデフォルトです。どちらも一意な指定手段ではないため、そうしたものはindex:で選択してください。

モデルは読み込まれるが何もアニメーションしない。 アニメーションで説明したとおり、モデルは内側の<pc-anim>が指示するまで何も再生しません。それが置かれているならanim.clipsを確認してください。モデルにアニメーションがないという警告とともに空のリストが返る場合、アニメーションはエクスポートを通過していません。

別ファイルのクリップが何もアニメーションさせない。 そのトラックは名前によってバインドされるため、クリップとモデルでノード名が一致している必要があります。モデルのhierarchy()を出力して見比べてください。

次のステップ