Blender Pythonでドット絵を3Dモデルに自動変換してUnityへ持っていく

ドット絵を3Dモデルにして使いたくなったので色々試してみました。

---

ドット絵(画像ファイル)を3Dモデルに自動変換してUnityに持っていきます。繰り返し実行できるようにBlender Pythonで実行します。
(Blender Pythonの学習も兼ねてます)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/2eb83db0-6d0e-0a4c-4892-225fb6cfa483.png)



# 変換処理の参考
https://www.youtube.com/watch?v=4yHrHn0FxXw
※ 動画では、Unityへ持っていくまでの手順を説明していません。また、動画の手順で作ったモデルをそのままUnityへ持っていこうとすると激重メッシュになってしまいます。

# インポート
```python
import bpy
import addon_utils
```
addon_utilsは、Blenderアドオンを管理できるモジュールです。

# 初期設定
`io_import_images_as_planes`アドオンを有効化して、画像が貼りついた平面のメッシュを読み込みます。
Blenderのレンダリングエンジンは`CYCLES`を指定します。`side_mat`は後述します。

```python
addon_utils.enable("io_import_images_as_planes",default_set=True)
bpy.context.scene.render.engine = 'CYCLES'
side_mat =  bpy.data.materials.new(name="side")
side_mat.diffuse_color = (0, 0, 0, 1)
```

# 画像読み込み
`import_image.to_plane`で画像を読み込み、読み込み時に生成されるマテリアルを`bpy.data.materials`で取得します。

```Python
dir_path = "ディレクトリパス"
file_name = 'Fish.png'
bpy.ops.import_image.to_plane(files=[{"name":file_name}], directory=dir_path, relative=False)
art_material = bpy.data.materials[file_name.split(".")[0]]
art_material.show_transparent_back = False
```

# シェーダーノード操作
シェーダーノードは取得したマテリアルの`node_tree`で取得することができます。
ただし、`node_tree.nodes`に登録されるノード名と生成時(`.new`)に指定するノードのType名が一致していない点は注意が必要です。
(生成時に指定するTypeは`node.bl_idname`で調べることができます)

```Python
# シェーダーノード操作
node_tree = art_material.node_tree
node_tree.nodes["Image Texture"].interpolation = 'Closest' #ドット絵なので補間しない

image_texture = node_tree.nodes["Image Texture"]
bsdf = node_tree.nodes["Principled BSDF"]
tex_coord = node_tree.nodes.new("ShaderNodeTexCoord")
mapping = node_tree.nodes.new("ShaderNodeMapping")

links = node_tree.links
links.new(tex_coord.outputs[3], mapping.inputs[0])
links.new(mapping.outputs[0], image_texture.inputs[0])
links.new(image_texture.outputs[0], bsdf.inputs[0]) 

mapping.inputs[1].default_value[0] = 0.5
mapping.inputs[1].default_value[1] = 0.5
mapping.inputs[1].default_value[2] = 0.5
```

linksはノード間の繋がりのことです。outputsはノードの右側のソケットで、上から0,1,2...です。inputsは左側のソケットです。
`default_value`はそのソケットの値です。XYZなど1つのソケットで複数の値を持つ場合、`default_value`は配列になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/035a5163-0be5-fcfb-285f-f19423f8ca42.png)

# ジオメトリノード操作
構造はシェーダノードとほとんど同じです。最初にジオメトリノードモディファイアを作成する必要があります(`bpy.ops.node.new_geometry_nodes_modifier()`)
モディファイア作成時、nodeグループに名前を付けることができなかったので、`bpy.data.node_groups[-1]`で最後に作成したモディファイアを取得しています。

`GeometryNodeExtrudeMesh`で押し出した部分の側面(アウトラインになるところ)はマテリアルが未設定なので、`side_mat`を指定します。

```Python
bpy.ops.node.new_geometry_nodes_modifier()
node_tree = bpy.data.node_groups[-1]
output = node_tree.nodes["Group Output"]
input = node_tree.nodes["Group Input"]

sub_mesh = node_tree.nodes.new(type="GeometryNodeSubdivideMesh")
extrude_mesh1 = node_tree.nodes.new(type="GeometryNodeExtrudeMesh")
extrude_mesh2 = node_tree.nodes.new(type="GeometryNodeExtrudeMesh")
delete_geo = node_tree.nodes.new(type="GeometryNodeDeleteGeometry")
join_geo = node_tree.nodes.new(type="GeometryNodeJoinGeometry")
flip_faces = node_tree.nodes.new(type="GeometryNodeFlipFaces")
input_pos = node_tree.nodes.new(type="GeometryNodeInputPosition")
add = node_tree.nodes.new(type="ShaderNodeVectorMath")
image_texture = node_tree.nodes.new(type="GeometryNodeImageTexture")
equal = node_tree.nodes.new(type="FunctionNodeCompare")
map_range = node_tree.nodes.new(type="ShaderNodeMapRange")
side_or = node_tree.nodes.new(type="FunctionNodeBooleanMath")
set_material = node_tree.nodes.new(type="GeometryNodeSetMaterial")

# デフォルトのリンクは邪魔なので全て消す。
links = node_tree.links
for l in links: links.remove(l)
    
links.new(input.outputs[0], sub_mesh.inputs[0])
links.new(sub_mesh.outputs[0], delete_geo.inputs[0])
# links.new(delete_geo.outputs[0], output.inputs[0])

links.new(input_pos.outputs[0], add.inputs[0])
links.new(add.outputs[0], image_texture.inputs[1])
links.new(image_texture.outputs[0], map_range.inputs[0])
links.new(image_texture.outputs[1], equal.inputs[0])
links.new(map_range.outputs[0], extrude_mesh1.inputs[3])
links.new(map_range.outputs[0], extrude_mesh2.inputs[3])
links.new(equal.outputs[0], delete_geo.inputs[1])
links.new(delete_geo.outputs[0], flip_faces.inputs[0])
links.new(flip_faces.outputs[0], extrude_mesh1.inputs[0])
links.new(delete_geo.outputs[0], extrude_mesh2.inputs[0])
links.new(extrude_mesh1.outputs[0], join_geo.inputs[0])
links.new(extrude_mesh2.outputs[0], join_geo.inputs[0])
links.new(extrude_mesh1.outputs[2], side_or.inputs[0])
links.new(extrude_mesh2.outputs[2], side_or.inputs[1])
links.new(side_or.outputs[0], set_material.inputs[1])

links.new(join_geo.outputs[0], set_material.inputs[0])
links.new(set_material.outputs[0], output.inputs[0])


sub_mesh.inputs[1].default_value = 5
extrude_mesh1.inputs[3].default_value = 0.1
extrude_mesh2.inputs[3].default_value = 0.1
add.inputs[1].default_value[0] = 0.5
add.inputs[1].default_value[1] = 0.5
add.inputs[1].default_value[2] = 0.5
map_range.inputs[3].default_value = 0.02
map_range.inputs[4].default_value = 0.05

delete_geo.domain = "FACE"
equal.operation = "EQUAL"
side_or.operation = "OR"

image_texture.inputs[0].default_value = bpy.data.images[file_name]
image_texture.interpolation = 'Closest'

set_material.inputs[2].default_value = side_mat

extrude_mesh1.inputs[4].default_value = False
extrude_mesh2.inputs[4].default_value = False
```

## extrude_mesh.inputs[4].default_value = False の理由
Extrude MeshのIndividualをFalseにしているのは、メッシュ化した時、モデルの内部に余計なポリゴンが作成されてしまうのを防ぐためです。これによって結構凸凹感が無くなってしまうのですが、良い感じに中身だけ消す方法が思いつきませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/96dfa77b-f29f-a4ed-7ca6-79f2029ef071.png)

チェックしないと内部もキューブで埋められる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/a87e71b1-5016-6632-4367-27f55d561a36.png)

### メッシュ化
ジオメトリノードはモディファイアなので、`modifier_apply`でメッシュに変換します。その後、接続できていない点を結合したり、余計な面を削除します。(結構無理やりになってしまった...)

```Python
# メッシュ化
bpy.ops.object.modifier_apply(modifier="GeometryNodes")

# 結構無理やり
bpy.context.scene.tool_settings.use_mesh_automerge = True
bpy.context.scene.tool_settings.use_mesh_automerge_and_split = True
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.transform.rotate(value=6.28319, orient_axis='X', orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False) # もっとスマートな方法があるかも
```


# UV展開とベイク
Blenderのシェーダーの設定をそのままUnityへ持っていくことはできないので、テクスチャにベイクして持っていきます。サイズを1024にしていますが、そんなに大きくなくて良いかもです。

ちなみにこの時オブジェクトモードに移っていないとベイクに失敗します。

```Python
# UV展開とテクスチャのベイク処理
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.material_slot_select()
bpy.ops.uv.smart_project()
node_tree = art_material.node_tree
baked_image_texture = node_tree.nodes.new("ShaderNodeTexImage")
image = bpy.data.images.new("Baked" + file_name.split(".")[0], width=1024, height=1024)
baked_image_texture.image = image

bpy.context.scene.cycles.bake_type = 'DIFFUSE'
bpy.context.scene.render.bake.use_pass_direct = False
bpy.context.scene.render.bake.use_pass_indirect = False

bpy.ops.object.editmode_toggle()
node_tree.nodes.active = baked_image_texture
bpy.ops.object.bake(type="DIFFUSE")

# テクスチャ保存
image.save_render(filepath= dir_path + "Baked" + file_name.split(".")[0] + ".png")
```

# FBXエクスポートしてUnityでインポート
fbx形式でエクスポートします。
エクスポートするスクリプトは[こちら](https://bluebirdofoz.hatenablog.com/entry/2020/04/07/025803)を使用

```Python
def export_targets_fbx
# 省略 https://bluebirdofoz.hatenablog.com/entry/2020/04/07/025803

export_targets_fbx(
    arg_filepath= dir_path +  file_name.split(".")[0] + ".fbx",
    arg_targetnames=[file_name.split(".")[0]]
)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/58eea4ae-d011-fbeb-192c-4b260d300966.png)

# おわりに
ここまで書いていてあれですが、UnityでSpriteに厚みを持たせるシェーダーを書いた方が楽なのではと思いました。

# コード全体
https://gist.github.com/s-n-1-0/a4b5a57ff842335622e32009bc7c6923

# その他の参考
+ [Blender のノードを Python から操作する](https://dskjal.com/blender/process-node-from-python.html)
+ [Getting data from a stored name attribute back to python - Coding / Python Support - Blender Artists Community](https://blenderartists.org/t/getting-data-from-a-stored-name-attribute-back-to-python/1561258)
	+ ノードの作り方
+ [Blenderでジオメトリノードで作成したオブジェクトをエクスポートする - 夜風のMixedReality](https://redhologerbera.hatenablog.com/entry/2024/05/29/215441)
	+ ジオメトリをapplyする
+ [Blender2.8で利用可能なpythonスクリプトを作る その１５（オブジェクト指定のエクスポート、異なる形式のエクスポート） - MRが楽しい](https://bluebirdofoz.hatenablog.com/entry/2020/04/07/025803)
	+ PythonでFBX出力

https://qiita.com/yukimituki11/items/dc945cca0b1fc429aa07

`bl_idname` でノードの名前を取得できる