位置座標を復元したい。

こんるる~、Pocolです。

また,前回は深度について書きましたが,今回はスクリーンスペース系のテクニックなどで有用な位置座標の復元について忘れないようにメモしておこうと思います。

D3DXの行列は行優先(row-major)形式なので,列優先形式として表すと

\begin{eqnarray}
M_{RH} = \begin{bmatrix} s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_f}{z_n – z_f} & \frac{z_n z_f}{z_n – z_f} \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}

と書けます。
 ビュー空間の位置座標を\((v_x, v_y, v_z, 1)^{\mathrm T}\)として,透視投影を行い,透視投影後の位置座標を\((p_x, p_y, p_z, p_w)^{\mathrm T}\)とすると,次のように書くことができます。

\begin{eqnarray}
\begin{bmatrix} p_x \\ p_y \\ p_z \\ p_w \end{bmatrix} &=& \begin{bmatrix} s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_f}{z_n – z_f} & \frac{z_n z_f}{z_n – z_f} \\
0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} v_x \\ v_y \\ v_z \\ 1 \end{bmatrix} \\
&=& \begin{bmatrix} s_x v_x \\ s_y v_y \\ v_z \left( \frac{z_f}{z_n – z_f} \right) + \frac{z_n z_f}{z_n – z_f} \\ -v_z \end{bmatrix}
\end{eqnarray}

 ここで,\(p_x\),\(p_y\),\(p_z\)を\(p_w\)で除算し,正規化デバイス座標系での位置座標\(P_x\), \(P_y\), \(P_z\)を計算すると

\begin{eqnarray}
P_x &=& \frac{p_x}{p_w} &=& -\frac{s_x v_x}{v_z} \tag{1} \\
P_y &=& \frac{p_y}{p_w} &=& -\frac{s_y v_y}{v_z} \tag{2} \\
P_z &=& \frac{p_z}{p_w} &=& -\frac{z_f}{z_n – z_f} \left( 1 \, + \, \frac{z_n}{v_z} \right) \tag{3}
\end{eqnarray}

となります。
 今,\(v_x\),\(v_y\)を復元する方法を考えたいので\(P_x\)と\(P_y\)についての式を\(v_x\),\(v_y\)について解きます。

\begin{eqnarray}
v_x &=& -\frac{v_z}{s_x} P_x \\
v_y &=& -\frac{v_z}{s_y} P_y
\end{eqnarray}

 DirectXの正規化デバイス座標系は\(x\)成分と\(y\)成分が\([-1, 1]\)で,\(z\)成分が\([0, 1]\)であるのでテクスチャ座標\(s\),\(t\)とすると,\(P_x\)と\(P_y\)との関係は次のように書くことができます。

\begin{eqnarray}
s &=& 0.5 P_x + 0.5 \tag{4} \\
t &=& -0.5 P_y + 0.5 \tag{5}
\end{eqnarray}

DirectXのテクスチャ座標を求めるために上下反転させていることに注意してください。
この式を\(P_x\),\(P_y\)について書くと,

\begin{eqnarray}
P_x &=& 2s – 1 \\
P_y &=& -2t + 1
\end{eqnarray}

となります。よって,\(v_x\)と\(v_y\)をこの式を使って書き直すと次のようになります。

\begin{eqnarray}
v_x &=& -v_z \frac{1}{s_x} (2s – 1) \\
v_y &=& -v_z \frac{1}{s_y} (-2t + 1)
\end{eqnarray}

ここで,前回の記事で導出した深度バッファから\(v_z\)を求める式は
\begin{eqnarray}
v_z = -\frac{z_f z_n}{d(z_n – z_f) + z_f}
\end{eqnarray}

であったので,深度バッファの値\(d\)と,ニアクリップ平面までの距離\(z_n\),ファークリップ平面までの距離\(z_f\),\(s_y = \cot(\frac{fovY}{2})\),\(s_x = s_y / aspect\),そしてテクスチャ座標\(s\)と\(t\)があればビュー空間の位置座標が復元することができることになります。
定数バッファとして渡せるようにパラメータをfloat4にパックします。成分の内訳は
\begin{eqnarray}
{\rm param} = \left(\frac{1}{s_x}, \frac{1}{s_y}, z_n, z_f \right)
\end{eqnarray}
とします。

\(s_x\)と\(s_y\)はもともと射影行列の要素なので,射影行列があれば簡単に計算することができます。以上を踏まえてコードに落とし込むと

param.x = 1.0f / projection._11; // matrix[0][0]成分.
param.y = 1.0f / projection._22; // matrix[1][1]成分.
param.z = nearClip; // ニアクリップ平面までの距離.
param.w = farClip;  // ファークリップ平面までの距離.

シェーダ側ではparamを使って次の復元処理を実装すればよいです。

// 深度バッファの値からビュー空間深度値を求めます.
float ToViewDepth(float hardwareDepth, float nearClip, float farClip)
{
    return -nearClip * farClip / (hardwareDepth * (farClip - nearClip) + farClip);
}

// テクスチャ座標と深度値からビュー空間位置座標を求めます。
float3 ToViewPos(float2 st, float hardwareDepth, float4 param)
{
    float z = ToViewDepth(hardwareDepth, param.z, param.w);
    float2 p = st * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f);
    return float3(-z * param.x * p.x, -z * param.y * p.y, z);
}

そんなわけでビュー空間深度さえ求めれば,単純な計算だけでビュー空間が復元できます。

ビュー空間から射影空間に戻す場合は,式(1), 式(2),式(3)を使って求められるので…

// ビュー空間から射影空間に変換します.
float3 ToProjPos(float3 viewPos, float4 param)
{
    return float3(
        -viewPos.x / (viewPos.z * param.x),
        -viewPos.y / (viewPos.z * param.y),
        -(param.w / (param.z - param.w)) * (1.0f + (param.z / viewPos.z)
    );
}

となります。

ビュー空間から,テクスチャ座標を求めたい場合は,さらに式(4),式(5)を使えばよいので

// ビュー空間からテクスチャ座標に変換します.
float2 ToTexCoord(float3 viewPos, float4 param)
{
    float2 p;
    p.x = -viewPos.x / (viewPos.z * param.x);
    p.y = -viewPos.y / (viewPos.z * param.y);
    return p * float2(0.5f, -0.5f) + float2(0.5f, 0.5f);
}

で求められるはずです。

もし、書き間違いとか「そもそも計算あってないよ!」という箇所あれば遠慮なく指摘してください (…というのも若干自信無いからです)。

深度の精度を高くしたい。

いつも忘れるので,忘れないようにメモをしておこうと思います。

Reverse-Z

DirectXの右手座標系の透視投影行列は[Microsoft 2018 a]で次のように記載されています。

xScale     0          0              0
0        yScale       0              0
0        0        zf/(zn-zf)        -1
0        0        zn*zf/(zn-zf)      0
where:
yScale = cot(fovY/2)
xScale = yScale / aspect ratio

D3DXの行列は行優先(row-major)形式なので,列優先形式として表すと…

\begin{eqnarray}
M_{perspectiveRH} = \begin{bmatrix} s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_f}{z_n – z_f} & \frac{z_n z_f}{z_n – z_f} \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}

となります。ただし,\(s_y = \cot(\frac{fovY}{2})\),\(s_x = \frac{s_y}{aspect}\),\(z_n\)はニアクリップ平面までの距離,\(z_f\)はファークリップ平面までの距離,\(fovY\)は垂直画角で,\(aspect\)は画面のアスペクト比とします。
今,\(z\)の範囲を\([0, 1] \rightarrow [1, 0]\)に変換したいので,\(z\)に対して-1倍して1を足せば範囲が変換できることになります。この変換を行うための行列を左からかけてReverse-Z形式にします。

\begin{eqnarray}
M_{reverse} M_{perspectiveRH} &=& \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & -1 & 1 \\
0 & 0 & 0 & 1 \end{bmatrix}
\begin{bmatrix}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_f}{z_n – z_f} & \frac{z_n z_f}{z_n – z_f} \\
0 & 0 & -1 & 0 \end{bmatrix} \\
&=& \begin{bmatrix}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_n}{z_f – z_n} & \frac{z_f z_n}{z_f – z_n} \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}

[Reed 2015]に記載されていますが,ファーを無限遠にするとちょっとだけ結果が良くなるケースがあるので,\(z_f \rightarrow \infty\)の極限をとります。\( z_n /(z_f – z_n)\)は分母が\(\infty\)になるのでゼロに近づくことと,\(z_f z_n / (z_f – z_n)\)は\(\infty/\infty\)の不定形になるので,分子・分母を\(z_f\)で割って極限をとると,次のように整理できます。

\begin{eqnarray}
M_{reverseInf} = \lim_{z_f \rightarrow \infty} \left( M_{reverse} M_{pervespectiveRH} \right) = \begin{bmatrix}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & 0 & z_n \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}

この行列を使ってベクトルを変換します。

\begin{eqnarray}
p = \begin{bmatrix}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & 0 & z_n \\
0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} v_x \\ v_y \\ v_z \\ 1 \end{bmatrix}
\end{eqnarray}

興味があるのは,深度だけなので射影変換後のz値(\(p_z\))とw値(\(p_w\))を求めます。

\begin{eqnarray}
p_z &=& z_n \\
p_w &=& -v_z
\end{eqnarray}

z値をw値で割ったのが深度バッファに出力されるデプス\(d\)になるので,

\begin{eqnarray}
d &=& \frac{p_z}{p_w} \\
&=& \, – \frac{z_n}{v_z}
\end{eqnarray}

よって,深度バッファの値\(d\)からビュー空間の深度\(v_z\)を逆算出する場合は,

\begin{eqnarray}
v_z = -\frac{z_n}{d}
\end{eqnarray}

で求めればよいことになります。
 コードに落とし込むと…

// farClip = ∞とするリバースZ深度バッファの値からビュー空間深度値を求めます.
float ToViewDepthFromReverseZInfinity(float hardwareDepth, float nearClip)
{
    return -nearClip / hardwareDepth;
}

となります。

 ちなみに,\(z_f\)の\(\infty\)を取らない場合のビュー空間深度は

\begin{eqnarray}
v_z = -\frac{z_f z_n}{d(z_f – z_n) + z_n}
\end{eqnarray}

で求められます。よって,リバースZを用いた場合の線形深度値は

\begin{eqnarray}
d_{linear} = -\frac{z_n}{d(z_f – z_n) + z_n}
\end{eqnarray}

となります。
 コードに落とし込むと

// リバースZ深度バッファの値からビュー空間深度値を求めます。
float ToViewDepthFromReverseZ(float hardwareDepth, float nearClip, float farClip)
{
    return -farClip * nearClip / (hardwareDepth * (farClip - nearClip) + nearClip);
}

// リバースZ深度バッファの値から[0, 1]の線形深度値を求めます。
float ToLinearDepthFromReverseZ(float hardwareDepth, float nearClip, float farClip)
{
    return -nearClip / (hardwareDepth * (farClip - nearClip) + nearClip);
}

となります。

線形深度

ついでに標準の射影行列(D3DXMatrixPerspectiveFovRH)を使った場合の線形深度の算出方法についてもメモしておきます。
まず,先ほどと同様に\(p_z\)と\(p_w\)を求めます。

\begin{eqnarray}
p_z &=& v_z (\frac{z_f}{z_n – z_f}) + \frac{z_f z_n}{z_n – z_f} \\
p_w &=& – v_z
\end{eqnarray}

除算して深度を求めます。

\begin{eqnarray}
d &=& \frac{p_z}{p_w} \\
&=& – \frac{v_z z_f}{v_z(z_n – z_f)} – \frac{z_f z_n}{v_z(z_n – z_f)} \\
\end{eqnarray}

上記の式を\(v_z\)について解き,ビュー空間深度を求めます。
\begin{eqnarray}
d(v_z(z_n – z_f)) &=& -v_z z_f – z_f z_n \\
d v_z z_n – d v_z z_f + v_z z_f &=& – z_f z_n \\
v_z(d(z_n – z_f) + z_f) &=& -z_f z_n \\
v_z &=& -\frac{z_f z_n}{d(z_n – z_f) + z_f}
\end{eqnarray}

線形深度は,ビュー空間深度をファークリップまでの距離で割り\([0, 1]\)の範囲内におさめたものになるので,
\begin{eqnarray}
d_{linear} &=& \frac{v_z}{z_f} \\
&=& -\frac{z_n}{d(z_n – z_f) + z_f}
\end{eqnarray}

となります。
 コードに落とし込むと…

// 深度バッファの値からビュー空間深度値を求めます.
float ToViewDepth(float hardwareDepth, float nearClip, float farClip)
{
    return -nearClip * farClip / (hardwareDepth * (nearClip - farClip) + farClip);
}

// 深度バッファの値から[0, 1]の線形深度値を求めます.
float ToLinearDepth(float hardwareDepth, float nearClip, float farClip)
{
    return -nearClip / (hardwareDepth * (nearClip - farClip) + farClip);
}

となります。

正射影

正射影についてもリバースZを一応求めてみます。
D3DXMatrixOrthoOffCenterRH行列は[Microsoft 2018 b]より

2/(r-l)      0            0           0
0            2/(t-b)      0           0
0            0            1/(zn-zf)   0
(l+r)/(l-r)  (t+b)/(b-t)  zn/(zn-zf)  1

と記載されています。この行列を列優先形式で書くと次のようになります。

\begin{eqnarray}
M_{orthoRH} = \begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & s_z & t_z \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{eqnarray}

ただし,\(s_x = \frac{2}{r – l}\),\(s_y = \frac{2}{t – b}\),\(s_z = \frac{1}{z_n – z_f}\),\(t_x = \frac{l + r}{l – r}\),\(t_y = \frac{t+b}{b-t}\),\(t_z = \frac{z_n}{z_n – _zf}\)とします。

この行列に対してリバースZ形式にするために左から行列を掛けます。

\begin{eqnarray}
M_{reverse}M_{orthoRH} &=& \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & -1 & 1 \\
0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & s_z & t_z \\
0 & 0 & 0 & 1
\end{bmatrix} \\
&=& \begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & -s_z & -t_z + 1 \\
0 & 0 & 0 & 1
\end{bmatrix} \\
&=& \begin{bmatrix}
\frac{2}{r – 1} & 0 & 0 & \frac{l+r}{l-r} \\
0 & \frac{2}{t – b} & 0 & \frac{t+b}{b-t} \\
0 & 0 & \frac{1}{z_f – z_n} & \frac{z_f}{z_f – z_n} \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{eqnarray}

結果として,正射影の場合のリバースZ形式は単純にニアクリップ平面とファークリップ平面を入れ替えしたものになります。

参考文献

・[Microsoft 2018 a] Microsoft, “D3DXMatrixPerspectiveFovRH function”, https://docs.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh
・[Microsoft 2018 b] Microsoft, “D3DXMatrixOrthoOffCenterRH function”, https://docs.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh
・[Reed 2015] Nathan Reed, “Depth Precision Visualized”, https://developer.nvidia.com/content/depth-precision-visualized

おしらせ

こんるる~。Pocolです。

皆様にご連絡があります。
執筆している書籍ですが,企画当初よりもページ数が120ページほど増えた関係で,初版の発行部数が減りました。
もう一度、言います。
「初版の発行部数が減りました」

どうしても手に入れたい!という方は,お早目にご購入の決断をしていただいた方が良いかもしれません。

さらに、もうひとつご連絡。
新型コロナウィルスの影響を受けまして,発売までもう少し時間をいただく運びになりました。
お待ちしていただいている皆様には申し訳ございませんが,何卒ご了承下さいますようお願い申し上げます。

正式な発売日が決定次第,ご連絡致します。
その頃にはコロナウィルスが治まっていることを願っています。

予防線。

こんにちわ、Pocolです。

今回は予防線を先に張っておこうと思いまして,記事を投稿します。

頑張って執筆している書籍のタイトル名ですが、まだ正式タイトルが決まっていません。
一応仮で「基礎から学ぶ」というのが今現在入っていますが,基礎というのは全然優しくありません。
「簡単なんでしょ?」とか勘違いされている方がいると,「全然簡単じゃねぇ!」とか怒る人もいるかもしれませんので,予め言っておきます。『簡単じゃないです。』

「基礎から」と言ってるだけで「簡単である」とは書かないように気を付けています。
「基礎=簡単」と思っている方がいらしたら,それは大いなる間違いです。

今回の執筆の書籍は,かなりガチ目な路線を狙いました。勿論,「基礎から」と仮題がついているので基本から説明するように心がけましたが,残念ながら執筆者の技量もあるため,読んだ人全員に理解できるものは提供できないのではないかと考えています。
ここ大事なので,もう一回言います『読んだ人全員に理解できるものではないかもしれません。』

勿論,星1がつくのは覚悟です(寧ろ、昨今だと星1が付かない方があからさまにオカシイような気さえします)。
万人受けする書籍ではありませんが,なるべく受け入れられるように努力はしました。

クソだと言ってもらうには一向に構いません。
それよりも,この書籍が役に立ったと1人でも言ってくださる方がいらっしゃったら,自分はそちらの意見の方が大変ありがたいです。
もともと救えないと思っていたものが,救える。それだけで本を書いた価値があります。
わからないものが分かったとか,ふーんと思っていたことがちゃんと納得できるようになったとかのご意見・ご感想があれば,今後の励みになります。

特に欲しい意見としては,「〇〇〇までは分かったが×××は…という理由で,全く理解できなかった。難しすぎる」とか「〇〇〇の説明がイメージつかなくて,わからないとか」とか今後につながる具体的にわからなかった理由や改善点というがあれば是非頂戴したいです。
「この本クソ!」みたいな意見は既に分かりきった当たり前の内容でして,なんの改善にもつながりません。どのレベルに合わせて書くべきかなどの修正方針にもつながりませんし,そもそも万人受けできるように努力はしていますが,それを目標とはしていませんし、当然問答無用に「クソ!」などいう人は分かりきっていまして,わからない人も多かれ少なけれいるだろうと目算していますので,単なるdisりは承知の上でして何にもなりません。出来ればどこがクソなのか理由を書いていただけるだけでもありがたいです。「こうしろ!」という意見を持ったdisりの方が改善につながりますので,具体的にどこがダメなのか?どうすると良くなるのか?なぜそう思うのか?などの生の声が頂戴したいです。
何の理由もなく「クソ!」という人は一定数いるようですので理由がないものに対しては,こちらで改善が図れません。先ほども述べましたとおりに「具体的にどこがダメなのか?」できれば「こうするともっと良くなる」という意見がありましたら,是非頂きたいです。

…というわけで先に予防線を張っておきました。

出来れば購入する際も,本当は書店などで立ち読みして本当に、本当に買うべきもの値するかどうかをきちんと判断していただいた上でご購入頂きたいです。
立ち読み等でご自分の目でご確認いただいてから,購入していただくのが確実かと思います。
きちんと自分が望むものと一致しているかどうかをご判断頂いてから,ご購入頂きたいです。

もう一度言いますが,専門書ですので,万人受けする書籍の類ではございません。
ネット等で様々なご意見があると思いますが,ご自分の目でこれは買うべきなのか?買わないべきなのかをきちんと確認した上で,後悔が無いようにご購入いただくのを強く推奨いたします。
そのためには,書店等で実物を見ていただいた上で買うのが最善であり,これを強く強く推奨いたします。

コンピュートシェーダで実行する際は…

こんにちわ,Pocolです。
最近、最適化の話とかを見るのがちょっとハマっています。

NVIDIAがthread-group ID swizzlingという最適化テクニックについての記事を投稿しています。

https://developer.nvidia.com/blog/optimizing-compute-shaders-for-l2-locality-using-thread-group-id-swizzling/

L2キャッシュを再利用できるようにアクセスパターンを変えることにより最適化を行うテクニックのようです。
2Dフルスクリーンのコンピュートシェーダを用いるものに重要となるテクニックだそうで,ポストプロセスやスクリーンスペース系の技法を実装する際には重宝しそうです。

上記のテクニックはGDC 2019で紹介されているもので,バトルフィールド5ではRTX 2080(1440p)で0.75msの改善があったと報告されています(SetStablePowerState(TRUE)での動作だそうです)。
また,GDC 2019で紹介したソースコードにバグがあり,X方向(N)に起動するスレッドグループの数の倍数である場合にのみ動作するものだったそうです。
修正したソースコードについても提示がされています。

上記の記事のHLSLコードが実際動くのか,コピってみて試したのがだめでした。
NVIDIAのWebページの方では,いくつかHTMLの変換ミスがあるっぽくてアスタリスク(*)が無くなったりしていて,そのままコピペしてもビルドエラーになるので注意してください。
そこで,D3D11で動くように実装を修正してみました。下記のような感じです。

// スレッドサイズ.
#define THREAD_SIZE (8)

// Shader Model 5系かどうか?
#define IS_SM5 (1)

///////////////////////////////////////////////////////////////////////////////
// ColorFilterParam structure
///////////////////////////////////////////////////////////////////////////////
cbuffer CbColorFilter : register(b0)
{
    uint2       DipsatchArgs : packoffset(c0);   // Dispatch()メソッドに渡した引数.
    float4x4    ColorMatrix  : packoffset(c1);   // カラー変換行列.
};

//-----------------------------------------------------------------------------
// Resources.
//-----------------------------------------------------------------------------
Texture2D<float4>   Input   : register(t0);
RWTexture2D<float4> Output  : register(u0);


//-----------------------------------------------------------------------------
//! @brief      スレッドグループのタイリングを行う.
//!
//! @param[in]      dispatchGridDim     Dipatch(X, Y, Z)で渡した(X, Y)の値.
//! @param[in]      groupId             グループID
//! @param[in]      groupTheradId       グループスレッドID.
//! @return     スレッドIDを返却する.
//-----------------------------------------------------------------------------
uint2 CalcSwizzledThreaId(uint2 dispatchDim, uint2 groupId, uint2 groupThreadId)
{
    // "CTA" (Cooperative Thread Array) == Thread Group in DirectX terminology
    const uint2 CTA_Dim = uint2(THREAD_SIZE, THREAD_SIZE);
    const uint N = 16; // 16 スレッドグループで起動.

    // 1タイル内のスレッドグループの総数.
    uint number_of_CTAs_in_a_perfect_tile = N * (dispatchDim.y);

    // 考えうる完全なタイルの数.
    uint number_of_perfect_tiles = dispatchDim.x / N;

    // 完全なタイルにおけるスレッドグループの総数.
    uint total_CTAs_in_all_perfect_tiles = number_of_perfect_tiles * N * dispatchDim.y - 1;
    uint threadGroupIDFlattened = dispatchDim.x * groupId.y + groupId.x;

    // 現在のスレッドグループからタイルIDへのマッピング.
    uint tile_ID_of_current_CTA = threadGroupIDFlattened / number_of_CTAs_in_a_perfect_tile;
    uint local_CTA_ID_within_current_tile = threadGroupIDFlattened % number_of_CTAs_in_a_perfect_tile;

    uint local_CTA_ID_y_within_current_tile = local_CTA_ID_within_current_tile / N;
    uint local_CTA_ID_x_within_current_tile = local_CTA_ID_within_current_tile % N;
 
    if (total_CTAs_in_all_perfect_tiles < threadGroupIDFlattened)
    {
        // 最後のタイルに不完全な次元があり、最後のタイルからのCTAが起動された場合にのみ実行されるパス.
        uint x_dimension_of_last_tile = dispatchDim.x % N;
    #if IS_SM5
        // SM5.0だとコンパイルエラーになるので対策.
        if (x_dimension_of_last_tile > 0)
        {
            local_CTA_ID_y_within_current_tile = local_CTA_ID_within_current_tile / x_dimension_of_last_tile;
            local_CTA_ID_x_within_current_tile = local_CTA_ID_within_current_tile % x_dimension_of_last_tile;
        }
    #else
        local_CTA_ID_y_within_current_tile = local_CTA_ID_within_current_tile / x_dimension_of_last_tile;
        local_CTA_ID_x_within_current_tile = local_CTA_ID_within_current_tile % x_dimension_of_last_tile;
    #endif
    }

    uint swizzledThreadGroupIDFlattened = tile_ID_of_current_CTA * N
      + local_CTA_ID_y_within_current_tile * dispatchDim.x
      + local_CTA_ID_x_within_current_tile;

    uint2 swizzledThreadGroupID;
    swizzledThreadGroupID.y = swizzledThreadGroupIDFlattened / dispatchDim.x;
    swizzledThreadGroupID.x = swizzledThreadGroupIDFlattened % dispatchDim.x;

    uint2 swizzledThreadID;
    swizzledThreadID.x = CTA_Dim.x * swizzledThreadGroupID.x + groupThreadId.x;
    swizzledThreadID.y = CTA_Dim.y * swizzledThreadGroupID.y + groupThreadId.y;

    return swizzledThreadID;
}


//-----------------------------------------------------------------------------
//      メインエントリーポイントです.
//-----------------------------------------------------------------------------
[numthreads(THREAD_SIZE, THREAD_SIZE, 1)]
void main
(
    uint3 groupId       : SV_GroupID,
    uint3 groupThreadId : SV_GroupThreadID
)
{
    uint2 id = CalcSwizzledThreaId(DipsatchArgs, groupId.xy, groupThreadId.xy);
    Output[id] = mul(ColorMatrix, Input[id]);
}

基本的には,いったんフラットなID(つまり通し番号)にして,そこから再算出するみたいな計算しているみたいです。
cpp側は下記のような感じです。

    // カラーフィルタ実行.
    {
        auto x = (m_TextureWidth  + m_ThreadCountX - 1) / m_ThreadCountX; // m_ThreadCountX = THREAD_SIZE. シェーダリフレクションで取得.
        auto y = (m_TextureHeight + m_ThreadCountY - 1) / m_ThreadCountY; // m_ThreadCountY = THREAD_SIZE. シェーダリフレクションで取得.

        auto pCB = m_CB.GetBuffer();
        CbColorFilter res = {};
        res.ThreadX = x;
        res.ThreadY = y;
        res.ColorMatrix = asdx::Matrix::CreateIdentity();

        m_pDeviceContext->UpdateSubresource(pCB, 0, nullptr, &res, 0, 0);

        auto pSRV = m_Texture.GetSRV();
        auto pUAV = m_ComputeUAV.GetPtr();
        m_CS.Bind(m_pDeviceContext.GetPtr());
        m_pDeviceContext->CSSetConstantBuffers(0, 1, &pCB);
        m_pDeviceContext->CSSetShaderResources(0, 1, &pSRV);
        m_pDeviceContext->CSSetUnorderedAccessViews(0, 1, &pUAV, nullptr);
        m_pDeviceContext->Dispatch(x, y, 1);

        ID3D11ShaderResourceView* pNullSRV[1] = {};
        ID3D11UnorderedAccessView* pNullUAV[1] = {};
        m_pDeviceContext->CSSetShaderResources(0, 1, pNullSRV);
        m_pDeviceContext->CSSetUnorderedAccessViews(0, 1, pNullUAV, nullptr);
        m_CS.UnBind(m_pDeviceContext.GetPtr());
    }

SV_DispatchThreadIDとかのメモ

たまに触らなくなると,すぐに忘れるので思い出せるようにメモしておきます。

前提として

// コンピュートシェーダ側.
[numthreads(dimX, dimY, dimZ)]
void main(...)
{
  ...
}
// cpp側
pCmdList->Dispatch(A, B, C);

としておく。

グループが A * B * C 出来上がる
例えば,Dispatch(3, 2, 1)とした場合は, 3 * 2 * 1 = 6個のグループになる。
(0, 0, 0), (1, 0, 0), (2, 0, 0)
(1, 1, 0), (1, 1, 0), (2, 1, 0)
という感じ。
上記のuint3型6つのものがSV_GroupIDとなる。

コンピュートシェーダでは,これらのグループごとにスレッドが生成される。
つまり,dimX * dimY * dimZ のグループスレッドができあがある。
例えば,[numthreads(2, 2, 1)]とした場合は,
(0, 0, 0), (1, 0, 0)
(0, 1, 0), (1, 1, 0)
と4つのグループスレッドが出来上がある。
上記のuint3型4つのものがSV_GroupThreadIDとなる。

一番細かい単位は,実行するスレッド。つまりディスパッチされたスレッドで
グループIDとグループスレッドIDから決まるので24個のディスパッチスレッドIDが生成される。
例えば,
a : [0, A)
b : [0, B)
c : [0, C)
の半開区間を用いて、SV_GroupIDを(a, b, c)として表し,

x : [0, dimX)
y : [0, dimY)
z : [0, dimZ)
の半開区間を用いて,SV_GroupThreadIDを(x, y, z)として表したとする。

このとき,SV_DispatchThreadIDはuint3型であり、そのIDは
(a, b, c) * (dimX, dimY, dimZ) + (x, y, z) で表される。

グループ番号は,SV_GroupThreadIDとnumthredsから算出され
SV_GroupIndex = x + (A) * y + (A * B) * z;
で求まる。
例えば,
[numthreads(2, 2, 1)]とした場合は0~3までの4グループ
[numthreads(10, 8, 3)]とした場合は0~239までの240グループ
となる。

Microsoftのドキュメントに図が載っているので,以上を踏まえて読むと分かるはず。
https://docs.microsoft.com/ja-jp/windows/win32/direct3dhlsl/sv-dispatchthreadid