第1章 線だけで立体を描く ― ワイヤーフレーム
この章の目的:三次元の点を二次元の画面へ写し、その点を線で結んで立体の形を表示する。まだThree.js・WebGL・WebGPUは使わない。後からAPIを学ぶために、何を自分で計算しているのかを理解する。 動かせる教材:index.html(外部ライブラリ・ビルド不要。ファイルを開くだけで動作)
1. なぜ最初にワイヤーフレームなのか
コンピュータグラフィックスは、最初から塗りつぶされた立体・光源・影・反射を扱えたわけではない。形状を点と線の接続関係で表すことは、3Dに必要な座標変換を最も少ない要素で学べる方法である。
1963年に発表されたIvan Sutherlandの Sketchpad は、ライトペンで画面上の線画を操作する先駆的な対話型グラフィックスシステムだった。ただしSketchpadそのものを、この章のような「3D立方体の透視投影プログラム」と同一視してはいけない。そこから発展したCADなどでは、三次元形状を線と曲線のネットワークで扱うワイヤーフレーム表現が広く使われた。
問い:立方体の背後にある辺を描いてしまったとき、どの線を消すべきだろうか。この問いが、後の隠線消去・隠面消去・Z-bufferへとつながる。
2. 形のデータ:8個の頂点・12本の辺
原点を中心に辺長2の立方体を用意する。頂点はそれぞれ P=(x,y,z) という三次元ベクトルで、各成分は −1 または +1 である。
const vertices = [
[-1,-1,-1], [1,-1,-1], [1,1,-1], [-1,1,-1],
[-1,-1, 1], [1,-1, 1], [1,1, 1], [-1,1, 1]
];
const edges = [
[0,1], [1,2], [2,3], [3,0],
[4,5], [5,6], [6,7], [7,4],
[0,4], [1,5], [2,6], [3,7]
];
[2,6] という辺は、頂点番号2と6を線分で結ぶという意味。頂点座標と接続関係を分けると、同じ形を移動・回転しても辺を定義し直す必要がない。
3. 回転:点に行列を掛ける
右手座標系を使い、x は右、y は上、z は奥(カメラから前方)を正とする。この教材では カメラが +Z方向を見る という規約を採用する。OpenGLなど別のAPIではカメラの正面方向を −Z とする慣習があるので、符号規約は混在させない。
縦ベクトル p=(x,y,z)^T に対し、Y軸の周囲を角度 θ 回す行列は
ゆえに
続いてX軸の周囲を角度 φ 回す。
順序は重要。ここでは R_x(φ) R_y(θ) p の順に適用する。一般に行列積は交換できず、R_x R_y ≠ R_y R_x。
画面へ写す前に、回転後の立方体を奥へ5単位移動する。
P_c=(X,Y,Z) が、以下の「カメラから見た座標」になる。今回の模型ではすべての頂点がカメラより前方に来る。
4. 透視投影:なぜ遠いものは小さいのか
焦点距離 f の位置に投影面があるとする。相似な三角形から、カメラ座標 (X,Y,Z) と画面上の投影面座標 (x_p,y_p) との関係は
したがって
Z が2倍になると、画面中心からの距離は半分になる。これが遠近感の原因となる。
画面の幅を W ピクセル、高さを H ピクセル、縦方向の視野角を α とすると、ピクセル単位の焦点距離は
Canvasでは画面原点が左上で、Yの正方向が下向きである。3DのY上向きとの向きの差を反転し、画面中央を原点に移すと
計算例
W=800, H=600, α=90° とすると f_px=300。カメラ座標の点 (1,1,5) の画面位置は
Zを10に変えると (430,270) になり、中心 (400,300) に近づく。
平行投影との比較
平行投影ではZで割らない。一定の倍率 s を使って
とする。Zが変わっても大きさは変化しない。CADや設計図に向いている。一方、遠近感は表れない。
5. 辺を線として描く
すべての頂点を変換・投影したら、辺の定義 [i,j] に従って、投影後の頂点 Q_i と Q_j を線で結ぶ。
for (const [i, j] of edges) {
context.beginPath();
context.moveTo(projected[i][0], projected[i][1]);
context.lineTo(projected[j][0], projected[j][1]);
context.stroke();
}
実行サンプルは、さらに 近クリップ面 Z ≥ 0.1 に線分を切り詰めている。投影式は Z=0 で割り算ができないため、背後や極端に近い点を無条件に投影してはいけない。見える側と見えない側をまたぐ線分では、Z=0.1 における交点を求めてから描く。
線分 A と B の補間を L(t)=A+t(B-A) とすると、交差位置は
これが基本的な線分の近クリッピングである。なお、この章では側面・遠方面による完全な視錐台クリッピングまでは実装していない。
6. 何がまだできないか
ワイヤーフレームはすべての辺を結ぶだけなので、奥の面に隠れた線も見える。座標のZを持っていても、線分描画だけでは前後関係が解決しない。
ここから課題は次のように発展する。
- 面と法線:三角形の向きや、視点を向いているかを調べる。
- 隠線・隠面消去:手前の面に隠された部分を描かない。
- Z-buffer:画素ごとに最も手前の奥行きを記録する。
- GPUのラスタライズ:頂点処理・三角形・深度テストを大量に並列化する。
- WebGL / Three.js:これらの手続きをAPIやシーン・オブジェクトに抽象化する。
注意:面の表裏判定だけでは、複数オブジェクトの隠れや部分的な遮蔽は解決しない。背面カリングと隠面消去は別の処理である。
7. 手を動かす課題
- 課題A:透視投影と平行投影を切り替え、手前と奥の辺の見かけの長さを比較する。
- 課題B:角度
θ=90°で(1,0,0)をY軸回転したときの座標を手計算する。 - 課題C:
wireframe-math.jsのcubeVerticesとcubeEdgesを書き換えて、三角錐を描く。 - 課題D:Canvas 2Dによる線の代わりに、自作のピクセル描画(整数演算による線分生成)を実装する。Bresenhamの線分アルゴリズムへの入口になる。
8. 実行とテスト
index.html をブラウザで開く(file:// でも動作)か、ディレクトリ内で次を実行する。
python3 -m http.server 8000
# http://localhost:8000/chapters/01-wireframe/ へアクセス(repoルートで起動した場合)
node --test chapters/01-wireframe/wireframe-math.test.js
分離したファイルは wireframe-math.js(計算)、app.js(Canvas描画・操作)、style.css(見た目)、index.html(画面)である。ビルドツール・Three.js・WebGL・WebGPUは一切必要ない。
参考資料(技術史)
- Ivan E. Sutherland, Sketchpad: A Man-Machine Graphical Communication System, MIT, 1963. https://dspace.mit.edu/handle/1721.1/14979
- Ivan E. Sutherland, 同題, AFIPS '63 Spring, pp.329–346, 1963. https://doi.org/10.1145/1461551.1461591
- J. E. Bresenham, Algorithm for computer control of a digital plotter, IBM Systems Journal, 4(1), 1965. https://xlinux.nist.gov/dads/HTML/bresenham.html
- Tor Dokken, The History of CAD and Examples of Its Use in Industry, SINTEF, 2010. https://www.sintef.no/en/publications/publication/0198cc835330-543e46ed-28eb-4f89-be0f-57788c6c0486/
次章のテーマ:三角形、法線ベクトル、外積、背面カリング。そして「なぜ見えない線が残るのか」の本格的な検討。
GitHub Actionsでの動作検証
Wireframe chapter tests は、数式テストに加え、Chromium + Playwright でCanvas描画の画素確認、角度変更、投影方式切替、FOVの表示・非表示、初期化、ポインタドラッグ、スマートフォン幅での描画を自動確認する。失敗時も可能な限りスクリーンショットをActions Artifactとして残す。