Blenderはゲームエンジンみたいなのに乗せるのに向いてない気がしたので、とりあえず作ってみた。
とりあえず関数を書いてみる
引数は、マテリアル名、オブジェクト名、テクスチャのサイズの3つでよさそう。
def bake_material_textures(original_material_name,object_name,size):
定型文+α
まず、レンダリングをCyclesにする。
# レンダリング設定
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.samples = 64
次に、ベイク先のオブジェクト用に、コピーする。
# オブジェクトをコピー
new_obj = bpy.data.objects[object_name].copy()
# オブジェクトのデータをコピー
new_mesh = bpy.data.objects[object_name].data.copy()
new_obj.data = new_mesh
bpy.context.collection.objects.link(new_obj)
コピーしたやつを選択する。
# オブジェクトを選択してアクティブにする
bpy.context.view_layer.objects.active = new_obj
マテリアルを選択する
# ベイクするマテリアルを設定
original_material = bpy.data.materials[original_material_name]
bpy.context.object.active_material = original_material
UVを作成。これがないとうまく行かなかった。
# ベイク用のUVマップを作成
obj = bpy.context.edit_object
if obj:
bpy.ops.mesh.uv_texture_add()
ファイル名は適当。
diffuse_texture_name = original_material_name + '_diffuse'
metallic_texture_name = original_material_name + '_metallic'
roughness_texture_name = original_material_name + '_roughness'
emission_texture_name = original_material_name + '_emission'
normal_texture_name = original_material_name + '_normal'
とりあえず、ディフューズ、メタリック、ラフネス、エミッション、ノーマルの5つをベイクすることにする。
テクスチャオブジェクトの作成
ディフューズ以外はNon-colorを指定しないと特にNormalは、ずれがよくわかる。
# テクスチャを作成
diffuse_texture = bpy.data.images.new(diffuse_texture_name, width=size, height=size)
metallic_texture = bpy.data.images.new(metallic_texture_name, width=size, height=size)
roughness_texture = bpy.data.images.new(roughness_texture_name, width=size, height=size)
emission_texture = bpy.data.images.new(emission_texture_name, width=size, height=size)
normal_texture = bpy.data.images.new(normal_texture_name, width=size, height=size)
metallic_texture.colorspace_settings.name = "Non-Color"
roughness_texture.colorspace_settings.name = "Non-Color"
emission_texture.colorspace_settings.name = "Non-Color"
normal_texture.colorspace_settings.name = "Non-Color"
マテリアルの作成
元のマテリアルをコピーするのが一番手っ取り早そう。
# 新しいマテリアルを作成する
new_material = original_material.copy()
ノード作成&取得
# マテリアルにノードを追加する
new_material.use_nodes = True
principled_node = new_material.node_tree.nodes["Principled BSDF"]
diffuse_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
metallic_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
roughness_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
emission_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
normal_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
# 画像テクスチャをノードに設定する
diffuse_node.image = diffuse_texture
metallic_node.image = metallic_texture
roughness_node.image = roughness_texture
emission_node.image = emission_texture
normal_node.image = normal_texture
use_nodesは忘れがち。
テクスチャのベイク
以下の関数を定義した。
def bake_texture_to(name,principled_node,new_material,float_value=False):
links = new_material.node_tree.links
link = principled_node.inputs[name].links
color_value_node = None
if(len(link) != 0):
links.new(link[0].from_socket, new_material.node_tree.nodes['Material Output'].inputs['Surface'])
else:
color_value_node = new_material.node_tree.nodes.new("ShaderNodeValue") if float_value else new_material.node_tree.nodes.new("ShaderNodeRGB")
color_value_node.outputs[0].default_value = principled_node.inputs[name].default_value
links.new(color_value_node.outputs[0], new_material.node_tree.nodes['Material Output'].inputs['Surface'])
bpy.ops.object.bake(type='EMIT')
if color_value_node:
new_material.node_tree.nodes.remove(color_value_node)
何をやっているかというと、
- プリンシプルBSDFが何にもつながっていないとき
=>デフォルトの値を出力 - 何かにつながっているとき
=>その値を出力
という分岐を行っている
ベイクするときに、bpy.ops.object.bake(type="DIFFUSE")などを使いたくなるが、これらは影等も焼きこまれてしまうので、このように、EMISSIONとしてベイクする必要があった。
また、ベイクする値がColorかFloatかによっても分ける必要があった。こういうときに、動的型付けは便利ですね。
ベイクしていく
bpy.context.view_layer.objects.active = new_obj
bpy.data.objects[object_name].select_set(False)
bpy.data.objects[object_name].hide_render = True
bpy.context.view_layer.objects.active.active_material = new_material
# テクスチャベイクを行う
new_material.node_tree.nodes.active = diffuse_node
bake_texture_to("Base Color",principled_node,new_material)
new_material.node_tree.nodes.active = metallic_node
bake_texture_to("Metallic",principled_node,new_material,float_value=True)
new_material.node_tree.nodes.active = roughness_node
bake_texture_to("Roughness",principled_node,new_material,float_value=True)
new_material.node_tree.nodes.active = emission_node
bake_texture_to("Emission",principled_node,new_material)
new_material.node_tree.nodes.active = normal_node
links = new_material.node_tree.links
links.new(principled_node.outputs['BSDF'], new_material.node_tree.nodes['Material Output'].inputs['Surface'])
bpy.ops.object.bake(type='NORMAL')
bpy.data.objects[object_name].hide_render = False
ノーマルは、先ほど定義した関数を使わずにベイクのオプションNORMALを使用してベイクする。
ノードの接続
最後に、ノードを適宜つないで完成。
# ノードを接続する
links.new(diffuse_node.outputs['Color'], principled_node.inputs['Base Color'])
links.new(metallic_node.outputs['Color'], principled_node.inputs['Metallic'])
links.new(roughness_node.outputs['Color'], principled_node.inputs['Roughness'])
links.new(emission_node.outputs['Color'], principled_node.inputs['Emission'])
normal_map = new_material.node_tree.nodes.new('ShaderNodeNormalMap')
links.new(normal_node.outputs['Color'], normal_map.inputs['Color'])
links.new(normal_map.outputs['Normal'], principled_node.inputs['Normal'])
アドオンにしていく。
アドオンのインフォ。
bl_info = {
"name": "Texture baker",
"description": "A Blender addon to bake material textures",
"author": "Your Name",
"version": (1, 0),
"blender": (3, 5, 0),
"location": "Properties > Material > Bake texture",
"category": "Material"
}
GUIはマテリアル名、オブジェクト名、サイズが定義できれば良いので、以下。
class BakePanel(bpy.types.Panel):
bl_label = "Bake Texture"
bl_idname = "OBJECT_PT_bake_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bake Texture"
bl_context = "objectmode"
@classmethod
def poll(cls, context):
return context.object is not None and context.object.active_material is not None
def draw(self, context):
layout = self.layout
row0 = layout.row()
row1 = layout.row()
row2 = layout.row()
if context.object is not None and context.object.active_material is not None:
row0.prop(context.object, "name", text="Object Name")
row1.prop(context.object.active_material, "name", text="Material Name")
row2.prop(context.scene.bake_size_holder,"size",text="Size")
else:
row = layout.row()
row.label(text="No active material")
operator = layout.operator("object.bake_material_textures", text="Bake")
operator.material_name = context.object.active_material.name
operator.object_name = context.object.name
operator.size = context.scene.bake_size_holder.size
ここで、BakeSizeHolderは以下のように定義した。
class BakeSizeHolder(bpy.types.PropertyGroup):
size: bpy.props.IntProperty(
name="Size",
description="Size of the texture to bake",
default=1024,
min=16,
max=8192
)
オペレーターは以下の通り。
class BakeMaterialTexturesOperator(bpy.types.Operator):
bl_idname = "object.bake_material_textures"
bl_label = "Bake material textures"
material_name: bpy.props.StringProperty()
object_name: bpy.props.StringProperty()
size: bpy.props.IntProperty()
def execute(self, context):
bake_material_textures(self.material_name,self.object_name,self.size)
self.report({'INFO'}, "Textures baked successfully")
return {'FINISHED'}
登録
アドオンの登録、解除は以下のようにした。
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.bake_size_holder = bpy.props.PointerProperty(type=BakeSizeHolder)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.bake_size_holder
特に説明することはない。
全体像
以下のようになった。
import bpy
import random
import string
bl_info = {
"name": "Texture baker",
"description": "A Blender addon to bake material textures",
"author": "Your Name",
"version": (1, 0),
"blender": (3, 5, 0),
"location": "Properties > Material > Bake texture",
"category": "Material"
}
def bake_texture_to(name,principled_node,new_material,float_value=False):
links = new_material.node_tree.links
link = principled_node.inputs[name].links
color_value_node = None
if(len(link) != 0):
links.new(link[0].from_socket, new_material.node_tree.nodes['Material Output'].inputs['Surface'])
else:
color_value_node = new_material.node_tree.nodes.new("ShaderNodeValue") if float_value else new_material.node_tree.nodes.new("ShaderNodeRGB")
color_value_node.outputs[0].default_value = principled_node.inputs[name].default_value
links.new(color_value_node.outputs[0], new_material.node_tree.nodes['Material Output'].inputs['Surface'])
bpy.ops.object.bake(type='EMIT')
if color_value_node:
new_material.node_tree.nodes.remove(color_value_node)
def bake_material_textures(original_material_name,object_name,size):
# レンダリング設定
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.samples = 64
# オブジェクトをコピー
new_obj = bpy.data.objects[object_name].copy()
# オブジェクトのデータをコピー
new_mesh = bpy.data.objects[object_name].data.copy()
new_obj.data = new_mesh
bpy.context.collection.objects.link(new_obj)
# オブジェクトを選択してアクティブにする
bpy.context.view_layer.objects.active = new_obj
# ベイクするマテリアルを設定
original_material = bpy.data.materials[original_material_name]
bpy.context.object.active_material = original_material
# ベイク用のUVマップを作成
obj = bpy.context.edit_object
if obj:
bpy.ops.mesh.uv_texture_add()
diffuse_texture_name = original_material_name + '_diffuse'
metallic_texture_name = original_material_name + '_metallic'
roughness_texture_name = original_material_name + '_roughness'
emission_texture_name = original_material_name + '_emission'
normal_texture_name = original_material_name + '_normal'
# テクスチャを作成
diffuse_texture = bpy.data.images.new(diffuse_texture_name, width=size, height=size)
metallic_texture = bpy.data.images.new(metallic_texture_name, width=size, height=size)
roughness_texture = bpy.data.images.new(roughness_texture_name, width=size, height=size)
emission_texture = bpy.data.images.new(emission_texture_name, width=size, height=size)
normal_texture = bpy.data.images.new(normal_texture_name, width=size, height=size)
# diffuse_texture.colorspace_settings.name = "Non-Color"
metallic_texture.colorspace_settings.name = "Non-Color"
roughness_texture.colorspace_settings.name = "Non-Color"
emission_texture.colorspace_settings.name = "Non-Color"
normal_texture.colorspace_settings.name = "Non-Color"
# 新しいマテリアルを作成する
new_material = original_material.copy() #(name=original_material_name + "_baked")
# マテリアルにノードを追加する
new_material.use_nodes = True
principled_node = new_material.node_tree.nodes["Principled BSDF"]
diffuse_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
metallic_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
roughness_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
emission_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
normal_node = new_material.node_tree.nodes.new('ShaderNodeTexImage')
# 画像テクスチャをノードに設定する
diffuse_node.image = diffuse_texture
metallic_node.image = metallic_texture
roughness_node.image = roughness_texture
emission_node.image = emission_texture
normal_node.image = normal_texture
bpy.context.view_layer.objects.active = new_obj
bpy.data.objects[object_name].select_set(False)
bpy.data.objects[object_name].hide_render = True
bpy.context.view_layer.objects.active.active_material = new_material
# テクスチャベイクを行う
new_material.node_tree.nodes.active = diffuse_node
bake_texture_to("Base Color",principled_node,new_material)
new_material.node_tree.nodes.active = metallic_node
bake_texture_to("Metallic",principled_node,new_material,float_value=True)
new_material.node_tree.nodes.active = roughness_node
bake_texture_to("Roughness",principled_node,new_material,float_value=True)
new_material.node_tree.nodes.active = emission_node
bake_texture_to("Emission",principled_node,new_material)
new_material.node_tree.nodes.active = normal_node
links = new_material.node_tree.links
links.new(principled_node.outputs['BSDF'], new_material.node_tree.nodes['Material Output'].inputs['Surface'])
bpy.ops.object.bake(type='NORMAL')
bpy.data.objects[object_name].hide_render = False
# ノードを接続する
links.new(diffuse_node.outputs['Color'], principled_node.inputs['Base Color'])
links.new(metallic_node.outputs['Color'], principled_node.inputs['Metallic'])
links.new(roughness_node.outputs['Color'], principled_node.inputs['Roughness'])
links.new(emission_node.outputs['Color'], principled_node.inputs['Emission'])
normal_map = new_material.node_tree.nodes.new('ShaderNodeNormalMap')
links.new(normal_node.outputs['Color'], normal_map.inputs['Color'])
links.new(normal_map.outputs['Normal'], principled_node.inputs['Normal'])
class BakePanel(bpy.types.Panel):
bl_label = "Bake Texture"
bl_idname = "OBJECT_PT_bake_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bake Texture"
bl_context = "objectmode"
@classmethod
def poll(cls, context):
return context.object is not None and context.object.active_material is not None
def draw(self, context):
layout = self.layout
row0 = layout.row()
row1 = layout.row()
row2 = layout.row()
if context.object is not None and context.object.active_material is not None:
row0.prop(context.object, "name", text="Object Name")
row1.prop(context.object.active_material, "name", text="Material Name")
row2.prop(context.scene.bake_size_holder,"size",text="Size")
else:
row = layout.row()
row.label(text="No active material")
operator = layout.operator("object.bake_material_textures", text="Bake")
operator.material_name = context.object.active_material.name
operator.object_name = context.object.name
operator.size = context.scene.bake_size_holder.size
class BakeMaterialTexturesOperator(bpy.types.Operator):
bl_idname = "object.bake_material_textures"
bl_label = "Bake material textures"
material_name: bpy.props.StringProperty()
object_name: bpy.props.StringProperty()
size: bpy.props.IntProperty()
def execute(self, context):
bake_material_textures(self.material_name,self.object_name,self.size)
self.report({'INFO'}, "Textures baked successfully")
return {'FINISHED'}
class BakeSizeHolder(bpy.types.PropertyGroup):
size: bpy.props.IntProperty(
name="Size",
description="Size of the texture to bake",
default=1024,
min=16,
max=8192
)
classes = (BakePanel, BakeMaterialTexturesOperator,BakeSizeHolder)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.bake_size_holder = bpy.props.PointerProperty(type=BakeSizeHolder)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.bake_size_holder
まとめ
BlenderのAPIの情報は少ないので、ChatGPTの助けをそれなりに借りた。
自分で必要なものがプログラムで作れるBlenderは神。