Controlling Blender from Julia using bpy and PythonCall.jl

Recently I have been trying to use Blender from Julia.
I have got a pretty good workflow using the rpyc package in Python. With this, we can import bpy in Julia using PythonCall.jl

This is the Python script to be run in Blender to achieve this:

import importlib
import threading
import time

if importlib.util.find_spec('rpyc') is None:
    import pip
    pip.main(['install', 'rpyc'])

from rpyc.utils.server import ThreadedServer
import rpyc

class SessionService(rpyc.Service):
    def __init__(self):
        self.session_globals = {}

    def exposed_execute(self, code):
        exec(code, self.session_globals)
        return self.session_globals.get('_')

    def exposed_get(self, name):
        return self.session_globals[name]

    def exposed_set(self, name, value):
        self.session_globals[name] = value


class LimitedThreadedServer(ThreadedServer):
    def __init__(self, *args, max_clients=0, **kwargs):
        self.max_clients = max_clients
        self._sem = threading.BoundedSemaphore(max_clients) if max_clients > 0 else None
        super().__init__(*args, **kwargs)

    def _accept_method(self, sock):
        if self._sem is not None:
            if not self._sem.acquire(blocking=False):
                print(f"Connection rejected: max {self.max_clients} clients reached")
                sock.close()
                return

            def wrapped_handler(sock):
                try:
                    self._authenticate_and_serve_client(sock)
                finally:
                    self._sem.release()

            t = threading.Thread(target=wrapped_handler, args=(sock,))
        else:
            t = threading.Thread(target=self._authenticate_and_serve_client, args=(sock,))

        t.daemon = True
        t.start()


if __name__ == '__main__':
    server = LimitedThreadedServer(
        SessionService,
        port=18861,
        max_clients=0, # 0 means unlimited number of clients can connect to the server.
        protocol_config={
            'allow_public_attrs': True,
            'allow_setattr': True,
            'allow_delattr': True,
        }
    )
    print("Server starting on port 18861...")
    server._start_in_thread()

Save this as blender_rpyc.py and then run Blender with:
blender --python path/to/blender_rpyc.py.

One can also load the script after opening Blender, and going to “Scripting” tab (on center-right at the top).

Then to connect to it from Julia:

begin
  using PythonCall, CondaPkg
  CondaPkg.add("rpyc")
  bpy = let
    rpyc = pyimport("rpyc")
    conn = rpyc.connect("localhost", 18861)
    conn.root.execute("import bpy")
    conn.root.get("bpy")
  end
end

This is a demo of this workflow, from Pluto.jl

Any suggestions or issue reports are welcome here!

The script to initialize rpyc if its not installed, is adapted from the workflow here: Running Julia inside Blender through VSCode (using PythonCall/JuliaCall) which required using juliacall (which in my testing, had issues with GPU-related packages, and frequently crashed Blender in general)

Nice! This is a much easier workflow than my setup. Thank you.