アルファテストの改良

こんばんみん。
Pocolです。

ネットで記事を漁っていたら,アルファテストの品質向上の手法についての記事を見つけました。
https://asawicki.info/articles/alpha_test.php5

以前ゲーム開発をしていた際に,キャラクタの髪の毛や動物の毛周りで困ることがあったので,ハッシュ化アルファテストなどを試してみたのですが,結構ちらつきがやっぱりきになっちゃうなーと思っていましたし,意外と計算量多いんですよね。もっと手軽でそれっぽい方法無いかなーって常々思っていたのですが,記事で紹介している手法はかなりシンプルなので,個人的には「これでよくね?」って感じています。
アルファ値を次のように変えるのと,事前準備としてPhotoShopなどでSolidifyフィルタを使用してテクスチャエッジ部分の色を引き延ばしてテクスチャを作成しておけばよいみたいです。

 float alphaNew = max(alpha, (1.0/3/0) * alpha + (2.0/3.0) * threshold);
 if (alphaNew < threshold)
     discard;

Wave組み込み命令トリック

こんばんわ。
Pocolです。

Angry Tomato!さんという方が,“Compute shader wave intinsics tricks”
という記事を書いているので紹介です。
この記事では,以下のテクニックを紹介しています。

  • Branch optimization
  • Calculate on one lane, read on all
  • Serialization of Writing Data
  • Scalarization
  • Multiple wave parallelization
  • Indirect dispatch thread group count calculation
  • Dividing the work between lanes

非常に面白い内容だと思うので,見ていない方は是非見るとよいでしょう。

WaveActiveLerp()について

こんちゃわ。Pocolです。
Wave組み込み命令の記事を漁っていたら,GithubにWaveActiveLerp()の実装を書いている人がいたので紹介しようと思います。
下記に説明の記事があります。
https://github.com/AlexSabourinDev/cranberry_blog/blob/master/WaveActiveLerp.md

実装は,https://github.com/AlexSabourinDev/cranberry_blog/blob/master/WaveActiveLerp_Shaders/WaveActiveLerp.hlslにあって,次のような感じみたいです。

uint WaveGetLastLaneIndex()
{
	uint4 ballot = WaveActiveBallot(true);
	uint4 bits = firstbithigh(ballot); // Returns -1 (0xFFFFFFFF) if no bits set.
	
	// For reasons unclear to me, firstbithigh causes us to consider `bits` as a vector when compiling for RDNA
	// This then causes us to generate a waterfall loop later on in WaveReadLaneAt :(
	// Force scalarization here. See: https://godbolt.org/z/barT3rM3W
	bits = WaveReadLaneFirst(bits);
	bits = select(bits == 0xFFFFFFFF, 0, bits + uint4(0, 32, 64, 96));

	return max(max(max(bits.x, bits.y), bits.z), bits.w);
}

float WaveReadLaneLast(float t)
{
	uint lastLane = WaveGetLastLaneIndex();
	return WaveReadLaneAt(t, lastLane);
}

// Interpolates as lerp(lerp(Lane2, Lane1, t1), Lane0, t0), etc
// 
// NOTE: Values need to be sorted in order of last interpolant to first interpolant.
// 
// As an example, say we have the loop:
// for(int i = 0; i < 4; i++)
//    result = lerp(result, values[i], interpolations[i]);
// 
// Lane0 should hold the last value, i.e. values[3]. NOT values[0].
// 
// WaveActiveLerp instead implements the loop as a reverse loop:
// for(int i = 3; i >= 0; i--)
//    result = lerp(result, values[i], interpolations[i]);
// 
// return.x == result of the wave's interpolation
// return.y == product of all the wave's (1-t) for continued interpolation.
float2 WaveActiveLerp(float value, float t)
{
	// lerp(v1, v0, t0) = v1 * (1 - t0) + v0 * t0
	// lerp(lerp(v2, v1, t1), v0, t0)
	// = (v2 * (1 - t1) + v1 * t1) * (1 - t0) + v0 * t0
	// = v2 * (1 - t1) * (1 - t0) + v1 * t1 * (1 - t0) + v0 * t0

	// We can then split the elements of our sum for each thread.
	// Lane0 = v0 * t0
	// Lane1 = v1 * t1 * (1 - t0)
	// Lane2 = v2 * (1 - t1) * (1 - t0)

	// As you can see, each thread's (1 - tn) term is simply the product of the previous thread's terms.
	// We can achieve this result by using WavePrefixProduct
		
	float prefixProduct = WavePrefixProduct(1.0f - t);
	float laneValue = value * t * prefixProduct;
	float interpolation = WaveActiveSum(laneValue);

	// If you don't need this for a continued interpolation, you can simply remove this part.
	float postfixProduct = prefixProduct * (1.0f - t);
	float oneMinusT = WaveReadLaneLast(postfixProduct);

	return float2(interpolation, oneMinusT);
}

いまのところで,使いどころがパッと浮かばないのですが,知っていればどこかで使えそうな気がしています。
…というわけで,WaveActiveLerp()の実装紹介でした。

WaveCompactValue()について

最近、最適化で忙しいPocolです。
皆さん、お元気でしょうか?

今日は,WaveCompactValue()を勉強しようかなと思いましたので,そのメモを残しておこうと思います。
この関数は,[Drobot 2017]で紹介された手法です。

スライドに掲載されている実装は下記のよう感じです。

uint WaveCompactValue( uint checkValue )
{
    ulong mask; // lane unique compaction mask
    for ( ; ; ) // Loop until all active lanes removed
    {
        uint firstValue = WaveReadFirstLane( checkValue );
        mask = WaveBallot( firstValue == checkValue ); // mask is only updated for remaining active lanes
        if ( firstValue == checkValue ) break; // exclude all lanes with firstValue from next iteration
    }
    // At this point, each lane of mask should contain a bit mask of all other lanes with the same value.
    uint index = WavePrefixSum( mask ); // Note this is performed independently on a different mask for each lane.
    return index;
}

これをHLSLに書き直すと次のような感じになるかとおもいます。

uint WaveCompactValue(uint checkValue)
{
  // レーンのユニークなコンパクションマスク.
    uint4 mask;
    
    // すべてのアクティブレーンが取り除かれるまでループ.
    for (;;)
    {
        // アクティブレーンの最初の値を読み取る.
        uint firstValue = WaveReadLaneFirst(checkValue);

        // mask は残っているアクティブレーンに対してのみ更新される.
        mask = WaveActiveBallot(firstValue == checkValue);

        // firstValue を持つすべてのレーンを次のイテレーションから除外する。
        if (firstValue == checkValue)
             break;
    }
    // この時点で、マスクの各レーンは、同じ値を持つ他のレーンのすべてのビットマスクを含んでいなければならない。
    uint index = WavePrefixSum(mask); // これはレーンごとに異なるマスクで独立して行われる。
    return index;
}

さて,このWaveCompactValue()ですが,どういった使い道があるかというと,分類分けに使用することができます。
元々の[Drobot 2017]では色々なスレッドからAtomic操作をすると重くなるため,Atomic操作を減らす目的のために使われていました。
詳細な説明は,[Drobot 2017]のスライド51にアニメーション付きで載っていますので,そちらを参照してください。
軽く図の説明だけ載せておきます。




ちなみにグループ分けのよう番号を別途作りたい場合は,

uint2 WaveCompactValue(uint checkValue)
{
  // レーンのユニークなコンパクションマスク.
    uint4 mask;

    // グループ分け番号.
    uint groupIndex = 0;
    
    // すべてのアクティブレーンが取り除かれるまでループ.
    for (uint i=0; ; ++i)
    {
        // アクティブレーンの最初の値を読み取る.
        uint firstValue = WaveReadLaneFirst(checkValue);

        // mask は残っているアクティブレーンに対してのみ更新される.
        mask = WaveActiveBallot(firstValue == checkValue);

        // グループ分け番号を更新.
        groupIndex = i;

        // firstValue を持つすべてのレーンを次のイテレーションから除外する。
        if (firstValue == checkValue)
             break;
    }
    // この時点で、マスクの各レーンは、同じ値を持つ他のレーンのすべてのビットマスクを含んでいなければならない。
    uint index = WavePrefixSum(mask); // これはレーンごとに異なるマスクで独立して行われる。
    return uint2(index, groupIndex);
}

のように実装すると良いみたいです。
WaveCompactValue()はタイルの分類分けやマテリアルの分類分けなんかの場面で有効活用できそうな気がしています。
…というわけで,WaveCompactValue()を使って分類分けすれば,無駄なAtomic操作を減らせるので,高速化できるよ!という話でした。

参考文献

・[Drobot 2017] Michal Drobot, “Improved Culling for Tiled and Clustered Rendering Call of Duty Infinite Warfare”, SIGGRAPH 2017 Advances in Real-time Rendering and Games course, https://advances.realtimerendering.com/s2017/index.html