3
$\begingroup$

Blender as a Python Module seems to be using a globally shared context.

Therefore if one wants to use Blender as a Python Module in a web service, every user will share the same Blender Context.

Is there a way to use Blender as a Python Module in a web service while isolating each users context from one another ?

Here is some example code:

import bpy
import tempfile

from fastapi import FastAPI, File, UploadFile
app = FastAPI()

# To receive uploaded files, first install python-multipart
@app.post("/read_homefile")
async def read_homefile(file: UploadFile = File(...)):
    temp_file_handle, temp_file_name = tempfile.mkstemp(suffix='.blend')
    with os.fdopen(temp_file_handle, "wb") as f:
        while True:
            contents = await file.read(128*1024)
            if not contents:
                break
            f.write(contents)
    bpy.ops.wm.read_homefile(filepath=temp_file_name)
    return {"status": "ok"}

@app.post("/import_fbx")
async def import_fbx(file: UploadFile):
    temp_file_handle, temp_file_name = tempfile.mkstemp(suffix='.fbx')
    with os.fdopen(temp_file_handle, "wb") as f:
        while True:
            contents = await file.read(128*1024)
            if not contents:
                break
            f.write(contents)

    bpy.ops.import_scene.fbx("EXEC_DEFAULT", filepath=temp_file_name)
    return {"status": "ok"}

If two users would use this code, they would change each others scene. This is not what I want of course.

$\endgroup$
1

1 Answer 1

3
+150
$\begingroup$

Unfortunately, I don't think there is any easy way to 'fix' Blender's choice of using global variables in bpy library. What one could do, however, is to write a separate python program that will be instanced for every user using your application separately. From your code, I assume that there will be stateful session for every user accessing your API. I don't think this forum is a place to discuss specifics of this implementation, as it's way more about software architecture and python than about the bpy library, however, I'll try to sketch a feasible implementation.

The basic idea is as follows: we need to write a web application that will spawn separate python programs as subprocess for every client session. Because we will be running this as separate programs, the bpy library will not be sharing context across these instances. We will give client a token to identify the session they started. We will then pass messages from the web application to the appropriate spawned subprocess. In this example, I'm going to sketch out the subprocess as a HTTP service. Depending on scale of your project, this might or might not be a good idea. Please refer to this blog post to learn more about interacting with child processes: https://eli.thegreenplace.net/2017/interacting-with-a-long-running-child-process-in-python/

Also, as I'm not very familiar with FastAPI, so you might need to fill in some gaps yourself.

Controller

The idea here is that we are not going to import bpy here, but make this into a hub from where every client posts to create their own session, with their own bpy instance.

# a global dictionary of sessions. for production, this should be replaced with a Redis instance or a similiar solution
sessions = {}


def create_new_session(session_id):
    return BlenderInstance(session_id)

# create new session - generate uniqe ID and spawn new ray_blender process 
@app.post("/new_session")
async def start_session():
    session_id = generate_new_session_id(sessions)
    sessions[session_id] = create_new_session(session_id)
    return {
        session_id: session_id # we return the session ID to the client
    }


# client identifies itself with session ID, and specifies the action they want to preform. This will be useful for most of the actions, 
@app.post("/preform_action/{action}/{session_id}")
async def preform_action(request: Request):
    params = await request.json() # request body may contain additional properties for the action, such as parametres for operators
    return sessions[session_id].run(action, params) #this should also be made async, see comment in the run method

Instance Wrapper

We create a separatelly running python process (in ray_blender.py), and create a method to pass messages to it.

    class BlenderInstance():
        def __init__(self, id):
            self.id = id
            self.port = generate_free_port(id) #you don't need to generate this from ID or anything - just make sure the port is valid and unoccupied
            self.process = subprocess.Popen(['uvicorn', 'ray_blender:app', '--host', '0.0.0.0', '--port', self.port],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)

        def run(self, action, params):
            response = requests.post(f'http://localhost:{self.port}/{action}', params) # I would recommend using aiohttp library to make this asynchronous
            return response

Subprocess - ray_blender.py

This is another FastAPI application, ment to run on local network only.


import bpy
from fastapi import FastAPI

app = FastAPI()

@app.post("/import_fbx")
async def import_fbx():
    # Your code depended on bpy here ...
    # I'll leave it to you to figure out how to properly create the file and pass the file path in here....
    bpy.ops.import_scene.fbx("EXEC_DEFAULT", filepath=temp_file_name)
    return {"status": "ok"}

@app.post("/read_homefile")
    # ...

@app.post("/create_cube")
    # ...

@app.post("/render")
    # ...

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .