Blender生成3Dモデルを自動テストする:Bounds・Silhouette・Motion QA
Export済みGLBの再import、形状比率、親子構造、床貫通、Root Motion、turntableまでを検証する3D asset QA。
Blender Pythonでモデルを自動生成すると、次に起きる問題があります。
「生成に成功した」モデルが、普通に見た目として壊れている。
scriptが例外なく終わることと、ゲーム用assetとして成立することは別です。
Slime Mercenariesと3D将棋では、3D assetに対してコードのunit testに近いvalidationと、renderによるvisual QAを両方使っています。
GLBを再importして検証する
Slime Mercenariesのenemy validatorは、Python source objectを直接見るのではなく、一度exportしたGLBをBlenderへ再importします。
bpy.ops.import_scene.gltf(
filepath=str(path)
)
これが重要です。
source sceneでは存在したnodeがexport optionで落ちている可能性があるからです。
Source scene is correct
!=
Exported asset is correct
最終的にruntimeへ渡すfileそのものを検査します。
Required nodeを検査する
enemyごとにrequired nodeを持ちます。
共通contractは次です。
EnemyRoot
BodyRoot
FaceRoot
Eye_L
Eye_R
AttackOrigin
EffectOrigin
GroundOrigin
validatorでは存在を確認します。
missing = [
name
for name in profile.required_nodes
if bpy.data.objects.get(name)
is None
]
if missing:
raise AssertionError(
"missing required nodes: "
+ ", ".join(missing)
)
これはvisual qualityというよりruntime APIの検査です。
Boundsで異常scaleを止める
root以下のmeshからworld boundsを計算します。
corners = []
for obj in root.children_recursive:
if obj.type != "MESH":
continue
corners.extend(
obj.matrix_world
@ Vector(corner)
for corner in obj.bound_box
)
そこからsizeを出し、degenerate geometryや異常scaleを止めます。
size = maximum - minimum
if min(size) <= 0.05:
raise AssertionError(
"degenerate bounds"
)
if max(size) > 5.0:
raise AssertionError(
"unexpectedly large "
"source bounds"
)
scaleを100倍間違えたassetをgalleryで初めて発見するより、build直後に止める方が速いです。
Silhouetteもratioで検査できる
見た目は完全には数値化できません。
ただし、「このcharacterで絶対に守りたいshape」はratioでかなり検査できます。
例えばGreat Mushroomは、
silhouette_rule=lambda s:
1.24 <= s.x / s.z <= 1.38
and s.x >= 2.0
としています。
これは「ボスキノコは横に広い」というart directionを数値化したものです。

完全なvisual scoreではありませんが、何かの修正で通常キノコの細い体型へ戻ってしまうregressionは検出できます。
部品の相対サイズを見る
character全体の大きさだけでは不十分です。
Great Mushroomでは部品のratioも見ています。
RelativeSizeRule(
"Cap",
"x",
0.95,
1.02,
)
RelativeSizeRule(
"CapLayer_Top",
"x",
0.56,
0.68,
)
こうすると、
帽子は存在する
しかし小さすぎて
character identityが消えた
というregressionを止められます。
Parent relationもvisual contractになる
animation用pivotが必要なassetでは、親子構造も検査します。
ParentRule(
"Cap",
"PrimaryRoot",
)
ParentRule(
"CapLayer_Back",
"SecondaryRoot",
)
ParentRule(
"CapLayer_Top",
"SecondaryRoot",
)
meshが正しい位置に見えても、parentが間違っているとanimation開始時に壊れます。
rest poseだけでは分からないため、hierarchyをcontractにします。
Forbidden nodeも持つ
Validationは「必要なもの」だけではありません。
例えばGreat Mushroomでは、
forbidden_prefixes=(
"BodyLobe_",
"SporePouch_",
)
を指定しています。
他variantの部品がcopy-pasteで混入するのを防ぐためです。
「余計なmeshがあっても動くからよい」とすると、後からcharacter identityとruntime motion targetが混ざります。
Mesh countは雑だが役に立つ
Great Mushroomには、
min_meshes=22
max_meshes=25
というrangeもあります。
mesh countは品質指標ではありません。
ただし22前後だったcharacterが修正後に5 meshになったなら、かなり高い確率で重要部品が落ちています。
逆に100 meshになった場合も、意図しない複製を疑えます。
安いsanity checkとしては有効です。
顔の比率も検査する
Slime Mercenariesでは、小さな画面でも顔を読める必要があります。一方、目が大きくなりすぎるとcharacter全体のsilhouetteより顔だけが支配的になります。
enemy validatorではeye objectのworld sizeとcharacter heightを比較しています。
if (
max(
eye_size.x,
eye_size.z,
)
/ size.z
> 0.12
):
raise AssertionError(
"eye-size regression"
)
さらに左右のeye位置が概ね対称か、local -Y側のfrontに置かれているかも確認します。
感覚的な修正でも、一度「ここを越えると崩れる」という範囲が分かればguardにできます。
Animationはground penetrationまで検査する
3D将棋の歩兵motionでは、keyframeだけでなく1/4 frame刻みでfloor penetrationを確認しています。
for tick in range(
4,
int(action.frame_end) * 4 + 1,
):
frame = tick / 4
scene.frame_set(
int(frame),
subframe=frame % 1,
)
for part in (
"Boot_L",
"Boot_R",
"Spear_Tip",
):
floor = min(
(
obj.matrix_world
@ vertex.co
).z
for vertex
in obj.data.vertices
)
assert floor >= -0.006
keyframe位置だけ確認すると、補間途中で床へ刺さるケースを見逃します。そのためsubframeまで見ます。
四脚や車輪では「支えがあること」も見る
桂馬や香車では逆に、「浮いていない」ことも重要です。
soles = [
min(
(
obj.matrix_world
@ v.co
).z
for v in obj.data.vertices
)
for obj in contacts
]
assert min(soles) > -0.005
assert min(soles) < 0.015
香車では4輪すべてが一定以上浮いていないかも確認します。
assert max(soles) < 0.018
見た目上の「接地感」を完全には判定できませんが、明らかなfloat / penetrationは自動で除外できます。
Gameplay rootが動いていないかを見る
3D将棋ではRoot Motion禁止なので、motion validatorで毎frame確認します。
assert (
rig.pose.bones["Root"]
.location.length
< 0.00001
)
assert (
rig.pose.bones["Hips"]
.location.length
< 0.00001
)
animationを修正した結果rootへtranslationが入っても、そのままproductionへ入りません。
これはvisual QAというよりGameplay contractのQAです。
Turntableは「見えない角度」をなくすために使う
静止画一枚では、裏側や上面の破綻を見逃します。
3D将棋にはturntable render scriptがあり、複数elevation × 複数azimuthを自動renderします。
for elev in elevations:
for i in range(steps):
deg = i * 360 / steps
cam.location = (...)
look_at(cam, target)
bpy.ops.render.render(
write_still=True
)
さらに真上90度も別にrenderします。
ほぼ真上のcameraだけでは隠れるplanar overlapを見つけるためです。
Motion sampleもframeを固定する
Animationも毎回動画を全部見るだけでは比較しづらいため、代表frameを固定してrenderできます。
idle_start
idle_peak
move_peak
anticipation_start
anticipation_peak
recovery
同じframeのbefore / afterを並べると、bodyの沈み、weapon位置、secondary motionの変化を比較しやすくなります。
最後はgameplay cameraで見る
ここまでvalidationしても、人間のvisual reviewは消せません。
自動checkが通っていても、
- mobileでは顔が読めない
- weaponがUIに隠れる
- attack directionが伝わらない
- silhouetteが他characterと似すぎる
- secondary motionが不自然
といった問題は残ります。
そのため最終QAは、
structural validation
↓
motion validation
↓
turntable / representative frames
↓
gallery
↓
actual game camera
の順で行います。
3D assetにもtest pyramidを作れる
最終的には、3D asset QAもソフトウェアtestとかなり似た構造になりました。
cheap deterministic checks
node / bounds / parent / ratio
↓
motion checks
ground / root / seam / direction
↓
rendered visual checks
turntable / key frames
↓
integration checks
Unity / browser gameplay camera
すべてを画像認識で自動採点する必要はありません。
機械で確実に落とせるregressionだけ先に落とし、人間は見た目の判断へ集中する。
Blender Pythonでassetを生成するなら、この分業がかなり効きます。