はじめに
基本的に調べてわかったこと、知っていること、確かめたことしか書かないつもりですがどうしてもわからないことはあるのでわからないなりに備忘録がてら書く記事です。
つまり、間違ったことも書いてるかもしれない、ということです。
VDMXにはQuartz Composer以外にもISF (Interactive Shading Format) やVuoなどが使えます。Quartz Composer は(Appleも見捨てた)レガシーな規格で、未来はないので、早いうちにISFや他のフォーマットをメインに使うべきです。(ついでに言うとISFのメインであるopenGLも程よくレガシーになってるのでそのうち切られると思うのでISFも勉強しても無駄かもしれませんが。Appleは2018年にopenGLのサポート打ち切りを発表してます。)
しかしながら、ISFについての記事は基本的にないので、調べながら、あとは試行錯誤しながら作っていくことになります。今回はVDMXのエフェクトプラグインを作りながら、備忘録がてらISFを読み解いていこうと思います。
ISFとは
ISFとは
ISFとは、と書いてはじめにwikipediaを引用しようと思いましたけど、wikipediaに記事がありませんでした。
詳しくはISFの公式ホームページにて。
ISF (Interactive Shading Format) とは、JSONとGLSL ( openGL Shader Language) によるシェーダープログラムです。
いわゆる動画ファイルをながすプリレンダムービーと比較して使われるジェネ系VJが使う、アレです。
コーディングにはヘッダーとしてJSON形式のリストと、命令処理を行うGLSLからなります。GLSLで記述するコードには、画面のサイズや角度と言った、描画に関するコードであるVertex Shaderと、各ピクセルや各ポイントに対して色やエフェクト処理を行うFragment Shaderの2つからなります。
ISF Editor
VDMXをインストールした際についてくるISF EditorはISFのコーディングに特化したエディタです。基本的にこれ使っとけばそれなりに不満なく使えると思います。
VDMXのページにISFの基本的なコーディング方法が載っているので、そちらを参照するのもいいですね。
実践
はじめに
某所でこんなやりとりが。
そういえばどなたかResolumeのColorPassのような選択した色以外モノクロにするエフェクト書ける人いませんか?
Resolumeのそのままの機能で良ければレゾのFFGLをそのままLibrary/Graphics/FreeFrame Plug-Insにつっこめば動きますね カラーバーないのと32bitですが、、
とのことで、Resolumeに内蔵しているColor Passというプラグインを作ります。
Color Pass Fx
Resolumeを持っている人やResolumeのデモを入れている人は実際に試してみるといいと思います。
パラメータとしてある2つのHue値に挟まれた色だけがのこり、それ以外の色はグレースケール化されるというプラグイン。HueとはHSVと呼ばれるRGBとは別の色表現で、色相と呼ばれたりします。カラーパレットから色を取り出すときにはRGBではなくHSV情報として選択されますね。
上図だと、青だけが残り、赤や緑がグレースケールになります。
ISFで実装するために
今回は変形処理や3D回転等はしないので、GLSLのなかでもVertex Shaderには触れずにFragment Shaderのみに関して処理すれば良いということです。
さて、ISFを勉強するにあたり、単純なコーディングの知識のほかに必要なこととしては以下のようなものです。
VDMXのISF エフェクトプラグインとして機能するために必要なコーディング方法を知る必要があります。つまりVDMXから映像の受け取り、逆に処理後の受け渡し。各種パラメータの設定方法です。
次に、基本的にGLSLのコードはRGBAに対して処理しないといけないので、RGBをHSVに変換する処理が必要です。また逆に最終的にはRGBAで出力する必要があるので逆の変換処理も必要です。
最後に、処理の方法ですが、Shaderのコーディングならではの制約があったりします。具体的には今回は条件分岐について。GPUを使った処理ではif()文による分岐はタブー視されているそうで、そこの処理について勉強します。
コーディング
準備
ISF Editorを起動します。
Loading/Playback UIは、各種ISFプラグインを管理するリストが並ぶウィンドウです。
左下にある「Load User ISFs」と「Lead System ISFs」ですが、Load User ISFsは「user/(ユーザー名)/ライブラリ/Graphics/ISF」に保存されたfsファイルが、「Load System ISFs」は「user/ライブラリ/Graphics/ISF」に保存されたfsファイルが読み込まれます。
VDMXはLoad System ISFsにあるfsファイルを読み取るので、作成はUser ISFsに、完成したらSystem ISFsにファイルを置いてやれはいいですね。
左上にある「Video Source」はSyphonが使えるので、VDMXからSyphon Outして実際にどう動くかたしかめながらコーディングできます。
さて、上部タブの「File」から「New ISF File」を押すと、新規ISFファイルを作成します。
新規ISFファイルはテンプレートから読み込んだコードを読み出すので、初めから幾分かのコードが書かれています。
Editor UIには大きく分けて4つの窓があり、上部は左から「ISF Fragment Source Code」「ISF Vertex Source Code」「ISF JSON Editor」、下部にコンパイル結果を表示する各種窓があります。
コーディングに関して、JSON部分は右のISF JSON Editorを使えば、一切の手書きなく書くことができます。
コーディングスタート
さて、実際のコーディングですが、何よりもまずVDMXからきちんと映像を受け渡すことが重要です。まず、VDMXから映像をもらって、何も処理せずに返すコードを書いてみます。
まずJSON Editorから「INPUTS」の「Create New」をクリックして(項目 1)、インプットを1つ増やします。名前は「inputImage」でtypeは「image」です(項目 2, 3)。
これはISFをエフェクトとして使った時に、エフェクトに映像を送るためのインプットで名前は必ず「inputImage」にしないといけません。
ここまでやると、Fragment Sourceにコメントアウトされた(/*
と*/
で囲まれた)JSONリストが表示されます。
JSONリストは完了です。
void main(){
vec4 src_rgb = IMG_THIS_PIXEL(inputImage);
gl_FragColor = src_rgb;
}
次にコーディングですが、ISFの主要となる関数はmain()
関数です。
基本的には関数は{ }で挟みこんだところが処理対象なのでmain()
で行われる処理はmain(){ ... }
で囲みます。
次の2行は 各々命令で、一つの命令ごとに「;
」を打って改行します。「;
」がない場合改行しても命令処理は続くものとされます。
vec4 src_rgb = IMG_THIS_PIXEL(inputImage);
まず IMG_THIS_PIXEL()
は引数の各ピクセルごとの値を読み出す関数です。今回はsrc_rgb
という変数を用意して、inputImage
の各フレームの各ピクセルを格納します。
普通のプログラミング言語と違って、時間軸やXY(Z)軸全てを同時に扱うのが、Shaderコーディングの特殊なところですね。
vec4
は4次元ベクトルという意味です。ベクトルといっても空間を表すものではなく、4つの変数が同時に動きますよ、という意味。今回はRGBAの情報が入るのでvec4
になります。Alphaの情報が入らない場合はvec3
でも良い、ということですね。
gl_FragColor = src_rgb;
gl_FragColor
はFragment Shaderとして最終的に出力する値を格納する変数です。つまりこの変数が宣言されていないと最終的には何も出力されない、ということですね。
今回はgl_FragColor
にsrc_rgb
を代入しているので、src_rgb
が出力されるということです。gl_FragColor
には基本的にvec4
の型の情報を代入させる必要があります。
以上で、VDMXから読み取った映像をそのまま出力する というISFコードが書けました。
vec型について
vec4 は基本的にRGBAの情報が読み取られます。
vec4 src_rgbとしましたが、RGB各々情報を取り出したい時には後ろに「. (ドット)」をつけて必要なベクトルを取り出すことができます。
float valRed = src_rgb.r;
と書けば、vec4 src_rgb
のうち、r (1番目の情報)を取り出すことができます。
ベクトル情報は「r」「g」「b」「a」を書けばそのベクトル情報を取り出すことができます。もちろんベクトルなので「x」「y」「z」「w」でも表記することができるので、
float valRed = src_rgb.x;
も同値を指します。
ベクトルは要素として表すことができるので、
vec3 redOnly = (1.0, 0.0, 0.0);
と書けば赤だけの画面を出すことができますね。
数値表記ですが、数字は0から1の間をとる浮動小数点のfloat型しか入れれないので、「1」ではなく「1.0」と「0」ではなく「0.0」という表記にする必要があります。ここを間違えるとコンパイルエラーになりますので注意。
また、ベクトルの要素は2つ以上を同時に読み出すことができるので、
vec3 redOff = (0.0, src_rgb.gb);
のように要素の中にベクトルをまとめて表記することもできます。
コーディング ( Color Pass)
RGB HSV変換
さて、基本的なところがわかったところで実際にコーディングしていきましょう。RGB HSV変換ですが、この変換は単純な数式で変換できるので自分でコーディングすれば書けます。
が、先代の知恵を借りるということで、RGB HSV変換をしているISFファイルがあればそっちから持ってこればいいですね(邪道。
実際に探してみるとVDMXのISFエフェクトの中に「Color Mask.fs」や「Color Control.fs」がHSVを使った処理をしていることがわかります。
これらのコードの中身をのぞいてみましょう。
これらのコードのFragment Sourceのなかに、
vec3 rgb2hsv(vec3 c);
vec3 hsv2rgb(vec3 c);
という、vec3 の型で RGBをHSVに変換しそうな関数と、HSVをRGBに変換しそうな関数があります。
ざっと覗くと、main()
関数が閉じた後に、
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
//vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
//vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);
vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
というコードがあります。
これがrgb2hsv()
とhsv2rgb()
という変換になることがわかります。
では、実際にこれらのコードを借りてやりましょう。
みてきたコードのソースと同じように、main()
関数より前に関数の宣言、main()
関数を閉じた後に関数の内容を記述します。
これでRGB HSV変換が自由に使えるようになりました。
void main(){
vec4 src_rgb = IMG_THIS_PIXEL(inputImage);
vec3 src_hsv = rgb2hsv(src_rgb.rgb);
src_rgb.rgb = hsv2rgb(src_hsv.xyz);
gl_FragColor = src_rgb;
}
さて、このRGB HSV変換はvec3
型を取るので、vec4
として取り出してきたsrc_rgb
からAlphaを除いたsrc_rgb.rgb
を引数にとってやれば良いですね。代入されたsrc_hsv
はvec3
型を取ります。
逆に、HSV RGB変換はsrc_hsv.xyz
を引数に取ります。src_hsv.rgb
でも良いですが、RGBではないので混乱をさけるためにxyzで表記しています。
さて、これでRGB HSV変換してHSV RGB変換をするというISFエフェクトが完成しました。
条件分岐
さて、次は条件分岐です。
が、その前に条件処理をするための値としてHue1とHue2が必要なので、JSON EditorからINPUTを2つ増やします。
hue1 と hue2 は「0.0」から「1.0」の間の値をとるfloat型です。
Hue 1とHue2に挟まれたところだけ色情報を残し、そのほかはグレースケールにします。
プログラミングで条件分岐といえばif()
文を使いますが、Shaderプログラミングの場合、各ピクセルや各ベクトルに対して条件分岐をさせると処理に負荷がかかるため基本的には避けられます。
if (color.a > 0.5) {
color.r = 1.0;
}
else {
color.r = 0.0;
}
のような条件分岐は、
color.r = color.a > 0.5 ? 1.0 : 0.0;
という条件演算子を使って計算させる方が基本的には処理が軽くなるらしい。
つまり、
<条件式> ? <真式> : <偽式>
がif()
文と同義だということ。
これを元に条件式を書くと、
src_hsv.y = (src_hsv.x <= hue1)&&(src_hsv.x >= hue2) ? src_hsv.y : 0.0;
となります。 条件式に&&を使うのはどうなんだろうと思いながら、書いてますが。
さて、ResolumeのColor Passのエフェクトには「Invert」のボタンがありますので、「Invert」を実装しましょう。
Invertなので、逆転すれば良いので、
src_hsv.y = (src_hsv.x <= hue1)&&(src_hsv.x >= hue2) ? 0.0 : src_hsv.y;
がInvertの条件演算になりますね。
さて、Invertボタンですが、JSON Editorからもう一つ新規に追加します。On/Offボタンなのでbool型です。
このボタンに対して条件分岐するので、
if(invert == false){
src_hsv.y = (src_hsv.x <= hue1)&&(src_hsv.x >= hue2) ? src_hsv.y : 0.0;
}else if(invert == true){
src_hsv.y = (src_hsv.x <= hue1)&&(src_hsv.x >= hue2) ? 0.0 : src_hsv.y;
}
です。
ここでif()
文の使用に関してですが、今回のboolean値に対して条件分岐をするような場合は、複雑に変化するShaderの値の比較ではなく、静的な値で条件分岐をするので、条件演算子を使わず条件式で分岐させてもGPUに負荷がかからないそうで。
というわけで、invertの実装も完了です。
初期値の設定
JSONで設定したパラメータは呼び出した際に初期値や値の範囲を設定することができますので、それを設定していきます。
コードの全貌
/*
{
"CATEGORIES" : [
"Color Effect"
],
"DESCRIPTION" : "Color Pass",
"ISFVSN" : "2",
"INPUTS" : [
{
"NAME" : "inputImage",
"TYPE" : "image"
},
{
"NAME" : "hue1",
"TYPE" : "float",
"MAX" : 1,
"DEFAULT" : 1,
"MIN" : 0
},
{
"NAME" : "hue2",
"TYPE" : "float",
"MAX" : 1,
"DEFAULT" : 0,
"MIN" : 0
},
{
"NAME" : "invert",
"TYPE" : "bool",
"DEFAULT" : 0
}
],
"CREDIT" : "Keisuke Kimura"
}
*/
vec3 rgb2hsv(vec3 c);
vec3 hsv2rgb(vec3 c);
void main() {
vec4 src_rgb = IMG_THIS_PIXEL(inputImage);
vec3 src_hsv = rgb2hsv(src_rgb.rgb);
if(invert == false){
src_hsv.y = (src_hsv.x <= hue1)&&(src_hsv.x >= hue2) ? src_hsv.y : 0.0;
}else if(invert == true){
src_hsv.y = (src_hsv.x <= hue1)&&(src_hsv.x >= hue2) ? 0.0 : src_hsv.y;
}
src_rgb.rgb = hsv2rgb(src_hsv.xyz);
gl_FragColor = src_rgb;
}
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);
vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
VDMXへの取り込み
作ったfsファイルは先述の通り、「user/(user名)/ライブラリ/Graphics/ISF」にあるので、これを「user/ライブラリ/Graphics/ISF」に移してやってからVDMXを起動すれば自動的に読み込んでくれます。
あとはLayer FXに呼び出してやれば完了ですね。
まとめ
ということで、初めてISFを触りましたが、ポイントさえ押さえれば基本的なことは簡単に実装出来そうです。
ISF (というかジェネ系) は変形させたり動かしたりというのが醍醐味なので、Fragment ShaderよりもVertex Shaderを使えるようにならないといけませんね。