3Dの基本的な知識を理解したくて、『3次元CGの基礎と応用[新訂版]( http://goo.gl/GKoQ4y )』という書籍を購入しました。薄くて読みやすそうな本なのですがサンプルソースがないので、勉強がてらサンプルを作りながら読み進めようと思います。誰でもすぐに追試ができるよう、HTML + Javascriptでサンプルを作っていきます。
完全なサンプルはこちら
https://github.com/nakaken0629/3dstudy
2.4. 透視投影
『遠くのものは小さく見え、近くのものは大きく見える』という基本的な考え方が透視投影です。概念としてはとても簡単で、距離と見かけ上の大きさが比例するというものです。
ただしあまりに視点に近いもの、あるいは視点の後ろにあるものは見えないようにするために、前方クリッピング平面と後方クリッピング平面でクリッピングする必要があります。今回はポリゴンの表示位置を後方クリッピング平面を超えないように調整することで、後方クリッピング平面でのクリッピング処理は省略しています。
異なる単位同士の変換比率と、各種大きさの定義
3次元での描画をするには、ピクセル系の単位(例:600px * 400px)と現実の単位(例:15cm * 10cm)が混在するため、それらの比率を確認しておく必要があります。以下の考え方で今回は比率と、それに伴う各種大きさの設定を行いました。
まず、スクリーンの大きさです。画面上に見えるスクリーンの大きさは600px * 400pxです。これを定規で測ってみるとおおよそ15cm * 10cmでした。そのため、1cm = 40pxとなります。また、自分の目とPCのモニターの距離はおよそ30cmでした。
次に前方クリッピング平面は、特に根拠はないのですが演算誤差でおかしな画像が表示されない値を調整しながら確認したところ、自分の目とモニターの中間地点(15cm)が良さそうな値でした。
最後に後方クリッピング平面ですが、視力1.0だと7.5mmの大きさのランドルト環(視力検査で使うアルファベットの『C』のようなもの)を5mの距離から正確に見えるそうです。ここから、視力1.0で大体1メートルくらいの物体が識別できる距離として、50,000cm(=500メートル)と定義しました。
参考:視力 (Wikipediaより)
まとめたものは、以下の通りです。
- 1cm = 40px
- スクリーン平面:15cm x 10cm
- 視点からスクリーン平面までの距離:30cm
- 視点から前方クリッピング平面までの距離:15cm
- 視点から後方クリッピング平面までの距離:50,000cm
クリッピング平面でのクリッピング処理
例えば、三角形面のポリゴンの一つの頂点だけがビューボリュームの外にあると、角が削れて四角形面になります。このようにクリッピング処理ではポリゴンの頂点の数が増える可能性があります。このクリッピング処理は次のアルゴリズムで作成しました。
- ポリゴン内のある頂点を起点に取る
- 起点がビューボリュームに含まれている場合、その点をクリッピング後のポリゴンの頂点として採用する
- 起点の次の点を終点とする。起点と終点の間にクリッピング平面がある場合、その交点をクリッピング後のポリゴンの頂点として採用する
- 上記の終点を新たな起点として、2.に戻る
スクリーンへの投影
ある頂点(x, y, z)をスクリーンへ投影した時の座標(x', y')は、次の内容で計算できます。
x' = (視点からスクリーン平面までの距離 / z) * x
y' = (視点からスクリーン平面までの距離 / z) * y
なお今回は、視点の中心が(0, 0)となるように、スクリーン平面を(-300, -200) - (300, 200)という座標系に変換しているので、その分の補正も入っています。
サンプルコード
var fillTriangle = function(p) {
/* 前方クリッピング平面でクリッピングする */
var z = 600;
var p2 = {'v': [], 'rgb': p.rgb};
for (var i = 0; i < p.v.length; i++) {
var j = (i + 1) % p.v.length;
var v1 = p.v[i];
var v2 = p.v[j];
if (v1.z >= z) {
/* 起点がビューボリューム内なら使用する */
p2.v.push(v1);
}
if ((v1.z >= z && v2.z < z) || (v1.z < z && v2.z >= z)) {
/* 起点と終点の間に前方クリッピング平面があれば、交点を使用する */
var x = (z - v1.z) * (v2.x - v1.x) / (v2.z - v1.z) + v1.x;
var y = (z - v1.z) * (v2.y - v1.y) / (v2.z - v1.z) + v1.y;
p2.v.push({'x': x, 'y': y, 'z': z});
}
}
p = p2;
/* 実座標からスクリーン座標への変換を行う */
var length = p.v.length;
var sx = [];
var sy = [];
for (var i = 0; i < length; i++) {
sx[i] = p.v[i].x * 1200 / p.v[i].z + 300;
sy[i] = p.v[i].y * 1200 / p.v[i].z + 200;
}
/* 変換後のポリゴンの描画を行う */
ctx.fillStyle = p.rgb;
ctx.beginPath();
ctx.moveTo(sx[0], sy[0]);
for (var i = 1; i <= length; i++) {
var index = i % length;
ctx.lineTo(sx[index], sy[index]);
}
ctx.closePath();
ctx.fill();
};