/**/

チュートリアル 46:
JavaScript を使ったマトリックスデータの操作

前のチュートリアルで見たように、Max の js オブジェクトを使って、Jitter オブジェクトのデータラインを手続き的な JavaScript のコード上で設計することができます。JavaScript の JitterObject と JitterMatrix オブジェクトによって、Max パッチャーの中で行う場合とほとんど同じように新しいJitter オブジェクトとマトリックスを作り、それらを扱うことができます。Max パッチャーを使って行うことが厄介であったり、難しかったりするような方法で Jitter マトリックスに格納されたデータを操作する必要がある場合は数多くあります。このチュートリアルでは、jsの中で、JitterMatrix オブジェクトのメソッドやプロパティを使ってマトリックスデータをどのように操作するかということについての様々な解決策を見ていくと同時に、JavaScript の中での jit.exprの使い方についても説明します。

このチュートリアルは、前章のチュートリアル45:「JavaScript 内での Jitter の使用に関するイントロダクション」を読了していることを前提としています。加えて、このチュートリアルでは Jitter OpenGL オブジェクトや jit.expr を使うため、前もってチュートリアル30:「3Dテキストの描画」チュートリアル31:「レンダリング・デスティネーション」チュートリアル39:「空間マッピング」を復習しておいた方が良いと感じるかもしれません。

・Jitter Tutorials フォルダの中にある、46jJavaScriptOperators.pat というチュートリアルパッチを開いて下さい。

このチュートリアルパッチでは、46jParticles.js というファイルをロードする js オブジェクトを使用しています。JavaScript コードは bang に応答して、Jitter マトリックスを生成します。この bang は、その後、パッチャー内にある jit.gl.render オブジェクトにも送信されています。

このファイルには、私たちがパッチからメッセージとして送信することができる様々なセッティングに応答する多くの関数が含まれています。


このJavaScript ファイルを含んでいるパッチャー

・パッチの左側にある qmetro オブジェクトの上の toggle ボックスをクリックして下さい。parts という名前の jit.window に表示される結果を観察して下さい。

この JavaScript コードは、シンプルなパーティクルシステムによって表されている「点の集合」を生成しています。パーティクルシステムは、本来、パーティクルと呼ばれるたくさんの(時として膨大な)空間上の点を操作するアルゴリズムを指します。これらのパーティクルは、時間の経過と共に、相互の関係、または空間内での他の主体との関係によってどのように動くかを決定する法則を持っています。基本的なレベルでは、パーティクルは空間座標を持っているだけです。パーティクルシステムにはそれ以外の情報を含めることもできます。そして、水の流れや煙などの、幅広く、多岐にわたる自然現象のプロセスのシミュレートを行うことが可能です。

パーティクルシステムは、私たちの環境のコンピュータによるシミュレーションで広く用いられ、このような多くのコンピュータ生成画像(Computer-generated imagery:CGI)アプリケーションの主要なテクノロジとなっています。


Jitter マトリックスによって生成されたパーティクルシステム

パーティクルの広大な世界

私たちの JavaScript シミュレーションで使われるパーティクルは、2組のランダムな点の集合の生成によって動作しています。それは、パーティクルの位置と速さを表すもの、および、引力を持った 3D 空間内の多くの空間上の位置を表すものです。この引力の中心はアトラクタと呼ばれ、フレームごとにパーティクルに作用して、徐々にパーティクルをその中心に向けて引きつけます。jit.window を見るとわかるように、このパーティクルシステムの点は、1つ、またはそれ以上の特異点に向かって徐々に潰されていきます。さもなければ、パーティクルがアトラクタ点の間で振動するような状態になるかもしれません。これは、アトラクタ点の間に生じる、次第に安定に向かうような相反する引力の場に捉えられるためです。

・パッチャーを一番手前のウィンドウにして、キーボードのスペースバーを押すか、jsオブジェクトに接続されている init と書かれたメッセージボックスをクリックして下さい。何回か試してみて、私たちのパーティクルシステムが異なる動作をするを観察して下さい。

js オブジェクトに対する init メッセージによって、パーティクルシステムをリブート(再起動)します。これによって、ランダムにパーティクルが散らされ、新しいアトラクタが生成されます。

・キーボードの「s」キーを押し下げたままにするか、smear $1 と書かれたメッセージボックスに接続された toggle ボックスをクリックして下さい。パーティクルは。移動した後にその軌跡を残すようになります。「s」キーを離して(または、 toggle ボックスのチェックを外して)、通常のビジュアライゼーション(視覚化)に戻して下さい。

時間の経過の中でパーティクルシステムを観察することによって、アトラクタに向かって潰れていく点の軌跡を見ることができます。


1組のシンプルな法則による様々な振る舞い.

jit.window をクリックして下さい。赤、緑、青の座標軸が空間いっぱいに表示されます。マウスを使って空間を回転させて下さい。ズームアウト([AL]または[Option] キーを押し下げたまま、ウィンドウ内でドラッグします)をしてみて下さい。異なった視点から、パーティクルシステムを再スタートさせて下さい。

jit.gl.handle オブジェクトは、jit.gl.render オブジェクトをコントロールして、パーティクルシステムを3次元の世界で表現することを可能にします。空間を回転させ、異なるパースペクティブを得ることによって、アトラクタがどのようにしてパーティクルを引き寄せるかを見ることができます。

覆いの下では

これまで、パッチの機能の大部分を見てきました(後ほど、もう一度見てみます)。ここでは、JavaScript コードに注目して、アルゴリズムがどのように構築されているかを見て行きましょう。

・チュートリアルパッチの jsオブジェクトをダブルクリックして下さい。このパッチの js オブジェクトのためのソースコードが書かれたテキストエディタが表示されます。このコードは、チュートリアルパッチと同じフォルダに ‘46jPatricles.js’というファイル名で保存されています。

この JavaScript コードは、パーティクルとアトラクタを Jitter マトリックスとして操作することによって動作しています。パーティクルシステムは、js オブジェクトが bang を受け取るごとに次の「世代」に更新されます。これは、Jitter マトリックスの中のデータに対して一連の操作を行うことによって実現されます。私たちは、Jitter のアーキテクチャを利用して、マトリックス全体に対する数値演算を一度に行うことができます。これは、すでににシステムがマトリックスとしてコード化されているためです。このことは、多くの場合、一度に適合されなければならない個別の値を持つデータに対する処理を行う上での速さ、明瞭性、効率において強みを発揮します(例えば、ここで行っているようにパーティクルを 配列(Array)としてコード化する場合などがそれにあたります)。

この JavaScript コードには、実際にパーティクルシステムを世代から世代へ更新するための3つの方法が含まれています。この各々はパーティクルを表すマトリックスデータの処理のために異なったテクニックを使用しています。パーティクルシステムは、JitterMatrix オブジェクトに対する一連の op() メソッドを使う1つのエントリとして処理することが可能で、jit.exprオブジェクトを使うことによって、あるいは、パーティクルシステムの中の1つの点ごと(または、1つのセルごと)に繰り返し処理を行うことによって実現されています。ここでは、それらに共通するコードを調べた後、各々を順番に見て行きます。

・JavaScript ファイルの最初にあるグローバルブロックに注目して下さい。

最初のコメントブロック、および インレット、アウトレットの宣言の後に、多くの変数が宣言、初期化されているのを見ることができます。ここには、いくつかの JitterObject や JitterMatrix オブジェクトが含まれています。このコードを詳細に検討することによって、パーティクルシステムを実現している方法についての概略を理解することができます。

var PARTICLE_COUNT = 1000; // パーティクルの頂点の数の初期値
var ATTRACTOR_COUNT = 3; // 引力点の数の初期値

この2つのグローバル変数 (PARTICLE_COUNTATTRACTOR_COUNT) は、パーティクルの数、および、このシミュレーションにおいて作用させたい引力点の数を決定するものです。この2つによって、パーティクル、およびアトラクタの情報を持つマトリックスの dim(訳注:マトリックスのサイズ)が決定されます。

// パーティクル、および速度を生成するための [jit.noise] オブジェクトを作ります。
var noisegen = new JitterObject("jit.noise"); noisegen.dim = PARTICLE_COUNT; noisegen.planecount = 3; noisegen.type = "float32";

// アトラクタを生成するための [jit.noise] オブジェクトを作ります var attgen = new JitterObject("jit.noise"); attgen.dim = ATTRACTOR_COUNT; attgen.planecount = 3; attgen.type = "float32";

パーティクルシステムは、init() 関数によってランダムに生成されます。これについては、後で調べます。ここでJitterObject オブジェクトとして作られた2つの jit.noise オブジェクトが init() 関数の中で実際にこの処理を実行します。これらは、システムによって指定されたパーティクル、およびアトラクタの数に対応したサイズの、float32 型の値を持つ1次元のマトリックスを生成する機能を提供します。jit.noise オブジェクトによって生成されるマトリックスは3つのプレーン数( planecount )を持っていて、それぞれが空間座標 x,y,z に対応しています。

// bang_expr() 関数のための2つの [jit.expr] オブジェクトを作ります

// 第1の式: 入力マトリックスの全てのプレーンの値を合計します。
var myexpr = new JitterObject("jit.expr"); myexpr.expr = "in[0].p[0]+in[0].p[1]+in[0].p[2]";

// 第2の式: a+((b-c)*d/e) を評価します。
var myexpr2 = new JitterObject("jit.expr"); myexpr2.expr = "in[0]+((in[1]-in[2])*in[3]/in[4])";

パーティクルシステムを更新する方法の1つは、JavaScript コードの中で2つの jit.expr オブジェクトを使うものです。コードのこの部分では、JitterObject オブジェクトを作り、それによって使用される数式を定義しています(expr アトリビュート)。この部分については、後でこれを使用しているコードを調べる時までそのままにして、先に進みましょう。

// データを格納するために必要となる Jitter マトリックスを作ります。

// パーティクルの頂点 x,y,z のマトリックス
var particlemat = new JitterMatrix(3, "float32", PARTICLE_COUNT);

// パーティクルの速さ x,y,z のマトリックス
var velomat = new JitterMatrix(3, "float32", PARTICLE_COUNT);

// 引力点 x,y,z のマトリックス(引力の中心)
var attmat = new JitterMatrix(3, "float32", ATTRACTOR_COUNT);

// 距離の総計のためのマトリックス
var distmat = new JitterMatrix(3, "float32", PARTICLE_COUNT);

// bang_op() 関数のためのテンポラリマトリックス
var tempmat = new JitterMatrix(3, "float32", PARTICLE_COUNT);

// bang_op() 関数のための加算用テンポラリマトリックス
var summat = new JitterMatrix(1, "float32", PARTICLE_COUNT);

// bang_op() 関数のためののもう1つの加算用テンポラリマトリックス
var summat2 = new JitterMatrix(1, "float32", PARTICLE_COUNT);

// 現時点の引力点を格納するスカラ・マトリックス
var scalarmat = new JitterMatrix(3, "float32", PARTICLE_COUNT);

// 加速度を格納するスカラ・マトリックス (expr_op() 関数のみ使用)
var amat = new JitterMatrix(1, "float32", PARTICLE_COUNT);

私たちのアルゴリズムは多くの JitterMatrix オブジェクトを必要とします。これらは、パーティクルシステムに関する情報を格納するために、そして、システムの各世代の処理を行っている間の一時的なデータ格納用として使用します。最初の3つのマトリックス(変数 particlimatvelomatattmat と関連づけられます)はそれぞれ、パーティクルの x、y、z 位置、x、y、z 方向の速さ、そしてアトラクタのx、y、z 位置を格納します。その他の6つのマトリックスはシステムの各世代を生成する演算のために用いられます。

var a = 0.001; // 加速度係数
var d = 0.01; // 減衰係数

この2つの変数は、パーティクスシステムの動作に関する2つの重要な側面をコントロールします。変数 a はパーティクルがアトラクタに引き寄せられる時の加速度をコントロールし、変数 d はパーティクルの現在の速度の世代ごとの減衰をコントロールします。この2番目の変数は、パーティクルが方向を変えたり、他のアトラクタへ引き寄せられられたりしやすいかに影響を与えます。

var perform_mode="op"; // デフォルトのパフォーム関数
var draw_primitive = "points"; // デフォルトの描画プリミティブ

最後のこの2つの変数は、パーティクルシステムの処理に使う3つのテクニックのうちの1つ( perform_mode )を定義します。さらに、JavaScript コードによって Max パッチャーに出力されるパーティクルのマトリックスをjit.gl.renderオブジェクトが視覚化する方法( draw_primitive )を定義します

初期化フェーズ

パーティクルシステムを作成するための第1段階は、パーティクルとそれに作用するすべての要因(このケースでは、アトラクタ点)の初期状態を生成することです。例えば、滝のシミュレートをしようと思う場合、全てのパーティクルが空間の一番上にあり、空間の一番下に固定された重力場がパーティクルのそれぞれの世代に対して作用するという設定でスタートするでしょう。ここでのシステムは、現実世界(リアルワールド)という点か見るとそれほど大掛かりなものではありません。パーティクルとアトラクタは 3D 空間の中で単にランダムな位置にあるだけです。

・JavaScript コードの中の loadbang() および init() 関数を見て下さい。

function loadbang() // Max パッチが開いた時にこのコードが実行されます。 { init(); // マトリックスの初期化 post("particles initialized.\n"); }

function init() // 初期化ルーチン...ロード時、および、パーティクルまたはアトラクタの数を // 変更した時に呼び出されます { // -1 〜 1 の範囲にわたって、ランダムなパーティクルのマトリックスを生成します noisegen.matrixcalc(particlemat, particlemat); particlemat.op("*", 2.0); particlemat.op("-", 1.0);
// -1 〜 1 の範囲にわたって、ランダムな速度(ベロシティ)のマトリックスを生成します noisegen.matrixcalc(velomat, velomat); velomat.op("*", 2.0); velomat.op("-", 1.0);
// -1 〜 1 の範囲にわたって、ランダムなアトラクタのマトリックスを生成します attgen.matrixcalc(attmat, attmat); attmat.op("*", 2.0); attmat.op("-", 1.0); }

js オブジェクトの loadbang() 関数は、js ファイルを含むMax パッチャーがロードされたときに実行されます。これは、オブジェクトがパッチの他の部分と共にインスタンス化された後に実行され、Max パッチ内の loadbangloadmess オブジェクトがメッセージをトリガするのと同時にトリガされます。ここでの loadbang() 関数は、単に init() 関数を呼出し、その後Max ウィンドウに、初期化処理が無事終了した事を報告する親切なメッセージを表示するものです。

JavaScript のloadbang() 関数は、js オブジェクトを含むパッチャーがロードされたときにのみ実行されます。この関数は、JavaScript を変更し、再コンパイルした場合に実行されるものではありません。

init() 関数は、Max パッチャーから(スペースバーによってメッセージボックスがトリガされて)呼び出されたときだけでなく、パッチをオープンしたときにも実行されます。init() 関数はまた、シミュレーションにおいてアトラクタやパーティクルの数を変更した場合にも呼び出されます。jit.noisematrixcalc() メソッドは、出力マトリックス(メソッドの2番目のアーギュメント)を0から1 までの間のランダムな値で満たします。これは、パッチャー内で jit.noise オブジェクトに bang を送った場合と同じです。この init() 関数では、3つのマトリックスの3つのプレーンをランダムな値で満たしています。これらのマトリックスはパーティクルの初期位置(particlemat)、パーティクルの初期速度(velomat)、そして、アトラクタの位置(attmat)を表しています。その後、JitterMatrix の op() メソッドを使って、これらのランダムな値を -1 から 1 の範囲にスケールしています。これは、マトリックスの値に 2を掛け、それから1 を引くことによって行います。

これで、パーティクルシステムの初期状態をセットアップが完了しました。そこで、世代ごとにパーティクルを処理する方法に注目しましょう。これは、JavaScript コードの中の3つの異なる方法のうちの1つを使って行われますが、そのどれを使うかは JavaScript コードの perform_mode 変数によって決定されます。

・このチュートリアルパッチャーで、パーティクルシステムを再スタートさせ、Perform routine という表示のある ubumenuオブジェクトを“op”から“expr”に切り替えて下さい。パーティクルシステムはそれまでと同じように動作するはずです。ubumenu を再び "iter" に切り替えて下さい。パーティクスシステムは動作したままですが、速度が非常に遅くなるはずです(パッチの左下部にある jit.gl.render オブジェクトに接続されている fpsgui オブジェクトのフレームレートに注目してください)。ubumenu を切り替えて“op”に戻して下さい。

ubumenu は、JavaScript コードの中で mode() 関数を通して perform_mode 変数の値を変更します。このチュートリアルで後ほど見て行きますが、メソッドの内の1つで、("iter") を使うものは、他の2つより非常に実行速度が遅いということを覚えておくことは重要です。これは、主にパーティクルシステムを更新する際に使われるテクニックの違いによって生じます。タスクを実行する関数を調べる中で、それがなぜかを見ていきたいと思います。

・JavaScript コードの bang() 関数を見て下さい。

function bang() // パーティクスシステムの繰り返しの1回分の処理を実行 { switch(perform_mode) { // 次のものから選択... case "op": // Jitter マトリックスオペレータ(演算子)を使用 bang_op(); break; case "expr": // アルゴリズムの大部分で[jit.expr] を使用 bang_expr(); break; case "iter": // マトリックス全体を通したセルごとの繰り返し処理 bang_iter(); break; default: // デフォルトのbang_op() を使用 bang_op() break; } // 現在設定されている描画プリミティブによる、新しいパーティクルの頂点のマトリックス // を出力 outlet(0, "jit_matrix", particlemat.name, draw_primitive); }

bang() 関数は JavaScript の switch() 文を使って、実際にパーティクルシステムの処理を実行するために bang() 関数の中から呼び出す関数を決めています。Max パッチャーの中で選択した perform_mode によって、3つの異なる関数( bang_op()bang_expr()bang_iter() )の内の1つを選択します。全てがうまく処理されると仮定して、メッセージ jit.matrix の後に particlemat マトリックス(その時点のシミュレータのパーティクルの持つ座標が格納されています)の名前(name)、さらに OpenGL の draw_primitive を付け加えて、Max に戻すよう出力しています。「Choose Your Own Adventure and Let’s Make a Deal」という大いなる伝統に基づいて、上記のそれぞれの関数によって表される、3つの異なったパフォームルーチンを調べてみましょう。

第1のドア:op()によるルート

bang_op() 関数のJavaScript ソースを見て下さい。

bang_op() 関数は、可能な限り JitterMatrix オブジェクトの op() メソッドを使って、マトリックスの内容を一斉に数値演算によって変更します。アルゴリズム上で必要となる Jitter マトリックスの数を制限するために、可能な限り同じ場所でこの処理を行います。処理の大部分は、パーティクルシステムのアトラクタごとに一回ずつ for() ループの中で複数回実行されます。このループが完了すると、速度マトリックス (velomat) の更新されたバージョンを得ます。その後、これをパーティクルマトリックス (patriclemat) に加え、パーティクルの新しい位置を定義します。

ひと言で言えば、次のような処理を行っています。

function bang_op() // Matrix のオペレータを使ってパーティクルのマトリックスを生成 // します。 { for(var i = 0; i < ATTRACTOR_COUNT; i++) // 引力点ごとに1回の繰り返し {

実行ループの回数をコントロールする変数 i にアトラクタ数を適用し、アトラクタ毎に1回、閉じ括弧( ) )までの間のコードを実行します。

// 現在処理中のアトラクタから、スカラ・マトリックスを作ります。 scalarmat.setall(attmat.getcell(i));

JitterMatrix オブジェクトの getcell() メソッドはアーギュメントで指定されたセルの数値を返します。setcell() メソッドはマトリックスのすべてのセルに値(または値の配列)をセットします。これらのメソッドは、Max パッチャー内で jit.matrix オブジェクトへ送られる同名のメッセージと同じ働きをします。この行では js オブジェクトに対し、処理しているアトラクタの座標をアトラクタ・マトリックス (attmat) から取り出してコピーし、JitterMatrix である scalarmat の全ての単独のセルごとに、これらの値をセットするよう命じています。 scalarmat マトリックスは、patriclemat マトリックスと同じ大きさ(システム内のパーティクルの数と同じ)を持っています。これにより、このマトリックスを op() メソッドの中でスカラ・オペランド(訳注:スカラ演算の対象となる数値)として使うことができます。

// 現在処理中のアトラクタの位置からパーティクルの位置を引き // テンポラリ・マトリックスの(x,y,z)に格納します tempmat.op("-", scalarmat, particlemat);

このコードでは、処理しているアトラクタ(scalemat)の位置から、パーティクルの位置(particlemat)を引き、op() 関数で使用している2つのマトリックスと同じ大きさを持ったテンポラリマトリックスの中に格納しています。このマトリックスは処理中のアトラクタからの個々のパーティクルの距離を表しています。

// デカルト座標上の距離のマトリックス(x*x,y*y,z*z)を作るために、値を2乗します distmat.op("*", tempmat, tempmat);

このコードでは、値を2乗する簡単な方法として tempmat マトリックスをそれ自身に掛けています。その後、結果は distmat マトリックスに格納されます。

// 距離マトリックスのプレーンの値の合計(x*x+y*y+z*z)を計算します。 summat.planemap = 0; summat.frommatrix(distmat); summat2.planemap = 1; summat2.frommatrix(distmat); summat.op("+", summat, summat2); summat2.planemap = 2; summat2.frommatrix(distmat); summat.op("+", summat, summat2);

コードのこのブロックでは、distmat マトリックスの個々のプレーンを取り出し、それらを全て加算して、summat というシングルプレーンのマトリックスに格納します。これを行うために、JitterMatrix の planemap プロパティを使ってソースとなるマトリックスのどのプレーンを使うかを指定し、frommatrix() メソッドを使ってその値をコピーしています。すべてを合計するためには、操作の補助用に2番目のテンポラリマトリックス(summat2)が必要になります。最初に distmat のプレーン 0(x の距離の2乗)を summat にコピーします。次に distmat のプレーン 1(y の距離の2乗)を summat2 にコピーします。その後、( op() メソッドを使って)summatsummat2 を加え、その結果を summmat に書き戻します。今度は、distmat のプレーン 2(z の距離の2乗)を summmat2 にコピーし、summatsummat 2 を再び合計します。このとき、summat には、すでに distmat の最初の2つのプレーンの値の合計が格納されていることを心に留めておいて下さい。この2回目の合計の結果は summmat に格納されるため、summat には distmat の3つのプレーンの合計値が含まれていることになります。

// 加速度の値によって距離をスケールします。 tempmat.op("*", a); // このフレームの引力を導き出すために、この距離を距離の合計で割ります。 tempmat.op("/", summat); // このフレームの移動量を得るために現在の速度の方向を加算します。 velomat.op("+", tempmat); }

これは、アトラクタごとの処理ループの最後のブロックになります。ここでは、tempmat マトリックス(処理中のアトラクタとパーティクルの距離が格納されています)に、変数 a に格納されている加速度を表す値を掛けています。そして、その結果を summat マトリックス(距離の2乗の合計)で割り、velomat マトリックスに格納されている、現在個々のパーティクルが持っている速さに加えています。この加算の結果は velomat に格納されます。

このすべてのプロセスは個々のアトラクタごとに繰り返されます。結果として、velomat マトリックスはパーティクルがどのくらい個々のアトラクタから離れているかに基づいて毎回加算されます。ループが終了したとき(i が最後のアトラクタのインデックスの値になったとき)には、velomat には、パッチャー内の全てのアトラクタからの引力の総計に対応した速度が格納されます。

// 移動の量による現在の位置からのオフセット: particlemat.op("+", velomat); // 次のフレームのために、ディケイ係数によってベロシティ(速度)を減らします: velomat.op("*", d); }

最終的に、このベロシティをパーティクルのマトリックスに加算します(particlemat + velomat)。そして、パーティクルマトリックスは新しいパーティクル位置のセットに更新されます。その後、ベロシティマトリックスを変数 d に格納されている値によって減少させます。こうして、シミュレーションは、パーティクルシステムの次の世代のために、この世代の速度(ベロシティ)の残りの値を保持します。

op() メソッドの連鎖したシリーズを使ってマトリックス全体に対するアルゴリズムを処理することにより、明らかにスピード上での大きなアドバンテージがもたらされます。これは、Jitter が大きなデータの集合に対する単純な数値演算を非常に速く実行することができるためです。しかし、いくつかの指摘するべき点(特に、合計マトリックス summat の生成において)があります。それは、コードが必要以上にぎこちなく思えることです。jit.expr を使うと、より複雑な数式を定義することができるため、この処理の多くの部分を1つの操作で行うことができます。

第2のドア:expr()によるルート

・JavaScript コードのグローバルブロックに戻って、jit.expr オブジェクトをインスタンス化しているコードを再確認して下さい。

// bang_expr() 関数のための2つの [jit.expr] オブジェクトを作ります
// 第1の式: 入力マトリックスの全てのプレーンの値を合計します。
var myexpr = new JitterObject("jit.expr"); myexpr.expr = "in[0].p[0]+in[0].p[1]+in[0].p[2]";
// 第2の式: a+((b-c)*d/e) を評価します。 var myexpr2 = new JitterObject("jit.expr"); myexpr2.expr = "in[0]+((in[1]-in[2])*in[3]/in[4])";

JavaScript コードの最初で、jit.expr オブジェクトをインスタンス化する2つの JitterObject オブジェクト(myexprmyexpr2)を作りました。第1のオブジェクトのための式は1つのマトリックス(in[0])を取り、そのプレーンを合計します( .p[n]という記法は、マトリックスのプレーン n に格納されたデータを参照します)。第2の式は5つのマトリックス(in[0] - in[4])を取り、第1のマトリックス(A) を、第2のものから第3のものを引いて(B-C)それに第4のもの(D)を掛けてから第5のもの(E)で割った結果に加えています。従って、myexpr2 という JitterObject は次の式を評価しています。

A+((B-C)*D/E)

bang_expr() 関数の式に注目して下さい。これを、bang_op() 関数の中で使ったものと比較して下さい。

bang_expr() 関数の基本的なアウトラインは、bang_op() 関数と同じものです。すなわち、シミュレーション中のアトラクタの数に基づいてループを繰り返し、最終的には、総計されたベロシティマトリックス(velomat)を得て終了し、これをパーティクルマトリックス(particlemat)をオフセットするために使用します。キーとなる相違点は、jit.expr の呼出しを挿入する場所にあります。

function bang_expr() // [jit.expr] を使ってパーティクルマトリックスを作ります { // 加速度の値からスカラ・マトリックスを作ります amat.setall(a);

上の行では、amat マトリックスの全てのセルを変数 a の値(加速度係数)で埋めています。これによって、後で使用する jit.expr の式の1つのオペランドとしてこのマトリックスを使用できるようになります。

for(var i = 0; i < ATTRACTOR_COUNT; i++) // 引力点ごとに1回の繰り返し処理を行います。 { // 現在処理中のアトラクタからスカラ・マトリックスを作ります scalarmat.setall(attmat.getcell(i)); // 現在処理中のアトラクタからパーティクルの位置を引き、テンポラリマトリックスに格納 // (x,y,z) tempmat.op("-", scalarmat, particlemat); // デカルト座標上の距離のマトリックス(x*x,y*y,z*z)を作るために2乗します distmat.op("*", tempmat, tempmat);

これは、bang_op() の内容とすべて同じ同じものです。ここでは、現在処理中のアトラクタとパーティクルの位置の差にもとづいて、距離の2乗を格納したマトリックスを導き出しています。

// 距離マトリックスのプレーンを合計します (x*x+y*y+z*z) : // "in[0].p[0]+in[0].p[1]+in[0].p[2]" : myexpr.matrixcalc(distmat, summat);

op() および frommatrix() メソッドを使って、distmat マトリックスのプレーンごとに合計していく代わりに、ここでは、distmat を3プレーンの入力マトリックス、summmat を1プレーンの出力マトリックスとして使い、単純に第1数式を評価しています。

// このフレームの移動量を導きます: // "in[0]+((in[1]-in[2])*in[3]/in[4])" : myexpr2.matrixcalc(        [velomat,scalarmat,particlemat,amat,summat], velomat); }

同様に、このアトラクタのループの最後の部分では、前のベロシティ・マトリックス(velomat)、現在処理中のアトラクタ点のスカラ・マトリックス(scalamat)、現在のパーティクルの位置(particlemat)、加速度のスカラ・マトリックス(amat)、現在の距離の合計のマトリックス(summat)に基づく1つの複合式から、ベロシティマトリックス velomat を導き出すことができます。これは、中間マトリックスを使って動作する op() 関数のシーケンス全体に比べ、非常にシンプルな(そして、より読み取りやすい)ものです。myexpr2 オブジェクトの matrixcalc() メソッドの中で、ブラケット([ と ])を使って入力マトリックスの配列を指定している点に注意して下さい。

// 現在の位置を移動量によってオフセットします。 particlemat.op("+", velomat); // 次のフレームのために、ディケイ係数によって速度(ベロシティ)を減少させます。 velomat.op("*", d); }

これは、bang_op()メソッドの中で行っていたものと同じです。ここでは、新しいパーティクル位置を生成し、ベロシティを減少させています。この新しいベロシティは、システムの次の世代の速度の初期値として使われます。

第3のドア:セルごとの処理

bang_iter 関数のコードを見て下さい。

bang_iter() 関数は、JavaScript コードで使っている他の2つのパフォームルーチンとは異なったやり方で動作します。マトリックスを1つのエンティティとして動作するのではなく、全てがセルを単位として実行され、アトラクタ位置のマトリックス(attmat)に対してだけでなく、パーティクルやベロシティ(速度)のマトリックスに対しても繰り返し処理を行います。ここでは、2組のネストされた for() ループのペアを用い、各々のセルの値を一時的に別の Array オブジェクトに格納しながら、これを行っています。この Array から値を取り出したり、格納したりするためには、JitterMatrix オブジェクトの getcell() および setcell1d() メソッドを使っています。

function bang_iter() // セル毎にパーティクルマトリックスを生成します { var p_array = new Array(3); // 単体のパーティクル用の配列 var v_array = new Array(3); // 単体の速度(ベロシティ)用の配列 var a_array = new Array(3); // 単体のアトラクタ(引力点)用の配列
for(var j = 0; j < PARTICLE_COUNT; j++) // パーティクルごとに1回の繰り返し処理 { // 配列に現在のパーティクルの値を入れます。 p_array = particlemat.getcell(j);
// 配列に現在のベロシティの値を入れます。 v_array = velomat.getcell(j); for(var i = 0; i < ATTRACTOR_COUNT; i++)
// 引力点ごとに一回の繰り返し処理 { // 配列を現在処理中のアトラクタで満たします a_array = attmat.getcell(i);
// このパーティクルから現在処理中のアトラクタmまでの距離を求めます var distsum = (a_array[0]-p_array[0])*(a_array[0]-p_array[0]); distsum += (a_array[1]-p_array[1])*(a_array[1]- p_array[1]); distsum += (a_array[2]-p_array[2])*(a_array[2]- p_array[2]);
// このフレームの移動量を導き出します v_array[0]+= (a_array[0]-p_array[0])*a/distsum; // x v_array[1]+= (a_array[1]-p_array[1])*a/distsum; // y v_array[2]+= (a_array[2]-p_array[2])*a/distsum; // z }
// 移動量によって現在の位置をオフセット p_array[0]+=v_array[0]; // x p_array[1]+=v_array[1]; // y p_array[2]+=v_array[2]; // z
// 次のフレームのために、速度(ベロシティ)をディケイ係数によって減じます v_array[0]*=d; // x v_array[1]*=d; // y v_array[2]*=d; // z
// Jitter マトリックスにこのパーティクルの位置をセットします particlemat.setcell1d(j, p_array[0],p_array[1],p_array[2]);
// Jitter マトリックスにこのパーティクルの速度(ベロシティ)をセットします velomat.setcell1d(j, v_array[0],v_array[1],v_array[2]); } }

パーティクルシステムを少しずつ(そして、各々のセルの値を格納する中間段階の Array を使って)更新することは、基本ときに同じ操作の繰り返しになり、システムにあるパーティクルの数だけこれを行うことになるという点に注意してください!このことは、パーティクルの数が少ない場合は著しく効率が悪くなるわけではないかもしれませんが、数千の点を扱い始めると、とたんに速度の低下は顕著なものになります。

その他の関数

・Max パッチャーに戻って、particles $1attractors $1accel $1decay $1 と書かれたメッセージボックスにそれぞれ接続されているナンバーボックスオブジェクトを変更して下さい。パーティクルの数を非常に大きな値や非常に小さな値に設定してみて下さい。acceldecay アトリビュートがシステムの反応をどのように変化させるか試してみて下さい。JavaScript の中でこれらの関数のコードを見てみましょう。

これらの関数のほとんどは、単に変数の値を変更し、場合によってはそれをスケールします(例えば、accel() および decay() は単に、それぞれ変数 a および d の値を変えるだけです)。同様に、mode() 関数は、perform_mode 変数の値を、使いたいパフォームルーチンを指定する文字列に変更します。

function mode(v) // パフォームモードを変更 { perform_mode = v; }

しかし、particles() および attractors() 関数は、変数の値(それぞれ、PARTICLE_COUNT および ATTRACTOR_COUNT)を変更するだけでなく、これらの値によってマトリックスの大きさを変更し、パーティクルシミュレーションを(init() 関数の呼出しによって)再起動させる必要があります。

function particles(v) // 使用するパーティクルの数を変更 { PARTICLE_COUNT = v;
// マトリックスのサイズ変更 noisegen.dim = PARTICLE_COUNT; particlemat.dim = PARTICLE_COUNT; velomat.dim = PARTICLE_COUNT; distmat.dim = PARTICLE_COUNT; attmat.dim = PARTICLE_COUNT; tempmat.dim = PARTICLE_COUNT; summat.dim = PARTICLE_COUNT; summat2.dim = PARTICLE_COUNT; scalarmat.dim = PARTICLE_COUNT; amat.dim = PARTICLE_COUNT;
init(); // パーティクルシステムの再初期化 } function attractors(v) // 使用する引力点の数の変更 { ATTRACTOR_COUNT = v;
// アトラクタ・マトリックスのサイズ変更 attgen.dim = ATTRACTOR_COUNT;
init(); //パーティクルシステムの再初期化 }

・Max パッチャーで、Drawing primitive と表示されている ubumenu オブジェクトを変更してみて下さい。様々なセッティングを試してみて、パーティクルシステムが描画される方法の変化に注目して下さい。JavaScript コードの primitive() 関数は、変数 draw_primitive の値を変更します。

このパーティクルシステムは、jit.gl.render オブジェクトにパーティクル位置のマトリックス(JavaScript コードの particlemat として参照されます)を送ることによって、ビジュアライズ(視覚化)しています。マトリックスには float32 データ型の3つのプレーンがあり、jit.gl.render はそれを頂点の x,y,z 座標値として解釈します。描画プリミティブは、jit_matrix uxxxxxxxxx というメッセージが追加されているシンボルとして、js オブジェクトから jit.render オブジェクトに送られますが、これによって、OpenGLの描画コンテキストでデータをビジュアライズ(視覚化)するための方法が定義されます。

この描画プリミティブの指定やOpenGL マトリックスフォーマットに関する詳しい情報は、補遺B:「OpenGL マトリックスのフォーマット」、あるいは、OpenGL 「レッドブック」を参照して下さい。


様々な描画プリミティブを使うことによる、パーティクルの様々なビジュアライズ(視覚化)方法

smear() 関数の JavaScript コードを見て下さい。これは、キーボードの‘s’キーを押し下げている間、軌跡を残しておくことができるようにするための関数です(これは、smear $1 と書かれたメッセージボックスに接続されたトグルボックスをトリガします)

function smear(v) // レンダラの消去色(erase color)のアルファ値を0にすることによっ // て、描画の軌跡を残す機能(smear)をオンにします。 { if(v) { // smear オン (アルファ=0 : alpha=0): outlet(0, "erase_color", 1., 1., 1., 0.); } else { // smear オフ (アルファ=0.1 : alpha=0.1): outlet(0, "erase_color", 1., 1., 1., 0.1); } }

js オブジェクトに smear メッセージを送ることによって、JavaScript コードは eras_color メッセージを jit.gl.renderオブジェクトに送ります。smear のアーギュメントが 1 の場合、erase_color のアルファを 0 にします。この結果、描画コンテキストは、パッチ内の qmetro によってトリガされる erase メッセージに応答した動作を行わなくなります。smear の値が 0 の場合、jit.gl.renderオブジェクトの erase_color アトリビュートを、アルファ値0.1に戻すため、レンダラは erase メッセージに応答してイメージの10 % を消します。これによって、パーティクルの動きのビジュアライズ(視覚化)を助けるために、少しだけ軌跡を表示するようになります。

訳注:上記の「smear の値が 0 の場合...」は、原文では「A smear value of 1 ... となっていますが、誤植と思われます。

・このパッチをもう少し色々と動作させ、パーティクルの生成や、ビジュアライズを可能にしている様々な方法を見て下さい。単純に、x, y, z 値をそのまま3プレーンのマトリックスとして jit.gl.render オブジェクトに渡すだけで、広範囲にわたって様々な興味深いシステムを作ることが可能です。

まとめ

JavaScript は、Jitter でマトリックスデータを操作するようなアルゴリズムを設計する場合、強力な言語環境となることができます。手続き型コードの中で様々なテクニック(op()メソッド、jit.expr オブジェクト、セルごとの繰り返し処理)を使って直接マトリックスに対する数値演算が実行できるという能力は、一度に大きなデータの集合を処理するツールとしてJitter を利用することを可能にしてくれます。

次のチュートリアルでは、JavaScript の中で、JitterObject オブジェクト自身のアクションによってコールバック関数をトリガする方法を見て行きます。

コードリスト

// 46jParticles.js // // 簡単な引力シミュレーションによる3D パーティクルジェネレータ // [js] の中で Jitter を使ってマトリックス数値演算を行うための // 異なるテクニックを紹介しています。 // // a 3-D particle generator with simple gravity simulation // demonstrating different techniques for mathematical // matrix manipulation using Jitter objects in [js]. // // rld, 7.05 //

inlets = 1; outlets = 1; var PARTICLE_COUNT = 1000; // パーティクルの頂点の数の初期値
var ATTRACTOR_COUNT = 3; // 引力点の数の初期値

//パーティクル、および速度を生成するための [jit.noise] オブジェクトを作ります。 var noisegen = new JitterObject("jit.noise"); noisegen.dim = PARTICLE_COUNT; noisegen.planecount = 3; noisegen.type = "float32"; // アトラクタを生成するための [jit.noise] オブジェクトを作ります var attgen = new JitterObject("jit.noise"); attgen.dim = ATTRACTOR_COUNT; attgen.planecount = 3; attgen.type = "float32";

// bang_expr() 関数のための2つの [jit.expr] オブジェクトを作ります // 第1の式: 入力マトリックスの全てのプレーンの値を合計します。 var myexpr = new JitterObject("jit.expr"); myexpr.expr = "in[0].p[0]+in[0].p[1]+in[0].p[2]"; // 第2の式: a+((b-c)*d/e) を評価します。 var myexpr2 = new JitterObject("jit.expr"); myexpr2.expr = "in[0]+((in[1]-in[2])*in[3]/in[4])";

// データを格納するために必要となる Jitter マトリックスを作ります。 // パーティクルの頂点 x,y,z のマトリックス var particlemat = new JitterMatrix(3, "float32", PARTICLE_COUNT); // パーティクルの速さ x,y,z のマトリックス var velomat = new JitterMatrix(3, "float32", PARTICLE_COUNT); // 引力点 x,y,z のマトリックス(引力の中心) var attmat = new JitterMatrix(3, "float32", ATTRACTOR_COUNT); // 距離の総計のためのマトリックス var distmat = new JitterMatrix(3, "float32", PARTICLE_COUNT); // bang_op() 関数のためのテンポラリマトリックス var tempmat = new JitterMatrix(3, "float32", PARTICLE_COUNT); // bang_op() 関数のための加算用テンポラリマトリックス var summat = new JitterMatrix(1, "float32", PARTICLE_COUNT); // bang_op() 関数のためののもう1つの加算用テンポラリマトリックス var summat2 = new JitterMatrix(1, "float32", PARTICLE_COUNT); // 現時点の引力点を格納するスカラ・マトリックス var scalarmat = new JitterMatrix(3, "float32", PARTICLE_COUNT); // 加速度を格納するスカラ・マトリックス (expr_op() 関数のみ使用) var amat = new JitterMatrix(1, "float32", PARTICLE_COUNT);

var a = 0.001; // 加速度係数 var d = 0.01; // 減衰係数 var perform_mode="op"; // デフォルトのパフォーム関数 var draw_primitive = "points"; // デフォルトの描画プリミティブ function loadbang() // Max パッチが開いた時にこのコードが実行されます。 { init(); // マトリックスの初期化 post("particles initialized.\n"); }

function init() // 初期化ルーチン...ロード時、および、パーティクルまたはアトラクタの数を変更したときに // 呼び出されます { // -1 〜 1 の範囲にわたって、ランダムなパーティクルのマトリックスを生成します noisegen.matrixcalc(particlemat, particlemat); particlemat.op("*", 2.0); particlemat.op("-", 1.0); // -1 〜 1 の範囲にわたって、ランダムな速度(ベロシティ)のマトリックスを生成します noisegen.matrixcalc(velomat, velomat); velomat.op("*", 2.0); velomat.op("-", 1.0); // -1 ? 1 の範囲にわたって、ランダムなアトラクタのマトリックスを生成します
attgen.matrixcalc(attmat, attmat); attmat.op("*", 2.0); attmat.op("-", 1.0); }

function bang() // パーティクスシステムの繰り返しの1回分の処理を実行 { switch(perform_mode) { // 次のものから選択... case "op": // Jitter マトリックスオペレータ(演算子)を使用 bang_op(); break; case "expr": // アルゴリズムの大部分で[jit.expr] を使用 bang_expr(); break; case "iter": // マトリックス全体を通したセル毎の繰り返し処理 bang_iter(); break; default: // デフォルトのbang_op() を使用 bang_op(); break; } // 現在設定されている描画プリミティブによる、新しいパーティクルの頂点のマトリックス // を出力 outlet(0, "jit_matrix", particlemat.name, draw_primitive); }

function bang_op() // Matrix のオペレータを使ってパーティクルのマトリックスを          // 生成します。 { for(var i = 0; i < ATTRACTOR_COUNT; i++) // 引力点ごとに1回の繰り返し { // 現在処理中のアトラクタから、スカラ・マトリックスを作ります。 scalarmat.setall(attmat.getcell(i)); // 現在処理中のアトラクタの位置からパーティクルの位置を引き // テンポラリ・マトリックスの(x,y,z)に格納します tempmat.op("-", scalarmat, particlemat); // デカルト座標上の距離のマトリックス(x*x,y*y,z*z)を作るために、値を2乗します distmat.op("*", tempmat, tempmat); // 距離マトリックスのプレーンの値の合計(x*x+y*y+z*z)を計算します。 summat.planemap = 0; summat.frommatrix(distmat); summat2.planemap = 1; summat2.frommatrix(distmat); summat.op("+", summat, summat2); summat2.planemap = 2; summat2.frommatrix(distmat); summat.op("+", summat, summat2); // 加速度の値によって距離をスケールします。 tempmat.op("*", a); // このフレームの引力を導き出すために、この距離を距離の合計で割ります。 tempmat.op("/", summat); // このフレームの移動量を得るために現在の速度の方向を加算します。 velomat.op("+", tempmat); } // 移動の量による現在の位置からのオフセット: particlemat.op("+", velomat); // 次のフレームのために、ディケイ係数によってベロシティ(速度)を減らします: velomat.op("*", d); }

function bang_expr() // [jit.expr] を使ってパーティクルマトリックスを作ります { // 加速度の値からスカラ・マトリックスを作ります amat.setall(a); for(var i = 0; i < ATTRACTOR_COUNT; i++) // 引力点ごとに1回の繰り返し処理を行います。 { // 現在処理中のアトラクタからスカラ・マトリックスを作ります scalarmat.setall(attmat.getcell(i)); // 現在処理中のアトラクタからパーティクルの位置を引き、テンポラリマトリックスに格納 // (x,y,z) tempmat.op("-", scalarmat, particlemat); // デカルト座標上の距離のマトリックス(x*x,y*y,z*z)を作るために2乗します distmat.op("*", tempmat, tempmat); // 距離マトリックスのプレーンを合計します (x*x+y*y+z*z) : // "in[0].p[0]+in[0].p[1]+in[0].p[2]" : myexpr.matrixcalc(distmat, summat); // このフレームの移動量を導きます: // "in[0]+((in[1]-in[2])*in[3]/in[4])" : myexpr2.matrixcalc([velomat,scalarmat,particlemat,amat,summat], velomat); } // 現在の位置を移動量によってオフセットします particlemat.op("+", velomat); // 次のフレームのために、ディケイ係数によって速度(ベロシティ)を減少させます velomat.op("*", d); }

function bang_iter() // セル毎にパーティクルマトリックスを生成します { var p_array = new Array(3); // 単体のパーティクル用の配列 var v_array = new Array(3); // 単体の速度(ベロシティ)用の配列 var a_array = new Array(3); // 単体のアトラクタ(引力点)用の配列 for(var j = 0; j < PARTICLE_COUNT; j++) // パーティクルごとに1回の繰り返し処理 { // 配列に現在のパーティクルの値を入れます p_array = particlemat.getcell(j); // 配列に現在のベロシティの値を入れます v_array = velomat.getcell(j); for(var i = 0; i < ATTRACTOR_COUNT; i++) // 引力点ごとに一回の繰り返し処理 { // 配列を現在処理中のアトラクタで満たします a_array = attmat.getcell(i); // このパーティクルから現在処理中のアトラクタmまでの距離を求めます var distsum = (a_array[0]-p_array[0])*(a_array[0]-p_array[0]); distsum+= (a_array[1]-p_array[1])*(a_array[1]-p_array[1]); distsum+= (a_array[2]-p_array[2])*(a_array[2]-p_array[2]); // このフレームの移動量を導き出します v_array[0]+= (a_array[0]-p_array[0])*a/distsum; // x v_array[1]+= (a_array[1]-p_array[1])*a/distsum; // y v_array[2]+= (a_array[2]-p_array[2])*a/distsum; // z } // 移動量によって現在の位置をオフセット p_array[0]+=v_array[0]; // x p_array[1]+=v_array[1]; // y p_array[2]+=v_array[2]; // z // 次のフレームのために、速度(ベロシティ)をディケイ係数によって減じます v_array[0]*=d; // x v_array[1]*=d; // y v_array[2]*=d; // z // Jitter マトリックスにこのパーティクルの位置をセットします particlemat.setcell1d(j, p_array[0],p_array[1],p_array[2]); // Jitter マトリックスにこのパーティクルの速度(ベロシティ)をセットします velomat.setcell1d(j, v_array[0],v_array[1],v_array[2]); } }

function particles(v) // 使用するパーティクルの数を変更 { PARTICLE_COUNT = v; // マトリックスのサイズ変更 noisegen.dim = PARTICLE_COUNT; particlemat.dim = PARTICLE_COUNT; velomat.dim = PARTICLE_COUNT; distmat.dim = PARTICLE_COUNT; attmat.dim = PARTICLE_COUNT; tempmat.dim = PARTICLE_COUNT; summat.dim = PARTICLE_COUNT; summat2.dim = PARTICLE_COUNT; scalarmat.dim = PARTICLE_COUNT; amat.dim = PARTICLE_COUNT; init(); // パーティクルシステムの再初期化 }

function attractors(v)
// 使用する引力点の数の変更 { ATTRACTOR_COUNT = v; // アトラクタ・マトリックスのサイズ変更 attgen.dim = ATTRACTOR_COUNT; init(); //パーティクルシステムの再初期化 } function accel(v) // 加速度をセット { a = v*0.001; } function decay(v) // ディケイ係数をセット { d = v*0.001; } function mode(v) // パフォームモードを変更 { perform_mode = v; } function primitive(v) // OpenGL の描画プリミティブを変更 { draw_primitive = v; }

function smear(v) //レンダラの消去色(erase color)のアルファ値を0 にする { //ことによって描画の軌跡を残す機能(smear)をオンにします if(v) { // smear オン (アルファ=0 : alpha=0): outlet(0, "erase_color", 1., 1., 1., 0.); } else { // smear オフ (アルファ=0.1 : alpha=0.1): outlet(0, "erase_color", 1., 1., 1., 0.1);
} }