Communication with Blender via Python RPC
My current 3D graphics setup revolves around Blender, ZBrush and, less often, Marmoset Toolbag 3. Transferring the meshes between ZBrush and Blender is the constant dance of exporting and importing (yes, I know about GoB but it’s not good enough for me). I strongly dislike this back and forth. I feel it adds a lot of friction, throws me off and wastes a lot of time. That makes me experiment with inter process communication. This is one of those experiments.
This experiment uses Python’s XMLRPC module. It allows for remote execution of server side defined methods. That means it’s a great way to expose Blender’s Python API to another process. This setup will work even if the client process isn’t running on the same machine as the server (Blender).
Good place to start is to go through the Blender’s side, server script:
import bpy
import threading
from xmlrpc.server import SimpleXMLRPCServer
HOST = "127.0.0.1"
PORT = 8000
def launch_server():
server = SimpleXMLRPCServer((HOST, PORT))
server.register_function(list_objects)
server.register_function(import_obj)
server.serve_forever()
def server_start():
t = threading.Thread(target=launch_server)
t.daemon = True
t.start()
def list_objects():
return bpy.data.objects.keys()
def import_obj(path:str):
status = bpy.ops.import_scene.obj(filepath=path)
return "OK"
The server gets started as a daemonized thread. Two functions get registered for the
SimpleXMLRPCServer
: list_objects
and import_obj
. A client which connects to this server
will be able to execute those methods. One of the Python’s side implementation quirk is that
the registered functions have to return something serializeable. list_objects()
returns a
list of objects in the scene, using a tuple of strings, import_obj
return an OK
string.
If you’d like to try this script, save it in the blender_install_dir/2.90/python/lib/funzone/rpc_server.py
file (be sure to create an empty __init__.py
file in the funzone
folder) and start the
server from Blender’s scripting tab, with:
import funzone.rpc_server as rpc_s
rpc_s.server_start()
At this point, the server runs in Blender’s background thread. Time to take a look at a possible client’s implementation:
import xmlrpc.client
HOST = "127.0.0.1"
PORT = 8000
client = None
def start():
global client
client = xmlrpc.client.ServerProxy(f"http://{HOST}:{PORT}")
Client’s implementation is very basic. That’s because at this point in time I’m still experimenting and I’m not putting everything in a script but rather using it in the Python’s REPL.
>>> import rpc_client as rpc_c
>>> rpc_c.start()
>>> rpc_c.client.list_objects()
>>> rpc_c.client.import_obj(r"C:\Users\mc\Desktop\test.obj")
This is an example in which I connect a client to the server and try executing both RPC functions. I don’t want to brag… but it actually works ;)
I don’t have a great use case yet. I was thinking about using a file browser, maybe nnn to quickly import meshes into Blender. nnn is a pretty niche, command line file browser and chances are you have no experience using it.
Update for 2020-10-25 <
I have written a prototype of a nnn plugin for quick mesh importing. nnn doesn’t support Windows but you can still run it, using the Linux WSL subsystem.
file_path=$2/$1
file_dir=$2
file=$1
PY_SCRIPT="
import xmlrpc.client
from pathlib import Path
HOST = '127.0.0.1'
PORT = 8000
with xmlrpc.client.ServerProxy(f'http://127.0.0.1:8000') as proxy:
# Very specific process of converting the file path. Since this is a nnn plugin
# it runs on the Linux (WSL) side. The path will be in a Unix format. Didn't find a way
# to use pathlib for path conversion to Windows format.
target_file = Path('$file_dir', '$file')
_target_file = str(target_file).split('/')
# When writing this script the split list started with ['', 'mnt', ...].
# Drop those two to build a proper Windows path.
_target_file = _target_file[2:]
# The 0th element should be the drive letter. Append ':' to the drive.
_target_file[0] = _target_file[0] + ':'
# Char 92 is a backslash but since this script is just a bash string it's safer to avoid
# using it explicitly. Using chr(92) is a workaround.
target_file = chr(92).join(_target_file)
print(target_file)
proxy.import_obj(target_file)
"
if test -f $file_path; then
echo "$PY_SCRIPT" | python3
fi
Here is a video of this setup in action.