Maya ではじめる OpenGL

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 の呼び出し元は、
1. mode: `OpenMayaRender.MGL_~ `
2. func: `OpenMayaRender.MHardwareRenderer.theRenderer().glFunctionTable()`

のように別になっていて、API1.0 と API2.0 の名前空間をキープしつつ上記の切り替えを行うのは面倒です。
そこで、`__getattr__` を使ったクラスインスタンスで一度まとめてみました。もう少し良い書き方ありそうですが。

```python:OpenMayaRender
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)

    ...
```

これで以下のようなアクセス形式に変更できます。

```python
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 で描画されドッキングされる不思議な感覚です。

```python:QtOpenGL
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 派生クラスと同期が可能になります。

```python:shared
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](https://github.com/takeno1027/cedec2017/blob/master/AssetManager/managerTool/screenShot.py) で紹介があったMayaのサンプルの中で、スクリーンショットを撮るために pixelBuffer を取得します。現在のビューは、`OpenMayaUI.M3dView.active3dView` で取得出来ます。

```python:buffer

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
[](https://qiita.com/yopio/items/de38ba7475f48d2487e1)
[](https://kiwamiden.com/how-to-get-mayas-ui-as-pyside)
[](https://kiwamiden.com/move-cmds-widget-to-pyside)
[](http://doc.qt.io/qt-5/qglwidget.html)
[](http://help.autodesk.com/view/MAYAUL/2018/JPN/?guid=__files_Shapes_Drawing_and_refresh_htm)
[](#http://flame-blaze.net/archives/2743)