Communication with Blender via Python RPC

My current CG setup revolves around Blender, ZBrush and, less often, Marmoset Toolbag 3. Transfering the meshes between ZBrush and Blender is the constant dance of exporting and importing. Yes, I know about GoB. 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’s why a lot of my experiments revolve around inter process communication.

My, most recent experiment uses Python’s XMLRPC module. It allows for executing server side defined methods from a client session. It’s basically a server running on the Blender’s side which interacts with Blender’s Python API, but controlled by another, client, Python session.

I don’t have a great use case yet. I was thinking about using a file browser, maybe nnn to import meshes into Blender. If you haven’t used nnn it’s a terminal file browser. I’ll hopefully be able to prepare a demo of that soon. For now lets just focus on the RPC setup.

A very simple code for the server would be:

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. It has two functions registered: list_objects and import_obj. One thing that seems to be important is to return something that Python can serialize from every registered function. For example, the import_obj function returns "OK" string.

Save the file with those contents 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()

The client 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}")

As you can see the client’s implementation is very basic. That’s because at this point in time I’m still experimenting. How am I using this then?

Just open a Python session in the funzone directory, import the module and call the functions from the 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")

Hopefully that explains the barebones setup well enough. Feel free to contact me if anything confuses you.

Update for 2020-10-25

I have quickly prototyped a working plugin for nnn. It’s possible to import meshes from nnn to Blender. It’s important to point out that you can’t run nnn in Windows which means that you have to run it in the Linux WSL subsystem. There should be a video, which shows how to set it up and how I’m using it, at the bottom of this post… if it’s not there you’re here too early.

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