世界を動かす技術を、日本語で。

500行の素のC++によるソフトウェアレンダリング

2026年7月23日原文(haqr.eu)

概要

  • OpenGLDirectX などの3DグラフィックスAPIの仕組みを学ぶための ソフトウェアレンダラー 作成手順
  • 外部ライブラリ不使用 で500行程度のC++コードのみで実装
  • TGA画像操作クラス のみ提供、ピクセル単位で描画を実装
  • 学習目的であり、 GPUプログラミング そのものではなく仕組みの理解が主眼
  • 完成コードとサンプル画像は GitHub で公開

500行C++によるソフトウェアレンダリング入門

  • OpenGLVulkanMetalDirectX の動作原理を理解するための簡易クローン実装

  • 多くの人が3DグラフィックスAPIの最初の壁でつまずく現状

  • 独自講義を通じて、学生が自力でレンダラーを作れるようになることを目指す教材

  • 第三者製グラフィックス関連ライブラリは一切不使用

  • 目標:三角形メッシュとテクスチャから画像を生成するシンプルな3Dレンダラー作成

    • 出力は 画像ファイル のみ、GUIなし
    • TGAファイル操作クラス を唯一の外部依存として提供
    • 最初に使える機能は「ピクセル単位で色をセットする」だけ
    • 線分や三角形描画機能も含めて全て手作業で実装
  • 自分で手を動かすことが理解の本質 であり、提供コードをそのまま使うのは推奨しない方針

  • 完成コードは GitHub で公開、学生向けの初期コードも別途提供

  • main.cpp の例

    • 64x64ピクセルのTGA画像を生成
    • 3点の座標に白色ピクセルを描画
    • コード例:
      #include "tgaimage.h"
      constexpr TGAColor white = {255, 255, 255, 255}; // BGRA順
      int main(int argc, char** argv) {
        constexpr int width = 64;
        constexpr int height = 64;
        TGAImage framebuffer(width, height, TGAImage::RGB);
        int ax = 7, ay = 3;
        int bx = 12, by = 37;
        int cx = 62, cy = 53;
        framebuffer.set(ax, ay, white);
        framebuffer.set(bx, by, white);
        framebuffer.set(cx, cy, white);
        framebuffer.write_tga_file("framebuffer.tga");
        return 0;
      }
      
  • コンパイル方法

    • git clone https://github.com/ssloy/tinyrenderer.git && cd tinyrenderer && cmake -Bbuild && cmake --build build -j && build/tinyrenderer obj/diablo3_pose/diablo3_pose.obj obj/floor.obj
    • 出力画像はframebuffer.tgaとして保存
  • 完成サンプル画像 もGitHubで公開中

教材の目的と注意点

  • 本教材は 現代的な3Dグラフィックスライブラリの構造 を緩やかに踏襲
  • GPUアプリケーションの書き方 ではなく、内部動作の理解を重視
  • 3Dライブラリを効率的に使いこなすための基礎知識 習得が目的
  • 自作レンダラーを通して本質的な仕組みを体感 する学習体験を推奨

Hackerたちの意見

ページに最初の画像が読み込まれたとき、びっくりしたよ…まさかあんなのが来るとは思わなかった。

それってどんな画像だったの?私が最初に見るのは顔写真なんだけど。個人的には、デモ用にティーポットを選んだかな。でも、著者が選んだモデルには特に衝撃的なところはないと思う。

ページに最初の画像が読み込まれたとき、びっくりしたよ…まさかあんなのが来るとは思わなかった。mbappe'?

このチュートリアルの中で、三角形のクリッピングについてちゃんと触れてくれるのがあればいいのに。これがソフトウェアレンダラーで一番苦労する部分なんだよね。実用的なものを作るなら、超基本的なシーンでも絶対に対処しなきゃいけないことだから。ジオメトリがビューのフラスタムと交差するたびに、その三角形をクリップする必要があるんだ。

それについては一章まるごとあるよ! https://gabrielgambetta.com/computer-graphics-from-scratch/1...

大きな三角形の属性補間が心配な場合だけ、三角形をクリップすればいいよ。これには二つの方法がある:(1) 破棄する(速いけどユーザー体験としてはイマイチ);または、(2) プリミティブ合成。フラスタムクリッピングはローカルタイルでのポイントピッキングによって有効になる。プリミティブ合成はちょっとしたFPの技術が必要だけど、逆変換されたクリッピング矩形に対してバリセンチック空間でやるのが一番簡単だよ。これで、ダブルや(より良い)固定小数点を使ってクリップの丸め誤差を慎重に制御できる。Abrashは整数固定小数点を使うのが好きだけど、これは歴史的なもので、現代の固定小数点は指数部のFPユニットを慎重に制御することで扱えるよ。大きな問題は、合成されたプリミティブの新しい頂点のZ値と1/Z値を再生成すること。あとは、遅延属性合成ラスタライザーを前提にすれば、自然に流れるはずだよ。私のラスタライザーのオープンソース版には例があるよ:OpenSWR.org。

自分のレンダラーも「クリッピングを実装するべき」って段階でいつも詰まってたけど、ついに覚悟を決めて、あまり苦労せずに動くものを書けたんだ。後で知ったけど、独立してサザーランド–ホッジマンアルゴリズムを「再発見」したよ(事前にググるのはズルだからね)。アルゴリズム自体はかなりシンプルで直感的だと思うけど、一番の壁は射影空間の奇妙さと同次座標を扱うことかな(実際、P₃(ℝ)でクリップする必要があるのは前面の平面だけで、残りは透視分割の後にクリップしてもいいけど、やってる間に全部一緒にやっちゃってもいいよね)。クリップ空間の平面方程式は超シンプルで、基本的にax + by + cz = wの形の6つの方程式がx = ±w、y = ±w、z = ±wに簡略化される。つまり、例えば頂点のx座標がw座標より大きい場合、その頂点は右のクリッピング平面の外にあるってこと。サザーランド–ホッジマンはこんな感じだよ:

平面で定義された半空間の内側に点があるならtrueを返す

def point_inside_plane(point, plane) -> bool:

単一のドット積、さらに簡略化できる

辺(p1, p2)が平面と交差するtを返す

def edge_intersect_plane(edge: (Point, Point), plane) -> float:

単一のドット積、さらに簡略化できる

単純な多角形の頂点と平面を与えられたら、

平面の内側に完全にある多角形の部分を返す

def clip_against_plane(poly: [Vertex], plane): let result: [Vertex] = [] let [(v_1, v_2), (v_2, v_3), ..., (v_n, v_1)] = poly.edges() for each (v_i, v_j) of the edges: let i_inside = point_inside_plane(v_i, plane) let j_inside = point_inside_plane(n_j, plane) if i_inside and j_inside: # v_jは次のイテレーションで追加される! result.push(v_i) else if not i_inside and not j_inside: pass # 何もしなくていい! else: # 一方は内側、もう一方は外側、クリップしなきゃ let t = edge_intersect_plane((v_i.pos, v_j.pos), plane) # 平面をまたぐ新しい頂点を合成 let v_new = Vertex( pos = lerp(t, v_i.pos, v_j.pos), # 各頂点属性 attrib = lerp(t, v_i.attrib, v_j.attrib) ) if i_inside: result.push(v_i); result.push(v_new) # v_jは捨てる else: result.push(v_new); result.push(v_j) # v_iは捨てる return result これを全ての平面に対して呼び出すだけで、1回の呼び出しの出力が次の呼び出しの入力になる!このプロセスの最終結果は、凸多角形(最大9頂点の三角形が6つの平面に対して)で、簡単に三角形分割できる。全体のプロセスを速くするために、いわゆるアウトコードを事前計算して、少なくとも1つの平面の外に完全にある三角形や、全ての平面の内側に完全にある三角形のクリッピングを避けることができるよ。

数ヶ月前にRustでこれをやったよ。コードは全部手書きで、LLMは使ってない。で、その上に小さな「ゲーム」を追加して、ピクセル化シェーダーや懐中電灯の端の色収差みたいな特殊効果も入れたんだ。興味がある人はここを見てね! https://github.com/kshitijl/tinyrenderer-rs リポジトリには進行中のスクリーンショットがたくさんあって、レンダラーがどんどん形になっていく様子が見れるよ。それに、面白いビジュアルバグもいっぱいあったし。たくさん学んだ!レンダリングの仕組み以外での最大の教訓は、現代のCPUが本当に速いってこと。シングルスレッドのCPUレンダラーでも、ちょっとした特殊効果を使ったインタラクティブな3Dゲームは十分に動かせるよ。

ソフトウェアレンダラーなのに、なんでwgpuを引っ張ってくるの?

このリソースと、John Vinceの「Mathematics for Computer Graphics」[1]は、私が自分のソフトウェアレンダラーを書くときに本当に欠かせなかったんだ。[2] これはLLMが登場するずっと前のことで、プロセス全体に少なくとも数ヶ月かかったよ。ほとんどはコンピュータグラフィックスの背後にある数学を理解しようとしたり、Cのセグメンテーションフォルトを追いかけたりしてた。楽しい時間だったね。[1]: https://www.amazon.co.uk/Mathematics-Computer-Graphics-John-... [2]: https://github.com/nkanaev/tipsy

: I. Sutherland and G. Hodgman. 1974. "Reentrant polygon clipping." Communications of the ACM, Volume 17, Issue. Available: https://dl.acm.org/doi/10.1145/360767.360802

これが新しいものだと思ったけど、ただのssloyのtinyrendererじゃん。記事には日付が必要だね、めっちゃ古いし。

Hacker Newsで議論の続きを見る