【UE5】Customノードでも使える命令 BRANCH FLATTEN UNROLL LOOP について

始まり

現在はレンダリングまわりに従事している筆者さんですが、最初から描画まわりに興味のあるタイプではなかったです。入社してからシェーダーとか諸々触り始めました。全く興味がない訳でもないけど、学生時代はゲームエンジンから作らされていた関係でそこまで手が回らなかった(実力不足)というのが実際ですけど。

んで、入社1年目の頃にタイトルにもある BRANCH FLATTEN UNROLL LOOP の存在を知りました。UNROLLLOOPは結果(間違って組んだ際のエラー内容とか)から理解しやすかったのですが、BRANCHFLATTENはよく分からずで当時は終わりました。

時が経った現在。

正確な時期は忘れましたが、意図的に使える程度に理解が進みました。

そんなわけで記憶の整理も兼ねて、当時知りたかったことを纏めていきます。

BRANCH と FLATTEN

BRACHFLATTENは以下のように使います。
※以降はCustomノードで書いている想定です。


BRACHの使用例

BRANCH
if (Value > 0.5)
{
    return 1.0;
}
return 0.0;


FLATTENの使用例

FLATTEN
if (Value > 0.5)
{
    return 1.0;
}
else
{
    return 0.0;
}


ご覧の通りif文と一緒に使用する命令です。

FLATTEN

FLATTENは、C++条件演算子: ?: と同じ or 同じようなものです。

上記の使用例ではFLATTENを明示的に指定していますが、?:な書き方でも同様な振る舞いをします。

// この書き方もFLATTENと同じ挙動
return Value > 0.5 ? 1.0 : 0.0;


基本的には?:な書き方をします。
例外的に可読性の観点から明示的にFLATTENを指定した方がいい場合もあります。


FLATTEN推奨な例

FLATTEN
if (ShapeType == 0.0)
{
    // Box
    float2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
else
{
    // Rhombus
    p = abs(p);
    float h = clamp(((b.x - 2.0 * p.x) * b.x - (b.y - 2.0 * p.y) * b.y) / dot(b, b), -1.0, 1.0);
    float d = length(p - 0.5 * b * float2(1.0 - h, 1.0 + h));
    return d * sign(dot(p, b) - b.x * b.y);
}


ShapeType == 0.0が真の場合には矩形を、偽の場合にはひし形を返す処理です。
計算量的(後ほど少し触れます)三項演算子な展開でおそらく問題ないでしょうが、これをFLATTEN指定なif構文ではなく、三項演算子で書いた場合はこのようになります。


FLATTENを使わずに ?: operator で頑張る場合

return ShapeType == 0.0 ? length(max((abs(p) - b), 0.0)) + min(max((abs(p) - b).x, (abs(p) - b).y), 0.0) : length(abs(p) - 0.5 * b * float2(1.0 - clamp(((b.x - 2.0 * abs(p).x) * b.x - (b.y - 2.0 * abs(p).y) * b.y) / dot(b, b), -1.0, 1.0), 1.0 + clamp(((b.x - 2.0 * abs(p).x) * b.x - (b.y - 2.0 * abs(p).y) * b.y) / dot(b, b), -1.0, 1.0))) * sign(dot(abs(p), b) - b.x * b.y);


横スクロール地獄&可読性ごみごみのごみ。

極端な例ですが、伝わりやすいでしょう?

このような場合にはFLATTENを明示的に指定してif構文を使用した方が優しいです。

逆に return Value > 0.5 ? 1.0 : 0.0; 程度のぱっと見で把握できる処理は、普通に三項演算子を使いましょう。

BRANCH

BRANCHは、if構文の場合に使用します。

???

ふふ、ちょっと意地悪な説明をしました。


BRACHの指定が"不要"なif(else)構文

if (Value > 0.5)
{
    return 1.0;
}
else
{
    return 0.0;
}


BRACHの指定が"必要"なif構文

BRANCH
if (Value > 0.5)
{
    return 1.0;
}
return 0.0;


if構文にelseが付いている場合はBRANCHの明示的な指定は不要です。

ただし、elseが付いていない場合は、明示的な指定が推奨されます。

面倒なのが必須(実質的には必須)ではなく推奨な点です。

結果はシェーダーコンパイラの解釈に依存するのですが、if構文にelseがない&BRANCH未指定の場合は FLATTEN三項演算子)として展開される可能性があるためです。


纏めると、if構文にelseが付いていない場合はBRANCH付与しておいて案件です。


んで、ここまで複雑になると訳が分からんという方も居るでしょう。

まぁ最初はそんなものじゃないですかね。

描画とかいう摩訶不思議なことやっているのですから、気楽にやりましょ。

先輩方は化け物ですが、そりゃUnityやUnreal Engineが台頭していない時代にバチバチにやってりゃ今じゃ考えられないほどアホほど強くならないと仕事できないでしょう。

だからと言ってそれを理由に、今の世代(我)が怠ける言い訳にはならんけど。

FLATTEN と BRANCH の使い分け

使い分けを意図的に出来るようになると『コード書いているな感』が増して更に楽しくなりますよね。

FLATTENで少し触れたように計算量によってどちらを使用するべきか変わります。

『計算量ってな~に?』と詳細を聞かれると筆者も困るので黙って欲しいですが、とりあえず重いのはFetchとpowとdivとか。

FLATTEN三項演算子で、両辺とも演算されます。つまり片方の計算コストがバカだと、その負荷が常時圧し掛かります。

BRANCHは通常の分岐処理なので真偽の結果で演算されるかが決定します。

んじゃBRANCH一択じゃない?と思うかもしれませんが、前提としてシェーダーは動的分岐苦手です。シンプルに負荷が高いのです。その昔は動的分岐が忌み嫌われ過ぎて皆さんstepで頑張っていたとかいないとかの記事がチラホラありせんか?最近のつよつよGPUさんだとパワープレイで苦手をぶん殴っている気配が若干しますが、仕様上苦手なことに変わりはないはず?です。

三項演算子は両辺を演算して最後にどっちかを代入するスタイルなので、動的分岐ほど苦手じゃないのです。

ただ結局のところ両辺の演算負荷と動的分岐の負荷を天秤に掛けることになるので、そのあたりは自分で試して答えを見つけてください。

筆者の定番は上記のようにFetch, pow, divあたりを判断材料にしています。

BRANCH と FLATTEN のアセンブリを覗きたい

深淵(アセンブリ)を覗きたい欲が湧いてきました。

三項演算子と動的分岐がどのように展開されているのか知らない筆者さん。

ちょっと興味が湧いたのでいい機会ですし調べてみます。

学生時代にはアセンブリ言語なんて微塵も興味なかったんですけどねぇ。

時が経ちましたねぇ。


BRACH - Source

BRANCH
if (Value > 0.5)
{
    return 1.0;
}
return 0.0;


BRACH - Assembly

lt r0.x, l(0.5000), r0.x
if_nz r0.x
  mov r0.y, l(1.0000)
endif
if_z r0.x
  mov r0.y, l(0)
endif


※前提として説明できるほど詳しくないのでドキュメント見ながら理解進めています。

r0.x比較結果を入れて、if_nz, if_zで真偽判定して演算結果をr0.yに格納って感じですか。

はぇ。。。

すげぇ。。。

昔の方々ってこれでコード書いてたのかよ。。。


FLATTEN - Source

FLATTEN
if (Value > 0.5)
{
    return 1.0;
}
else
{
    return 0.0;
}


FLATTEN - Assembly

lt r0.x, l(0.5000), r0.x
mov r0.y, l(1.0000)
mov r0.z, l(0)
movc r0.x, r0.x, r0.y, r0.z


r0.xに比較結果を入れるところは同じだけど、それぞれを演算結果をr0.yr0.zに格納して、最後のmovcr0.xの結果に則ってr0.yr0.zを、r0.xに格納(上書き?)している感じですかね。

このmovcってのがifより軽いんですかね。移動させているだけだから軽いのかな。

アセンブリで見ると片方演算と両辺演算の違いが視覚的になるので理解が進みますね。

なるほどです。筆者的には満足しました。

UNROLL と LOOP

UNROLLLOOPは以下のように使います。


UNROLLの使用例

float Weight = 0.0;
UNROLL
for (uint i = 0; i < 9; i++)
{
    Weight += 1.0;
}


LOOPの使用例

uint SampleCount = GetSampleCount();
float Weight = 0.0;
LOOP
for (uint i = 0; i < SampleCount; i++)
{
    Weight += 1.0;
}


ご覧の通りfor文と一緒に使用する命令です。

※BRANCHとFLATTENの深淵を覗いたら燃え尽き症候群を発症しました。
(訳:楽しかった!!!これ以上は説明したくない!!!)

UNROLL

固定長なfor文!!!

LOOP

可変長なfor文!!!

UNROLL と LOOP の使い分け

UNROLLの方が速いです。

ただし、コードが全展開される関係で諸々が静的である必要などの制約があります。

一旦はUNROLLを付与してみてエラー内容的に修正不可なら諦めてLOOPを使うのがベターかもです。

ループ数が動的(Constant Bufferで制御する場合も含む)だと確実にダメです。

#definestaticが安定ですかね。

UNROLLはいつか輪郭線の記事を書くのでその時に併せて紹介できればなぁと思ってます。

UNROLL と LOOP の深淵は覗かない

いつか覗きます。

また発火したら続きを書いてその時に公開 or 更新すればいいでしょと思うじゃん。

その結果、RenderDocの記事をね、1月頃に書き始めたの。

未だに公開できていない。

これが現実よ。

書きたいこと書いて気持ちよくなったら、興味なくなっちゃうのが筆者さんなの。

自分の性格に合ったスタイルで頑張るのが一番ストレスフリーで楽しいのです。

つまりはもうおわり!!!

おわり!!!

最後に注意。

GLSL言語(主にモバイル系)だとこいつら(命令)は指定できないので面白味が少ないです。

書く分には大丈夫です。#defineで無に置換されるので。

雑談

ここから先は年齢制限のかかっている作品を取り扱うページとなります。


表示する

真面目な話だけで終わっては個人ブログの意味がないのでえちえちなゲームの話でもしますか。



対象年齢:全年齢
発売予定:2025年 11月末予定

・・・エッチしないの?

Q:未成年でも支援できますか?
A:可能ですが、CAMPFIREの規約上、保護者の許可が必要です。また、弊社からのお礼パッチはダウンロードできませんので予めご了承ください。

お礼パッチ=エッチってこと?

分かんねぇけど今回はストーリーに期待しているからどっちでもいいや。

最初 ALcot20周年記念グッズ が特盛プランにしか含まれねぇのかと勘違いしていましたが、スクロールしたら別枠が用意されていてホッとしました。

うーむ・・・残念ながらビジュアル的な推しがひとりもいないのでヒロインプランは見送りですね。お財布的には助かった。

ちなみに最新の推しはセレオブのトウリさんですね。もうシンプルに顔が好きすぎる。

Q:活動報告の内容をSNSやblogで発信したいのですがいいですか?
A:活動報告の内容(テキスト、画像等)を外部に転載する行為は禁止致します。

はーい。筆者もNDAに縛られたお仕事をしているのでお漏らしはしないですわよ。

ちょっと話ズレるけどNDAという内容的に結構重いものを皆さん締結している割にリーク情報が後を絶たないのは何故なのでしょうか。言い方悪いですけどゲームの新情報程度、経歴ぶっ壊す危険性を犯してまでバラすことですかね。筆者の天秤では測れないから気持ちが分からないですわ。

という本業に絡むちょっとしたお気持ち表明。


ビジュアル的な推しは居ないから最小限の組み合わせかな。

  • グッズアッパープラン
  • ALcotメモリアルグッズオンリープラン

後は気長に待つだけですね。

筆者も上の方々から聞いた程度で詳細な内訳は知りませんが、ゲーム制作ってユーザーが考える以上にビックリするほど資金が必要ですからねぇ。

地味に初めてのCF

筆者さんSNSやってないからこういう最新情報が届くまでに結構なタイムラグがあるのちょっと困る。ALcotがブランドを畳むことすら、気まぐれでALcotのホームページを見たか、FANZAの『CF応援』の文言で、CFってなんや経由でしたからね。それならSNSアカウント作ればいいじゃないと思うかもしれませんが、勝手なイメージで申し訳ないですが、なんか負の話題が多そうで、それらをチラホラ目に入れていたら精神衛生上良くなさそうな気がして未だに踏み切れていないです。あとぶっちゃけ広報が必要なほど個人ブランドを確立している訳ではないので、デメリットを背負ってまでの必要性を感じないというのもある。公式生放送は見るのが面倒くさい。

ブログだったらこっちが一方的に語って終われますから、キャッチボールする必要なくてストレスフリーですわ。あとは技術のアーカイブにもなるし。結構自分の書いた記事見返す。もう非公開にしちゃったけど、エンジン改造とかSMAAは頻繁に見てた。『あの実装なんだっけなー』とかで飛ぶのに意外と適しているのよね。ソリューション開くのって意外と手間なのよ。特にUEってエンジン単体でもプロジェクトがデカいから爆速立ち上げは難しいのよね。

5.2のSMAAはブランチあるので良ければ覗いてね。
https://github.com/kafues511/UnrealEngine/tree/smaa/5.2

SMAAとエンジン改造はUE5.4が正式リリースされたら、最新対応して記事にしようかなぁと計画していたり、いなかったり。ぶっちゃけUEは嫌いだからあんまり触りたくない。でもみんながUE使うから覚えておかないと支障が出る。くそが。。。。

まぁでも手が出るほど嫌いならUEがいない業界にジョブチェンジしているので、そこまでじゃないんですよね。部分的にすっごく嫌いなところがある。んでそればっかり触るから煮詰まっちゃうの。私は悪くない。

よし。前半は真面目な話題、後半は不真面目な話題。これで均衡を維持できました。(?)

ぶっちゃけCFの報告したいから書いたネタなんだけどね。

理由は不純ですが結果としてはアウトプットできているので良いでしょう。


ちょっと待ったーーー。

今確認したらトウリさんの特典2店舗増えてるんだけど?!?!

おーーーい。

缶バッチは要らんけど、それ以外は気になるぞ。

あ、ゲーマーズの特典画像更新されてる。

あ、かわいい。

やっぱ買おう。

ふふん♪

筆者もくくるさんのような天才エンジニアを目指して頑張りたいと思います。
もちろん茉優先輩のような研究職も素敵です。
現実性はさておき、理想の方々がいっぱい居て、幸せな筆者さんです。

さてと、雑談はここまで、コード書こ。