ドット絵を3Dモデルにして使いたくなったので色々試してみました。
ドット絵(画像ファイル)を3Dモデルに自動変換してUnityに持っていきます。繰り返し実行できるようにBlender Pythonで実行します。
(Blender Pythonの学習も兼ねてます)
変換処理の参考
https://www.youtube.com/watch?v=4yHrHn0FxXw
※ 動画では、Unityへ持っていくまでの手順を説明していません。また、動画の手順で作ったモデルをそのままUnityへ持っていこうとすると激重メッシュになってしまいます。
インポート
import bpy
import addon_utils
addon_utilsは、Blenderアドオンを管理できるモジュールです。
初期設定
io_import_images_as_planes
アドオンを有効化して、画像が貼りついた平面のメッシュを読み込みます。
BlenderのレンダリングエンジンはCYCLES
を指定します。side_mat
は後述します。
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
で取得します。
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
で調べることができます)
# シェーダーノード操作
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
は配列になります。
ジオメトリノード操作
構造はシェーダノードとほとんど同じです。最初にジオメトリノードモディファイアを作成する必要があります(bpy.ops.node.new_geometry_nodes_modifier()
)
モディファイア作成時、nodeグループに名前を付けることができなかったので、bpy.data.node_groups[-1]
で最後に作成したモディファイアを取得しています。
GeometryNodeExtrudeMesh
で押し出した部分の側面(アウトラインになるところ)はマテリアルが未設定なので、side_mat
を指定します。
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にしているのは、メッシュ化した時、モデルの内部に余計なポリゴンが作成されてしまうのを防ぐためです。これによって結構凸凹感が無くなってしまうのですが、良い感じに中身だけ消す方法が思いつきませんでした。
メッシュ化
ジオメトリノードはモディファイアなので、modifier_apply
でメッシュに変換します。その後、接続できていない点を結合したり、余計な面を削除します。(結構無理やりになってしまった...)
# メッシュ化
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にしていますが、そんなに大きくなくて良いかもです。
ちなみにこの時オブジェクトモードに移っていないとベイクに失敗します。
# 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形式でエクスポートします。
エクスポートするスクリプトはこちらを使用
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]]
)
おわりに
ここまで書いていてあれですが、UnityでSpriteに厚みを持たせるシェーダーを書いた方が楽なのではと思いました。
コード全体
その他の参考
- Blender のノードを Python から操作する
-
Getting data from a stored name attribute back to python - Coding / Python Support - Blender Artists Community
- ノードの作り方
-
Blenderでジオメトリノードで作成したオブジェクトをエクスポートする - 夜風のMixedReality
- ジオメトリをapplyする
-
Blender2.8で利用可能なpythonスクリプトを作る その15(オブジェクト指定のエクスポート、異なる形式のエクスポート) - MRが楽しい
- PythonでFBX出力
bl_idname
でノードの名前を取得できる