mcpField
T
threejs-asset-pipeline

threejs-asset-pipeline

github.com/cloudai-x開発3.8k1.9k 回インストール1 週間前に更新
リポジトリ

threejs-asset-pipeline について

Three.js 向けに glTF の検査、検証、圧縮、最適化を扱うアセットパイプラインのスキルです。CLI と SDK の手順に加え、コーデックに応じたローダー設定や React Three Fiber 用の出力も説明します。

主な機能

  1. 01glTF ファイルの検査と検証
  2. 02ジオメトリとテクスチャデータの最適化
  3. 03Draco または Meshopt によるモデル圧縮
  4. 04KTX2、WebP、AVIF テクスチャの生成
  5. 05対応する Three.js ローダーの設定
  6. 06gltfjsx による React コンポーネント生成

活用シーン

  1. 01最適化前のモデル内容の確認
  2. 02ブラウザー配信用の軽量アセット作成
  3. 03圧縮 glTF 用ローダーの設定
  4. 04CI でのアセット変換の自動化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cloudai-x/threejs-skills --skill threejs-asset-pipeline

インストール後、Claude Code で/threejs-asset-pipelineを入力して読み込まれたか確認します。

詳細

Three.js アセットパイプライン

three.js r186 を対象としています。例では WebGLRenderer を使用し、WebGPU と TSL のセクションでは WebGPURenderer を使用します。

クイックスタート

コマンドラインで圧縮してから、結果を読み込みます。inspect を実行して、処理による削減量を前後で確認してください。

npx @gltf-transform/cli inspect model.glb
npx @gltf-transform/cli optimize model.glb model.opt.glb --compress draco --texture-compress webp --texture-size 2048
npx @gltf-transform/cli inspect model.opt.glb

Draco の出力には DRACOLoader のアタッチが必要です。WebP テクスチャはブラウザーでデコードされます。

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const sun = new THREE.DirectionalLight(0xffffff, 2); // glTF PBR materials render black without lights or an environment
sun.position.set(3, 5, 2);
scene.add(sun, new THREE.HemisphereLight(0xffffff, 0x444444, 1.5));

const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(2, 1.5, 3);
camera.lookAt(0, 0, 0);

const gltf = await new GLTFLoader()
  .setDRACOLoader(new DRACOLoader()) // the Draco WASM decoder loads from the three package
  .loadAsync("model.opt.glb");
scene.add(gltf.scene);

function animate() {
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

検査と検証

バイトデータの場所を推測してはいけません。inspect は概要(ジェネレーター、extensionsUsed、extensionsRequired)に加えて、シーン、メッシュ、マテリアル、テクスチャ、アニメーションごとに 1 つのテーブルを出力します。そのため、ジオメトリ主体のファイルとテクスチャ主体のファイルを一目で見分けられます。

npx @gltf-transform/cli inspect model.glb
npx @gltf-transform/cli inspect model.opt.glb --format csv
npx @gltf-transform/cli validate model.glb --ignore ACCESSOR_WEIGHTS_NON_NORMALIZED

extensionsRequired はランタイムの契約です。KHR_draco_mesh_compression、EXT_meshopt_compression、KHR_texture_basisu はそれぞれ、以下のローダー設定に対応します。一方、KHR_mesh_quantization、EXT_mesh_gpu_instancing、EXT_texture_webp、EXT_texture_avif はランタイムでの設定は不要です。validate は公式の glTF バリデーターを実行し、エラー、警告、ヒントを報告します。エラーが見つかるとゼロ以外の終了コードを返すため、CI のゲートとして使用できます。このスキルでファイルを書き出すコマンドは、完了時に入力と出力のサイズをログに記録します。どのテーブルで削減できたかを確認するには、出力に対して inspect を実行してください。

optimize コマンド

optimize は、一般的な処理を 1 回の実行で連続して行います。dedup、instance、palette、flatten、join、weld、simplify、resample、prune、sparse data の順に処理した後、テクスチャを圧縮し、最後にジオメトリを圧縮します。フラグを指定しない場合は meshopt 圧縮を適用し、テクスチャを元の形式のまま再圧縮します。デフォルト設定は積極的ですが汎用的なため、まず inspect を実行してから調整してください。

# draco geometry + webp textures, capped at 2048px
npx @gltf-transform/cli optimize model.glb model.opt.glb --compress draco --texture-compress webp --texture-size 2048
# GPU textures instead of download size, keep every triangle
npx @gltf-transform/cli optimize model.glb model.opt.glb --texture-compress ktx2 --simplify false

主なフラグ(ブール値には --simplify false のように明示的な値を指定します):

  • --compress draco|meshopt|quantize — ジオメトリとアニメーションの圧縮(デフォルトは meshopt)。
  • --texture-compress ktx2|webp|avif — テクスチャのコーデック(デフォルトは auto。元の形式のまま再圧縮します)。
  • --texture-size <px> — テクスチャの最大寸法。デフォルトは 2048 です。
  • --simplify <bool>、--simplify-ratio <0-1>、--simplify-error <fraction>、--simplify-lock-border <bool> — メッシュの簡略化。デフォルトでは ratio は 0、error は 0.0001 です。
  • --instance <bool>、--instance-min <count> — 繰り返し使用されるメッシュの GPU インスタンシング。
  • --join <bool>、--join-meshes <bool>、--join-named <bool>、--flatten <bool> — ドローコールの削減。--join には --flatten が必要です。
  • --weld <bool> — 重複する頂点を統合します。draco は --weld false を指定しても単独で weld を行いますが、simplify が weld を行うのはインデックスのないプリミティブのみです。そのため、簡略化する場合は --weld true を維持してください。
  • --prune <bool>、--resample <bool>、--palette <bool>、--sparse <bool>、--meshopt-level medium|high。

--texture-compress ktx2 を指定すると、optimize はビットストリームを自動で選択します。normal、occlusion、metallic-roughness の各スロットには UASTC を、それ以外には ETC1S を使用します。このスキルの対象範囲外の代替手段としては gltfpack が同様の処理をカバーし、ここでは gltf-transform で十分です。

ジオメトリの圧縮

トレードオフの異なる3種類のコーデックがあります。プロジェクト単位ではなく、モデルごとに選択してください。

  • Draco(--compress draco、draco コマンド):三角形メッシュのジオメトリのみを圧縮します。アニメーション、モーフターゲット、テクスチャ、ポイント、ラインは圧縮されません。ランタイム:DRACOLoader。
  • Meshopt(--compress meshopt、meshopt コマンド):ポイント、ライン、三角形のジオメトリ、モーフターゲット、アニメーショントラックを圧縮し、デコードも非常に高速です。ネットワーク転送時には、ロスレスの gzip または brotli と組み合わせてください。ランタイム:MeshoptDecoder。
  • Quantize(quantize コマンド):KHR_mesh_quantization に基づいて、浮動小数点属性を8ビットまたは16ビット整数として保存します。ディスク上と GPU メモリ上のサイズを削減でき、デコーダーを組み込む必要もありません。GLTFLoader は量子化された属性をネイティブに読み取ります。
npx @gltf-transform/cli draco model.glb model.opt.glb --method edgebreaker --quantize-position 14
npx @gltf-transform/cli meshopt model.glb model.opt.glb --level high
npx @gltf-transform/cli quantize model.glb model.opt.glb
npx @gltf-transform/cli gzip model.opt.glb

gzip は入力ファイルと同じ場所に model.opt.glb.gz を出力します。Content-Encoding: gzip を指定して配信すると、ブラウザーが透過的に解凍します。Draco や JPEG テクスチャに対しては追加の効果は小さく、meshopt や quantize に対して最も効果を発揮します。

draco、meshopt、optimize はチェーンの最後のコマンドにしてください。モデルを書き換える他のコマンドは、ファイルの読み込み時に Draco と meshopt のジオメトリをデコードし(「Decoded KHR_draco_mesh_compression. Further compression will be lossy.」という警告が表示されます)、圧縮されていない状態で書き戻すためです。

テクスチャの圧縮

異なる2つの課題があります。KTX2(Basis Universal)は、GPU がトランスコード先のいずれかに対応している場合、圧縮状態のまま GPU メモリに保持され(そうでない場合、KTX2Loader は非圧縮の RGBA にフォールバックします)、高速にアップロードされます。GPU メモリとパフォーマンスの最適化に適しています。一方、WebP と AVIF はダウンロードサイズを縮小しますが、GPU メモリ内では生ピクセルに解凍され、メモリを4〜8倍多く使用することがあります。

# KTX2: ETC1S for color (smaller, lossier), UASTC for normal maps (larger, cleaner)
npx @gltf-transform/cli etc1s model.glb model.opt.glb --quality 128
npx @gltf-transform/cli uastc model.glb model.opt.glb --level 4
# Download size: WebP or AVIF, quality 1-100
npx @gltf-transform/cli webp model.glb model.opt.glb --quality 80
# Cap dimensions of PNG or JPEG textures; never enlarges them
npx @gltf-transform/cli resize model.glb model.opt.glb --width 1024 --height 1024

KTX2 の出力(etc1s、uastc、または optimize --texture-compress ktx2)には、KTX-Software の ktx コマンド(4.4.0以降)が必要なため、先にインストールしてください。WebP と AVIF はプロセス内でエンコードされます。単独の etc1s コマンドと uastc コマンドは、すべてのテクスチャを1つのビットストリームに変換します。スロットごとの分割(法線、オクルージョン、メタリック・ラフネスには UASTC、その他には ETC1S)は optimize --texture-compress ktx2 が自動で行います。カスタム分割が必要な場合は、--slots で名前を指定してテクスチャの役割を選択します。ランタイム:KTX2 には detectSupport を指定した KTX2Loader が必要です。WebP と AVIF はブラウザーでデコードされます。

シーン最適化コマンド

inspect で問題がバイト数ではなく、ドローコール数または頂点数にあると判明した場合は、次を実行してください。

npx @gltf-transform/cli weld model.glb model.opt.glb
npx @gltf-transform/cli simplify model.glb model.opt.glb --ratio 0.5 --error 0.001
npx @gltf-transform/cli join model.glb model.opt.glb
npx @gltf-transform/cli instance model.glb model.opt.glb --min 5
npx @gltf-transform/cli dedup model.glb model.opt.glb
npx @gltf-transform/cli prune model.glb model.opt.glb
  • weld はビット単位で同一の頂点をマージし、プリミティブにインデックスを付けます(ポイントはスキップされます)。draco は自動で weld を実行します。simplify が weld を実行するのはインデックスのないプリミティブのみで、meshopt は実行しません。また、meshopt の並べ替え処理はインデックスのないプリミティブをスキップするため(インデックス付きのプリミティブがない場合は警告が表示されます)、simplify と meshopt の前に weld を実行してください。
  • simplify は三角形を削除します。--error(メッシュの範囲に対する割合)を超えない範囲で、--ratio(保持する頂点の割合)を目標にします。--lock-border true を指定すると、開いたエッジが保持されます。これは非可逆処理です。
  • join は互換性のあるプリミティブをマージして、ドローコール数を削減します。最初に dedup と flatten を実行します。同じメッシュを大量に再利用するシーンでは頂点数が増えるため、代わりに instance を使用してください。
  • instance は、複数のノードから参照されるメッシュに対して EXT_mesh_gpu_instancing を出力します(--min のデフォルトは5です)。GLTFLoader は実行時の設定なしで、これらのバッチを InstancedMesh として描画します。アニメーションを含むファイルはスキップされます。また、インスタンス化したメッシュではマテリアルを共有する必要があります。
  • dedup は、エクスポーターが残した重複するアクセサー、マテリアル、メッシュ、スキン、テクスチャをマージします。
  • prune は、シーンから参照されていないプロパティを削除します。シーンにアタッチされた対象がないアニメーションも含まれます。

各出力形式のローダー設定

パイプラインの選択によって、ローダーの設定が決まります。Draco デコーダーと Basis トランスコーダーは、デフォルトで three パッケージから読み込まれます。これらのファイルを自分でホストする場合に限り、setDecoderPath または setTranscoderPath を呼び出し、その後、使用する three のリリースに合わせてパスを固定してください。アプリごとに KTX2Loader を1つ作成し、GLTFLoader 間で共有します。有効なインスタンスごとに専用のトランスコーダーとワーカーが読み込まれ、複数が同時に有効だと three.js が警告を出すためです。

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";

// Wire every codec the pipeline may emit; a missing one throws at load time
const gltfLoader = new GLTFLoader()
  .setDRACOLoader(new DRACOLoader()) // KHR_draco_mesh_compression
  .setMeshoptDecoder(MeshoptDecoder) // EXT_meshopt_compression
  .setKTX2Loader(new KTX2Loader().detectSupport(renderer)); // KHR_texture_basisu

const gltf = await gltfLoader.loadAsync("model.opt.glb");
scene.add(gltf.scene);

不足しているものがあると、それぞれ次のエラーが発生します。DRACOLoader がない Draco ファイルでは「No DRACOLoader instance provided」、デコーダーがない meshopt ファイルでは「setMeshoptDecoder must be called before loading compressed files」、KTX2Loader がない KTX2 ファイルでは「setKTX2Loader must be called before loading KTX2 textures」、detectSupport が実行されていない KTX2Loader では「Missing initialization with .detectSupport( renderer )」です。WebP および AVIF テクスチャ、量子化属性、EXT_mesh_gpu_instancing には、これらは不要です。

スクリプト化されたパイプライン

CLI では対応しきれなくなった場合(プロジェクトごとのデフォルト設定、CI、多数のファイルなど)は、SDK を使って同じ変換をスクリプト化します。node pipeline.mjs で実行する Node スクリプトです。

// deps: @gltf-transform/core @gltf-transform/extensions @gltf-transform/functions draco3dgltf
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { dedup, prune, weld, draco } from "@gltf-transform/functions";
import draco3d from "draco3dgltf";

const io = new NodeIO()
  .registerExtensions(ALL_EXTENSIONS) // an extension left out is dropped on read, or throws if required
  .registerDependencies({
    "draco3d.decoder": await draco3d.createDecoderModule(), // reads Draco input
    "draco3d.encoder": await draco3d.createEncoderModule(),
  });

const document = await io.read("model.glb");
await document.transform(
  dedup(),
  weld(), // shared vertices, before compression
  prune(), // drop what no Scene references
  draco({ method: "edgebreaker", quantizePosition: 14 }),
);
await io.write("model.opt.glb", document);

圧縮は I/O クラスがファイルを書き込むときに実行されるため、最後のステップにします。すべての拡張機能を登録してください。I/O が書き込むのは登録済みの拡張機能のみなので、WebP テクスチャや量子化属性に対応する拡張機能が登録されていないと、ファイルが無効になります。代わりに meshopt を使う場合は、await MeshoptEncoder.ready の後に registerDependencies へ "meshopt.encoder": MeshoptEncoder を追加し(MeshoptEncoder は meshoptimizer パッケージから読み込みます)、meshopt({ encoder: MeshoptEncoder }) を渡してください。スクリプトでのテクスチャ圧縮には、エンコーダーとして sharp を使う textureCompress({ encoder, targetFormat: "webp" }) を指定します。

gltfjsx での React 出力

gltfjsx はモデルを React Three Fiber コンポーネントに変換します。読み込んだだけのブラックボックスではなく、編集できる宣言的な JSX になります。

npx gltfjsx model.glb --transform

これにより Model.jsx が生成されます(@react-three/fiber 用のコンポーネントです。.tsx にするには --types を指定します)。また、--transform を指定しているため、アセットも書き換えられます。model-transformed.glb には、パレットマテリアル、結合されたメッシュ、溶接および不要部分を削除したジオメトリ、Draco 圧縮、WebP に再エンコードされたテクスチャが含まれます。テクスチャの最大サイズは --resolution で指定し(デフォルトは 1024)、ノーマルマップは JPEG に再エンコードされ、2048 px と --resolution の大きい方が上限になります。生成されたコンポーネントは、このファイルを読み込みます。簡略化するには、--ratio と --error を指定して --simplify を追加します。コンポーネント側(Canvas、Suspense、useGLTF)については、threejs-react-three-fiber スキルを参照してください。

WebGPU と TSL

上記のローダーとアドオンは WebGPURenderer でも同じです。読み込まれたマテリアルは WebGPURenderer 自身がノードマテリアルとしてレンダリングします(mesh.material は GLTFLoader が作成したクラスのままです)。順序に関する注意点が1つあります。KTX2Loader.detectSupport(renderer) にはレンダラーのバックエンドが必要なため、await renderer.init() の後にローダーを作成してください。

import * as THREE from "three/webgpu";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
// renderer is an initialized WebGPURenderer (its init() was awaited)
const gltfLoader = new GLTFLoader()
  .setKTX2Loader(new KTX2Loader().detectSupport(renderer));
const gltf = await gltfLoader.loadAsync("model.opt.glb");
scene.add(gltf.scene);

よくある間違い

  • import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js" → import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js":three.js がドキュメントで示しているインポートマップでは、three/examples/jsm の指定子は解決されません。
  • npx @gltf-transform/cli optimize model.glb out.glb --compress ktx2 → npx @gltf-transform/cli optimize model.glb out.glb --compress draco --texture-compress ktx2:テクスチャコーデックには --texture-compress を使用します。--compress で指定できるのは draco、meshopt、quantize です。
  • npx @gltf-transform/cli draco model.glb a.glb && npx @gltf-transform/cli webp a.glb out.glb --quality 80 → npx @gltf-transform/cli webp model.glb a.glb --quality 80 && npx @gltf-transform/cli draco a.glb out.glb:draco、meshopt、optimize 以外の各コマンドは、ファイルの読み込み時にDracoおよびmeshoptジオメトリをデコードし、圧縮せずに書き出します。そのため、ジオメトリの圧縮は最後に行います。
  • new GLTFLoader().loadAsync("draco.glb") → new GLTFLoader().setDRACOLoader(new DRACOLoader()).loadAsync("draco.glb"):DRACOLoaderを指定せずにKHR_draco_mesh_compressionファイルを読み込むと、"No DRACOLoader instance provided" がスローされます。
  • new GLTFLoader().loadAsync("meshopt.glb") → new GLTFLoader().setMeshoptDecoder(MeshoptDecoder).loadAsync("meshopt.glb"):EXT_meshopt_compressionファイルを読み込むと、"setMeshoptDecoder must be called before loading compressed files" がスローされます。
  • new GLTFLoader().loadAsync("ktx2.glb") → new GLTFLoader().setKTX2Loader(new KTX2Loader().detectSupport(renderer)).loadAsync("ktx2.glb"):KHR_texture_basisuファイルを読み込むと、"setKTX2Loader must be called before loading KTX2 textures" がスローされます。
  • setKTX2Loader(new KTX2Loader()) → setKTX2Loader(new KTX2Loader().detectSupport(renderer)):detectSupportを実行していないKTX2Loaderでは、"Missing initialization with .detectSupport( renderer )" がスローされます。
  • dracoLoader.setDecoderPath(cdnPathFromAnotherRelease) → new GLTFLoader().setDRACOLoader(new DRACOLoader()):Dracoデコーダーはデフォルトでthreeパッケージから読み込まれます。別のリリースに固定されたパスを指定すると、ローダーと互換性がなくなります。
  • dracoLoader.setDecoderConfig({ type: "js" }) → new DRACOLoader():r185ではsetDecoderConfigは非推奨です。指定しない場合、DRACOLoaderはWebAssemblyが利用できる環境ではWASMデコーダーを使用します。
  • ktx2Loader.detectSupportAsync(renderer) → await renderer.init(); ktx2Loader.detectSupport(renderer);:r181ではdetectSupportAsyncは非推奨です。レンダラーの初期化後にdetectSupportを呼び出してください。
  • npx @gltf-transform/cli dequantize model.glb out.glb → npx @gltf-transform/cli quantize model.glb out.glb:GLTFLoaderは、実行時の設定なしでKHR_mesh_quantization属性を読み込みます。そのため、独自のコードでモデル単位の浮動小数点頂点データが必要な場合にのみ、dequantizeを使用してください。量子化された属性は正規化された整数値を保持し、量子化された位置はノードのトランスフォームによって元のスケールに戻されます。

関連項目

  • threejs-loaders - 実行時の処理:読み込みの進行状況、キャッシュ、スキニングモデルの複製、パイプラインで生成されたデータの破棄について説明します。
  • threejs-react-three-fiber - gltfjsxコンポーネントを使用するReactアプリ:Canvas、useGLTF、Suspense、dreiヘルパーについて説明します。
  • threejs-performance - パイプラインの効果を測定する方法:ドローコール、GPUメモリ、renderer.infoについて説明します。

コメント