non vorrei lavorare

昔はおもにプログラミングやガジェット系、今は?

blenderでgRPC(クライアント&サーバ)を動かした

こんばんは、子供達がなぜかプレデターブームです。紙とセロハンテープで多数のプレデターを作っています。@kjunichiです。

背景

blenderpythonにちょっかい出して遊んでいる。 gRPCがpythonでも動くことを知ったので、blender + gRPCをやってみることに。 今回は、macOS mojaveで取り組んだ。

準備

blenderにpipを入れる

以下のQiitaの記事のようにまずpipを入れる

qiita.com

クライアント編

.protoファイルから生成したモジュールを読み込むには

Googleの公式サンプルがそのままでは、.protoから生成したであろうモジュールが 見つからないエラーとなってしまった。

動的にモジュールのサーチパスを追加するには

以下のようにsysモジュールで対応できた

import sys
sys.path.append('/Users/junichi/work/grpc/examples/python/helloworld/tmp')

この対応以外はそのまま動いた。

blenderでgRPCクライアントを動かした

諸事情により、ポートを50051から50053に変更して動かしている。

サーバーをblenderpythonで以下のように動かしておく。

/Applications/blender.app/Contents/Resources/2.80/python/bin/python3.7m greeter_server.py 

クライアントのコードは以下。

from __future__ import print_function

import logging

import bpy
import os
import sys

import grpc

sys.path.append('/Users/junichi/work/grpc/examples/python/helloworld')
import helloworld_pb2
import helloworld_pb2_grpc

def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'):

    def draw(self, context):
        self.layout.label(text = message)
        #self.layout.label(message)

    bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)




def run():
    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
    # used in circumstances in which the with statement does not fit the needs
    # of the code.
    with grpc.insecure_channel('localhost:50053') as channel:
        stub = helloworld_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
    #print("Greeter client received: " + response.message)
    ShowMessageBox(message = "Greeter client received: " + response.message)


logging.basicConfig()
run()

実行結果

f:id:kjw_junichi:20190318211402p:plain

サーバ編

Google純正のgrpcモジュールのthreadを使った方法だとblenderが即死

blenderGUIにかかわる処理を行わなければセーフなのだけれども、 例えば、クライアント編のようにダイアログを受信時に表示するなどしたとたん、 blenderが即死する。

これは、おそらくメインスレッド以外でGUI関連の処理を行っているからだと思われる。

Pythonでも3.6以降はコルーチンが使える

threadの利用はなんとなく、Blenderと相性悪い予感はしていたので、困ったなぁとあきらめかけていたが、 最近の3系のpythonではthreadの他にコルーチンが扱えることを知った。

そこで調べてみると、grpcをピュアpythonで実装したgrpclibなるモジュールがあり、こちらはコルーチンを使用することが サンプルにも記載されていて、こちらを使うことにした。

純正grpcの.protoファイルから生成したモジュールがそのまま使えなかった

grpclibのREADMEに従い、.protoファイルからモジュールを生成した。

/Users/junichi/work/grpc/examples/python/helloworld/
mkdir tmp
cd tmp
cp ../
export PATH=$PATH:/Applications/blender.app/Contents/Resources/2.80/python/bin
/Applications/blender.app/Contents/Resources/2.80/python/bin/python3.7m -m grpc_tools.protoc -I ../../../protos/  --python_out=. --python_grpc_out=. helloworld.proto

スクリプト実行中でも様子を見れるようにした

bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

を入れることで、gRPCのクライアントから要求があるたびに、ビューポートが更新され、 それっぽい動きになった。

成果物

import sys
import asyncio
import time

#from grpclib.utils import graceful_exit
from grpclib.server import Server
from grpclib.client import Channel

import bpy

sys.path.append('/Users/junichi/work/grpc/examples/python/helloworld/tmp')
import helloworld_pb2
import helloworld_grpc

def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'):

    def draw(self, context):
        self.layout.label(text = message)

    bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)


class Greeter(helloworld_grpc.GreeterBase):
    gy = 0

    async def SayHello(self, stream):
        request = await stream.recv_message()
        message = f'Hello, {request.name}!'
        bpy.ops.mesh.primitive_cube_add(size=1, view_align=False, enter_editmode=False, location=(0, self.gy, 0))
        bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
        self.gy=self.gy+5
        ShowMessageBox("This is a message : %s" % request.name)
        await stream.send_message(helloworld_pb2.HelloReply(message=message))


async def test(*,host='127.0.0.1',port=50052):
    loop = asyncio.get_event_loop()

    # start server
    server = Server([Greeter()], loop=loop)
    await server.start(host, port)
    print(f'Serving on {host}:{port}')
    await asyncio.sleep(30)
    server.close()
    await server.wait_closed()            


asyncio.run(test())

実行結果

www.youtube.com

学んだこと

  • pythonのモジュールパスを動的に追加する方法。
  • マルチスレッドでGUI操作には要注意。
  • Pythonでもコルーチンがつかえる。
  • grpclibモジュールを使えば、BlenderでもgRPCのサーバを立てられ、GUI処理も普通に記述できる。
  • pythonスクリプト実行中に3D viewを更新する方法を知った。

課題

  • マルチスレッドで処理を行い、メインスレッドになんらかの方法で通信してGUI処理を行う方法を探す

参考資料

関連記事

1年前の記事