第2章 面の向きから「見えない線」を考える
この章の目標:第1章の「頂点と辺」に「面」を加え、外積・内積で表裏を計算する。背面カリングと隠面消去の違いを2個の立方体で実験する。 操作サンプル / 第1章
1. 線だけでは隠れが分からない
第1章では立方体の8頂点と12本の辺を描いた。遠くにある辺もすべて見えた。次は6枚の四角形の面を追加する。どの頂点から次の頂点へ進むか、という順序も情報の一部になる。
const faces = [
[0,3,2,1], // -Z 側
[4,5,6,7], // +Z 側
[0,4,7,3], // -X 側
[1,2,6,5], // +X 側
[0,1,5,4], // -Y 側
[3,7,6,2] // +Y 側
];
この順序では外積で求めた法線は立方体の外側を向く。頂点の順序を逆にすると法線も逆向きになる。カメラが +Zを見る という第1章の座標規約を踏襲する。
2. 外積で法線を求める
同一平面上の3頂点 A,B,C から、辺ベクトル a=B−A と b=C−A を作る。二つの辺の両方に垂直な法線 n は外積で与えられる。
例えば A=(0,0,0), B=(1,0,0), C=(0,1,0) なら n=(0,0,1)。頂点BとCを交換すると n=(0,0,-1)。本章は法線を正規化せず使うが、表裏に必要な内積の符号は変わらない。
3. 内積でカメラを向く面を決める
面の中心を C_f とし、面からカメラへ向かうベクトルを v = camera − C_f とする。カメラ座標系ではカメラは原点なので v = −C_f。
外向き法線を使う本章では、dが正なら表面がカメラを向く。ゼロ付近は誤差を考慮する。
回転前の立方体を奥へ5動かした例では、手前(−Z側)の面の法線が (0,0,-4)、面の中心が (0,0,4)。したがって d=16 で正面となる。反対側(+Z側)の面は負になる。
const n = cross(subtract(B,A),subtract(C,A));
const center = mean(face.map(i => cameraVertices[i]));
const facing = dot(n,center.map(x => -x));
const front = facing > 1e-8;
4. 背面カリングを実装する
Back-face culling は、不透明な閉じた物体の裏を向く面を、描画対象から外す最適化である。
サンプルの ② 背面カリング は、正面を向く四角形に属する辺だけを描く。真正面から見た立方体なら12本の辺から4本に減り、斜めから3面が見えれば9本になる。ただし立体同士が重なった際の隠れは分からない。手前の立方体Bが奥の立方体Aを隠していても、Aの表面の線が表示されてしまう。
実際のOpenGLとの違い:ここは法線と視点の内積を使って説明する教育用実装である。OpenGLの面カリングは通常、投影後の頂点順序(winding/符号付き面積)で判定する。照明用の法線を直接使うわけではない。
5. 2個の立方体で比較する
サンプルの切替を順番に操作する。
- ① 全辺:各立方体12本の辺を全部描く。
- ② 背面カリング:各立方体について、裏向き面しか持たない辺を消す。しかしAの線がBの手前に見えてしまう。
- ③ Painter法:表向きの面を遠い順に不透明に塗り重ねる。手前の面が奥の面を覆う。
③は単純なPainter's algorithm の実演。面中心のZが大きいもの(+Zが奥)から描く。
visibleFaces.sort((a,b) => b.depth-a.depth);
for (const face of visibleFaces) {
// 投影した多角形を不透明に塗る
}
ただし、面の交差、循環的な重なり、面の中心が前後関係を代表しないケースでは単純ソートは破綻する。背面カリングともPainter法とも違い、画素ごとの深度を比較する方法が第3章の Z-buffer である。
6. レンダリング技術史とのつながり
1960〜70年代は、線だけの図に隠線・隠面処理や塗りつぶしを加える研究が進んだ。1972年、M. E. Newell、R. G. Newell、T. L. Sancha は A Solution to the Hidden Surface Problem で、表示する面の順序や複雑な場合の分割を扱った。これは「奥から塗ればよいのか」という課題を考える重要な資料である。
現在のGPUにも背面カリングはあるが、背面カリングと遮蔽の判定は別であり、後者には深度テストなどが使われる。
参考資料:
- Newell, M. E. et al., A Solution to the Hidden Surface Problem (1972): https://content-animation.org.uk/htmls/newell.htm
- Khronos OpenGL Wiki, Face Culling: https://wikis.khronos.org/opengl/Face_Culling
- OpenGL FAQ, Why doesn't face culling use the surface normal?: https://www.opengl.org/archives/resources/faq/technical/clipping.htm
7. 演習
- A:A=(0,0,0)、B=(0,1,0)、C=(0,0,1) の法線を求める。頂点順を逆にすると?
- B:回転を0°へ戻して、何枚の面が正面になるか計算する。
- C:立方体2個の表示をONにし、全辺→背面カリング→Painter法で、何が消えたか説明する。
- D:
app.js内の立方体Bを移動して、面が交差する状況を作る。Painter法の破綻を観察する。 - E:OpenGLの
glFrontFaceとglCullFaceがどの情報を参照するか調べる。
8. コードとテスト
face-math.js に外積、内積、面中心、背面判定を実装。app.js はCanvas 2D描画。第1章の wireframe-math.js の回転・投影を再利用するため、ここでもGPU APIは不要。
node --test chapters/02-hidden-lines/face-math.test.js
node scripts/build-site.mjs
python3 -m http.server 8000 --directory dist
# http://localhost:8000/chapters/02-hidden-lines/
第3章では三角形のラスタライズとZ-bufferを実装する。