GenExpr

GenExpr

GenExpr は gen パッチャーが内部で使用する言語です。これはMax自体からは独立した方法で演算を記述するために使用されます。実際に演算を行うためには、様々な Gen オブジェクト(gen~ や jit.gen 等)によって、 これをCPUやGPUが使用するマシンコードに変換します。

GenExpr 言語は expr や codebox オブジェクトを使って、直接 Gen パッチャー内で使用することができます。これらのオブエジェクトは内部に書かれた式を分析し、必要な数のインレットやアウトレットを自動的に生成するため、内部に書かれた演算に対してパッチコードを接続することができます。 オブジェクトボックスを使って記述された演算とGenExpr 言語とで処理速度は全く変わらない点に注意して下さい。genパッチャーがコンパイルされる場合、最も処理に適した手段を使用するために、これらはすべて単一の表現に統合されます。

Gen パッチャーと codebox オブジェクト

GenExpr言語はGenパッチャー内でMaxをパッチングする環境を補うために設計されています。これにより、Maxのグラフィカルな語法を補助するために使用される並列テキストメカニズムが提供されます。一例を挙げると、Maxオブジェクトにはインレット、アウトレット、アーギュメント、アトリビュートといった概念がありますが、これらの構成要素とユーザが定義したGenExpr関数の構成要素は密接な対応関係にあります。さらに、GenExpr言語には 、GenExprが埋め込まれている expr や codebox オブジェクトのインレットやアウトレットを明確に参照するための、in, in1, in2, … や out, out1, out2 … といったキーワードがあります。

パッチに codebox を追加し、これをコンパイルします。

  • ロックされていないパッチャーウィンドウの空白部分をクリックし、"n" (new の頭文字) を入力して新しいオブジェクトボックスを作ります。このときカーソルが内部に表示 されるので、 'codebox' という語を入力してリターンキーを押します。リターンキーが押された時点で新しい codebox が生成されます。

  • codebox の中にGenExpr言語のコードを入力します。入力が終わったら codebox ウィンドウの最下部にあるCompile ボタンをクリックして、コードのコンパイルと実行を行います。

言語の基礎的事項

GenExpr 言語の構文は CやJavaScriptの構文と似たもので、上記の codex に書かれているようなシンプルな式文ですが、複数の文を書く場合には行末にセミコロンが必要です。次の codebox にはそれぞれ GenExprの有効な式が書かれています。左側の codebox のように、アウトレットの割当てがない1つの式が書かれている場合には、out1が割当てられていると見なされます。この式にセミコロンがない点に注意して下さい。1つの式しかない場合、セミコロンがあるものと見なされます。

しかし、複数の文が存在する場合、セミコロンが必要です。左側の codeboxにはセミコロンがないためエラーになります。右側の codebox は正しく動作します。

expr オペレータは codebox と同等の働きをしますが、構文の強調表示や複数行の表示といったテキストエディタ機能を持っていません。expr は短い、1行の式を用いる場合に役立ちます。これにより、ユニットとして動作するような一連のオブジェクト間のパッチ接続の手間を省くことができます。

expr や codebox は inN や outN (Nはインレット/アウトレットの位置)というキーワードを探索してインレットやアウトレットの数を決定します。in1 と out1 はそれぞれ左端のインレット、アウトレットを表します。便宜上、キーワード in、out はそれぞれ in1、out1と同等のものと見なします。

Gen パッチャーの中で作られるほとんどのオブジェクトは、GenExprの内部の関数、グローバル変数、宣言、あるいは定数として表すことができます。オブジェクトが持つインレットの数は、関数が取る引数(アーギュメント)の数に対応します。例えば、atan2というオブジェクトは2つのインレットを持つもので、次のように2つの引数を取ります。

out = atan2(in1, in2)

パラメータ

GenExprで宣言されたパラメータは、パッチ内の param オペレータと同様な機能を持ちます。パラメータはGenExpr のコードの中のインレットやアウトレット(inN や outN)が置かれるメイン部分でのみ宣言できます。これは、パラメータが、パッチャーの中でオブジェクトボックスのGen オペレータと同じレベルで動作するためです。

GenExpr の中のParam 宣言は、必要に応じてパラメータに名前を与え、生成します。GenExprの中でParam 宣言されたものと同じ名前を持つ param オブジェクトボックスが存在する場合、GenExprの Paramは単にparam オブジェクトボックスのエイリアスになるだけです。同じ名前の paramオブジェクトボックスが存在しない場合には、暗黙のうちにこれが生成されます。上のコードでは、offset はオブジェクトボックス param のエイリアスとなり、amp は新しいグローバルな param を生成します。

(訳注:原文では パラメータ名がそれぞれ、origin、scale となっていますが、上の例の場合、offset がオブジェクトボックスと同じ名前、ampが新しい名前になります)

コメント

GenExprのコメント文は、1行のもの、複数行に渡るものがあり、そのどちらもC言語の構文スタイルに倣っています。1行のコメントは // で始まり、複数行のコメントは /* から次の */ までで定義されます。

// this is a comment, below we sample an input

pix = sample(in1, norm);

複数の返値

オブジェクトボックスが複数のインレットとアウトレットを持つことができるのと同様に、GenExprの関数は複数の引数(アーギュメント)を取り、複数の値を返すことができます。 cartopol オブジェクトは2つのインレットと2つのアウトレットを持っていますが、同様に GenExpr の cartopol 関数は2つの引数(アーギュメント)を取り、2つの値を返します。コードでは、これを

r, theta = cartopol(x, y)

という形で表します。 複数の値を返す関数は変数のリストに対して適用することができます。このときの構文は次のような形になります。

変数1, 変数2, 変数3, … = <式>

複数の値を返す関数が、単独の値に適用された場合には、使用されない返値は単純に無視されます。GenExprコンパイラは不必要な演算を取り除く働きを持っています。 cartopol という関数は

r, theta = sqrt(x*x+y*y), atan2(y, x)

という形に展開できますが、theta を取り除いた場合、

r = sqrt(x*x+y*y), atan2(y, x)

になります。コンパイラはこれを

r = sqrt(x*x+y*y)

という形に単純化します。 逆に、thetaだけを使用する場合、Genコンパイラは r のための演算を取り除きます。

notused, theta = sqrt(x*x+y*y), atan2(y, x)
out = theta;

では、効率を考えて次のようになります。

theta = atan2(y, x);
out = theta;

出力の計算において中間式が共通といったより複雑な場合でも、コンパイラは不必要な演算を取り除くことができます。そのため、関数の返値を全く使用しない場合でもパフォーマンスが低下することはありません。左辺の変数のリストがそれぞれをカンマで区切れば良いように、右辺の関数もカンマで区切られたリストにすることができます。

sum, diff = in1+in2, in1-in2
out1, out2 = diff, sum

左辺の変数の数が右辺の関数の数より多い場合、余った変数には値0が与えられます。例えば、

out1, out2 = in1

では

out1, out2 = in1, 0

になります。 右辺の関数式の中に複数の値を返すものがある場合、その関数式がリストの最後尾であるとき以外は2番目以降の返値は無視されます。これは文章にすると複雑ですが、これらの例を見て頂くと分かりやすいでしょう。

返値が使われないケース

2番目の返値が破棄され、cartopolは最適化されます。

r = cartopol(x, y)

余分な変数が割り当てられているケース

c余った変数の値として0が与えられます。

x, y = in1

は、次のようになります。

x, y = in1, 0

変数式のリストの中に、複数の値を返すものがあるケース

最後尾の関数式に限り、複数の値を返すことができます。ここでは、右辺のcartopol はリストの最後ではないため、2番目の返値は破棄されます。

r, out1 = cartopol(x, y), in1

次の cartopol はリストの最後尾にあるため2つの値を返します。

out1, r, theta = in1, cartopol(x, y)

関数式が他の関数呼び出しの引数(アーギュメント)として用いられた場合にも、同じ原則が適用されます。次の例では、poltocar の2つの出力が min の2つの入力に結びつけられています。

out = min(poltocar(in1, in2))

GenExpr の関数定義

GenExpr の中で新しい関数を定義する方法は、既存のプログラミング言語の場合とほぼ同様です。GenExprにはデータ型がないため、 関数のアーギュメント(引数)は単に変数名だけで指定されます。基本的な関数の定義の方法、および、それと等しい働きを持つパッチの例を次に示します。

複数の値を返す関数は次のように定義します。

Jitter の Gen オブジェクトでシリンダ形状を作成する関数は次のようになります。

GenExpr の中のシンプルな関数はパッチによる接続でも簡単に行うことができます。しかし上のシリンダ形状のようなより複雑な関数では、すぐに大きなものになってしまいがちです。特にGenExprコードの中でこれが何回も使用されるような場合はなおさらでしょう。これはテキストによる表現の有利な点です。

オペレータのアトリビュート

Gen パッチャーにはアトリビュートを持つオブジェクトがあります。Jitter の Genオペレータ sample が持つ boundmode アトリビュートはその例と言えます。GenExprでは、 関数の引数(アーギュメント)はオペレータのインレットに対応し、関数が返す値はアウトレットに対応します。アトリビュートは キー/値 という形の引数(アーギュメント)として使うことができます。次はその例です。

out = sample(in, norm, boundmode="mirror");

この例は、境界の外側でミラーリング処理を行うように設定した sample オペレータを使用するものです。アトリビュートは boundmode をキー、"mirror"を値として設定されています。Max メッセージの概念は Gen パッチャーには存在しないため、アトリビュートを動的に変更することはできません。このことは GenExprでも同様です。アトリビュートの値は定数でなければなりません。アトリビュートが数値を取る場合、これを変数から代入することはできません。

GenExpr ファイルの要求(require オペレータ)

GenExpr でオペレータを定義した場合、これらを簡単に別ファイルとして保存し、再利用することができます。これにより、コードボックスで再度オペレータ定義を入力する必要がなくなります。別ファイルで定義されたオペレータをインクルードするためには require オペレータを使用します。require オペレータは .genexpr の名前を取り、その定義内容を読み込みます。次に示すものは全て .genexpr ファイルを取得する有効な方法です。

require("mylib")
require("mylib");
require"mylib"
require"mylib";

上のコードでは、require の呼び出しがきっかけとなって codebox が 'mylib.genexpr' というファイルを Max サーチパスから探索します。ここで要求された .genexpr ファイルは他のファイルから要求することが可能です。ファイルが複数の場所から要求された場合でも、ファイルのロードは1回だけです。

GenExpr ファイルは組み込みのMax テキストエディタで作成、編集することができます。GenExprファイルが gen オブジェクトの中で要求され、そのファイルが編集された後に保存された場合、gen オブジェクトはファイルが変更されたことをファイル監視によって検知し、新たな変更点を反映させるために再度コンパイルを行います。

GenExpr と Jitter での入力

Jitter の Gen オブジェクトは Jitter マトリックス(jit.matrix)やオブジェクト上のテクスチャ(jit.gl.texture)の両方、あるいは片方を利用することができます。Gen パッチャー内では、in オペレータは入力されるマトリックスやテクスチャ全体、およびそれらの中の現在処理されているセル(カレントセル)を表します。ほとんどの場合、in オペレータは処理されるカレントセルを表しています。in がマトリックスやテクスチャの全体を表すのは、sample と nearest オペレータの場合のみです。in オペレータが sample と nearest の左側の入力に接続された場合のみ、入力の中の任意の場所からデータを取り出すために使用することができます。このことは sample や nearest が GenExpr の中で使用される場合でも同様です。

GenExpr のコードがコンパイルされる場合、sample や nearest への入力の第1アーギュメントが実際にGenパッチャーの入力であるかどうかを検証し、もしそうでない場合にはエラーが返されます。この検証処理は、関数呼び出しの際にも行われるため、sample や nearest は関数内部で問題なく使用することができます。

GenExpr と Jitter の座標(Coordinate )処理

Jitter の Gen パッチャー内での座標処理(norm, snorm, cell, dim )はオペレータとグローバル変数のハイブリッドになっている特殊なオペレータです。次の例では、これらのオペレータを使用する場合に共に有効な2つの構文を示しています。

out1 = norm * dim();

上の例の最初のインスタンス norm は構文上ではグローバル変数ですが、dim は関数呼び出しになっています。すべての座標処理オペレータはこの文法上の慣習に従います。

テクニカルノート

GenExpr は型を持たない言語です。変数の型は、Gen の領域や Gen オブジェクトの入力に基づき、コンパイラによって自動的に与えられます。Gen の変数はまた、デフォルトではローカルなスコープを持つものであるため、JavaScript の var のようなキーワードを伴った宣言を必要としません。今の所、GenExpr には配列というデータ構造の概念がないため、配列の記法 [index] は存在しません。