4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UnityAdvent Calendar 2024

Day 24

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

Posted at

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


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

image.png

変換処理の参考

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は配列になります。

image.png

ジオメトリノード操作

構造はシェーダノードとほとんど同じです。最初にジオメトリノードモディファイアを作成する必要があります(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にしているのは、メッシュ化した時、モデルの内部に余計なポリゴンが作成されてしまうのを防ぐためです。これによって結構凸凹感が無くなってしまうのですが、良い感じに中身だけ消す方法が思いつきませんでした。

image.png

チェックしないと内部もキューブで埋められる。
image.png

メッシュ化

ジオメトリノードはモディファイアなので、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]]
)

image.png

おわりに

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

コード全体

その他の参考

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?