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

Blenderはゲームエンジンみたいなのに乗せるのに向いてない気がしたので、とりあえず作ってみた。
# とりあえず関数を書いてみる
引数は、マテリアル名、オブジェクト名、テクスチャのサイズの3つでよさそう。
```py
def bake_material_textures(original_material_name,object_name,size):
```
## 定型文+α
まず、レンダリングをCyclesにする。
```py
    # レンダリング設定
    bpy.context.scene.render.engine = 'CYCLES'
    bpy.context.scene.cycles.samples = 64
```
次に、ベイク先のオブジェクト用に、コピーする。
```py
    # オブジェクトをコピー
    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)
```
コピーしたやつを選択する。
```py
    # オブジェクトを選択してアクティブにする
    bpy.context.view_layer.objects.active = new_obj
```
マテリアルを選択する
```py
    # ベイクするマテリアルを設定
    original_material = bpy.data.materials[original_material_name]
    bpy.context.object.active_material = original_material
```
UVを作成。これがないとうまく行かなかった。
```py
    # ベイク用のUVマップを作成
    obj = bpy.context.edit_object
    if obj:
        bpy.ops.mesh.uv_texture_add()
```
ファイル名は適当。
```py
    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は、ずれがよくわかる。
```py
    # テクスチャを作成
    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"
```
## マテリアルの作成
元のマテリアルをコピーするのが一番手っ取り早そう。
```py
    # 新しいマテリアルを作成する
    new_material = original_material.copy()
```
## ノード作成＆取得
```py
    # マテリアルにノードを追加する
    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は忘れがち。

## テクスチャのベイク
以下の関数を定義した。
```py
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かによっても分ける必要があった。こういうときに、動的型付けは便利ですね。

### ベイクしていく
```py
    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を使用してベイクする。

## ノードの接続
最後に、ノードを適宜つないで完成。
```py
    # ノードを接続する
    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'])
```

# アドオンにしていく。
アドオンのインフォ。
```py
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はマテリアル名、オブジェクト名、サイズが定義できれば良いので、以下。
```py
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は以下のように定義した。
```py
class BakeSizeHolder(bpy.types.PropertyGroup):
    size: bpy.props.IntProperty(
        name="Size",
        description="Size of the texture to bake",
        default=1024,
        min=16,
        max=8192
    )
```
オペレーターは以下の通り。
```py
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'}
```
# 登録
アドオンの登録、解除は以下のようにした。
```py
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
```
特に説明することはない。

# 全体像
以下のようになった。
```py
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は神。