見出し画像

For-Each/For-Loop

For-Each/For-Loopの種類

Tabを押してForEachを検索すると、複数のFor-Each/For-Loopノードの候補が表示されます。これらは名前や設定が違いますがほぼ同じノードです。Block BeginノードBlock Endノードから成り立ちます。
For-Each/For-Loopは様々なユースケースに対応できるように、設定によって柔軟に挙動が変わるようになっています。設定項目が多いため、あらかじめ一般的なユースケースに使える用に設定済みのノードの組み合わせが用意されています。

用意されているFor-Each/For-Loopの種類
  • For-Each Number: 一定回数繰り返したい場合。

  • For-Each Point: 入力されたジオメトリのポイント毎に繰り返したい場合。

  • For-Each Primitive: 入力されたジオメトリのプリミティブ毎に繰り返したい場合。

  • For-Each Connected Piece: 入力されたジオメトリのつながっているピース毎に繰り返したい場合。

  • For-Each Named Primitive: 指定したプリミティブアトリビュートの値が同じプリミティブを一つの纏まりとして、それ毎に繰り返したい場合。

  • For-Loop with Feedback: 1ループの結果を次のループの入力として繰り返し処理したい場合。

もちろん、実際の制作では上記では対応できないケースというのが出てきます。その場合は、上記のノードの設定値を適宜変更していくことになりますが、設定の組み合わせの多さもあって、入門者には少し理解しづらいところがあります。
この記事では、まず簡単にFor-Each/For-Loopの仕組みを説明した後に、逆引き的に実際のユースケースに対応した設定値を紹介することによって、For-Each/For-Loopを理解し、使いこなせるようになることを目指します。

For-Each/For-Loopの設定

最も気にしなければいけないのは、データの流れ方です。
For-Each/For-Loopは設定によってデータの流れ方が変わるため、気を付けないとパッと見は動いているように見えても予期せぬ結果となります。

データの流れ方に影響する設定値は大きく3つあります。
Block BeginノードのMethodBlock EndノードのIteration Method、Gather Methodです。

Block BeginノードのMethodは以下の4つ。これはループの開始時のデータの受け取り方を指定します。

  • Fetch Feedback: 1つ前のループの結果を受け取る。1回目のループは入力されたジオメトリを使う。

  • Extract Piece or Point: 入力されたジオメトリからピースまたはポイントを抜き出す。

  • Fetch Metadata: ループのメタデータを受け取る。メタデータにはループの総回数や現在のループ回数の情報が入っています。よく使います。

  • Fetch Input: 入力を受け取ります。ループ内の処理からは影響を受けません。

Block EndノードIteration Methodは以下の3つ。これはループのイテレーションをどの様な単位で行うかを指定します。

  • By Pieces or Points: ピースまたはポイント毎にループする。

  • By Count: 回数を指定してループする。

  • Auto Detect From Inputs: インプットに応じて自動でBy Pieces or PointsかByCountになる。(混乱を避けるために明示的に指定したほうが良いと思います。)

Gather Methodは以下の2つです。これはループの結果をどのように処理するかを指定します。

  • Feedback Each Iteration: 前のループの結果を現在のループの結果で上書きする。

  • Merge Each Iteration: 全てのループの結果を最後に全て合体させる。

Block Beginノードは1つのFor-Each内に幾つでも存在できます。これを活かして後述する様々なユースケースに対応していきます。
一方、Block Endノードは1つのFor-Eachで1つしか存在できません。

主要な設定値をずらっと書きましたがこれだけでも段々頭が混乱してましたね…。
一つずつ、データの流れを確認していきたいと思います。

Begin Blockノード > Fetch Feedback

1回目のイテレーションではBlock Beginノードのインプットからデータが入ってきて、2回目以降は前回のイテレーションのデータを受け取ります。

Begin Blockノード > Extract Piece or Point

入力ジオメトリからピースまたはポイントを抜き出します。
この設定値はEnd Blockノードの設定と関連しているので、他の設定値よりも少々複雑です。
この設定値を使うにはEnd BlockノードのIteration MethodでBy Pieces or Pointsを選ばなければエラーになります。
また、ジオメトリを入力する方法が3種類あるので他の設定値よりも少々複雑です。以下の3種類です。

  • Begin Blockノードの入力

  • Begin Endノードの2番目の入力

  • Begin Endノード > Piece Block Pathで指定したノード(ただしBegin Endノードの2番目の入力に指定するとそちらが優先され、Piece Block Pathは使用できなくなります。)

End Blockノード > Piece Attributeでピースを分けるために使うアトリビュートを指定しなければいけません。このアトリビュートの値が同じプリミティブまたはポイント同士は1つのピースとみなされます。

この設定値のデータの流れはこの様なイメージです。

Begin Blockノード > Fetch Metadata

この設定値ではループで使用されるメタデータを取得することできます。メタデータには以下のようなものがあります。

  • iteration: 現在のループ回数

  • numiterations: 何回ループするか。Block EndノードのIterationsに対応。

  • ivalue: 現在のループで使用されている値。int型。Block Endノード > Iteration MethodがBy Countの時のみ表示される。

  • value: 現在のループで使用されている値。float型。Block Endノード > Iteration MethodがBy Countの時のみ表示される。

iterationとvalue(ivalue)は何が違うのかと思われるかもしれません。iterationはループの回数ですが、value(ivalue)はBlock EndノードのStart ValueやIncrementの設定の影響を受けます。

  • Start Value: 初期値

  • Increment: 1回のループでvalue(ivalue)をいくら増減させるか。

iterationはループごとに必ず+1されます。2回目のループであれば値は必ず1です。それに対して、value(ivalue)はStart Value、Incrementの設定によって変わってきますので、2回目のループだからと言って必ずしも1になるわけではありません。どういった用途に使用するのかはユースケースの章で取り上げます。

Block Beginノード > Fetch Input

Block Beginノードの入力を使用します。一番シンプルな設定値です。ループでどんな処理がされているかに関わらず、全てのループで入力されたジオメトリを使用します。
(矢印がループ回数分並んでいるイメージ)

以上がFor-Each/For-Loopのループ開始時のデータの流れになります。次はループの終わりのデータの流れを見ていきます。

Block Endノード > Feedback Each Iteration

前のループの結果を現在のループの結果で上書きします。ループの最終結果は、最後のイテレーションの結果となります。

プログラマーの方は、for文内でミュータブルな変数を変更してくコードをイメージできるかと思います。

float result = 0;
for (int i = 0; i < 10; i++) {
    result += i;    
}

@result = result; // 45

Block Beginノード > Fetch FeedbackBlock Endノード > Feedback Each Iterationの役割の違いについて、最初は混乱するかもしれません。どちらもFeedbackという単語が入っていますし、どちらも実際に前のループの結果がフィードバックされているイメージを持ってしまいます。
Block Beginノード > Fetch Feedbackはあくまでループの開始時のデータの取得方法です。Block Endノードの設定がFeedback Each IterationでもMerge Each Iterationでも、Block Beginノード > Fetch Feedbackに設定すれば前のループの結果を取得します。
対して、Block Endノード > Feedback Each Iterationはループ結果の処理方法です。Block Beginノードの設定に関わらず、前のループの結果を現在のループの結果で上書きします。

Block Endノード > Merge Each Iteration

全てのループの処理が最後に合体されます。ループの最終結果は各イテレーションの結果をMergeノードで合体させた様な結果になります。

プログラマーの方は、for文内で、配列の各要素に結果を代入していくイメージを持たれるかもしれません。

float result[] = array();
for (int i = 0; i < 10; i++) {
    result[i] = i;    
}

f[]@result = result; // [ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 ]

ユースケース

イテレーション回数(metadata)を参照する

イテレーション回数などのmetadataを参照するには、Block Beginノード > MethodFetch Metadataに設定し、そのdetailアトリビュートを参照します。

// エクスプレッションの場合
detail('../foreach_count3', 'iteration', 0)

// VEXの場合
detail('op:../foreach_count3', 'iteration', 0);

metadataのvalueを使用する

等間隔でキューブを配置する例を考えます。キューブ同士の間隔は任意に設定したいとします。

Block Endノード > Incrementにキューブ同士の間隔となる値を設定します。
イテレーションごとにforeach_count1ノードのdetailアトリビュートにあるvalueにこの値が足されます。

foreach_count1ノードのdetailアトリビュート > valueをTransformノードで参照してキューブを配置します。

ジオメトリを複数回カットする

ここではBlock Beginノード > Fetch FeedbackBlock Endノード> Feedback Each Iterationに設定して、1つのスフィアに繰り返しBooleanを適用しています。

異なるピースごとに大きさをランダムに設定する

Block Beginノード > Extract Piece or Pointでピースごとにイテレーションし、その結果をBlock Endノード > Merge Each Iterationで最後に合体させています。

For-Each/For-Loop中に変数を使う

前のイテレーションの結果決まった値を、次のイテレーションで使用したいというケースはかなりの頻度であります。
メインの処理でBegin Blockノード > Fetch Feedbackが使えればジオメトリのPointやPrimitiveアトリビュートに値を設定してそれを次のイテレーションに引き継げます。

ただし、メインの処理がBegin Blockノード > Fetch InputExtract Piece or Pointの時でも前のイテレーションで決まった値を次のイテレーションで使用したい場合はどうすればよいでしょうか。
Fetch InputExtract Piece or Pointではイテレーションの開始時は前のイテレーションのジオメトリを引き継がないため、アトリビュートに設定した値はなくなってしまいます。
その場合は、Begin Blockノードを追加して、Fetch Feedbackに設定します。そのノードのアトリビュートをメインの処理で参照します。
例として、以下の画像のような形状をつくるケースを考えてみます。
アルゴリズムの概略は以下の通りです。

  • 1段目に大きめのキューブを置く

  • 2段目以降は前段のキューブをx、z方向にランダムな値で縮小したものを、前段のキューブの高さに合わせて配置する。y方向のスケールはランダムとする。

この場合、前段のキューブの位置とサイズを何かしらの方法で取得する必要があります。この例では以下の様にしました。

各イテレーションではキューブをTransformノードで移動、スケールして配置します。全てのイテレーションの結果を合体させて最終的なキューブが積みあがった形状を作るので、メインの処理はBlock Beginノード > Fetch InputBlock Endノード > Merge Each Iterationとしています。Iteration MethodはBy Countです。

肝心の前イテレーションのキューブの位置、サイズはBoundノードでdetailアトリビュートに記録されます。3つ目のBlock Beginノード(foreach_begin1_metadata1)はFetch Feedbackを設定しています。このノードのdetailアトリビュートに前イテレーションでBoundノードによって記録されたデータが入ってきます。前イテレーションのデータが入ってきますので、初回のイテレーションでは何もデータがありません。
Block Endノード > Iterationsを1にして、このBlock Beginノードのdetailアトリビュートをみると何もデータがないことがわかります。

Attribute Wrangleで前イテレーションのdetailアトリビュートを参照して、現在のイテレーションで配置するキューブの位置とサイズを計算しています。初回のイテレーションではデータがないため、デフォルト値を設定します。

// iteration = 0
int ite = detail('op:../foreach_count1', 'iteration');
if (ite == 0) { 
    v@pos = set(0, 0, 0);
    v@scale = set(10, rand(0), 10);
    
    return;
}

// 1 <= iteration
float xform[] = detail('op:../foreach_begin1_metadata1', 'xform');
float radii[] = detail('op:../foreach_begin1_metadata1', 'radii');
float prevSizeX = radii[0] * 2;
float prevSizeZ = radii[2] * 2;

float y = xform[13] + radii[1];

float scaleY = rand(y);

float minScale = 0.6; // 小さくなりすぎないようにする
float scaleX = prevSizeX * (minScale + (1 - minScale) * rand(xform[12]));
float scaleZ = prevSizeZ * (minScale + (1 - minScale) * rand(xform[13]));
float x = xform[12] - radii[0] + (scaleX / 2);
float z = xform[14] - radii[2] + (scaleZ / 2);


v@pos = set(x, y, z);
v@scale = set(scaleX, scaleY, scaleZ);

@pos、@scaleはAttribute Wrangleノードの次のTransformノードで参照するためのdetailアトリビュートです。

キャプチャで見切れているTranslate Yのエクスプレッションは以下の通りです。

detail('../attribwrangle1/', 'pos', 1) + (detail('../attribwrangle1/', 'scale', 1) / 2)

この様に複数の種類のBlock Beginノードを組み合わせて使うことで、前イテレーションの値を取得するなどの複雑な処理にも対応できるようになります。

ループの後の処理でループ内で計算された結果の値を使う

上記のケースでは前イテレーションの結果を参照する方法について説明しましたが、ループが全て終わった後に最終的な結果の値を参照するにはどうしたらよいでしょうか。
例えば上記の例の続きで、ループの後に最後のキューブの位置とサイズを知りたいケースを考えます。

直感的に考えると、Block EndノードのdetailアトリビュートにBoundノードで計算された最後のキューブの位置とサイズが入っていそうです。
これは半分正解です。もしBlock EndノードがFeedback Each Iterationに設定されていれば、予想通りにBlock Endノードのdetailアトリビュートに求めるデータが入っています。
しかし、Merge Earch Iterationを設定した場合は違います。そのdetailアトリビュートには最後のイテレーションのBoundノードの結果は入っていません。(一体何の値が入っているのかというのはあまり調べ切れていませんが、Block Endノード > Iterationsを増減させてみた感じでは、1回目のイテレーションの結果が入っているようでした。)

それではFetch Feedbackを設定したBlock Beginノード(上記の例ではforeach_begin1_metadata1)のdetailアトリビュートはどうでしょうか。
実はこれも間違いです。ここには最後の1つ前のdetailアトリビュートの値が入っています。つまり10回イテレーションが実行されたとしたら9回目のイテレーションで設定されたdetailアトリビュートの値が入っています。

では一体どのノードに最後のイテレーションで設定されたdetailアトリビュートの値が入っているかというと、detailアトリビュートを設定したノード自身になります。
上記の例ではBoundノードになります

よってループの外のノードでbound1のdetailアトリビュートを参照することで、ループの最後に決まった値を参照することができます。

デバッグのためのTips

Block Endノード > Iterations / Max Iterationsを増減することで任意の回数のイテレーションまでを1つづつ見ることができます。

Block Endノード > Single Passでは特定の回数目のイテレーションだけを実行して状況を確認することができます。
ただし、n-1回目までの挙動をシミュレートするわけではないので注意が必要です。例えば前イテレーションの結果を踏まえて実行するようなループ処理では期待通りには動かないでしょう。n-1回目までの挙動も含めてみたい場合は、 IterationsまたはMax Iterationsを使用します。

もしこの記事があなたのお役に立てたなら幸いです。 よろしければサポートをお願いします。今後の制作資金にさせていただきます!