Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9232

Node.jsでglTFモデルを圧縮してthree.jsで読む込む(DRACO/meshoptimizer)

$
0
0

概要

WebGL表現でハイポリ(30K ~ 140K Poly)のglTFモデルを複数使用した際に、転送量やローディングパフォーマンスの観点からできる限りファイルサイズを落としたくて、Node.jsでglTFファイルを圧縮する環境を作ったのでその紹介です。

リポジトリはこちら

glTFのファイルサイズを軽量化する方法はいくつかありますが、本記事ではDRACOmeshoptimizerによる2通りの圧縮方法を紹介します。

環境構築

yarnとNode.jsのバージョンは以下です。

yarn@v1.22.5
node@v12.21.0

プロジェクトディレクトリを作成、移動してpackage.jsonを生成します。

$ mkdir gltf-compressor
$ cd gltf-compressor
$ yarn init -y

srcディレクトリを作成しglTFを格納します。

$ mkdir src

今回はサンプルデータとして、mixamoからダウンロードしたアニメーション付きXBOTのモデルがどのくらい軽量化できるか比較してみます。

mixamo.gif

mixamoからダウンロードできるのは.fbx.dae形式なので、BlenderでglTFにエクスポートしておきます。

アニメーションマテリアル頂点ポリゴン
1228,31249,112
gltf-compressor
├── package.json
└── + src
    └── + Capoeira
        ├── + Capoeira.bin
        └── + Capoeira.gltf

圧縮前のglTFは2.3MBありました。

before.png

DRACO圧縮

DRACO圧縮にはgltf-pipelineパッケージを使用します。

gltf-pipelinefs-extraglobpathをインストールし、DRACO圧縮用のJSファイルcompress-draco.jsを作成します。

$ yarn add gltf-pipeline fs-extra glob path
$ touch compress-draco.js
gltf-compressor
├── + compress-draco.js
├── node_modules
│   └── ...
├── package.json
├── src
│   └── ...
└── yarn.lock

yarn dracoコマンドを実行した際に対応するJSが実行されるように、package.jsonscriptsを追加します。

package.json
{"name":"gltf-compressor","version":"1.0.0","main":"index.js","license":"MIT","scripts":{"draco":"node compress-draco.js"},"dependencies":{"fs-extra":"^9.1.0","glob":"^7.1.6","gltf-pipeline":"^3.0.2","path":"^0.12.7"}}

Node.jsでDRACO圧縮をするコードです。

compress-draco.js
constglob=require('glob');constfs=require('fs-extra');constpath=require('path');constgltfPipeline=require('gltf-pipeline');constsrcDir='src';constdistDir='dist';/**
 * glTFをDRACO圧縮
 * @param {string | string[]} globs 
 */constcompressGltfWithDraco=(globs)=>{glob(globs,async(err,files)=>{if(err)return;for(constfileoffiles){constfilePath=path.resolve(file);constgltf=fs.readJsonSync(filePath);// gltfのJSONを読み込むconstoptions={resourceDirectory:path.dirname(filePath),// gltfのリソースディレクトリ(親フォルダ)dracoOptions:{compressionLevel:10}// DRACO圧縮率MAX};const{glb}=awaitgltfPipeline.gltfToGlb(gltf,options);// gltf -> glbconstoutFilePath=filePath.replace('.gltf','-draco.glb').replace(srcDir,distDir);// 出力先awaitfs.mkdirp(path.dirname(outFilePath));// distディレクトリがなかったら作成awaitfs.writeFileSync(outFilePath,glb);// glbファイル出力console.log(`[draco] ${outFilePath}`);}});};compressGltfWithDraco(`./${srcDir}/**/*.gltf`);

globsrcフォルダ内にある.gltfファイルの配列を取得し、.gltfJSONとして読み込ませたものとオプションをgltfPipeline.gltfToGlb()に渡して圧縮します。

glTFを圧縮しているのは以下の部分です。

constoptions={resourceDirectory:path.dirname(filePath),// gltfのリソースディレクトリ(親フォルダ)dracoOptions:{compressionLevel:10}// DRACO圧縮率MAX};const{glb}=awaitgltfToGlb(gltf,options);// gltf -> glb

用意しておいたコマンドを実行してDRACO圧縮をかけます。

$ yarn draco

distディレクトリにDRACO圧縮後の.glbファイルが出力されます。

gltf-compressor
├── compress-draco.js
├── + dist
│   └── + Capoeira
│       └── + Capoeira-draco.glb
├── node_modules
│   └── ...
├── package.json
├── src
│   └── Capoeira
│       ├── Capoeira.bin
│       └── Capoeira.gltf
└── yarn.lock
BeforeAfter圧縮率
2.3MB804KB-64.79%

after-draco.png

three.jsで読み込む

GLTFLoader.setDracoLoader()DRACOLoaderのインスタンスを渡して読み込みます。
DRACO圧縮のデコードに必要なdraco_decoder.jsdraco_decoder.wasmthree/examples/js/libs/dracoに用意されているので、自分のサーバーにコピーしてDRACOLoader.setDecoderPath()でパスを指定する必要があります。

参考:https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader

import{GLTFLoader}from'three/examples/jsm/loaders/GLTFLoader';import{DRACOLoader}from'three/examples/jsm/loaders/DRACOLoader';constdracoLoader=newDRACOLoader();dracoLoader.setDecoderPath('/path/to/draco_decoder/');constloader=newGLTFLoader();loader.setDRACOLoader(dracoLoader);loader.load('Capoeira-draco.glb',(gltf)=>{scene.add(gltf.scene);});

meshopt圧縮

meshopt圧縮にはgltfpackパッケージを使用します。

gltfpackをインストールして、meshopt圧縮用のJSファイルcompress-meshopt.jsを作成します。

$ yarn add gltf-pack
$ touch compress-meshopt.js
gltf-compressor
├── compress-draco.js
├── + compress-meshopt.js
├── dist
│   └── ...
├── node_modules
│   └── ...
├── package.json
├── src
│   └── ...
└── yarn.lock

yarn meshoptコマンドを実行した際に対応するJSが実行されるように、package.jsonscriptsを追加します。

package.json
{"name":"gltf-compresser","version":"1.0.0","main":"index.js","license":"MIT","scripts":{"draco":"node compress-draco.js","meshopt":"node compress-meshopt.js"},"dependencies":{"fs-extra":"^9.1.0","glob":"^7.1.6","gltf-pipeline":"^3.0.2","gltfpack":"^0.15.2","path":"^0.12.7"}}

Node.jsでmeshopt圧縮をするコードです。

compress-meshopt.js
constcp=require('child_process');constglob=require('glob');constgltfPack=require('gltfpack');constfs=require('fs-extra');constpath=require('path');constsrcDir='src';constdistDir='dist';constpaths={'basisu':process.env['BASISU_PATH'],'toktx':process.env['TOKTX_PATH']};/**
 * gltfpack用インターフェース
 */constgltfPackInterface={read:(path)=>{returnfs.readFileSync(path);},write:(path,data)=>{fs.writeFileSync(path,data);},execute:(command)=>{// perform substitution of command executable with environment-specific pathsconstpkv=Object.entries(paths);for(const[k,v]ofpkv){if(command.startsWith(k+'')){command=v+command.substr(k.length);break;}}constret=cp.spawnSync(command,[],{shell:true});returnret.status==null?256:ret.status;},unlink:(path)=>{fs.unlinkSync(path);}};/**
 * compress gltf -> glb
 * @param {string} inputPath 
 * @param {string} outputPath 
 * @return {Promise<string>}
 */constpackGltf=(inputPath,outputPath)=>{constoutput=outputPath||inputPath.replace('.gltf','.glb');constcommand=`-i ${inputPath} -o ${output} -cc`;// コマンドライン引数(必要に応じてオプションを追加)constargs=command.split(/\s/g);// コマンドライン引数の配列returngltfPack.pack(args,gltfPackInterface).catch(err=>{console.log(err.message);});};/**
 * glTFをmeshoptimizer圧縮
 * @param {string | string[]} globs 
 */constcompressGltfWithMeshopt=(globs)=>{glob(globs,async(err,files)=>{if(err)return;for(constfileoffiles){constfilePath=path.resolve(file);constoutFilePath=filePath.replace('.gltf','-meshopt.glb').replace(srcDir,distDir);// 保存先awaitfs.mkdirp(path.dirname(outFilePath));// distディレクトリがなかったら作成awaitpackGltf(filePath,outFilePath);// gltf -> glbconsole.log(`[meshopt] ${outFilePath}`);}});};compressGltfWithMeshopt(`./${srcDir}/**/*.gltf`);

glTFひとつを圧縮するpackGltf関数を用意して、globで取得したファイルリストに対して回しています。

gltfPackInterfaceの部分はgltfpack/cli.jsの実装から引っ張ってきたので、なぜこういう実装になっているかわかりません。(拡張性をもたせるため?)

constpackGltf=(inputPath,outputPath)=>{constoutput=outputPath||inputPath.replace('.gltf','.glb');constcommand=`-i ${inputPath} -o ${output} -cc`;// コマンドライン引数(必要に応じてオプションを追加)constargs=command.split(/\s/g);// コマンドライン引数の配列returngltfPack.pack(args,gltfPackInterface).catch(err=>{console.log(err.message);});};

用意しておいたコマンドを実行してmeshopt圧縮をかけます。

$ yarn meshopt

distディレクトリにmeshopt圧縮後の.glbファイルが出力されます。

gltf-compressor
├── compress-draco.js
├── compress-meshopt.js
├── dist
│   └── Capoeira
│       ├── Capoeira-draco.glb
│       └── + Capoeira-meshopt.glb
├── node_modules
│   └── ...
├── package.json
├── src
│   └── Capoeira
│       ├── Capoeira.bin
│       └── Capoeira.gltf
└── yarn.lock
BeforeAfter圧縮率
2.3MB247KB-89.16%

after-meshopt.png

three.jsで読み込む

GLTFLoaderのDocumentationには記載されていませんが、GLTFLoader.setMeshoptDecoder()というメンバ関数が用意されていて、デコーダーもthree.js内に含まれています。

こちらはGLTFLoader.setMeshoptDecoder()MeshoptDecoderをそのまま渡します。

参考:https://threejs.org/examples/#webgl_loader_gltf_compressed

import{GLTFLoader}from'three/examples/jsm/loaders/GLTFLoader';import{MeshoptDecoder}from'three/examples/jsm/libs/meshopt_decoder.module.js';constloader=newGLTFLoader();loader.setMeshoptDecoder(MeshoptDecoder);loader.load('Capoeira-meshopt.glb',(gltf)=>{scene.add(gltf.scene);});

比較

圧縮形式BeforeAfter圧縮率
DRACO2.3MB804KB-64.79%
meshoptimizer2.3MB247KB-89.16%

今回のサンプルモデルだけで比較するとDRACOよりmeshoptimizerのほうが圧縮率が高かったです。

元ファイルのサイズが2.3MBもあったのに247KBまで落とせるというのはWebパフォーマンス的にもかなりありがたいですね。

いくつかモデルを圧縮して比較した印象では、モデルの作り方やジオメトリの構造によってはDRACO圧縮のほうがサイズを落とせる場合があったり、meshopt圧縮をすると一部のメッシュが破綻することがあったので、どちらか一択という訳にはいきませんでした。
使用するモデルの中身に応じて圧縮形式を適宜使い分ける必要がありそうです。

Links

gltf-pipeline
gltfpack
gltf-transform(試してないが多機能そうなnpmパッケージ)
ポリゴン数やテクスチャサイズなどを自動で最適化してくれるオンラインサービス(月20回までは無料)


Viewing all articles
Browse latest Browse all 9232