Maya の Qt で頑張って GUI を書いていると、ごちゃごちゃしたボタンや画面スペースを逼迫するウィンドウを整理して、1枚のキャンバスでHUDベースのインターフェースを使いたい、と思うことが度々あります。
コンテンツ制作をサポートするツール制作において、OpenGL や DirectX を叩く機会はほぼないですが、必要でないものを知ることで解決手段の幅を広げるのもまた真ということで、苦手意識のあった OpenGL に手を出してみることにしました。(ゲームだとDirectXなんですが)
幅広い問題解決手段を得るためにあえて不自由な低レベルに潜ってみる、ということが本記事の趣旨です。
注意点
環境の準備が手軽な Python で検証を行っていますが、移植すれば C++ でも問題なく動くと思います。ただ注意点として C と Python 間で型を意識しないといけないので、ctypes
によるキャストが多発します。保守や速度で不安が出たら C++ で書き換えるのが良いかもしれません。
OpenMayaRender の glFunction の差異を吸収する
高速でよりPythonらしいAPI2.0 も OpenGLコマンドをコールするために、PythonAPI 1.0 が必要です。
Mode と Function の呼び出し元は、
- mode:
OpenMayaRender.MGL_~
- func:
OpenMayaRender.MHardwareRenderer.theRenderer().glFunctionTable()
のように別になっていて、API1.0 と API2.0 の名前空間をキープしつつ上記の切り替えを行うのは面倒です。
そこで、__getattr__
を使ったクラスインスタンスで一度まとめてみました。もう少し良い書き方ありそうですが。
import inspect
class MayaOpenGL(object):
"""opengl wrapper"""
def __getattr__(self, item):
def _getGL(mod):
for cmd, _ in getmembers(mod):
if cmd == item:
setattr(
self, cmd,
(lambda str: dict(getmembers(mod))[str])(cmd)
)
return getattr(self, item)
from maya import OpenMayaRender as _mgl
for gl in _mgl, _mgl.MHardwareRenderer.theRenderer().glFunctionTable():
if _getGL(gl):
return _getGL(gl)
raise TypeError("%s is not found." % item)
...
これで以下のようなアクセス形式に変更できます。
gl = MayaOpenGL()
gl.glClear(gl.MGL_COLOR_BUFFER_BIT)
gl.glBegin(gl.MGL_TRIANGLE_FAN)
gl.glColor3f(1, 0, 0)
gl.glVertex3f(-0.5, -0.5, 0)
...
動的にアクセス先を変更するので、devkit導入済みでオートコンプリートの設定をしていると補完されなくなります。使い勝手的に一長一短かもしれません。
QGLWidget と Maya Widget の共存
QtはOpenGLを描画できるQGLWidgetを持っていますが、GLFunctionを持っていません。PythonでOpenGLコマンドのコールを行うには PyOpenGL
を pipインストールするか、
ctypes.cdll.OpenGL32
によるラッピング、Maya環境に限れば先に書いた PythonAPI 1.0 の OpenMayaRender
を使うことができます。
QtOpenGL.QGLWidget を継承した派生クラスでは initializeGL
と resizeGL
と paintGL
を実装します。MayaQWidgetDockableMixin
モジュールを多重継承したクラスのインスタンスが Maya の OpenGL で描画されドッキングされる不思議な感覚です。
from PySide2 import QtOpenGL, QtWidgets
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
class MayaQtGLWidget(MayaQWidgetDockableMixin, QtOpenGL.QGLWidget):
def __init__(self, *args, **kwargs):
super(MayaQtGLWidget, self).__init__(*args, **kwargs)
self.setWindowTitle("OpenGLWidget v%s.%s" % (
self.glft.format().majorVersion(),
self.glft.format().minorVersion()
))
self.resize(200, 240)
def initializeGL(self):
gl.glClearColor(0.1, 0.1, 0.1, 1)
def resizeGL(self, w, h):
gl.glViewport(0, 0, w, h)
gl.glLoadIdentity()
gl.glOrtho(0, 1, 0, 1, -1, 1)
def paintGL(self):
gl.glClear(gl.MGL_COLOR_BUFFER_BIT)
gl.glBegin(gl.MGL_TRIANGLE_FAN)
gl.glColor3f(1, 0, 0)
gl.glVertex3f(-0.5, -0.5, 0)
gl.glColor3f(0, 1, 0)
gl.glVertex3f(0.5, -0.5, 0)
gl.glColor3f(0, 0, 1)
gl.glVertex3f(0, 0.5, 0)
gl.glEnd()
def floatingChanged(self, flag):
self.glDraw()
def dockCloseEventTriggered(self):
self.glDraw()
def show(self):
super(MayaQtGLWidget, self).show(dockable=True, floating=True)
Mayaのウィジェットそのものを組み込みたいだけなら modelEditor
の方がはるかに楽なのですが、両方知っておくと柔軟に対応できます。ただ MayaQWidgetDockableMixin
が buggy でドッキング時に描画が消失してしまうことがあるので、なんらかの対策が必要です。
OpenGL を組み込んだノード制作(途中)
Maya のプラグイン制作で OpenGL を中に組み込む際に必要なことは、beginGL
と endGL
内で OpenGL のコールを行うことだけです。PythonAPI 2.0 の OpenMayaRender も併用します。(最新のグラフィクスAPIを見たあとに OpenGL を調べてみると、Apple が deprecate に持っていく感覚もなんとなくわかる気がします...)
Viewport2.0 と LegacyViewport の分岐もあってややこしいので、環境を絞って作成するのが一番良いです。
http://help.autodesk.com/view/MAYAUL/2018/JPN/?query=beginGL&devTool=Python にサンプルがあるので、興味がある方は覗いてみて下さい。私も勉強します。
OpenGLViewport から sharedContext の取得
これは公式のテクニカルのノートに書かれている内容そのままで、Mayaのglコンテクストを取得することで自作 QGLWidget 派生クラスと同期が可能になります。
from PySide2 import QtWidgets, QtOpenGL
from shiboken2 import getCppPointer, wrapInstance
widget = QtWidgets.qApp.property("mayaSharedQGLWidget")
glWidget = wrapInstance(
long(getCppPointer(widget)[0]), QtOpenGL.QGLWidget)
glWidget.format()
OpenGL pixelBuffer を Viewport から取得
昨年の CEDEC で紹介があったMayaのサンプルの中で、スクリーンショットを撮るために pixelBuffer を取得します。現在のビューは、OpenMayaUI.M3dView.active3dView
で取得出来ます。
class MayaOpenGL(object):
# ...
def capture(self, imgFilePath, resize=(-1, -1)):
# format
view = self.view()
w = view.portWidth()
h = view.portHeight()
view.refresh()
# read pixel
view.beginGL()
img = OpenMaya.MImage()
img.create(w, h)
self.glReadBuffer(self.MGL_FRONT)
cdll.OpenGL32.glReadPixels(
0, 0, w, h, self.MGL_RGBA, self.MGL_UNSIGNED_BYTE,
c_void_p(img.pixels())
)
view.endGL()
# save image
if resize != (-1, -1):
img.resize(resize[0], resize[1], True)
ext = os.path.splitext(imgFilePath)[1][1:]
img.writeToFile(imgFilePath, ext)
def view(self):
return OpenMayaUI.M3dView.active3dView()
glReadPixels
のポインタ渡しの部分が、OpenMayaRender でうまく再現できなかったので、参考コード通りに ctypes
の経由の glReadPixels
と c_void_p
を使って実現しています。
まとめ
Viewport の描画をウィジェット内に組み込んだり、オリジナルフォーマットの組み込み用途、バックエンドを知った上でのプロトタイプ制作手段として、知っておいて損はない内容にまとまりました。
ここにノードエディタを組み合わせれば、Mayaをホストにしたリッチなアプリケーションが作れそうですね。
参考
https://area.autodesk.jp/column/tutorial/maya_game_engine/01_encouragement/
https://dftalk.jp/?p=3175
https://github.com/volodinroman/mayaViewportPainter