0
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?

More than 1 year has passed since last update.

Blenderであらゆるテクスチャをベイクするアドオンを作成した。

Posted at

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は神。

0
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
0
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?