20210829のGoに関する記事は3件です。

FBXファイルをちょっとだけ理解した

備忘録を兼ねたメモです. 面倒くさそうな構造のファイルだなと思って今まであまり関わらないようにしてましたが,ファイル自体は頻繁に目にするので中身を読んでみました.雰囲気から推測したことも多く含まれるので注意.間違っている箇所もあるかもしれないので,ツッコミは歓迎です. FBXとは Autodesk製品で使われるファイルフォーマットです.3Dモデルデータをやりとりにデファクトスタンダードに近い形で使われているフォーマットの割には,仕様が不明瞭だったり,ツールごとの対応に差があって困るのは有名な話のようです(要出典) Wikipedia: https://ja.wikipedia.org/wiki/FBX 参考にした情報へのリンク 断片的な情報はあちこちにありますが,ボーンやブレンドシェイプでモデルを動かすところまで実装するために,色々見る必要がありました. https://help.autodesk.com/view/FBX/2017/ENU/ フォーマット自体の情報は乏しいけど,どんな値をどう扱えばよいかはFBX SDKのドキュメントが参考になる https://www.slideshare.net/L1048576/fbx-1-1 Nodeのパーサを書く上で一番参考になった(日本語) https://code.blender.org/2013/08/fbx-binary-file-format-specification/ Blenderのドキュメントも参考にしました 他 TODO あと,手元のPCにFBXを読み書きできるソフトがインストールされてなかったので,ニコニ立体やSketchfabからダウンロードしたFBXファイルを参考にしました. FBXを他のフォーマットに変換するツールを書いた 理解が正しいか確認するためにGoでパーサを実装してみました.とりあえず,Material, Model, Geometry, Deformerあたりを読み込んで,MQOやglTFに変換したらそれらく動いたので大丈夫そう. テキスト,バイナリフォーマットの両方とFBX7.5以降のバージョンにも対応しているので世の中のFBXは大体読める気がします.一方,確認のためだけに作ったので,確認に必要ない値は捨てていたり面倒な箇所は端折っています. FBXファイルの構造 3層に分けて捉えると理解しやすいです. Nodeとシリアライズフォーマット ツリー構造を持つNode Nodeは名前といくつかのAttributeを持つ ASCIIとBinaryフォーマットの二種類ある FBX Object ID(整数値),種類(Model,Material,Geometryなど),プロパティ等を持つ Objectの種類ごとにデフォルト値などを定義したPropertyTemplateがある 参照関係を持つ シーングラフ Model, Geometry, Material, Deformer等のFBX Objectで構成されるシーン Model がユーザが目にするツリーを構成するオブジェクト Model が Geometry, Material などを参照する Node Node は名前といくつかの属性値と子のNodeのリストを持ち,木構造を表現します. NodeName: Attr1, Attr2,... { ChildNode1: ... ChildNode2: ... ... } Blenderのドキュメントではノードが持つ値を Property と呼んでいますが,後で出てくるObjectのプロパティと紛らわしいのと,AutodeskのドキュメントにはAttributeという単語が現れるのでこの説明ではそちらに合わせます. バージョンごとの差異やASCIIフォーマット時の文字列の扱いなどは,このスライドが参考になりました. 文字コードはUTF-8みたいですが,SJISのFBXも世の中にあったりして闇が深そう. バイナリフォーマット 構造は単純なのでBlenderのドキュメントなどを読めば悩む箇所は無いと思います. すべて 0 で埋められた NULL-record はほとんど場合は無視できそうですが,最後のノードとファイルのフッタの境界を検出するために必要なもののようです(ファイル直下のオブジェクト数が最後まで分からない構造になっている理由は不明). 実装: https://github.com/binzume/modelconv/blob/master/fbx/binary_parser.go ASCIIフォーマット バイナリフォーマットと全く同じ構造ですが,読み込み時に型が分からないことと配列の格納方法が少し異なります. 実装: https://github.com/binzume/modelconv/blob/master/fbx/text_parser.go 数値: intとfloatの区別がつかないので配列をパースするときに少し困る.1e-10などの指数表記も許可 文字列: HTML等で使われる実体参照("等)に似たエスケープがされています.オブジェクト名の区切りの "\x00\x01" は "::" に置き換えられます 配列: *5 {a: 1,2,3,4,5} のように*配列サイズと "a"というノードに実施の値が入る.複数の配列があった場合に,a,b,c...となるのかは不明. バイト列: バイナリフォーマットにあるFileId等のバイト列の値は扱わないようです.バイナリデータを持つオブジェクトの場合はbase64でエンコードするなどして格納します. Object FBXファイルの直下には以下のようなノードが並んでいます. FBXHeaderExtension GlobalSettings: 座標系やシーン全体の設定 Documents Definitions: オブジェクトの種類ごとの定義,プロパティのデフォルト値など Objects: オブジェクトのリスト Connections: オブジェクトの接続関係 Takes FBXの構造上,特に重要なのが,Objects, Connections, Definitions です. Objects ドキュメント内のオブジェクトがすべて列挙されています. Objects: { Geometry: 1576875632, "Geometry::", "Mesh" { Properties70: { P: "Color", "ColorRGB", "Color", "",0.690196078431373,0.101960784313725,0.101960784313725 } Vertices: *1234 { ..... } PolygonVertexIndex: *18196 { .... } LayerElementUV: 0 { .... } } Model: 255650640, "Model::hello", "Mesh" { Properties70: { P: "Lcl Translation", "Lcl Translation", "", "A+",100,0,0 P: "Lcl Rotation", "Lcl Rotation", "", "A+",0,0,-90 ...... } } .... } 各 Object はノード名の他に数値のIDとオブジェクト名,用途を表す文字列を Attribute として持ちます. Objects内にはフラットにオブジェクトが並んでいるだけで,これだけでは関連や木構造はわかりません.オブジェクト間の参照関係や親子関係は後述するConnectionsに格納されます. プロパティ 各オブジェクトは Properties70 というノードを持ち,その下に含まれる名前付きのプロパティは頻繁に使います.ここで見つからない場合は下記の Definitions にある値をデフォルト値として使います.(70が何を意味する数字なのか説明は見つかりませんでした.FBX7.0を意味してるのかと思いましたが全く的外れな気もします) 各プロパティは P: "Name", "Type", "Label", "Flags", Values... という構造です.ノード名のPは特に意味がなく何でも良いようです. Type にはプロパティの種類が入っていますが,int, bool, Vector3D, KString, KTime あたりは良いとして,Lcl Translation とか Visibility みたいなのもあってフリーダムな感じです.雰囲気的にはデータ型ではなくモデリングソフトのUI上にどのように表示するかを表しているようです. Connections オブジェクト間の接続関係が書かれています.オブジェクト同士の接続は有向グラフになっていて,複数のオブジェクトから同じオブジェクトが参照されます.循環参照が許可されるかどうかは不明です. ID=0のオブジェクトはObjectsには存在しませんが,シーンのルートノードを意味します. Connections: { C: "OO",1576875632,0 C: "OO",255650640,1576875632 } この例では,上の Objects にある,Model がシーン直下に存在し,Geometryを持っていることが分かります. "OO"はオブジェクト同士の接続,"OP"はプロパティに接続されます.オブジェクトはプロパティの値自体と同一構造というわけではないので,名前付きの参照として理解するのが良さそうです. Definitions オブジェクトに対象のプロパティがない場合は,Definitions内のPropertyTemplateを参照する必要があります. 以下の例では,Modelオブジェクトの場合は Translation = [0,0,0], Rotation = [0,0,0], Scaling = [1,1,1], Visibility = 1 が初期値として使われることが分かります. Definitions: { Version: 100 Count: 3040 ObjectType: "Model" { Count: 194 PropertyTemplate: "KFbxNode" { Properties70: { P: "Lcl Translation", "Lcl Translation", "", "A+",0,0,0 P: "Lcl Rotation", "Lcl Rotation", "", "A+",0,0,0 P: "Lcl Scaling", "Lcl Scaling", "", "A+",1,1,1 P: "Visibility", "Visibility", "", "A+",1 ...... } } } } GlobalSettings 色々な座標系をサポートするために,それぞれの軸をどう扱うかが格納されています. UpAxis, FrontAxis, CoordAxis と UpAxisSign, FrontAxisSign, CoordAxisSign を見て座標系を決定します. 今回はプログラム内での座標系を特に決めてないので,GlobalSettingsから作った行列を一番最後(グローバル側)から掛けることにしました.配列等からVectorに変換する時点で入れ替えたほうが扱いやすいかもしれません. GlobalSettingsはIDも持たないしシーンにも含まれませんが,Definitionsには含まれる少し特殊なオブジェクトのようです. シーングラフ ここからが本番.ID=0の架空のオブジェクトをシーンのルートとみなしてオブジェクトを辿っていきます. - Scene(ID=0) - Model1 - Model2 (Mesh) - Geometry - Material1 - Material2 - Model3 - Model4 - Model5 Model シーン直下にいくつかのModelが存在して木構造になっています. メッシュを表すModelであれば,GeometryとMaterialを参照しています.このあたりまでは,FBX以外でもよく見る構造ですね. Model: 255650640, "Model::hello", "Mesh" { Properties70: { P: "Lcl Translation", "Lcl Translation", "", "A+",100,0,0 P: "Lcl Rotation", "Lcl Rotation", "", "A+",0,0,-90 ...... } } .... Modelの座標系 Modelはスケール,回転,移動のいわゆるTRSを持っています.メッシュをレンダリングする時などにGeometryなどが持つ頂点座標などにこれが適用されます. ツールを実装したりするときは,変換行列そのものが入ってるとありがたいのですが,残念ながらModelは変換行列を持ってないのでTRSそれぞれのプロパティから計算する必要があります.(DeformerとかPoseNodeは普通にMatrixで持ってるのに...) オブジェクトのプロパティを眺めるとLcl Translation,Lcl Rotation,Lcl Scalingとかが並んでいるのですぐわかると思います.(Lcl = Local.たぶん) TranslateMatrix(translation) * EulerToRotationMatrix(rotation, rotationOrder?) * ScaleMatrix(scaling) みたいにすると良さそう.プロパティは省略可能なので見つからなければ,PropertyTemplateからデフォルト値を探します.Rotationはオイラー角のようですが,回転順序も設定によって変わります(ZYXがデフォルト?).SphericXYZとか単なるオイラー角じゃなさそうなのもありますが,多くのツールは無視してるっぽいので今回も無視します. FBXファイルを見ながら計算してみると,それっぽい行列が得られました. ……ここまでなら,世界は平和だったのだけど現実はもっと厳しいようです.モデリングソフト上でのpivotやoffset周りの操作を知らないと計算できないし,いまや同じ会社の製品なのに3ds MaxとMayaでも違うあたりが闇の深さを感じさせます. PreRotationは比較的よく使われてるようなので,今回は気休めとして Translation * PreRotation * Rotation * Scaling としました. Geometry 頂点・面・法線などのジオメトリデータを保持します.BlendShapeに使うShapeもGeometry内で持ちます. Geometry: 1576875632, "Geometry::", "Mesh" { Vertices: *1234 { ..... } PolygonVertexIndex: *18196 { .... } LayerElementUV: 0 { .... } Shape: "morph_a" { ... } } 一番基本となるオブジェクトの形状を表す配列がGeometryオブジェクト直下に格納されています. Vertices 頂点配列.頂点数 * 3個 の数値の配列 PolygonVertexIndex 面 頂点インデックスを表す整数値の配列 Edges エッジ.頂点のペアの配列 FBXは任意の頂点数の面が扱えるので,PolygonVertexIndex は負の値を使って面の境界を表します.負の値は2の補数になっているので,ビットを反転するか,(N*-1)-1 して面の最後の頂点とします. LayerElement 頂点座標と面以外の情報は,LayerElement という構造で格納されています.例えば,LayerElementNormal, LayerElementUV, LayerElementMaterial に法線やUVやマテリアルが格納されます. LayerElementは実データやインデックスの配列とそれをどのように扱うかを持ちます.配列の名前や中身は種類によりまちまちなので,個別に扱う必要がありあそうです. MappingInformationType: ByPolygonVertex 面の頂点ごと (PolygonVertexIndex) ByControlPoint 頂点ごと (Vertices) ByPolygon 面ごと ByEdge エッジごと AllSame すべて同じ値 実装が面倒そうですが,実際に利用可能なタイプの選択肢は少ないです.たとえば,ほとんどの場合,Materialは面ごとにしか設定できないと思うので,ByPolygon 以外は考慮する意味がありません.ただ,すべて同じMaterialの場合に AllSame として格納されている場合があるので,最低限 ByPolygon + AllSame の場合について実装する必要があります. ReferenceInformationType: Direct: 実際のデータが配列で格納されている Index 現状のFBXでは使われない IndexToDirect: 実データ+インデックスの配列として格納されている 実際にはインデックスが指す配列が要素内に無い場合も意味的にインデックスとして使われるなら,IndexToDirect になるようです.例えば,LayerElementMaterial がIndexToDirectの場合,Materialsにマテリアル番号の配列が入っていますが,これはModelに紐付けられたMaterialの暗黙的な配列へのインデックスとして解釈するのだと思います(たぶん) Shape Geometry は複数の Shape を持つことが可能でBlendShape等に使えます.独立したオブジェクトとすることも,(Connectionsで参照されるのではなく)Geometry内に持たせることも可能なようです. Shape: "morph_a" { Indexes: *111 { ... } Vertices: *333 { ... } Normals: *333 { ... } } 通常のGeometry 内では LayerElement として扱われていたはずの Normals がここでは何の属性も伴わずに出現するのが少し不気味です. フォーマット上は Normals を ByPolygon とかに設定できますが,その場合は Vertices と一対一対応しなくなるので,何が起きるのか謎です. Material マテリアルの情報です. DiffuseColor + DiffuseFactor のように色 + 係数で表現しているようです ShadingModel にPhong, Lambertなどのシェーディングの種類が入っていますが大文字小文字は区別されていなさそう テクスチャはTextureオブジェクトを参照しています Deformer ボーンによるスキニングなどのGeometry を変形させるための頂点ごとのウエイトを管理します. Skinning Geometry → Deformer(Skin) → SubDeformer(Cluster) → Skeleton内のModelノード という参照関係です.Skinを表すDeformerにボーンごとのSubDeformerがぶら下がります. Deformer: 1878370368, "Deformer::", "Skin" { Version: 100 Link_DeformAcuracy: 50 } Deformer: 813549504, "SubDeformer::", "Cluster" { Version: 100 UserData: "", "" Indexes: *1234 { .... } Weights: *1234 { .... } Transform: *16 { .... } TransformLink: *16 { .... } } SubDeformerの Indexes と Weights に頂点インデックスとボーンのウエイトの配列が格納されます. 関節の初期位置は,SubDeformerが指すModelの座標ではなく,TransformLink から計算する必要があります. Skeleton内のModelの座標は初期位置ではなく,現在の変形状態を適用した状態で保存されています(おそらく). Skeletonを構成するModelノードは他のModelと区別できない気がするので,Skeletonのルートノードなどの概念は無いように見えます(要確認) BlendShape BlendShapeの場合は以下のような参照関係になり,BlendShapeChannelにShapeの頂点ごとの重みが格納されます. Geometry → Deformer(BlendShape) → SubDeformer(BlendShapeChannel) → Shapeノード Deformer: 813549500, "::SubDeformer", "BlendShapeChannel" { Version: 100 DeformPercent: 0 FullWeights: *123 { ... } } 感想 FBXは3Dモデルをやりとりするのに使われてはいるけど,モデルデータを表現する以上にモデリングソフト上での状態を保持する目的で作られてる感じがつらい.アニメーション周りは,機会があれば.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語 のpanic を正しくrecover する

Go言語 のpanic とrecover について panic について Go言語には、例外処理のために一般に使われるerrors に加えて、panic という処理が存在する。 基本的には回復不能と判断された実行時エラーを発生させる機構。 recover について panic を取得し、エラー処理を実装できる機構。これがないとプログラムはクラッシュしてしまう。 使用上の注意点 言語仕様なのでドキュメントを読めば書いてあるが、慣れた人ほど注意した方が良いこと recover() は関数内部で実行されることで初めてpanicを受け取ってくれる 下記のコードを実行すると、panic が回収されず、そのままプログラムが終了する package main func main() { recover() panic("パニックが回収されませんでした") } 一方で次のコードのように、関数内部で受け取ってやることでちゃんとpanicが回収されて、プログラムが正常に終了する package main import "fmt" func main() { defer func() { if r := recover(); r != nil { fmt.Println("パニックが回収されました") } }() panic("パニックが回収されませんでした") } 以上のように、panic は正しく受け取ってあげないとプログラムが全落ちしてしまうので気をつけて使おう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語で構造体データをファイルに書き出し/読み込みをする方法メモ

はじめに やりたいこと Goのプログラム内で作成した構造体のデータをバイナリにシリアライズし、ファイルに保存します。 そしてプログラムを次回実行したときには、そのファイルを読み込んでデシリアライズし、構造体のデータをメモリに乗せます。 丁度、Pythonであればpickleで出来るようなことを、Goで実行する方法のメモです。 コード 概要 Goの標準ライブラリであるgobを用います。 Encoderというクラスで構造体データをバイナリにシリアライズし、Decoderというクラスでそれをデシリアライズします。 ※サンプルコードからは、例外処理を省いています。 書き込み import ( "encoding/gob" ) // 今回保存したいデータ var stageTiles [screenLength][screenLength]tileInfo type tileInfo struct { SpritesheetNum int TileType string } // 中略 // セーブファイルの作成 file, _ := os.Create("savefile.gob") defer file.Close() // エンコーダーの作成 encoder := gob.NewEncoder(file) // エンコード encoder.Encode(stageTiles) 注意したいのが、シリアライズする構造体のフィールドです。構造体のフィールドが他のパッケージからアクセスできるようになっていないと(i.e. 先頭が大文字になっていないと)、値はシリアライズされるデータに含まれません。 Structs encode and decode only exported fields. - https://pkg.go.dev/encoding/gob 上のコードの場合、tileInfoというtypeの二重配列をファイルに保存していますが、最初私はこのtypeのフィールド名を小文字にしていたため、読み込み時に値が初期値(intなら0, stringなら空文字)となっていて、困惑しました。 読み込み import ( "encoding/gob" ) // 読み込んだデータを保持する変数 var stageTiles [screenLength][screenLength]tileInfo type tileInfo struct { SpritesheetNum int TileType string } // 中略 file, _ := os.Open("savefile.gob") defer file.Close() decoder := gob.NewDecoder(file) err = decoder.Decode(&stageTiles) 最後に 参考 データのやりとりに gob を使う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む