C++
OpenGL
VST
JUCE
Assimp
JUCEDay 9

DAW上にティラノサウルスを召喚した

近年、 Microsoft WordVim などのアプリケーションが、ティラノサウルス暴走用プラットフォームとして進化し12、注目を集めています。[要出典]

僕は思いました。

「なんでDAW(音楽製作ソフト)上ではティラノサウルスが暴走できないんだ?」


できました

VST.rexというオーディオプラグインを作成しました。

名前の由来は、オーディオプラグイン規格のデファクトスタンダードであるVST (Virtual Studio Technology)3 をもじったものです。あなたの仮想の音楽スタジオ上でティラノサウルスが暴れ回ります。

https://github.com/hotwatermorning/vstrex


機能


  • 入力される音の大きさに合わせてティラノサウルスの大きさが変動します。

  • DAWから渡されたテンポとオートメーションカーブに応じて走るスピードを変更できます。

  • 回転するスピードもオートメーションカーブで調整できます。

  • プラグインを複数個ロードすれば、複数のティラノサウルスを暴走させられます。


仕組み

このプラグインは、エディターウィンドウ (通常プラグインのつまみなどのUIを表示するウィンドウ。今回はVST.rexのロゴ画像を表示している) とは別に独自のウィンドウを作成し、そのウィンドウ上に3DモデルのデータをOpenGLで描画して、ティラノサウルスを表示しています。

エディターウィンドウとは別のウィンドウを作成することで、プラグインのエディターウィンドウ領域を離れて、画面全体の好きな箇所にティラノサウルスを表示できるようになっています。

3Dモデルの読み込みにはThe Open-Asset-Importer-Lib (Assimp) を利用し、描画とアニメーションには、Skeletal-Animation-Libraryを使用しています。

そして、(プラグインなので) DAW側からオーディオ信号が送られてくるので、そこから簡易的に音量やテンポの情報を検知して、それをもとに3Dモデルの描画パラメータを変更し、大きさや走るスピードなどを変化させています。


直面した問題と回避策

今回、OpenGLや3DGC周りをあまり分からないまま作り始めたので、色々な問題に直面しました。以下にそれをまとめておきます。


3Dモデルが読み込めない その1

最初にこのプラグインを思いついた時には、Word上で読み込んで話題になっている3Dモデルをそのまま利用できればいいと思っていたのですが、それをアニメーション付きで読み込む方法がわからなくて断念しました。

https://www.remix3d.com/details/G009STZ07K67?section=other-models

これを Windowsのペイント 3Dで読み込んでfbxやglbファイルとして書き出してもアニメーションの情報は含まれていないようだったので、この方法は諦めました。 (なにかいい方法があったら教えてください)


3Dモデルが読み込めない その2

ちょうどその頃、 @mattn さんのvim-trexが公開されました。

そこでは、BabylonJSというライブラリのリポジトリに含まれるgltfファイルをダウンロードしてくる仕組みになっていたので、それと同じもの利用できると良さそうだと思って、そのgltfファイルをC++で読み込む仕組みを探しました。

C++でモデルファイルを読み込むには Assimp ライブラリが便利そうということを知って、Assimpを使ってそのgltfファイルを読み込むのをいろいろ試していたのですが、どうにもモデルが崩れて正しく読み込めません。Assimpのサンプルプログラム上でも同様に崩れたので、Assimp側の問題だったようです。

なにかパラメータを変えて読み込めばうまくいく方法もあるのかもしれませんが、ちょっと自分の知識レベルでは難しかったです。

(Assimpのgithubリポジトリにはgltf関連のissueがいくつか登録されているので、もしかするとgltfファイルの読み込み周りは結構buggyなのかも知れません)

結局別のモデルファイルを用意して利用するようにしました。


マウスクリックが透過しない その1

先に書いたとおり、このプラグインは、エディターウィンドウ (ロゴ画像を表示しているウィンドウ) の他に、独自のウィンドウを作成し、そのウィンドウ上にティラノサウルスを表示しています。

実はこのとき、独自のウィンドウは2つ作成しています。

単にティラノサウルスを描画して表示するだけなら、作成するウィンドウは1つで良いはずでしたが、macOSとOpenGLに関する問題によって、2種類のウィンドウが必要になりました。

どうやら、macOSだと、OpenGLで描画するウィンドウのマウスクリックが透明領域で透過しないようです。

参考: https://groups.google.com/a/chromium.org/forum/#!topic/chromium-dev/d3-DYB7a1Ug

参考: https://sonson.jp/blog/2006/04/04/opengl-cocoa-1/

透明領域でマウスクリックが透過しないということは、ティラノサウルスを囲む矩形部分のクリックが全部プラグインに取られてしまって、ティラノサウルス周辺にあるDAWのコントロールをクリックできなくなってしまいます。これは音楽製作に支障がでてしまうのでよくありません。ティラノサウルスの暴走と音楽製作は両立しなければいけません。

ティラノサウルスが表示されている所だけをクリック対象にして、その周辺の透明領域のマウスクリックを透過させるために、このプラグインでは以下のような回避策を導入しました。


  • マウスクリックを完全に透過する透明なウィンドウ (RenderingWindow) を1つ作成しその上にOpenGLでティラノサウルスを描画する。

  • その描画結果を画像として取得する。

  • もう一つの別のウィンドウ (DrawingWindow) 上に、その画像を、OpenGLを使用せずに描画する。

このように、 RenderingWindowとDrawingWindowという2つのウィンドウを作成することで OpenGLを使ったとき起こるマウスクリックが透過しない問題はクリアできました。


マウスクリックが透過しない その2

マウスクリックの透過について、JUCE側にも一つ問題がありました。

今回望ましい挙動は、透明領域ではマウスクリックを透過し、非透明領域ではマウスクリックを検知したいというものです。

ところが、juce::DocumentWindowで作成したウィンドウは、透明/非透明領域に関わらず、ウィンドウ領域全体でマウスクリックを完全に無視 (透過) するか、完全に検知するかのどちらかの挙動しか選べませんでした。

juce::DocumentWindowクラスが内部的に利用しているNSWindowクラスは本来、(OpenGLによる描画でない限り) 望ましい挙動をするはずです。

しかし、JUCEはNSWindowクラスを初期化する際にsetIgnoresMouseEventsというメソッドを必ず呼び出す仕組みになっていて、NSWindowクラスは一度でもsetIgnoresMouseEventsメソッドを呼び出すと、本来の望ましい挙動が無効になってしまうのです。

これを回避するには、setIgnoresMouseEventsメソッドの呼び出しを無視するようにJUCE側のソースを改変する必要がありました。 (非公開関数を使用することでこの問題を回避できるという情報もありましたが、僕のところではうまくいかない感じがしたので、呼び出しを無視するように対処しました)

参考: http://xcatsan.blogspot.com/2008/11/blog-post_05.html

参考: http://xcatsan.blogspot.com/2008/11/blog-post_06.html

そこでJUCEのリポジトリの5.4.1時点からforkしたリポジトリを用意し、改変を加え、これをプラグインビルド時に使用するようにしました。

改変した内容: https://github.com/hotwatermorning/JUCE/commit/f8ada5f00b638139f1c5b02f63ce50f4e7482ce6


モデルが正しく描画されない

今回、ティラノサウルスのモデルを透明ウィンドウに描画し、その結果を画像として取り出す仕組みにしたわけですが、そうするとティラノサウルスのモデルの前後が正しく描画されない問題が発生しました。

画像として描画結果を取り出すために、juce::OpenGLFrameBufferクラスを利用したのですが、このクラスが用意するFrameBufferでは深度バッファが有効にならないため、前後関係が考慮されずに変な描画結果になってしまいました。

juce::OpenGLFrameBufferクラスの内部実装では深度バッファの仕組みがサポートされているのですが、これを有効にする仕組みは用意されていないようで (なぜ?) 、結局この仕組みがデフォルトで有効になるように、JUCEのソースに手を加える必要がありました。 (この変更も上記のforkしたリポジトリのコミットに含まれています。)


まとめ

このネタを思いついてから作り上げるまでに色んな試行錯誤が必要で結構大変でしたが、とりあえず公開できて、twitterでもたくさんいいねをもらえて良かったです。

まだ時々うまく表示されないバグがあったり、多分今は使用しているライブラリ側の実装で固定パイプラインによる実装になっててシェーダー使って描画したほうが負荷が下がっていいんじゃないかとか気になるので、引き続きOpenGL勉強しながらブラッシュアップしていこうと思います。