注意
実装とイメージ画には生成AIを使用しています。忌避されている方はブラウザバックをお願いいたします
前回( https://qiita.com/YKVKDX12/items/9b236c97522ee15f1583)
にて短いながらもラスタライザの中核部分のみに焦点を絞ってこういうことをラスタライザはやりますよ。と説明をしました。
ソフトウェアラスタライザを実装するにあたって、実際はラスタライズそのものの実装だけでなくレンダリングパイプラインそのものの実装をする必要があります(ここはイントロダクションの記事で説明してしまうと注意がそれるので、あえて説明をしませんでした)
DirectX11でソフトウェアラスタライザの記事を見ている方はすでにご存じだとは思いますが、一応簡単に
レンダリングパイプラインをざっと・・・
これをコンピュートシェーダーを使った実装について、要所要所の説明をしていこうと思います
// 1. 頂点変換
簡単な説明
頂点変換は、いわゆる通常の頂点シェーダーでも行われるWVP(ワールドビュープロジェクション)座標変換ですね。
一応大まかな流れをイメージ画に
最初、3Dモデルはそれぞれローカル空間と呼ばれる自分たちの座標空間と呼ばれる空間に閉じこもってます
なに昼間からいびきかいて寝てんだ
寝ているところすみませんが、外に出ろ(ワールド空間への座標変換)
これでカメラに3Dモデルを映す準備ができたので・・・
君にはカメラの中の異次元空間に閉じこもってもらいます。
これでスクリーンに映る準備ができました・・・・
そこからさらにモニターの中に閉じ込めます。強く生きろよ💛
これで晴れて3Dモデルがモニターに映りました
// 3. スクリーン座標への変換 (Viewport Transform)
スクリーン座標空間への変換とは?
3D空間上のオブジェクトを、2D上に表示できるようペラペラぺっちゃんこにする作業である
「推しのアイドルの3Dフィギュアを無情にもスクリーンに映る2Dにする」
ようなものである
しかし2Dでは遠近法を律儀に表現できようもないので・・・・
近くにあるものを大きく表現、遠くにあるものを小さく表現する。
まず、これがプロジェクション変換です。
次にスクリーン座標空間への変換
これは今までの座標空間では原点(0,0)は中央点だったが、これが
左上が原点となる。ふざけんな統一しろ。
そこにプロジェクション変換した3Dモデルをスクリーン座標空間にもってくるのだが・・・・
ここで今まで中央の原点対応だったものを、左上の原点基準にして置き換えていく計算を
していくのだ。
さらに、スクリーンのピクセル一つはもちろん浮動小数点数で数えることができない
そこで3Dモデルのデータがあるピクセルの座標情報を整数に丸める
(小数点以下は切り捨てられる。悲哀!)
こうして夢ある者たちは現実に丸め込まれていくのね・・・・
といった具合になる
それだけではない。小数点数に押し込まれた画像データにはところどころ欠けが出てしまうので・・・・
これを補間してごまかすのだ
次、三角形の内外判定はこうです
三角形の内外判定が終わったら、ラスタライズの中核となるクリッピングに移ります(今回はあまり意味がありませんが・・・)
バックフェイスカリングと
隠面消去のアルゴリズムの一種になる深度テストですね。
// バックフェイスカリング (反時計回りを正とする場合、負なら裏面)
一応この記事にカリングの原理は説明してありますが・・・
URL飛ぶのめんどくさい人のためにざっとこんな画像を用意してあります。
次は隠面消去の代表的なアルゴリズム、深度テストです
隠面消去とはカメラから見えてない3D情報はデータを破棄してまおうという
アルゴリズムになります。
深度テスト(深度バッファリング)※Zバッファリングとも呼ばれることも
新しい点の深度がバッファの値より小さい(カメラに近い)場合のみ、ピクセルを上書きし、深度バッファの値も更新
原理はソートですね。なのでZソートと呼ばれることもあります。
最後に、補間です
// 7. パースペクティブ・コレクト補間 (重要)
これは・・・遠近法を再現しようとした際に発生する画像のゆがみを補正する補間ですね
イメージとしてはこんな感じです
左絵のように曲がりゆがんだ画像に補正をかけて、右絵のようにまっすぐな線に直して遠近感を表現することです
// Colorの補間
点と点の間の欠けた箇所を計算式で補うのが補間の考え方です
まぁ、今回の色の補間はこうなので
float2 uv0_p = v0_raw.uv * invW0;
float2 uv1_p = v1_raw.uv * invW1;
float2 uv2_p = v2_raw.uv * invW2;

筆での絵具の伸ばし方ですね
線形補間と多項式補間とよばれるものがありますが
まっすぐ伸ばす線形補間であれば
くねくね曲がりくねった筆の伸ばし方なら
つまり補間は数学、微分積分や二次関数・・・といった要素も入り組んでくるのです
補間は奥が深いアルゴリズムの一つでもあります。
最後に結果の出力です
// テクスチャサンプリング
float4 texColor = BaseTexture.SampleLevel(BaseSampler, finalUV, 0);
// 最終カラー決定
bestColor = finalVertexColor * texColor;
これをカラーバッファ(フレームバッファ)にコピーします
// 結果書き込み
OutputTexture[dispatchThreadID.xy] = bestColor;
で、なんだかんだありつつもウィンドウに出力される画像がこうです
以上でラスタライザの説明は終わります
フルコード
// --- メイン関数 ---
[numthreads(16, 16, 1)]
void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID)
{
// 現在処理中のピクセル座標 (中心)
float2 p = float2(dispatchThreadID.x, dispatchThreadID.y) + 0.5f;
// 画面外チェック
if (p.x >= ScreenSize.x || p.y >= ScreenSize.y) return;
// 深度バッファの初期値 (1.0 = 最も奥)
float bestDepth = 1.0f;
// 背景色 (クリアカラー)
float4 bestColor = float4(0.1f, 0.1f, 0.15f, 1.0f);
// ----------------------------------------------------------------
// 全三角形ループ (ピクセル主導レンダリング)
// ----------------------------------------------------------------
for (uint i = 0; i < TriangleCount; ++i)
{
// 頂点データの取得
uint idx = i * 3;
Vertex v0_raw = VertexBuffer[idx];
Vertex v1_raw = VertexBuffer[idx + 1];
Vertex v2_raw = VertexBuffer[idx + 2];
// 1. 頂点変換 (Local -> Clip Space)
float4 c0 = mul(float4(v0_raw.pos, 1.0f), WorldViewProj);
float4 c1 = mul(float4(v1_raw.pos, 1.0f), WorldViewProj);
float4 c2 = mul(float4(v2_raw.pos, 1.0f), WorldViewProj);
// 3. スクリーン座標への変換 (Viewport Transform)
// NDC (-1~1) -> Screen (0~w, 0~h)
float2 s0, s1, s2;
s0.x = (c0.x * invW0 + 1.0f) * 0.5f * ScreenSize.x;
s0.y = (1.0f - c0.y * invW0) * 0.5f * ScreenSize.y; // Y反転
s1.x = (c1.x * invW1 + 1.0f) * 0.5f * ScreenSize.x;
s1.y = (1.0f - c1.y * invW1) * 0.5f * ScreenSize.y;
s2.x = (c2.x * invW2 + 1.0f) * 0.5f * ScreenSize.x;
s2.y = (1.0f - c2.y * invW2) * 0.5f * ScreenSize.y;
// 4. ラスタライズ判定 (エッジ関数)
float area = EdgeFunction(s0, s1, s2);
// バックフェイスカリング (反時計回りを正とする場合、負なら裏面)
if (area <= 0) continue;
float w0 = EdgeFunction(s1, s2, p);
float w1 = EdgeFunction(s2, s0, p);
float w2 = EdgeFunction(s0, s1, p); // 正しい計算
// 5. 三角形の内外判定
if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
// 重心座標の正規化
w0 /= area;
w1 /= area;
w2 /= area;
// 6. 深度テスト (Z値の線形補間)
// NDCのZ (c.z / c.w) を補間するのが一般的ですが、
// ここでは簡易的に W の逆数を使って深度判定します (1/Wが大きい=Wが小さい=手前)
float interpolatedInvW = w0 * invW0 + w1 * invW1 + w2 * invW2;
float currentW = 1.0f / interpolatedInvW;
// 深度バッファ更新チェック (Wが小さい方が手前)
// ※NDC深度(0~1)を使う場合は z < bestDepth
// ここでは簡易的なZテストとして比較
float currentDepth = c0.z * invW0 * w0 + c1.z * invW1 * w1 + c2.z * invW2 * w2;
currentDepth *= currentW; // 復元
if (currentDepth < bestDepth) {
bestDepth = currentDepth;
// 7. パースペクティブ・コレクト補間 (重要)
// UVやColorは直接 w0,w1,w2 で補間すると歪むため、
// 一度 (Value / W) を補間し、最後に W を掛けて復元する。
// UVの補間
float2 uv0_p = v0_raw.uv * invW0;
float2 uv1_p = v1_raw.uv * invW1;
float2 uv2_p = v2_raw.uv * invW2;
float2 finalUV = (w0 * uv0_p + w1 * uv1_p + w2 * uv2_p) * currentW;
// Colorの補間
float4 col0_p = v0_raw.color * invW0;
float4 col1_p = v1_raw.color * invW1;
float4 col2_p = v2_raw.color * invW2;
float4 finalVertexColor = (w0 * col0_p + w1 * col1_p + w2 * col2_p) * currentW;
// テクスチャサンプリング
float4 texColor = BaseTexture.SampleLevel(BaseSampler, finalUV, 0);
// 最終カラー決定
bestColor = finalVertexColor * texColor;
}
}
}
// 結果書き込み
OutputTexture[dispatchThreadID.xy] = bestColor;
}














