取代 Stamp 的 Foreach 應用

承接上篇,延伸說明 Foreach 取代 copy stamp 的使用與最佳化。

這邊用一些粒子從球狀變成方塊並往下位移的簡單效果來說明。

方塊與球的型變

建立一個 box,設成 Polygon Mesh,給予足夠的面數(範例是 10x10x10 )。

Attribute Wrangle 寫出型變:

vector size = getbbox_size(0); //取得大小
vector direction = normalize(v@P); //每個點的方向,因為在原點所以較為省略
vector target_pos = direction * size.x / 2.0; //大小除以二為半徑

float mix = 0; // 注意mix變數,後面會再提到。

v@P = lerp(v@P, target_pos, mix); // 便可以藉由mix變數 `0~1` 調整型變

結果如下:

建立點

接下來要建立 template points,這種概念很常用到,一般要做大量物件的自定義動態,都會先轉成 points ,做完動態後再 instance 回去。
這邊比起用 grid,更傾向於用 vex 製作,更為單純快速。

float dist = 0.1; //點之間的間隔

for (int x = 0; x < 30; x++) {
    for (int z = 0; z < 30; z++) {
        addpoint(0, set(x, 0, z) * dist);
    }
}

這樣製造了900個間隔0.1單位的粒子。

設定動態

在製作動態時,一定是先把不需要重複計算的屬性都先設定完,最後一步再來依照屬性驅動動態,畢竟一動起來就是 time dependent,效能會大幅降低。

自製公式

根據過往工作的經驗,歸納出一個常用的公式:

float getProgress(float order, animation, duration) {
    float start = (1 - duration) * order;
    float end = order + duration - duration * order;
    float result = fit(animation, start, end, 0.0, 1.0);
    return result;
}

只要代入 orderanimationduration,就可以取得每個點的 progress 來進行動畫。

  • animation: 整個動畫的長度,基本上連結到 channel 由 key 驅動。
  • order: 每個點的順序,代表 progress 開始的順序
  • duration: 每個點在整個動畫長度的佔比
  • progress: 結算出每個點在該 animation 時間段的的行進值

以上數值皆為 0 ~ 1

主要的概念在於,只要調整 animation 的開始 0 到結尾 1,就可以計算出每個點的進程。

附上圖表說明:

一整段 animation,不同的點有不同的 order 順序。
animation 一開始,order 0.0 的點也會跟著開始。
animation 一結束,order 1.0 的點也會跟著結束。
而這 animation 的過程,就是根據每個點自己的 order 還有 duration,算出 progress

如果 duration 變短,order 0.0 的點跟 order 1.0 的點在 animation 的間隔就會拉大,那點彼此之間的交互關係就較不明顯。

可能還有點抽象,先繼續實做下去。

取得order

要進行上面的公式,先取得 order,範例的情況用亂數取得即可。

@order = rand(i@ptnum);

取得progress

藉由上面取得的 order,再加上連結 channel 取得 animationdurationanimation 設定為第1格為 0,第100格為 1,是一個線性的100格動態。
duration 隨意,範例數值為 0.2

float getProgress(float order, animation, duration) {
    float start = (1 - duration) * order;
    float end = order + duration - duration * order;
    float result = fit(animation, start, end, 0, 1);
    return result;
}

float animation = ch('animation'); // 1~100格 0.0~1.0 的數值動態
float duration = ch('duration'); // 0.2

@progress = getProgress(@order, animation, duration); // 將取得的 progress 屬性輸出

驅動位移

有了 progress 屬性後,來做一個從 progress 0 時在高處,progress 1 時在原點的位移動畫。

float start_height = ch('start_height'); // progress 為0時的高點,這邊為2

v@P.y = lerp(start_height, v@P.y, @progress); // 因為y在原點,第一項數值就不相加了

這樣就完成了一個基本的動態,複製成方塊後的效果如下:

如果 duration 調成 0.6,彼此之間 progress 行進過程拉長就會產生交疊,像這邊 animation 有100格則每個 progress100 * 0.6 = 60格:

那如果把 order 改成

@order = relbbox(0, v@P).z;

依照Z軸的順序,又是另一種效果:

驅動型變

可以發現有 progress 後,驅動動畫的操作直覺許多。
接下來就要來驅動型變,還記得我們剛剛第一段設定的 mix 值嗎?
如同上一篇設定了 type 來讓 foreach 迭代,這次我們設定 mix 屬性來迭代。

如果這邊是用 copy stamps,會需要每一顆粒子都依照 mix 屬性去 instance 一個球到方塊的中間型態。
但用 foreach 的話,我們可以減少 mix 的差異化,用最少的迭代量去複製所有點。

照這想法設定 mix 屬性:

i@mix = (int)((1.0 - @progress) * 100);

注意這邊,我們將計算出的浮點數乘上100之後化為整數,這樣最多的差異也只會是一百種,等於最多也只會迭代100次,這是非常重要的最佳化,尤其在粒子有幾十百萬的時候還能保持時間軸可拖動的關鍵。
甚至依據鏡頭的不同,更可以減為 *10,只迭代10次,畢竟整個球變成方塊的過程不會看得那麼細。

接上foreach

建立 foreach block,並將 Piece Attribute 設為 mix

方塊的型變節點改成嵌在 foreach block 裡面,因應方才我們寫的架構做些小更動:

vector size = getbbox_size(0);
vector direction = normalize(v@P);
vector target_pos = direction * size.x / 2.0;

float mix = point(1, 'mix', 0); // 採樣點的mix屬性
mix /= 100; // 因為前面乘了100,這邊要除回來

v@P = lerp(v@P, target_pos, mix); 

這樣便完成了。

結論

foreach block 配合正確的迭代屬性是很重要的最佳化流程,甚至可以搭配 compiled block 去做異步執行。
當然,在做 foreach block 前一定要先想想,這件事沒辦法在 Attribute Wrangle 去執行嗎?在 vex 去 Run Over 絕對是最快的,真的沒辦法再用 foreach block,那 copy stamp 就盡量不要碰了。

progress 的概念這邊只是初步介紹,延伸彈性很大,如下面影片,可以上了兩層 animation、針對不同驅動去用 chramp 重新設定 progress、對 orderduration 去做多層次設定等延伸方法。

範例檔案

最後附上範例檔案