夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

BlenderのPythonでサーバーを立てMessagePackでメッシュを送信する MixedRealityModelingToolsその① 概要とメッシュの取得

本日はBlender枠です。

現在BlenderからUnity製アプリケーションに向けてネットワーク経由でメッシュ情報を双方向に通信するパッケージを開発しています。

MixedRealityModelingToolsという仮称で開発していますが今回はBlender側のコア機能を紹介します。

github.com

BlenderTCP通信を行う

BlenderTCP通信を行うサンプルコードは以下で解説しています。

redhologerbera.hatenablog.com

簡単に再解説するとsocketを使用してクライアントを構築しています。

import bpy
import socket
import threading

class ClientHandler(threading.Thread):
    def __init__(self, client_socket):
        threading.Thread.__init__(self)
        self.client_socket = client_socket

    def run(self):
        try:
            while True:
                request = self.client_socket.recv(1024)
                if not request:
                    break
                print("[*] Received: %s" % request.decode())
        except Exception as e:
            print(f"Error while handling client: {e}")
        finally:
            self.client_socket.close()
class ServerThread(threading.Thread):
    def __init__(self, bind_ip, bind_port):
        threading.Thread.__init__(self)
        self.bind_ip = bind_ip
        self.bind_port = bind_port
        self.clients = []

    def run(self):
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.bind((self.bind_ip, self.bind_port))
        server.listen(5)
        print("[*] Listening on %s:%d" % (self.bind_ip, self.bind_port))

        while True:
            client, addr = server.accept()
            print("[*] Accepted connection from: %s:%d" % (addr[0], addr[1]))
            client_handler = ClientHandler(client)
            client_handler.start()
            self.clients.append(client)

    def close_all_clients(self):
        for client in self.clients:
            client.close()

threadingを使用してメインスレッドでの処理を開けることでフリーズを割けています。

次の文でサーバーのIPアドレスとポートを指定してserver_threadを実行しています。

# Run the server in a new thread
server_thread = ServerThread("0.0.0.0", 9998)
server_thread.start()

データの転送はclient.sendallを使用して送信しています。

client.sendall("送るデータ")

〇メッシュの取得

メッシュを送信するためには現在選択されているメッシュ情報を取得する必要があります。

筆者が実装している関数は以下になります。

def get_mesh_data():
    obj = bpy.context.view_layer.objects.active
    print(f"Active object name: {obj.name}")

    # Add Triangulate modifier
    triangulate_mod = obj.modifiers.new(name="Triangulate", type='TRIANGULATE')
    triangulate_mod.keep_custom_normals = True
    triangulate_mod.quad_method = 'BEAUTY'
    triangulate_mod.ngon_method = 'BEAUTY'

    # Apply modifiers and get the new mesh data
    bpy.context.view_layer.update()
    depsgraph = bpy.context.evaluated_depsgraph_get()
    obj_eval = obj.evaluated_get(depsgraph)
    temp_mesh = bpy.data.meshes.new_from_object(obj_eval)
    bpy.ops.mesh.customdata_custom_splitnormals_clear()
    
    # Get vertices and triangles
    vertices = [[v.co.x, v.co.y, v.co.z] for v in temp_mesh.vertices] 

    triangles = []
    for p in temp_mesh.polygons:
        triangles.extend(p.vertices)

    # Remove Triangulate modifier
    obj.modifiers.remove(triangulate_mod)

    # Get normals
    normals = [[v.normal.x, v.normal.y, v.normal.z] for v in temp_mesh.vertices]  

    # Don't forget to remove the temporary mesh data
    bpy.data.meshes.remove(temp_mesh)
    print(f"Mesh data generated: vertices={len(vertices)}, triangles={len(triangles)}, normals={len(normals)}")
    return (vertices, triangles, normals)

冒頭二行で現在選択されているオブジェクトをobjとして定義しています。またその名前をログとして出力しています。

    obj = bpy.context.view_layer.objects.active
    print(f"Active object name: {obj.name}")

メッシュを送信する際に問題となるのがソフトウェアによってNゴンが許されているものと許されていないソフトがある点です。

Nゴンとはメッシュの持つポリゴンの数が最低である3以上のポリゴンを指します。

redhologerbera.hatenablog.com

BlenderではNゴンが許容されているため、例えば次のようなキューブの例ではBlender上では6面のポリゴンとなっていますが、三角ポリゴンではこれをあらわすのに12面必要になります。

UnityではこのようなNゴンは使用できずすべてのメッシュが3つの頂点から構成される三角ポリゴンとなっています。

つまりBlenderでメッシュを送信する際は三角面化のモディファイアを使用してNゴンを削除する必要があります。

  # Add Triangulate modifier
    triangulate_mod = obj.modifiers.new(name="Triangulate", type='TRIANGULATE')
    triangulate_mod.keep_custom_normals = True
    triangulate_mod.quad_method = 'BEAUTY'
    triangulate_mod.ngon_method = 'BEAUTY'

 # Apply modifiers and get the new mesh data
    bpy.context.view_layer.update()
    depsgraph = bpy.context.evaluated_depsgraph_get()
    obj_eval = obj.evaluated_get(depsgraph)
    temp_mesh = bpy.data.meshes.new_from_object(obj_eval)
    bpy.ops.mesh.customdata_custom_splitnormals_clear()

このコードではobj.modifiers.new("モディファイア名")を使用して三角面化モディファイアを適応しています。

このようにして一度Nゴンをなくしたメッシュのデータを取得します。

 # Get vertices and triangles
    vertices = [[v.co.x, v.co.y, v.co.z] for v in temp_mesh.vertices] 

    triangles = []
    for p in temp_mesh.polygons:
        triangles.extend(p.vertices)

この処理では[x,y,z]の三次元ベクトルになるようにメッシュの数データを取得します。

 verticesは3次元ベクトルの配列になります。

 同様にtrianglesとしてメッシュのインデックスを取得します。

    # Remove Triangulate modifier
    obj.modifiers.remove(triangulate_mod)

 メッシュデータの取得が完了したらモディファイアは不要のため一度適応した三角面化モディファイアを削除しています。

 最後に頂点、メッシュインデックスに続きメッシュの法線データを取得します。

  # Get normals
    normals = [[v.normal.x, v.normal.y, v.normal.z] for v in temp_mesh.vertices]  

最後にメッシュを取得するために作成した仮のメッシュを削除しています。

    # Don't forget to remove the temporary mesh data
    bpy.data.meshes.remove(temp_mesh)
    print(f"Mesh data generated: vertices={len(vertices)}, triangles={len(triangles)}, normals={len(normals)}")
    return (vertices, triangles, normals)

このようにしてメッシュの頂点、インデックス、法線が取得できました。

〇コード全文

こちらのコードをBlenderPythonとして実行することでオブジェクトプロパティウィンドウに新しい項目とボタンが表示されます。

ボタンを選択することでUnityにデータを送信できます。

Unity側のコードは別途紹介します。

import bpy
import socket
import threading
import msgpack
import struct

class ClientHandler(threading.Thread):
    def __init__(self, client_socket):
        threading.Thread.__init__(self)
        self.client_socket = client_socket

    def run(self):
        try:
            while True:
                request = self.client_socket.recv(1024)
                if not request:
                    break
                print("[*] Received: %s" % request.decode())
        except Exception as e:
            print(f"Error while handling client: {e}")
        finally:
            self.client_socket.close()

class ServerThread(threading.Thread):
    def __init__(self, bind_ip, bind_port):
        threading.Thread.__init__(self)
        self.bind_ip = bind_ip
        self.bind_port = bind_port
        self.clients = []

    def run(self):
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.bind((self.bind_ip, self.bind_port))
        server.listen(5)
        print("[*] Listening on %s:%d" % (self.bind_ip, self.bind_port))

        while True:
            client, addr = server.accept()
            print("[*] Accepted connection from: %s:%d" % (addr[0], addr[1]))
            client_handler = ClientHandler(client)
            client_handler.start()
            self.clients.append(client)

    def close_all_clients(self):
        for client in self.clients:
            client.close()

# Run the server in a new thread
server_thread = ServerThread("0.0.0.0", 9998)
server_thread.start()

def send_message_to_unity(message):
    for client in server_thread.clients:
        try:
            client.send(message.encode())
        except Exception as e:
            print(f"Error while sending message to client: {e}")
            
import numpy as np


def send_mesh_data_to_unity(mesh_data):
    vertices, triangles, normals = mesh_data

    # Convert numpy arrays to list
    vertices_list = np.array(vertices, dtype='<f4').flatten().tolist()
    triangles_list = np.array(triangles, dtype='<i4').flatten().tolist()
    normals_list = np.array(normals, dtype='<f4').flatten().tolist()

    # データを辞書として構築
    data_dict = {
        'vertices': vertices_list,
        'triangles': triangles_list,
        'normals': normals_list
    }

    # MessagePackでシリアライズ
    serialized_mesh_data = msgpack.packb(data_dict)

    #print(f"Serialized data (bytes): {serialized_mesh_data.hex()}")
    verification_mesh_data(serialized_mesh_data)  
    # ここでデシリアライズの確認を行う
    try:
        deserialized_data = msgpack.unpackb(serialized_mesh_data)
        print("Deserialization success!")
        #print(deserialized_data)  # もし必要ならば、デシリアライズされたデータを出力する
    except Exception as e:
        print(f"Deserialization error: {e}")
        return  # エラーが発生した場合、関数をここで終了する

    for client in server_thread.clients:
        try:
            client.sendall(serialized_mesh_data)
            #print(serialized_mesh_data)
          
        except Exception as e:
            print(f"Error while sending mesh data to client: {e}")



class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.send_message"
    bl_label = "Send Message"

    def execute(self, context):
       # send_message_to_unity("Hello from Blender!")
        mesh_data = get_mesh_data()
        send_mesh_data_to_unity(mesh_data)
        return {'FINISHED'}

class CustomPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Send Message Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"

    def draw(self, context):
        layout = self.layout
        layout.operator("object.send_message")


bpy.utils.register_class(SimpleOperator)
bpy.utils.register_class(CustomPanel)

def get_mesh_data():
    obj = bpy.context.view_layer.objects.active
    print(f"Active object name: {obj.name}")

    # Add Triangulate modifier
    triangulate_mod = obj.modifiers.new(name="Triangulate", type='TRIANGULATE')
    triangulate_mod.keep_custom_normals = True
    triangulate_mod.quad_method = 'BEAUTY'
    triangulate_mod.ngon_method = 'BEAUTY'

    # Apply modifiers and get the new mesh data
    bpy.context.view_layer.update()
    depsgraph = bpy.context.evaluated_depsgraph_get()
    obj_eval = obj.evaluated_get(depsgraph)
    temp_mesh = bpy.data.meshes.new_from_object(obj_eval)
    bpy.ops.mesh.customdata_custom_splitnormals_clear()
    
    # Get vertices and triangles
    vertices = [[v.co.x, v.co.y, v.co.z] for v in temp_mesh.vertices]  # この部分を変更

    triangles = []
    for p in temp_mesh.polygons:
        triangles.extend(p.vertices)

    # Remove Triangulate modifier
    obj.modifiers.remove(triangulate_mod)

    # Get normals
    normals = [[v.normal.x, v.normal.y, v.normal.z] for v in temp_mesh.vertices]  # この部分を変更

    # Don't forget to remove the temporary mesh data
    bpy.data.meshes.remove(temp_mesh)
    print(f"Mesh data generated: vertices={len(vertices)}, triangles={len(triangles)}, normals={len(normals)}")
    return (vertices, triangles, normals)
 
   ```