BlenderからGLB / FBXへ:ゲーム用3DアセットのExport契約を固定する
Node、Socket、Bone、Axis、Scale、export対象をコードで固定し、BrowserとUnityへ安全に渡すasset pipeline。
Blenderでモデルを作れても、ゲーム側へ正しく渡せなければasset pipelineとしては不十分です。
実運用では、形状そのものより次のような境界で不具合が起きます。
- scaleが違う
- forward axisが違う
- 必要nodeがexportされていない
- preview cameraまで混入した
- hidden meshが欠けた
- rig bone名が変わった
- runtimeが期待するsocket名が消えた
3D将棋とSlime Mercenariesでは、GLB / FBX exportを単なる「保存形式」ではなく、ゲームとのAPI境界として扱っています。
GLBではruntime nodeを固定する
Slime Mercenariesのenemy GLBには、familyが違っても共通nodeを持たせています。
EnemyRoot
BodyRoot
FaceRoot
Eye_L
Eye_R
AttackOrigin
EffectOrigin
GroundOrigin
Mushroom familyなら追加でStem、Cap、Mouthなどがあります。
runtimeはgeometryの頂点構造ではなく、こうしたsemantic nodeへ依存します。
AttackOrigin
-> projectile / hit effect start
EffectOrigin
-> VFX anchor
GroundOrigin
-> ground-relative effect
meshを作り直しても、このcontractを保てばruntime側は変わりません。
Sourceの名前とruntimeの意味を分ける
Sword SlimeにはWeaponSocketがあります。
重要なのは、
"剣meshの名前"
!=
"武器を取り付ける意味的なanchor"
であることです。
geometry名を直接APIにすると、造形変更がgame code変更へ波及します。
socketやoriginをsemantic APIにしておけば、剣が一つのmeshから複数partへ分割されてもruntime contractを維持できます。
GLB export optionも固定する
Slime GLBは次のprofileで出しています。
bpy.ops.export_scene.gltf(
filepath=str(output_path),
export_format="GLB",
export_yup=True,
export_apply=False,
export_morph=True,
export_morph_normal=False,
export_morph_tangent=False,
export_cameras=False,
export_lights=False,
)
ポイントは「毎回GUIで設定する」のではなく、export option自体をsourceへ含めることです。
Shape Keyをruntimeで使うためexport_morphを有効にし、preview camera/lightは不要なので除外する、という意図がGit履歴へ残ります。
Unity FBXではaxis変換を明示する
3D将棋のFBXではUnity向けにexport条件を固定しています。
bpy.ops.export_scene.fbx(
filepath=str(path),
use_selection=True,
object_types={
"EMPTY",
"MESH",
"ARMATURE",
},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_UNITS",
use_space_transform=True,
bake_space_transform=False,
axis_forward="-Z",
axis_up="Y",
use_mesh_modifiers=True,
add_leaf_bones=False,
bake_anim=False,
embed_textures=False,
)
「Blenderで正面に見える」だけでは不十分です。
engineへimportした後のforward / up / scaleをexport profileとして固定します。
Export前に対象hierarchyを選び直す
3D将棋ではscene全体を無条件exportしません。
objects = descendants(root)
bpy.ops.object.select_all(
action="DESELECT"
)
for obj in objects:
obj.hide_viewport = False
obj.hide_render = False
obj.hide_set(False)
obj.select_set(True)
hidden stateのglyphもruntime assetには必要なので、export前に明示的に対象へ戻します。
逆にpreview用objectはroot hierarchyへ入れないためexportされません。
「混入してはいけないもの」も検証する
兵士exportでは、旧盤Geometryがsceneへ残っていたら失敗させます。
if any(
obj.name == "Board_9x9"
or obj.name.startswith("Grid_X_")
or obj.name.startswith("Grid_Y_")
for obj in bpy.context.scene.objects
):
raise RuntimeError(
"Legacy board geometry exists "
"in the soldier scene."
)
必要objectが存在するかだけでなく、存在してはいけないobjectもcontractにできます。
このチェックは、以前に盤と兵士のgenerator責務が混ざっていたことへの再発防止です。
Bone contractもexport前に止める
rigged soldierはUnity Humanoidが必要とするbone集合をexport前に確認します。
required = {
"Hips",
"Spine",
"Head",
"UpperArm_L",
"LowerArm_L",
"Hand_L",
"UpperArm_R",
"LowerArm_R",
"Hand_R",
"UpperLeg_L",
"LowerLeg_L",
"Foot_L",
"UpperLeg_R",
"LowerLeg_R",
"Foot_R",
}
不足していればFBXを書きません。
missing = (
required
- set(
armature.data.bones.keys()
)
)
if missing:
raise RuntimeError(
"Humanoid export blocked; "
f"missing bones: {sorted(missing)}"
)
engine import後のerrorを、asset build時のerrorへ前倒ししています。
盤の座標contractはasset acceptanceにする
3D将棋の盤はゲームロジックと強く結びついているため、acceptance条件も明示しています。
Board bounds ≈ (9.50, 0.56, 9.50)
Cell_5_5 = Unity origin
Adjacent cell distance = 1.0
file +1 = +X
rank +1 = -Z
さらに、
Board_Base = 9.5 x 9.5 x 0.55
Horizontal Grid = 10
Vertical Grid = 10
Marker混入なし
Preview Camera/Light/Ground混入なし
まで確認します。
「見た目が盤っぽい」ではなく、gameplay coordinate systemの一部としてassetを検証します。
Runtime scaleでごまかしすぎない
UnityやThree.js側でscale、rotation、offsetを補正すれば見た目を合わせることはできます。
ただし補正が大量になると、「正しいsource scaleがどれか」が分からなくなります。
runtime scaleはcamera上の見せ方として必要な場合もありますが、
- unit convention
- forward axis
- origin
- socket位置
のような意味的contractまでruntime補正へ押し込まないようにしています。
GLBとFBXで共通している考え方
形式は違っても、pipelineの考え方は同じです。
| 項目 | GLB / Browser | FBX / Unity |
|---|---|---|
| Root | SlimeRoot / EnemyRoot | Soldier / Armature root |
| Anchor | Socket / Origin | Bone / Empty |
| Axis | glTF Y-up | -Z forward / Y up |
| Morph | Shape Keyをexport | 必要に応じて別管理 |
| Animation | runtime motionも利用 | Humanoid / Generic clip |
| Preview object | exportしない | exportしない |
| Validation | node / bounds | bone / hierarchy / scale |
ゲーム側にとって重要なのはファイル形式より、import後にもsemantic contractが残っていることです。
Asset pipelineをAPI設計として考える
最終的に役立った考え方は、Blenderからのexportを「ファイル保存」と考えないことでした。
Blender source
|
| asset API
v
GLB / FBX
|
| runtime contract
v
Three.js / Unity
APIならrequired field、forbidden field、type、version、validationを考えます。
3D assetにも同じように、
- required node
- parent relation
- axis
- bounds
- bone name
- animation ownership
を持たせると、モデル数が増えても壊れ方を限定しやすくなりました。