見出し画像

PythonでBlenderを動かそう!(リモートサーバー上でBlenderのレンダリングをする特殊な人向け)

初めまして!Arrowです。

いつもは自分の人生や体験についてnoteを書くことが多いのですが、今回は雰囲気を変えて技術よりのnoteを書いていこうと思います。

なぜ、今回Blenderに関するnoteを書こうと考えたのかというと、私は研究でBlenderを使った刺激を作成することが多いのですが、Pythonを使ってBlenderを動かす日本語の情報サイトが少なすぎる!!と言う問題に直面しました。(しかも、リモート接続でサーバーにつなぎ、レンダリングを行う人なんてあまりいないし)
そこで、自分の知っている範囲での知識を書き、今後出てくるかもしれないPythonでblenderを扱うエンジニアやデザイナー達の一助になればいいなと思っています。

それでは行きましょう!

想定されるアーキテクチャ

今回、自分が扱う全体の構成は以下の図のような形です。
実機からSSH接続でGPUサーバーに繋ぐ形です。このとき、実機にもサーバー上にもBlenderの最新版をダウンロードし、適切に動くようにしてあります。サーバーのBlenderをダウンロードする方法は以下のURLを確認してみてください。
https://docs.blender.org/manual/ja/3.0/getting_started/installing/linux.html

このアーキテクチャでのそれぞれの役割は
・PC(実機):Blenderでシーン全体の構成やレンダリング方式の設定を行う。
・サーバー:レンダリングを行う。物体やライトの位置を少し変更する。

全体のレンダリングまでの流れ

まず、実際のサーバー上でレンダリングを行うまでの大まかの流れを書きます。

  1. PC(実機)でオブジェクトの配置や編集、マテリアルの設定、アニメーションの設定、レンダリングの設定を決め、実際にレンダリングを開始する直前まで作成する。

  2. ターミナル上でサーバーとSSH接続し、scpコマンドでBlenderファイルとテクスチャファイルを送る。

  3. サーバー上でPythonコードを用いて、テクスチャやマテリアルを貼り直したり、レンダリングごとにライトの位置やマテリアルパラメータを変えられるようにする。

  4. サーバー上でレンダリングする。

このようにする理由としては、実機でレンダリングするにはレンダリングにあまりにも時間がかかる(特にCyclesエンジンを使用する場合)ので、ツヨツヨGPUがあるサーバーに投げてしまいたいからです。
(故にこの記事が刺さる層は少なそう。研究や開発に使う人ぐらいかな…)

で、この流れでレンダリングをする際に問題になるのが、どうやってPythonでテクスチャやマテリアルを貼り直すのかとマテリアルのノードツリーを書くのかだと思いますので、それについて書いていこうと思います。

外部ファイルからマテリアルを適用する場合

おそらくBlenderを触っている中でPoly Havenなどで3Dモデルをダウンロードして、シーン内で使用する場合があると思います。

まず、PCの実機で作成したBlenderファイル内にはオブジェクトの情報(形状、位置、大きさ、回転など)と、マテリアルとテクスチャの絶対パスの情報が入っています。
このため、SSHでサーバーに送った際にマテリアルの情報は抜けてしまい、そのままレンダリングするとオブジェクトの表面がピンク色(No info)の状態になってしまうと思います。
したがって、Poly Havenなどでダウンロードしたマテリアル情報もSSHでサーバーに送り、Pythonコード上で再度マテリアルとテクスチャの絶対パスを変更するコードを書く必要があります。

例えば、Poly Havenでお風呂に浮かべるアヒルをダウンロードしてシーン内で使用する場合、以下の画像のようなファイル構造になっていると思います。上のrubber_duck_toy_4k.blendがマテリアルの情報を持っており、下のtexturesにはマテリアルで使用されているテクスチャ画像が入っています。どちらもレンダリングに必要なので、scpコマンドでディレクトリごとサーバーに送ってください。

そして、サーバー上で新しくPythonファイルを作成し、以下のコードを書いてください。

import bpy
import os

# マテリアルをリンクして適用
def link_and_apply_material(material_path, material_name, obj_name):
    try:
        if not os.path.isfile(material_path):
            print(f"Material path does not exist or is not a file: {material_path}")
            return
        
        # マテリアルが既に存在するか確認
        if material_name in bpy.data.materials:
            new_material = bpy.data.materials[material_name]
            print(f"Material {material_name} already exists.")
        else:
            # 外部ファイルからマテリアルをリンク
            with bpy.data.libraries.load(material_path, link=True) as (data_from, data_to):
                if material_name in data_from.materials:
                    data_to.materials = [material_name]
                else:
                    print(f"Material {material_name} not found in {material_path}")
                    return
            
            # リンクされたマテリアルを取得
            new_material = bpy.data.materials.get(material_name)
            if not new_material:
                print(f"Failed to link material: {material_name}")
                return
            print(f"Material linked: {new_material.name}")

        # 新しいマテリアルを指定されたオブジェクトに適用
        for obj in bpy.context.scene.objects:
            if obj.type == 'MESH' and (obj_name is None or obj.name == obj_name):
                if len(obj.data.materials) == 0:
                    obj.data.materials.append(new_material)
                else:
                    obj.data.materials[0] = new_material
                print(f"Applied {new_material.name} to {obj.name}")
        
        #print("Material applied successfully")
        
    except Exception as e:
        print(f"Failed to link and apply material: {e}")

#blendファイルへのパスとマテリアル名、オブジェクト名
materials = [
    (".blendファイルへの絶対パス", "マテリアル名", "オブジェクト名"),
   (".blendファイルへの絶対パス", "マテリアル名", "オブジェクト名")
]

for material_path, material_name, obj_name in materials:
    link_and_apply_material(material_path, material_name, obj_name)

# レンダリングエンジンをCyclesに設定
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.display_settings.display_device = 'sRGB'
bpy.context.scene.view_settings.view_transform = 'Filmic'
bpy.context.scene.view_settings.look = 'None'
bpy.context.scene.view_settings.exposure = 0.0
bpy.context.scene.view_settings.gamma = 1.0

# GPUレンダリングを有効にする
bpy.context.scene.cycles.device = 'GPU'
prefs = bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type = 'CUDA'  # または 'OPENCL' や 'OPTIX'
prefs.get_devices()
for device in prefs.devices:
    device.use = True

# 出力設定(動画の時)
bpy.context.scene.render.image_settings.file_format = 'FFMPEG'
bpy.context.scene.render.ffmpeg.format = 'MPEG4'
bpy.context.scene.render.filepath = "出力先のパス"

# FFMPEG設定
bpy.context.scene.render.ffmpeg.codec = 'H264'

# フレーム範囲設定
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 360
bpy.context.scene.frame_step = 1

途中でprint文が多く入っているのはどの部分でエラーを吐いているのを確認するために挿入しています。不必要な場合は削除してください。
もし、マテリアルが読み込まれているのにレンダリングした時にピンク色(テクスチャ情報の読み込み失敗)になってしまう場合は以下のコードをimport os の後に入れてみてください。

# 既存のマテリアルを削除
for material in bpy.data.materials:
    bpy.data.materials.remove(material)

このコードで既存マテリアルをリフレッシュし、その後のコードで再度繋ぎ合わせるようにすることでうまくいくと思います。

materialsのコード部分で使用するマテリアル名とオブジェクト名はblender上のものを使用してください。以下の画像の場合はマテリアル名がfood_apple_01.001でオブジェクト名がfood_apple_01になります。

もし以下の画像のように複数マテリアルが繋がっている場合はblender上でマテリアルごとに分割するのをお勧めする。

一つのオブジェクト内に複数マテリアルが使用されている
編集モードにして、Pキーを押してマテリアルごとに分割するを選択する

ここまでで外部ファイルをそのままシーンに配置する場合ならうまくレンダリングされるはずだ。

code上でノードを接続してマテリアルを反映させる場合

皆さんの中には自分でシェーダーエディターでマテリアルを作って、それをオブジェクトに反映させたいよ!と言う方もいるかもしれないので、その場合について書いていきます!

import bpy
import os
import math

def add_texture(new_texture_path, material_name, obj_name, scale_x, scale_y, scale_z, rotate_x, rotate_y, rotate_z):
    # マテリアルを作成または取得
    material = bpy.data.materials.get(material_name)
    if material is None:
        material = bpy.data.materials.new(name=material_name)
        print("make new one")

    # ノードを使用するように設定
    material.use_nodes = True
    nodes = material.node_tree.nodes
    links = material.node_tree.links

    # 既存のノードをクリア
    nodes.clear()

    # ノードを追加
    bsdf_node = nodes.new(type='ShaderNodeBsdfPrincipled')
    bsdf_node.location = (0, 0)

    texture_node = nodes.new(type='ShaderNodeTexImage')
    texture_node.location = (-400, 0)
    try:
        if bpy.data.images.get(new_texture_path) is None:
            texture_node.image = bpy.data.images.load(new_texture_path)
            print("texture is OK")
        else:
            texture_node.image = bpy.data.images[new_texture_path]
            print("texture is OK2")
    except Exception as e:
        print(f"Failed to load texture: {e}")


    mapping_node = nodes.new(type='ShaderNodeMapping')
    mapping_node.location = (-800, 0)

    # 度数法を弧度法に変換して回転を設定
    rotation_degrees = (rotate_x, rotate_y, rotate_z)
    rotation_radians = [math.radians(degree) for degree in rotation_degrees]
    mapping_node.inputs['Rotation'].default_value = rotation_radians
    mapping_node.inputs['Scale'].default_value = (scale_x, scale_y, scale_z)

    texture_coord_node = nodes.new(type='ShaderNodeTexCoord')
    texture_coord_node.location = (-1200, 0)

    bump_node = nodes.new(type='ShaderNodeBump')
    bump_node.location = (-400, -200)
    bump_node.inputs['Strength'].default_value = 1.0

    # ノードを接続
    links.new(texture_coord_node.outputs['UV'], mapping_node.inputs['Vector'])
    links.new(mapping_node.outputs['Vector'], texture_node.inputs['Vector'])
    links.new(texture_node.outputs['Color'], bsdf_node.inputs['Base Color'])
    links.new(bump_node.outputs['Normal'], bsdf_node.inputs['Normal'])

    # マテリアル出力ノードを追加
    output_node = nodes.new(type='ShaderNodeOutputMaterial')
    output_node.location = (200, 0)

    # BSDFノードとマテリアル出力ノードを接続
    links.new(bsdf_node.outputs['BSDF'], output_node.inputs['Surface'])

    # オブジェクトにマテリアルを適用
    obj = bpy.data.objects.get(obj_name)
    if obj is not None:
        if len(obj.data.materials) == 0:
            obj.data.materials.append(material)
        else:
            obj.data.materials[0] = material
        print(f"Material '{material_name}' applied to '{obj_name}'.")
    else:
        print("Object '{obj_name}' not found.")

add_material = [
    ("画像テクスチャのパス","マテリアル名","オブジェクト名",マッピングノードのx方向の大きさ,マッピングノードのy方向の大きさ,マッピングノードのx方向の大きさ,マッピングノードのx軸方向回転の大きさ,マッピングノードのy軸方向回転の大きさ,マッピングノードのz軸方向回転の大きさ),
    ("画像テクスチャのパス","マテリアル名","オブジェクト名",マッピングノードのx方向の大きさ,マッピングノードのy方向の大きさ,マッピングノードのx方向の大きさ,マッピングノードのx軸方向回転の大きさ,マッピングノードのy軸方向回転の大きさ,マッピングノードのz軸方向回転の大きさ)
]

for new_texture_path, material_name, obj_name, scale_x, scale_y, scale_z, rotate_x, rotate_y, rotate_z in add_material:
    add_texture(new_texture_path, material_name, obj_name, scale_x, scale_y, scale_z, rotate_x, rotate_y, rotate_z)

# レンダリングエンジンをCyclesに設定
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.display_settings.display_device = 'sRGB'
bpy.context.scene.view_settings.view_transform = 'Filmic'
bpy.context.scene.view_settings.look = 'None'
bpy.context.scene.view_settings.exposure = 0.0
bpy.context.scene.view_settings.gamma = 1.0

# GPUレンダリングを有効にする
bpy.context.scene.cycles.device = 'GPU'
prefs = bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type = 'CUDA'  # または 'OPENCL' や 'OPTIX'
prefs.get_devices()
for device in prefs.devices:
    device.use = True

# 出力設定(動画の時)
bpy.context.scene.render.image_settings.file_format = 'FFMPEG'
bpy.context.scene.render.ffmpeg.format = 'MPEG4'
bpy.context.scene.render.filepath = "出力先のパス"

# FFMPEG設定
bpy.context.scene.render.ffmpeg.codec = 'H264'

# フレーム範囲設定
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 360
bpy.context.scene.frame_step = 1

上に書いたコードは以下の画像のようなシェーダーノードを用いる場合である。

もしプリンシプルBSDF上のbase colorやSpecularなどを直接的に変えたい場合は以下のコードをノード接続の前に入れると良い

# ノードのプロパティ名を確認して設定
bsdf_node.inputs['Base Color'].default_value = (1, 1, 1, 1)
bsdf_node.inputs['Metallic'].default_value = 0.0
bsdf_node.inputs['Specular IOR Level'].default_value = 0.9
bsdf_node.inputs['Roughness'].default_value = 0.0

Specularに関してはblenderの4.2から'Specular IOR Level'と言う名前で定義されるように変更されたらしいので、バージョンによる違いに注意してください。

コードの途中にあるmapping_node.location = (-800, 0)などの.locationについてはシーン内の場所を表しているのではなく、シェーダーエディター上でのノードの場所を定義しているだけなので、あまり気にしないでください。

終わりに

今回はこの辺で終わろうと思います!(一万字近くになってきたので)
また、他に書いた方がいいことがあれば執筆します!

それでは良きBlenderライフを!

この記事が気に入ったらサポートをしてみませんか?