Dracoでメッシュを圧縮してみる
技術統括部エンジニアの鈴木です。この記事ではDracoライブラリを使ってメッシュを圧縮する方法を述べようと思います。
Dracoとは
3DCGは、通常、3次元空間内に大量のポリゴン(主に三角形あるいは四角形)を描画することで行われます。このときポリゴンは一連の頂点の集合で構成されます。Dracoは、この頂点座標や、どの頂点同士が繋っているか、どのようにテクスチャを貼るかといった情報を圧縮・展開するオープンソースのライブラリで、Googleが開発しています。
見た目を変えない程度の不可逆圧縮にも対応しており、高い圧縮率が期待できます。一方で、マテリアルのテクスチャの圧縮には対応していないので、そのような目的には別途WebPなどの画像フォーマットを利用すると良いでしょう。
この記事の目標
Dracoのリポジトリのページには、OBJやPLYのファイルからDracoを使って圧縮する(エンコードする)コマンドラインツールや、C++で展開する(デコードする)方法、JavaScriptで圧縮・展開する方法などが簡単に書かれています。
一方で肝心のC++で圧縮する方法が明記されていません。(本記事執筆当時)
3D形状を扱う際に必ずしもOBJやPLYを使うとは限りませんし、独自形式の3D形状にてDracoを使う場合には、C++からDracoの圧縮を利用したいことでしょう。
この辺りについて日本語で書かれている記事が見当たらなかったので、ここでは、Visual Studioを使って、C++からDracoの圧縮を利用するための具体的な手順の簡単な例を示そうと思います。動作確認にはBlenderを使うことにします。
Dracoのビルド
ダウンロード
Dracoのリポジトリは以下にあります。
https://github.com/google/draco
コマンドラインでしたら以下のようにリポジトリを取得できます。以下ではディレクトリ D:\work
以下にこのリポジトリを取得したものとします。例としてgit for windows 2.32.0.2のGit Bashにて実行する例を記します。
sakira@Mel MINGW64 ~
$ cd /d/work/
sakira@Mel MINGW64 /d/work
$ git clone https://github.com/google/draco.git
Cloning into 'draco'...
remote: Enumerating objects: 5607, done.
remote: Counting objects: 100% (70/70), done.
remote: Compressing objects: 100% (47/47), done.
remote: Total 5607 (delta 28), reused 39 (delta 15), pack-reused 5537
Receiving objects: 100% (5607/5607), 89.95 MiB | 26.30 MiB/s, done.
Resolving deltas: 100% (3864/3864), done.
Updating files: 100% (715/715), done.
BUILDING.md
の内容とある程度重複しますが、以下にWindows 10上のVisual Studio 2019でのビルドの実際の手順を記します。
CMakeでのビルド設定
CMakeは以下からダウンロードできます。Latest ReleaseのWindows x64 Installerをインストールすれば良いでしょう。本記事では執筆時点での最新版である3.21.1を利用しました。
https://cmake.org/download/
ディレクトリ D:\work\draco\
の中にディレクトリ build_dir
を作成し、CMake GUIにて以下のように「Where is the source code」と「Where to build this binaries」を設定してから、「Configure」ボタンを押します。
generatorにはVisual Studio 2019を選択して、それ以外はデフォルトに設定して「Finish」ボタンを押します。
ちょっと待つとConfigureが終了するので、必要に応じてビルドオプションを変更しましょう。今回はデフォルトのまま何も変えずに「Generate」ボタンを押しました。
ディレクトリ D:\work\draco\build_dir
の中に draco.sln
が生成されたのが確認できるはずです。
Visual Studioでのビルド
以下では Visual Studio 2019 を動作確認に使っています。(なお、業務では Visual Studio 2015 でも同様にビルドしたものを、ライブラリとして利用できていることを確認しています。) エディションについてはコミュニティと Professional にて確認しました。
「C++によるデスクトップ開発」もインストールしておいて下さい。
インストールが終わったらVisual Studioを起動して、先程作成した D:\work\draco\build_dir\draco.sln
を開きます。
ソリューションエクスプローラにて「ALL_BUILD」がスタートアッププロジェクトに設定されていることを確認し、「ビルド」メニューから「ソリューションのビルド」を選択するとビルドが実行されます。
構成をDebugでビルドした場合、 build_dir\Debug
に draco.lib
や draco_encoder.exe
が生成されていることが確認できるでしょう。
動作確認
生成された draco_encoder.exe
や draco_decoder.exe
が実際に動くことを確認してみましょう。また、動作確認用のOBJファイルはDracoリポジトリの testdata
ディレクトリに格納されていますので、これを使ってみます。(なお、 draco_encoder の使い方の説明は リンク先 に簡単に書いてあります。)
ファイル testdata\bunny_norm.obj
を build_dir\Debug
にコピーしておいて、コマンドプロンプトにて以下のように draco_encoder.exe
を動かしてみます。
D:\work\draco\build_dir\Debug>draco_encoder -i bunny_norm.obj -o a.drc
Encoder options:
Compression level = 7
Positions: Quantization = 11 bits
Normals: Quantization = 8 bits
Encoded mesh saved to a.drc (1605 ms to encode).
Encoded size = 69169 bytes
For better compression, increase the compression level up to '-cl 10' .
これでDracoで圧縮された a.drc
が生成されます。では逆に以下のように a.drc
から a.obj
を展開してみます。
D:\work\draco\build_dir\Debug>draco_decoder -i a.drc -o a.obj
Decoded geometry saved to a.obj (737 ms to decode)
ファイルサイズを見てみると、元の bunny_norm.obj
に比べてDraco圧縮した a.drc
は1.15%にまでファイルサイズが小さくなっていることがわかります。
また、a.obj
をダブルクリックすると(Windowsのデフォルト設定では)3Dビューアでバニーが表示されます。元のファイルである bunny_norm.obj
のダブルクリックした結果と比較して、見た目に変化がないことがわかるでしょう。
DracoのC++ API
Dracoには点群の圧縮・展開などの機能や、圧縮時の様々なパラメータなどが用意されていますが、この記事ではメッシュのデフォルトパラメータでの圧縮までの手順を記すに留めることにします。主な流れは、(a) draco::Mesh
クラスへのアトリビュート(頂点ごとの座標や法線など)の登録、(b) 三角形頂点インデックスの登録、そして (c) draco::Mesh
に登録された情報の圧縮となります。以下にこの記事のコードで利用するAPIだけをさらっと説明しますね。
頂点アトリビュートの登録
メッシュの頂点アトリビュートや三角形頂点インデックスは draco::Mesh
クラスのインスタンスに格納します。メッシュ(draco::Mesh
)で利用するアトリビュートを登録してから、そのアトリビュートに情報(座標やベクトルなど)を登録するという流れです。
メッシュへのアトリビュートの登録は AddAttribute
メソッドを用います。メソッドの引数には、アトリビュートの種類(POSITIONやNORMALなど)と、頂点数を指定します。
各アトリビュートへの情報の登録には、 draco::Mesh
の attribute
メソッド経由で呼び出す draco::PointAttribute::SetAttribute
メソッドを用います。メソッドの引数には、何番目の頂点かと、実際の情報(座標やベクトルなど)を指定します。
三角形頂点インデックスの登録
おおまかには、メッシュの各アトリビュートに対して明示的に頂点番号の割り当てることを示し、何番目のインデックスが何番目の頂点に対応するかを登録し、各ポリゴンがどの頂点から成るかを登録するという流れです。
明示的な頂点番号の割り当てを示すためには、 draco::PointAttribute::SetExplicitMapping
メソッドを用います。メソッドの引数には頂点インデックスの個数を指定します。なお、頂点番号を明示的に指定せず、0番インデックスは0番頂点、1番インデックスは1番頂点…という具合に割り当てる場合には draco::PointAttribute::SetIdentityMapping
メソッドを用います。
何番目のインデックスが何番目の頂点に対応するかを登録するには、 draco::PointAttribute::SetPointMapEntry
メソッドを用います。メソッドの引数には頂点番号とインデックス番号を指定します。
最後にポリゴンを登録します。Dracoのメッシュで用いるポリゴンは三角形のみようで、draco::Mesh::Face
は以下のように宣言されています。 typedef std::array<PointIndex, 3> Face;
各三角形に対して、このFaceのインスタンスに3つのインデックス番号を格納し、 draco::Mesh::SetFace
メソッドによってメッシュに格納します。メソッドの引数にはFace番号とFaceインスタンスを指定します。
圧縮
メッシュの圧縮にはエンコーダ draco::Encoder
を用います。圧縮データの格納先はバッファ draco::EncoderBuffer
です。エンコーダのインスタンスに圧縮パラメータを設定した上で EncodeMeshToBuffer
メソッドを呼び出します。メソッドの引数には、メッシュとバッファを指定します。
実行に成功すると、バッファ draco::EncoderBuffer
に圧縮後のバイト列が格納されます。このバイト列は data
メソッドと size
メソッドを介して読み取ることができます。
利用例(サンプルコード)
このDracoライブラリの簡単な利用例を示すことにしましょう。目標としては、トーラス(ドーナツの形状)をDracoで圧縮し、それをglTFに包んで、Blenderでこのトーラスが表示させることにしましょう。マテリアルやライティングなどは省略します。
この目標を達成するため、できるだけ無駄な処理は省きつつも、読みにくくなりにくいよう心掛けてコードを書いたつもりです。(もし読みにくかったらごめんなさい!) 一連のコードのリポジトリは以下に置いておきますので、何となれば実際に動かしてデバッガで動きを追って下さい。
https://bitbucket.org/sakira/dracotorus/src/master/
サンプルC++コードの概要
トーラスの頂点座標と法線ベクトルと頂点インデックスを計算して、それらからDracoで圧縮したデータを格納したファイルを作成するC++コードを作成しました。開発環境としてはWindows 10上のVisual Studio 2019を用いましたが、環境依存はないようにしたつもりなので、ほとんど同じコードが他の環境でも動くんじゃないかと思います。
Visual Studioでのソリューション作成
Dracoライブラリを用いるので、Dracoの各種ヘッダファイルと draco.lib へのアクセスが可能となるようにVisual Studioソリューションを作成する必要があります。
コマンドラインで動くプログラムで充分なので、プロジェクトのテンプレートには「空のプロジェクト」を選び、ソリューション名及びプロジェクト名は「DracoTorus」としました。
DracoTorusプロジェクトにはインクルードディレクトリとライブラリディレクトリとリンクするライブラリを指定するがあります。通常の開発では、Dracoのヘッダファイルと draco.lib の置き場所も整理してすべきでしょうが、今回はその手間を省いて D:\work\draco
にDracoのソリューションがあるものと想定します。
インクルードディレクトリには
D:\work\draco\src
と D:\work\draco\build_dir
を追加しました。前者にはDracoのソースファイル(拡張子.cc)とヘッダファイル(.h)が含まれ、後者にはDracoビルドのコンフィギュレーションを記した draco/draco_features.h
が含まれます。
ライブラリディレクトリには draco.lib
が作成されたディレクトリ D:\work\draco\build_dir\Debug
を指定します。ここではDebug構成のための設定なので、Debugビルドの draco.lib
が格納された場所を記していますが、それ以外の構成のためにはそれに応じた場所を指定して下さい。
リンクするライブラリには draco.lib
を追加します。
ソースコードとその簡単な説明
DracoTorusプロジェクトに作成したコードのファイルは main.cpp
、 Torus.h
、 Torus.cpp
の3つです。この内、Torus.{h,cpp}
はトーラスの形状を計算する Torus
クラスを定義し、 main.cpp
はDracoでトーラスを圧縮してファイルに保存します。
Torus.hとTorus.cpp
https://bitbucket.org/sakira/dracotorus/src/ab3381d2f5ad687643cfe31bb1d94972887a72d7/Torus.h#lines-6
トーラスの形状の頂点座標と法線ベクトルと頂点インデックスを計算します。Dracoに直接関係ないので内部の説明は省きます。
クラス Torus
のコンストラクタの引数には、トーラスの大きさを指定する2つの数値を渡します。この2つの数値を torusRadius
と tubeRadius
と称することにします。
メソッド Generate
は頂点座標、法線ベクトル、頂点インデックスを生成します。このメソッドの2つの引数は上図のtorus方向とtube方向への分割数を示します。
残りの3メソッド GetPositions
、 GetNormals
、 GetIndices
は各々、頂点座標、法線ベクトル、頂点インデックスを取得するために使います。
AddAttribute
ファイル main.cpp
の AddAttribute
は、メッシュに頂点座標や法線ベクトルを格納する関数です。
以下はメッシュが持つアトリビュートを宣言し、アトリビュートIDを取得しています。
GeometryAttribute attribute;
attribute.Init(
type, // attribute type (like POSITION or NORMAL)
nullptr, // buffer
3, // number of component
DT_FLOAT32, // data type
false, // normalized
sizeof(float) * 3, // byte stride
0); // byte offset
const int attributeId = mesh.AddAttribute(attribute, false, vertexCount);
このコード利用する2種類のアトリビュートPOSITIONとNORMALのデータ型は同じなので、ここではコードを共通化しています。メソッド AddAttribute
がアトリビュートを mesh
に登録します。最初の引数(attribute
)が各頂点のデータ型を指定し、2番目の引数(false
)は identity mapping を使わない事を指定し、3番目の引数(vertexCount
)が頂点数を指定します。(identity mappingは、0番インデックスは0番頂点、1番インデックスは1番頂点…という具合の頂点割り当て方法です。)
以下は実際の頂点座標や法線ベクトルを mesh
に格納しています。
for (uint32_t vIndex = 0; vIndex < vertexCount; ++vIndex)
mesh.attribute(attributeId)->SetAttributeValue(
AttributeValueIndex(vIndex),
vector3s.data() + vIndex * 3);
メソッド SetAttributeValue
が個々の値(頂点座標あるいは法線ベクトル)を mesh
に格納している部分です。引数には頂点番号と値へのポインタを指定しています。
なお、この関数は最後にアトリビュートIDを返しています。これはDracoのバイナリをglTFに含める際にアトリビュートIDが必要となるためです。
SetFaces
ファイル main.cpp
の SetFaces
は、メッシュに頂点インデックスを格納する関数です。
以下は、POSITIONとNORMALの両アトリビュートにて、同じ長さの頂点インデックス配列の利用を宣言しています。
mesh.attribute(positionAttributeId)->SetExplicitMapping(indices.size());
mesh.attribute(normalAttributeId)->SetExplicitMapping(indices.size());
以下では、POSITIONとNORMALの両アトリビュートに対して頂点インデックスを設定しています。
for (uint32_t vIndex = 0; vIndex < indices.size(); ++vIndex)
{
const PointIndex pIndex(vIndex);
const AttributeValueIndex avIndex(indices[vIndex]);
mesh.attribute(positionAttributeId)->SetPointMapEntry(pIndex, avIndex);
mesh.attribute(normalAttributeId)->SetPointMapEntry(pIndex, avIndex);
}
以下では三角形達を mesh
に登録しています。このコードでは既に fIndex
に三角形の頂点順に並べた頂点インデックス達を格納しているため、ここでは単に3つずつの値を face
に格納してそれを SetFace
メソッドで mesh
に登録するのみです。
for (FaceIndex fIndex(0); fIndex < static_cast<uint32_t>(indices.size() / 3); ++fIndex)
{
Mesh::Face face;
for (uint32_t cIndex = 0; cIndex < 3; ++cIndex)
face[cIndex] = fIndex.value() * 3 + cIndex;
mesh.SetFace(fIndex, face);
}
圧縮
メッシュ mesh
に格納した幾何情報を圧縮するのは main
関数内の以下の部分です。
draco::Encoder encoder;
encoder.SetSpeedOptions(0, 0);
draco::EncoderBuffer buffer;
const draco::Status status = encoder.EncodeMeshToBuffer(mesh, &buffer);
圧縮を行う encoder
にはいくつかの圧縮パラメータを指定できますが、このコードでは SetSpeedOption
のみを指定しています。ここでは、圧縮速度も展開速度も最低とし、圧縮効率が最高になるようにだけ設定しています。
最後の行の実行が成功すれば、buffer にはDraco圧縮されたデータのバイト列が格納されます。このデータへの先頭ポインタは buffer.data()
にて、バイト長は buffer.size()
にてアクセスできます。
このサンプルコードでは、このバイト列を torus.drc
というファイルに書き出しています。
glTF
軽く調べた範囲ではDracoで圧縮された幾何データをそのまま確認できるビューアは見当たりませんでした。そこで、glTFでDracoを包んで確認することにしました。glTFであればBlenderなど複数のビューアで表示できます。
以下が実際に書いたglTFコードです。
Dracoデータの記述
単純な内容なので、glTFはテキストエディタで書いてみます。glTFのバージョンは2.0とします。以下にglTF 2.0の仕様へのリンクを貼ります。
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0
テキストエディタにはVisul Studio Codeを使います。Visual Studio Codeには「glTF Tools」というExtensionがあるので、これを利用しました。
このExtensionを使うと、glTFとしてのフォーマットチェックや3Dモデルのプレビューも使えて便利です。
以下に、このglTFコードの内でDracoに関連する箇所のみを簡単に説明します。
MeshPrimitive
このglTFのシーンはMeshを一つのみ含み、そのメッシュは1つだけMeshPrimitive を含みます。この MeshPrimitive はアトリビュートとしてPOSITIONとNORMALを持ち、各々の型は0番と1番のアクセサに定義されます。アクセサはこの後で示します。頂点インデックスの型は2番アクセサで定められています。
"primitives": [
{
"attributes": {
"POSITION": 0,
"NORMAL": 1
},
"indices": 2,
MeshPrimitive のmodeは4、つまり、TRIANGLESとしています。各種定数の定義は仕様書に記されており、modeについては以下にあります。
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#primitivemode
"mode": 4,
glTFの本体仕様にはDraco圧縮は含まれていませんが、拡張仕様に定められています。
https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression
実際のデータ(バイト列)は0番bufferViewに格納され、アトリビュートの0番がPOSITIONで1番がNORMALであることを示しています。これらの番号は前述の関数 AddAttribute の戻り値であるアトリビュートIDに対応しています。
"extensions": {
"KHR_draco_mesh_compression": {
"bufferView": 0,
"attributes": {
"POSITION": 0,
"NORMAL": 1
}
}
}
}
]
Accessor
アクセサは0番, 1番, 2番を定めており、各々(上記の通り)POSITION用、NORMAL用、頂点インデックス用です。componentTypeの箇所の5126と5125は各々FLOATとUNSIGNED_INTを示します。
"accessors": [
{
"componentType": 5126,
"count": 100,
"type": "VEC3",
"min": [
-3.0, -3.0, -1.0
],
"max": [
3.0, 3.0, 1.0
]
},
{
"componentType": 5126,
"count": 100,
"type": "VEC3"
},
{
"componentType":5125,
"count": 600,
"type": "SCALAR"
}
],
Buffer, BufferView
BufferとBufferViewは各々0番の1個だけです。
0番BufferViewは0番Bufferのオフセット0から始まり大きさは2506です。2506はDracoTorusが出力した torus.drc
のファイルサイズです。
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 2506
}
],
0番Bufferは以下のように定めています。長さは2506で、実際のデータ(バイト列)は torus.drc
の中身であるとします。
"buffers": [
{
"byteLength": 2506,
"uri": "torus.drc"
}
],
Extensionの指定
glTFのルートに、どのようなExtensionが利用されているのか、そしてその内のどれが必須であるかを指定する必要があります。今回はDracoを指定するExtensionが利用できない場合の代替手段を用意していないので、このExtensionを必須なものと指定します。
"extensionsRequired": [
"KHR_draco_mesh_compression"
],
"extensionsUsed": [
"KHR_draco_mesh_compression"
]
Blenderでの表示
動作確認にはBlender 2.93.2を用いました。Blenderは以下からダウンロードできます。
ファイルメニューのインポートからglTFを選ぶと、上で記述した torus.gltf
を選択できます。これを選択すると画面に読み込まれたトーラスが表示されます。
うまくいきました! これでDracoを使うのも簡単ですね。