GOMファイルフォーマットを作るよ Game Oriented Model Format(GOMフォーマット)を作っていくよ。シンプルで、アニメーションだけ含んだゲーム制作初心者向けフォーマットだよ。 GOM(Game Oriented Model Format)草案 GOMフォーマット仕様書 Game Oriented Model Format (.gom) Version 1.0 (Complete Specification) 1. 目的 GOMは、FBX等のDCCデータをゲーム実行向けに最適化し、 オフライン変換して保存するためのフォーマットである。 設計目標 スキニングはモデル(geometry)空間で完結 インスタンスごとに別Transform・別アニメ時間を安全に扱える FBX差(ノード階層・geometry transform)を吸収 ワールド直スキニング事故を防止 実行時にFBXSDKやufbxなどのライブラリに依存しない 同一FBXから常に同一GOMが生成される(決定性保証) 2. 空間設計ポリシー(不変条件) Invariant A: 頂点空間 頂点は常に geometry(=モデルローカル)空間 スキニング結果も geometry 空間 instanceWorld は描画直前に1回のみ適用 禁止事項 スキニング途中で world に焼く geometry_to_world を途中適用する 3. 行列規約(最終固定) GOM v1では以下を固定する。 数値型:float32 メモリ並び:row-major 演算規約:row-vector 右掛け(p' = p * M) 行列は常に 4x4 座標系:Left-handed / Y-up / meters 4. ファイル形式 GOMはテキスト形式(GOMT0001)とバイナリ形式(GOMB0001)を持つ。 マジックナンバー(先頭8バイト) GOMT0001 GOMB0001 5. テキスト形式(GOMT0001) UTF-8(BOMなし)、構造リテラル形式。 5.1 Skeleton Skeleton = { boneCount = , bones = [ { parent = , name = "", path = "", bindLocal = [[...],[...],[...],[...]] } ] } bones配列順がboneIndex pathは同名衝突対策 5.2 MeshPart Meshes = [ { name = "", meshBindGeoToModel = [[...],[...],[...],[...]], vertices = [ { p,n,t,uv,bi,bw } ], indices = [ ... ], submeshes = [ { materialIndex, indexStart, indexCount } ], inverseBind = [ [[...]] ] } ] ウェイト規約(必須) 最大4影響 合計1.0正規化 weight降順、同値はboneIndex昇順タイブレーク 5.3 Animation Animations = [ { name = "", duration = , rootBoneIndex = , flags = ["HasRootMotion"], tracks = [ { bone, keys[] } ] } ] 補間:T/S=linear、R=slerp 6. MeshPart規約 Rule M1 FBXメッシュノード単位でMeshPart生成 マテリアル分割はSUB0で表現 Rule M2 以下の場合はMeshPartを分割する: 別スケルトン参照 inverseBindが異なる 頂点レイアウトが異なる 7. バイナリ形式(GOMB0001) 7.1 固定ヘッダ(512 bytes) Little Endian固定。 7.1.1 固定値 magic は常に GOMB0001 (8 bytes ASCII) headerSize は常に 512 formatVersion は常に 1 7.1.2 列挙値(enum)定義 本仕様の enum はすべて uint32 とする。 未知の enum 値を受け取った場合、読み込みはエラーとする(v1)。 endianness(将来用) v1ではファイルはLittle Endian固定だが、将来拡張のためヘッダに enum を持つ。 ENDIAN_LE = 1 (v1は常にこれ) ENDIAN_BE = 2 (予約) coordinateSystem CS_LH_YUP_METERS = 1 matrixConvention 行列の「格納順+演算規約」を表す。 MC_ROW_MAJOR_ROW_VECTOR_RIGHT_MUL = 1 定義: 行列は row-major で格納される(m00,m01,m02,m03,m10,...) 位置は row-vector として扱い p' = p * M (p=[x y z 1]) 7.1.3 validationFlags(bitfield) validationFlags は uint32 。 bit0(1<<0): bindPosePass bit1(1<<1): weightsPass bit2(1<<2): submeshMaterialsPass 予約:bit3以降は将来用。v1では0。 7.1.4 ChunkTableの指定 chunkTableOffset == 0 の場合:ChunkTableなし chunkTableOffset != 0 の場合:ファイル先頭からのバイトオフセットで ChunkTable 先頭を指す chunkTableCount はエントリ数 char magic[8] // GOMB0001 uint32 headerSize // 512 uint32 formatVersion // 1 uint32 coordinateSystem // enum uint32 matrixConvention // enum uint8 sourceHash[32] // SHA-256 raw uint32 converterVersionStr // STR0 index uint32 validationFlags // bitfield uint32 chunkTableOffset uint32 chunkTableCount uint8 reserved[...] // 0埋め 8. バイナリ全体の堅牢ルール(バイナリ専用) 8.1 Endianness Little Endian固定 8.2 アライメント チャンクは16バイト境界にアライン 8.3 チャンク基本構造 uint32 fourcc uint32 size uint32 version uint32 flags byte payload[size] pad to 16 8.4 チャンクサイズ上限チェック(必須) sizeがファイル末尾を超える場合は即エラー sizeが実装上限を超える場合はエラー 8.5 未知チャンク 未対応fourccはsize分スキップ可能 8.6 ChunkTable(任意) chunkTableOffset!=0 の場合、ChunkEntry配列を保持 8.7 MSH0.flags(任意) bit0 = indices16 bit1 = hasGBTM 予約ビットは0 9. バイナリチャンク厳密レイアウト STR0 uint32 stringCount uint32 offsets[stringCount] uint32 dataSize uint8 data[dataSize] SKEL uint32 boneCount int32 parent[boneCount] uint32 nameId[boneCount] uint32 pathId[boneCount] float32 bindLocal[boneCount][16] MSH0 uint32 meshNameId uint32 vertexFormat uint32 vertexCount uint32 indexCount uint32 boneCount uint32 flags uint32 reserved[2] VB0 uint32 vertexCount uint32 stride uint32 vertexFormat uint32 reserved uint8 data[vertexCount*stride] IB0 uint32 indexCount uint32 indexSizeBytes uint32 reserved[2] uint8 data[indexCount*indexSizeBytes] SUB0 uint32 submeshCount struct { int32 materialIndex; uint32 indexStart; uint32 indexCount; } IBND uint32 boneCount float32 inverseBind[boneCount][16] BND0 float32 aabbMin[3] float32 aabbMax[3] float32 sphereCenter[3] float32 sphereRadius GBTM(任意) float32 meshBindGeoToModel[16] ANIM uint32 clipCount CLP0 uint32 clipNameId float32 duration float32 sampleRate int32 rootBoneIndex uint32 flags uint32 trackCount Track構造: int32 boneIndex uint32 keyCount for each key: float32 time float32 tr[3] float32 rot[4] float32 sc[3] 10. 変換ツール検証(必須) Bind整合 ウェイト合計 Submesh完全被覆 boneIndex範囲チェック index範囲チェック 11. 実装安全規約(v1推奨) 11.1 読み込み上限値(推奨) 実装は以下の上限を持つことを推奨する(超過時はエラー)。 maxBoneCount = 4096 maxVertexCount = 10,000,000 maxIndexCount = 30,000,000 maxClipCount = 1024 maxTrackCount = 8192 maxKeyCount = 1,000,000(クリップ単位) ※ 実装側で変更可能だが、無制限読み込みは禁止。 11.2 必須チャンク条件(バイナリ) 各 MSH0 は以下の子チャンクを必須とする: VB0 IB0 SUB0 IBND BND0 GBTM は任意。 IB0.indexSizeBytes は 2 または 4 のみ許可。 それ以外は読み込みエラー。 11.3 アニメキー時刻規約 keys[].time は昇順必須 0 <= time <= duration ループ再生時は time = fmod(time, duration) を推奨 11.4 inverseBind の定義 inverseBind[i] は、bind姿勢における boneWorld[i] の逆行列に相当する。 スキニングパレットは以下で構成される: palette[i] = boneWorld_current[i] * inverseBind[i] これにより、geometry 空間内でスキニングが完結する。 End of GOM Specification v1.0 GOM Format quick Reference GOM + MTA フォーマット 簡易仕様書 Game Oriented Model Format + Motion TimeLine Animation バージョン: GOM v1.0 / MTA v1.0 最終更新: 2026-03-18 📖 目次 概要 GOM フォーマット MTA フォーマット 基本規約 ファイル構造 主要チャンク ウェイト正規化 決定性保証 互換性 実装ガイド テキストフォーマット よくある質問(FAQ) まとめ 概要 GOM(Game Oriented Model) 目的 : FBX等のDCCデータをゲーム実行向けに最適化し、オフライン変換して保存する 設計目標 : スキニングはモデル(geometry)空間で完結 インスタンスごとに別Transform・別アニメ時間を安全に扱える FBX差(ノード階層・geometry transform)を吸収 ワールド直スキニング事故を防止 実行時にFBXSDKに依存しない 同一FBXから常に同一GOMが生成される(決定性保証) MTA(Motion TimeLine Animation / GOMTA) 目的 : GOMのアニメーション部分のみを独立させた兄弟フォーマット 特徴 : メッシュ/マテリアルを含まない スケルトン実体を含まない(skeletonSig のみ) 複数のアニメーションファイルを用意して動的に切り替え可能 異なる GOM ファイルでも同じスケルトンなら適用可能 GOM フォーマット ファイル形式 形式 マジックナンバー 説明 テキスト GOMT0001 UTF-8、構造リテラル形式 バイナリ GOMB0001 Little Endian、16-byte アライン 含まれる要素 Skeleton(SKEL) : ボーン階層、bind pose Mesh(MSH0) : 頂点、インデックス、サブメッシュ Material(MATL) : Phong最小セット、テクスチャ参照 Animation(ANIM/CLP0) : ボーンアニメーション String Table(STR0) : 文字列テーブル(名前、パス) ファイルサイズ ヘッダ: 512 bytes チャンク: 可変長(16-byte アライン) MTA フォーマット ファイル形式 形式 マジックナンバー 説明 バイナリ MTAB0001 Little Endian、16-byte アライン 注意 : v1 ではテキスト形式(MTAT0001)は非サポート。詳細は テキストフォーマット セクション参照。 含まれる要素 String Table(STR0) : 文字列テーブル(必須) Animation(ANIM/CLP0) : アニメーションクリップ skeletonSig : スケルトン互換性チェック(SHA-256, 32 bytes) 除外される要素 ❌ メッシュ(MSH0, VB0, IB0, SUB0, IBND) ❌ マテリアル(MATL) ❌ スケルトン実体(SKEL) ❌ Geometry Transform(GBTM) スケルトン互換性チェック 実行時判定 : if (memcmp(gom.skeletonSig, mta.skeletonSig, 32) != 0) { error("Skeleton signature mismatch!"); } skeletonSig 生成 : SHA-256( boneCount (uint32, little-endian) + foreach boneIndex in bonePath 辞書順: bonePath (UTF-8) + '\0' + parentIndex (int32, little-endian) ) 基本規約 空間設計ポリシー(Invariant A) GOM の最重要設計原則。 Invariant A: 頂点空間 頂点は常に geometry(=モデルローカル)空間 スキニング結果も geometry 空間 instanceWorld は 描画直前に1回のみ適用 禁止事項 : ❌ スキニング途中で world に焼く ❌ geometry_to_world を途中適用する 描画時の変換式 : // 正しい変換順序 vec4 skinnedPos = vertex * skinning; // geometry 空間 vec4 worldPos = skinnedPos * GBTM * instanceWorld; // world 空間 vec4 clipPos = worldPos * viewProj; // clip 空間 この設計の利点 : インスタンシング安全 : 同一メッシュを異なる位置/回転で配置可能 アニメーション独立 : インスタンスごとに異なるアニメ時間を適用可能 FBX差の吸収 : ノード階層の違いを完全に隠蔽 事故防止 : ワールド直スキニングによる破綻を防止 行列規約 項目 値 数値型 float32 メモリ並び row-major 演算規約 row-vector 右掛け ( p' = p * M ) 行列サイズ 4x4(常に) 行列演算の具体例 : // ベクトルは行ベクトルとして扱う vec4 p = {x, y, z, 1.0f}; vec4 p_transformed = p * M; // 右から掛ける // 複数の変換 vec4 result = p * M1 * M2 * M3; // 左から順に適用 メモリレイアウト(row-major) : float m[16] = { m00, m01, m02, m03, // 第1行 m10, m11, m12, m13, // 第2行 m20, m21, m22, m23, // 第3行 m30, m31, m32, m33 // 第4行 }; // アクセス方法 float value = m[row * 4 + col]; 変換順序の例 : // スキニング → GBTM → インスタンス変換 → ビュー → プロジェクション vec4 worldPos = vertex * skinning * GBTM * instanceWorld; vec4 viewPos = worldPos * view; vec4 clipPos = viewPos * projection; // または一度に vec4 clipPos = vertex * skinning * GBTM * instanceWorld * view * projection; 座標系 項目 値 ハンドネス Left-handed 上方向 Y-up 単位 meters バイナリ規約 項目 値 エンディアン Little Endian アラインメント 16-byte パディング値 0x00 ファイル構造 GOM/MTA バイナリファイル構造 GOM ファイル構造 +-------------------------+ offset 0 | Header (512 bytes) | +-------------------------+ offset 512 | STR0 Chunk | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | MATL Chunk | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | SKEL Chunk (任意) | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | MSH0 Chunk | | header (16) | | VB0, IB0, SUB0, | | IBND, BND0, GBTM | | padding (0x00) | +-------------------------+ ALIGN16(...) | ANIM Chunk (任意) | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | CLP0 Chunk(s) (任意) | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ MTA ファイル構造 +-------------------------+ offset 0 | Header (512 bytes) | | skeletonSig (32 bytes)| | at offset 76 | +-------------------------+ offset 512 | STR0 Chunk (必須) | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | ANIM Chunk | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | CLP0 Chunk #1 | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | CLP0 Chunk #2 | | header (16) | | payload (...) | | padding (0x00) | +-------------------------+ ALIGN16(...) | ... (複数クリップ可) | +-------------------------+ 主な違い : GOM : MATL, SKEL, MSH0 を含む(フルモデルデータ) MTA : STR0, ANIM, CLP0 のみ(アニメーションのみ) MTA ヘッダ : offset 76 に skeletonSig (32 bytes) を含む ヘッダ構造(512 bytes) GOM ヘッダ オフセット フィールド 型 サイズ 説明 0 magic char[8] 8 "GOMB0001" 8 headerSize uint32 4 = 512 12 formatVersion uint32 4 = 1 16 coordinateSystem uint32 4 = 1 (LH/Y-up/meters) 20 matrixConvention uint32 4 = 1 (row-major) 24 sourceHash uint8[32] 32 SHA-256 of source 56 converterVersionStrId uint32 4 STR0参照ID 60 validationFlags uint32 4 ビットフィールド 64 chunkTableOffset uint64 8 0 = なし 72 chunkTableCount uint32 4 ChunkEntry数 76 reserved uint8[436] 436 0埋め MTA ヘッダ オフセット フィールド 型 サイズ 説明 0 magic char[8] 8 "MTAB0001" 8 headerSize uint32 4 = 512 12 formatVersion uint32 4 = 1 16 coordinateSystem uint32 4 = 1 (LH/Y-up/meters) 20 matrixConvention uint32 4 = 1 (row-major) 24 sourceHash uint8[32] 32 SHA-256 of source 56 converterVersionStrId uint32 4 STR0参照ID 60 validationFlags uint32 4 v1 では 0 固定 64 chunkTableOffset uint64 8 0 = なし 72 chunkTableCount uint32 4 ChunkEntry数 76 skeletonSig uint8[32] 32 SHA-256 of skeleton 108 reserved uint8[404] 404 0埋め 重要な違い : GOM: offset 76 から reserved[436] MTA: offset 76 から skeletonSig[32]、offset 108 から reserved[404] GOM ヘッダテーブルと MTA ヘッダテーブルの後に以下を追加: validationFlags ビット定義 validationFlags は変換ツールが実行した検証をビットフラグで記録する。 ビット マスク 名称 意味 bit0 1<<0 VALIDATED_BIND_MATRICES inverseBind * bindLocal = Identity を検証済み bit1 1<<1 VALIDATED_WEIGHTS 全頂点のウェイト合計が 1.0 であることを検証済み bit2 1<<2 VALIDATED_SUBMESHES サブメッシュが完全被覆(隙間・重複なし)を検証済み bit3 1<<3 VALIDATED_INDICES 全インデックスが範囲内を検証済み bit4 1<<4 VALIDATED_BONE_INDICES 全boneIndexが範囲内を検証済み bit5 1<<5 VALIDATED_MATERIAL_INDICES 全materialIndexが範囲内を検証済み その他のビットは予約(v1では0)。 変換ツール側(書き込み) : uint32_t flags = 0; flags |= (1 << 0); // VALIDATED_BIND_MATRICES flags |= (1 << 1); // VALIDATED_WEIGHTS flags |= (1 << 2); // VALIDATED_SUBMESHES flags |= (1 << 3); // VALIDATED_INDICES flags |= (1 << 4); // VALIDATED_BONE_INDICES flags |= (1 << 5); // VALIDATED_MATERIAL_INDICES header.validationFlags = flags; ローダー側(読み込み) : // 参考情報として使用してもよいが、 // ローダーは独自の検証を省略してはならない bool bindValidated = (header.validationFlags & (1 << 0)) != 0; bool weightsValidated = (header.validationFlags & (1 << 1)) != 0; // 未知のビットは無視(警告を出してもよい) uint32_t unknownBits = header.validationFlags & ~0x3F; // bit0-5以外 if (unknownBits != 0) { // 警告: 未知の検証フラグが立っている } 重要な規約 : 変換ツールは実行した検証に対応するビットを立てる ローダーは validationFlags を参考情報として扱う ローダーは validationFlags に関係なく独自の検証を実行しなければならない 未知のビットは無視してよい(警告推奨) チャンク共通構造(16 bytes) struct ChunkHeader { uint32_t fourcc; // チャンク識別子(4文字) uint32_t size; // ペイロードサイズ(バイト) uint32_t version; // v1 では 1 固定 uint32_t flags; // v1 では 0 固定 }; fourcc 規約 フォーマット : 正確に 4 文字 の ASCII 文字 大文字のみ 数字を許可 文字セット: [A-Z0-9] 3文字 fourcc の扱い : 3文字の識別子(例: VB0 , IB0 )は、null 終端で 4 バイトに格納する: 'VB0\0' // null終端で4バイト 'IB0\0' // null終端で4バイト 有効な例 : STR0 , MATL , SKEL , MSH0 VB0 , IB0 , SUB0 , IBND BND0 , GBTM , ANIM , CLP0 無効な例 : str0 (小文字は不可) Mesh (小文字を含む) MAT (長さが 4 でない) MATLL (長さが 4 を超える) 実装例 : // fourcc を uint32 として扱う(little-endian) uint32_t fourcc_vb0 = 0x30304256; // 'VB0\0' を little-endian で格納 // V=0x56, B=0x42, 0=0x30, \0=0x00 // バイト配列として比較 if (memcmp(&chunk_fourcc, "VB0\0", 4) == 0) { // VB0 チャンク処理 } 注意事項 : fourcc は 必ず 4 バイト として扱う 3文字の場合は null 終端( \0 ) で埋める スペース( )埋めは使用しない 主要チャンク STR0(文字列テーブル)- 必須 構造 : stringCount: uint32 offsets: uint32[stringCount] stringData: char[] // UTF-8, null終端 規約 : 重複排除 辞書順ソート(UTF-8 byte-wise) ID: 0xFFFFFFFF = なし MATL(Material Slots)- GOM 必須 構造 : materialSlotCount: uint32 slots: MaterialSlot[materialSlotCount] MaterialSlot : nameId: uint32 flags: uint32 kd, ks, ke: float32[3] shininess, opacity: float32 diffuseMapId, normalMapId, specularMapId, emissiveMapId: uint32 デフォルト白マテリアル : kd = (1.0, 1.0, 1.0) ks = (0.04, 0.04, 0.04) ke = (0.0, 0.0, 0.0) shininess = 32.0 opacity = 1.0 SKEL(Skeleton)- GOM 任意 構造(SoA形式) : boneCount: uint32 parent: int32[boneCount] // -1 = 親なし nameId: uint32[boneCount] // STR0参照 pathId: uint32[boneCount] // STR0参照 bindLocal: float32[boneCount][16] // row-major 4x4 規約 : parent[i] < i(循環参照防止) bonePath 辞書順でソート MSH0(MeshPart Container)- GOM 必須 内部チャンク : VB0: 頂点バッファ IB0: インデックスバッファ(uint32) SUB0: サブメッシュ定義 IBND: InverseBind行列 BND0: BindLocal行列(任意) GBTM: MeshBindGeoToModel(任意) VF01 頂点フォーマット(72 bytes) : position: float32 x3 (12 bytes) normal: float32 x3 (12 bytes) tangent: float32 x4 (16 bytes) // w=handedness uv0: float32 x2 (8 bytes) boneIndex: uint16 x4 (8 bytes) boneWeight: float32 x4 (16 bytes) ANIM / CLP0(Animation) ANIM 構造 : clipCount: uint32 clips: CLP0[clipCount] CLP0 構造 : nameId: uint32 duration: float32 // seconds rootBoneIndex: int32 interpMode: uint32 // IM_LINEAR_SLERP = 1 flags: uint32 // HasRootMotion, Loop(予約) trackCount: uint32 tracks: Track[trackCount] Track 構造 : boneIndex: uint32 trKeyCount, rotKeyCount, scKeyCount: uint32 trKeys: TrKey[] rotKeys: RotKey[] scKeys: ScKey[] 補間規約 : Translation/Scale: 線形補間 Rotation: slerp(dot<0 なら flip) ウェイト正規化 規約 GOM v1 では、すべての頂点のボーンウェイトは以下の規約に従う: 最大4影響 : 5つ以上の影響は上位4つのみ保持 合計1.0 : ウェイト合計を 1.0 に正規化 ソート規約 : boneWeight 降順 同値の場合は boneIndex 昇順 正規化アルゴリズム 入力 : n 個のボーン影響 (boneIndex[i], boneWeight[i]) 出力 : 最大4個の正規化された影響 手順 : // 1. weight 降順、同値なら boneIndex 昇順でソート std::sort(influences.begin(), influences.end(), [](auto& a, auto& b) { if (a.weight != b.weight) return a.weight > b.weight; // 降順 return a.boneIndex < b.boneIndex; // 昇順 }); // 2. 上位4つのみ保持 if (influences.size() > 4) { influences.resize(4); } // 3. 合計を計算 float sum = 0.0f; for (auto& inf : influences) { sum += inf.weight; } // 4. 正規化 for (auto& inf : influences) { inf.weight /= sum; } // 5. 4個未満の場合は0埋め while (influences.size() < 4) { influences.push_back({0, 0.0f}); } 例 例 1: 3影響 入力: bi=[3,1,2], bw=[0.5,0.3,0.2] 出力: bi=[3,1,2,0], bw=[0.5,0.3,0.2,0.0] // 既に降順、0埋め 例 2: ソートが必要 入力: bi=[1,3,2], bw=[0.3,0.5,0.2] ソート後: bi=[3,1,2], bw=[0.5,0.3,0.2] 出力: bi=[3,1,2,0], bw=[0.5,0.3,0.2,0.0] 例 3: 5影響(トリミング必要) 入力: bi=[0,1,2,3,4], bw=[0.4,0.3,0.2,0.08,0.02] 上位4つ: bi=[0,1,2,3], bw=[0.4,0.3,0.2,0.08] 合計: 0.98 正規化: bi=[0,1,2,3], bw=[0.408,0.306,0.204,0.082] 例 4: 同値の場合 入力: bi=[3,1,2], bw=[0.5,0.25,0.25] ソート: bi=[3,1,2], bw=[0.5,0.25,0.25] // 0.25 同値は boneIndex 昇順 出力: bi=[3,1,2,0], bw=[0.5,0.25,0.25,0.0] 検証 変換ツールは以下を保証しなければならない: [x] すべての頂点が 4 影響以下 [x] ウェイト合計が 1.0 ± 1e-4 [x] boneWeight 降順、同値なら boneIndex 昇順 [x] boneIndex が範囲内( 0 <= boneIndex < boneCount ) 決定性保証 ソート規約 すべての順序は UTF-8 byte-wise 辞書順 で確定: bonePath : SKEL の boneIndex 順序 meshNodePath : MSH0 の MeshPart 順序 materialKey : MATL の materialIndex 順序 clipName : MTA の CLP0 順序 比較方法 // UTF-8 byte-wise 比較(memcmp / strcmp) // ロケール依存禁止 // 大文字小文字折りたたみ禁止 浮動小数点 出力直前に float32 へキャスト VertexKey は bitwise 一致比較 検証は許容誤差(1e-5 推奨) 互換性 GOM v1.0 サポート範囲 : スキンメッシュ / スキンなしメッシュ Phong 最小マテリアル ボーンアニメーション(T/R/S) ルートモーション抽出(X/Z移動、Y軸回転) 制限 : 頂点フォーマット: VF01 のみ UV: 1セットのみ インデックス: uint32 のみ 頂点カラー: 非対応 MTA v1.0 サポート範囲 : 複数アニメーションクリップ skeletonSig 互換性チェック 決定性保証 制限 : GOM の SKEL と互換性がある場合のみ適用可能 boneIndex は GOM の boneCount 未満でなければならない 実行時互換性チェック GOM 側 : // SKEL から skeletonSig を計算 uint8_t gom_sig[32]; compute_skeleton_sig(skel, gom_sig); MTA 側 : // ヘッダから skeletonSig を読み込み uint8_t mta_sig[32]; memcpy(mta_sig, mta_header.skeletonSig, 32); 判定 : if (memcmp(gom_sig, mta_sig, 32) != 0) { error("Skeleton mismatch!"); } 実装ガイド 推奨上限値 項目 推奨値 maxBoneCount 4096 maxVertexCount 10,000,000 maxIndexCount 30,000,000 maxMaterialSlotCount 65,535 maxSubmeshCount 65,535 maxClipCount 1024 maxTrackCount 8192 maxKeyCountPerClip 1,000,000 検証規約 必須チェック: Bind Pose Consistency : inverseBind * bindLocal = Identity Weight Normalization : sum(boneWeight) = 1.0 ± 1e-4 Bone Index Range : 0 <= boneIndex < boneCount Index Range : 0 <= index < vertexCount Submesh Coverage : 重複なし、隙間なし、3の倍数 Material Index Range : 0 <= materialIndex < materialSlotCount ローダー実装例 void ParseGOM(const uint8_t* data, size_t fileSize) { // 1. ヘッダを読み込む(512 bytes) const GOMBHeader* header = reinterpret_cast(data); ValidateHeader(header); // 2. チャンクを順次読み込む size_t offset = 512; while (offset < fileSize) { const ChunkHeader* chunk = reinterpret_cast(data + offset); // サイズ検証 if (offset + 16 + chunk->size > fileSize) { Error("Invalid chunk size"); return; } // payload 処理 const uint8_t* payload = data + offset + 16; switch (chunk->fourcc) { case FOURCC('STR0'): ParseSTR0(payload, chunk->size); break; case FOURCC('SKEL'): ParseSKEL(payload, chunk->size); break; // ... 他のチャンク default: // 未知チャンクはスキップ break; } // 次のチャンクに移動(16-byte アライメント) offset = ALIGN16(offset + 16 + chunk->size); } } テキストフォーマット GOMT0001(GOM テキスト形式) 概要 : UTF-8(BOMなし) 構造リテラル形式 人間が読み書き可能 デバッグやプロトタイピングに便利 基本構造 Skeleton = { ... } MaterialSlots = [ ... ] Meshes = [ ... ] Animations = [ ... ] Skeleton 例 Skeleton = { boneCount = 3, bones = [ { parent = -1, name = "Root", path = "Root", bindLocal = [...] }, { parent = 0, name = "Spine", path = "Root/Spine", bindLocal = [...] }, { parent = 1, name = "Arm_L", path = "Root/Spine/Arm_L", bindLocal = [...] } ] } MaterialSlots 例 MaterialSlots = [ { name = "DefaultMaterial", phong = { kd = [1.0, 1.0, 1.0], ks = [0.04, 0.04, 0.04], ke = [0.0, 0.0, 0.0], shininess = 32.0, opacity = 1.0 }, maps = { diffuse = "textures/diffuse.png", normal = "", specular = "", emissive = "" } } ] Meshes 例 Meshes = [ { name = "CubeMesh", meshBindGeoToModel = [...], // 4x4 行列 vertices = [ { p = [x, y, z], n = [nx, ny, nz], t = [tx, ty, tz, tw], uv = [u, v], bi = [0, 0, 0, 0], bw = [1.0, 0, 0, 0] }, // ... 他の頂点 ], indices = [0, 1, 2, 2, 3, 0, ...], submeshes = [ { materialIndex = 0, indexStart = 0, indexCount = 36 } ], inverseBind = [ [...], [...], [...] ] // boneCount 個の 4x4 行列 } ] Animations 例 Animations = [ { name = "WalkCycle", duration = 1.0, rootBoneIndex = 0, flags = 0, tracks = [ { boneIndex = 0, trKeys = [ { time = 0.0, value = [0, 0, 0] }, ... ], rotKeys = [ { time = 0.0, value = [0, 0, 0, 1] }, ... ], scKeys = [ { time = 0.0, value = [1, 1, 1] }, ... ] }, // ... 他のボーンのトラック ] } ] MTA テキスト形式(v1 では非サポート) v1 では MTA のテキスト形式(MTAT0001)は存在しません 。 理由 : 決定性保証の困難(改行コード、空白、エンコーディング等) パーサの複雑性増大 v1 の設計方針(deterministic binary, AI safe)に集中 v1 でサポートされる形式 : ✅ MTAB0001 (バイナリ形式のみ) ❌ MTAT0001 (テキスト形式は非サポート) v2 以降での検討事項 : テキスト形式が本当に必要かどうか(デバッグ用途) 必要な場合、canonical form をどう定義するか バイナリ形式で十分な場合、追加しない可能性もある v1 実装者への注意 : "MTAT0001" 以外のマジックナンバーは読み込みエラーとする エラーメッセージ例: "Unsupported MTA format. Only MTAB0001 is supported in v1." テキスト形式とバイナリ形式の比較 項目 テキスト形式 バイナリ形式 可読性 ◎ 人間が読める ✕ バイナリ ファイルサイズ ✕ 大きい ◎ 小さい パース速度 △ 遅い ◎ 速い デバッグ ◎ 簡単 △ ツール必要 プロトタイピング ◎ 手書き可能 ✕ ツール必須 プロダクション △ 非推奨 ◎ 推奨 推奨用途 : テキスト形式 : 開発中のデバッグ、プロトタイピング、テストデータ作成 バイナリ形式 : 最終ビルド、プロダクション環境、大規模データ 変換ツール実装例 // 1. FBX を読み込む(FBXSDK) // 2. bonePath 辞書順でボーンをソート // 3. meshNodePath 辞書順でメッシュをソート // 4. materialKey 辞書順でマテリアルをソート // 5. 頂点を geometry 空間に変換 // 6. ウェイトを正規化(最大4影響、合計1.0) // 7. GOM/MTA を出力(16-byte アライン) よくある質問(FAQ) Q1: GOM と FBX の違いは? A : GOM は FBX のゲーム実行用最適化版です。 項目 FBX GOM 実行時依存 FBXSDK 必要 不要 geometry 空間 ノードごとに異なる 統一 決定性 保証なし 保証あり ファイルサイズ 大きい 小さい パース速度 遅い 速い Q2: MTA は必須ですか? A : いいえ、任意です。GOM 単独でも動作します。 使い分け : GOM のみ : ANIM チャンクにアニメーションを含める 小規模プロジェクト、アニメーション数が少ない場合 GOM + MTA : アニメーションを独立管理 大規模プロジェクト、動的なアニメーション切り替えが必要な場合 Q3: テキスト形式とバイナリ形式、どちらを使うべき? A : 用途によります。 用途 推奨形式 理由 デバッグ GOMT(テキスト) 人間が読める、Git diff が見やすい プロトタイピング GOMT(テキスト) 手書き可能、素早い修正 プロダクション GOMB(バイナリ) 高速、ファイルサイズ小 MTA MTAB(バイナリのみ) v1 ではテキスト非サポート Q4: skeletonSig が一致しないエラーが出ます A : 以下を確認してください: チェックリスト : bonePath が一致しているか GOM と MTA で同じボーン階層か? ボーン名が完全に一致しているか? parentIndex が一致しているか 親子関係が同じか? boneCount が一致しているか ボーン数が同じか? 重要 : bindLocal が異なっていても、階層構造(bonePath + parentIndex)が同じなら適用可能です。 デバッグ方法 : // GOM の skeletonSig を出力 uint8_t gom_sig[32]; compute_skeleton_sig(gom.SKEL, gom_sig); printf("GOM skeletonSig: "); for (int i = 0; i < 32; ++i) printf("%02x", gom_sig[i]); printf("\n"); // MTA の skeletonSig を出力 printf("MTA skeletonSig: "); for (int i = 0; i < 32; ++i) printf("%02x", mta.header.skeletonSig[i]); printf("\n"); Q5: 複数の MTA ファイルを同時に使えますか? A : はい、可能です。 条件 : すべて同じ skeletonSig clipName が重複しない ファイル例 : character.gom character.idle.mta character.walk.mta character.run.mta character.attack.mta 使い方 : // すべての MTA を読み込む LoadMTA("character.idle.mta"); LoadMTA("character.walk.mta"); LoadMTA("character.run.mta"); // clipName で再生 PlayClip("idle"); PlayClip("walk"); PlayClip("run"); 注意 : GOM と MTA で同じ clipName があるとエラーになります。 Q6: 頂点フォーマットは変更できますか? A : v1 では VF01 のみサポートです。 VF01 の内容 : position: float32 x3 normal: float32 x3 tangent: float32 x4 uv0: float32 x2 boneIndex: uint16 x4 boneWeight: float32 x4 v2 以降の拡張予定 : 複数 UV セット 頂点カラー 追加の tangent カスタム頂点属性 Q7: 決定性保証とは何ですか? A : 同一の入力から常に同一の出力が生成されることを保証します。 具体例 : # 同じ FBX を変換すると、常に同じ GOM が生成される ./converter input.fbx -o output1.gom ./converter input.fbx -o output2.gom diff output1.gom output2.gom # 差分なし 保証される項目 : ボーンの順序(bonePath 辞書順) メッシュの順序(meshNodePath 辞書順) マテリアルの順序(materialKey 辞書順) アニメーションクリップの順序(clipName 辞書順) 浮動小数点値(float32 精度で一致) なぜ重要か : バージョン管理(Git)で差分が明確 自動テストが安定 ビルドの再現性が保証される Q8: ウェイトが正規化されないとどうなりますか? A : スキニング結果が破綻します。 正常な場合 : boneWeight = [0.5, 0.3, 0.2, 0.0] // 合計 1.0 異常な場合 : boneWeight = [0.5, 0.3, 0.2, 0.1] // 合計 1.1(スケールが変わる) boneWeight = [0.5, 0.3, 0.1, 0.0] // 合計 0.9(スケールが変わる) 対策 : 変換ツールは必ずウェイトを正規化 ローダーは validationFlags を確認(参考情報) ローダーは独自に検証( sum(boneWeight) == 1.0 ± 1e-4 ) まとめ GOM の利点 FBX差を完全に吸収 geometry 空間でスキニング完結 決定性保証(同一FBX→同一GOM) 実行時にFBXSDK不要 MTA の利点 アニメーションの独立管理 ファイルサイズ削減(SKEL省略) 異なるGOMでも適用可能(skeletonSig一致) 複数アニメーションの動的切り替え 詳細仕様 詳細は以下を参照: GOM 詳細仕様 : Docs/GOM_Docs/spec_Sources/Original/INDEX.md MTA 詳細仕様 : Docs/GOM_Docs/spec_Sources/Original/spec_binary/20_mta_format.md 変更履歴 : Docs/GOM_Docs/spec_Sources/Original/changes/CHANGELOG.md 作成日 : 2025-01-XX バージョン : GOM v1.0 / MTA v1.0 ライセンス : プロジェクト固有 End of Quick Reference