本日はBlender枠です。
現在BlenderからUnity製アプリケーションに向けてネットワーク経由でメッシュ情報を双方向に通信するパッケージを開発しています。
MixedRealityModelingToolsという仮称で開発していますが今回はBlender側のコア機能を紹介します。
〇BlenderでTCP通信を行う
BlenderでTCP通信を行うサンプルコードは以下で解説しています。
簡単に再解説すると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以上のポリゴンを指します。
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)
このようにしてメッシュの頂点、インデックス、法線が取得できました。
〇コード全文
こちらのコードをBlenderでPythonとして実行することでオブジェクトプロパティウィンドウに新しい項目とボタンが表示されます。
ボタンを選択することで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) ```