描画
ここまで長くなりましたが書いてきたコードで立方体を描画してみましょう。
8つの頂点を線で結んだだけの単純なオブジェクトです。
先ほども書いた通り3Dでは画面の中心を基準にXは右・Yは上・Zは手前に数値が大きくなるので、頂点は原点を基準に-1~1の範囲で配置します。
さらに拡縮率100を3方向に掛け合わせることで一辺200の立方体の頂点が完成します。
var v = new Array();
v[0] = new vertex3d({
vertex : {x:-1,y:1,z:1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[1] = new vertex3d({
vertex : {x:1,y:1,z:1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[2] = new vertex3d({
vertex : {x:1,y:-1,z:1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[3] = new vertex3d({
vertex : {x:-1,y:-1,z:1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[4] = new vertex3d({
vertex : {x:-1,y:1,z:-1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[5] = new vertex3d({
vertex : {x:1,y:1,z:-1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[6] = new vertex3d({
vertex : {x:1,y:-1,z:-1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
v[7] = new vertex3d({
vertex : {x:-1,y:-1,z:-1},
size : {x:100,y:100,z:100},
rotate : {x:20,y:-20,z:0},
position : {x:0,y:0,z:0}
});
ここで頂点を-100~100、拡縮率は1で記述しても構いません。
必要な頂点インスタンスが用意できたらタイマー関数内でアップデートと描画を行ないます。
せっかくなので回転もさせてみましょう。
//オブジェクトの回転角変数
var cubeRotate = {
x : 0,
y : 0,
z : 0
};
var loop = function() {
setTimeout(function() {
ctx.clearRect(0, 0, 500, 500); //canvasクリア
camera.update(); //注視距離・視線角のアップデート
//回転角を加算
cubeRotate.x += 0.5;
cubeRotate.y += 1;
cubeRotate.z += 3;
//各頂点に回転角を代入し頂点をアップデート
for(var i=0; i<v.length; i++) {
v[i].affineIn.rotate = cubeRotate; v[i].vertexUpdate();
};
// それぞれの頂点座標を結ぶ
ctx.beginPath();
ctx.moveTo(v[0].affineOut.x, v[0].affineOut.y);
ctx.lineTo(v[1].affineOut.x, v[1].affineOut.y);
ctx.lineTo(v[2].affineOut.x, v[2].affineOut.y);
ctx.lineTo(v[3].affineOut.x, v[3].affineOut.y);
ctx.lineTo(v[0].affineOut.x, v[0].affineOut.y);
ctx.moveTo(v[4].affineOut.x, v[4].affineOut.y);
ctx.lineTo(v[5].affineOut.x, v[5].affineOut.y);
ctx.lineTo(v[6].affineOut.x, v[6].affineOut.y);
ctx.lineTo(v[7].affineOut.x, v[7].affineOut.y);
ctx.lineTo(v[4].affineOut.x, v[4].affineOut.y);
ctx.moveTo(v[0].affineOut.x, v[0].affineOut.y);
ctx.lineTo(v[4].affineOut.x, v[4].affineOut.y);
ctx.moveTo(v[1].affineOut.x, v[1].affineOut.y);
ctx.lineTo(v[5].affineOut.x, v[5].affineOut.y);
ctx.moveTo(v[2].affineOut.x, v[2].affineOut.y);
ctx.lineTo(v[6].affineOut.x, v[6].affineOut.y);
ctx.moveTo(v[3].affineOut.x, v[3].affineOut.y);
ctx.lineTo(v[7].affineOut.x, v[7].affineOut.y);
ctx.stroke(); //描画
loop();
}, 1000/60);
};
loop();
実行結果
サンプルコード(jsdo.it)
キューブが描画されました。次は線ではなく面を描画してみましょう。
どの面かが分かりやすいよう一面ずつ色を塗り分けます。
描画部分をこのように記述します。
//面1の描画
ctx.beginPath();
ctx.moveTo(v[0].affineOut.x, v[0].affineOut.y);
ctx.lineTo(v[1].affineOut.x, v[1].affineOut.y);
ctx.lineTo(v[2].affineOut.x, v[2].affineOut.y);
ctx.lineTo(v[3].affineOut.x, v[3].affineOut.y);
ctx.closePath();
ctx.fillStyle = "hsla(0, 100%, 70%, 1)";
ctx.fill();
//面2の描画
ctx.beginPath();
ctx.moveTo(v[1].affineOut.x, v[1].affineOut.y);
ctx.lineTo(v[5].affineOut.x, v[5].affineOut.y);
ctx.lineTo(v[6].affineOut.x, v[6].affineOut.y);
ctx.lineTo(v[2].affineOut.x, v[2].affineOut.y);
ctx.closePath();
ctx.fillStyle = "hsla(30, 100%, 70%, 1)";
ctx.fill();
//面3の描画
ctx.beginPath();
ctx.moveTo(v[5].affineOut.x, v[5].affineOut.y);
ctx.lineTo(v[4].affineOut.x, v[4].affineOut.y);
ctx.lineTo(v[7].affineOut.x, v[7].affineOut.y);
ctx.lineTo(v[6].affineOut.x, v[6].affineOut.y);
ctx.closePath();
ctx.fillStyle = "hsla(60, 100%, 70%, 1)";
ctx.fill();
//面4の描画
ctx.beginPath();
ctx.moveTo(v[4].affineOut.x, v[4].affineOut.y);
ctx.lineTo(v[0].affineOut.x, v[0].affineOut.y);
ctx.lineTo(v[3].affineOut.x, v[3].affineOut.y);
ctx.lineTo(v[7].affineOut.x, v[7].affineOut.y);
ctx.closePath();
ctx.fillStyle = "hsla(90, 100%, 70%, 1)";
ctx.fill();
//面5の描画
ctx.beginPath();
ctx.moveTo(v[4].affineOut.x, v[4].affineOut.y);
ctx.lineTo(v[5].affineOut.x, v[5].affineOut.y);
ctx.lineTo(v[1].affineOut.x, v[1].affineOut.y);
ctx.lineTo(v[0].affineOut.x, v[0].affineOut.y);
ctx.closePath();
ctx.fillStyle = "hsla(120, 100%, 70%, 1)";
ctx.fill();
//面6の描画
ctx.beginPath();
ctx.moveTo(v[3].affineOut.x, v[3].affineOut.y);
ctx.lineTo(v[2].affineOut.x, v[2].affineOut.y);
ctx.lineTo(v[6].affineOut.x, v[6].affineOut.y);
ctx.lineTo(v[7].affineOut.x, v[7].affineOut.y);
ctx.closePath();
ctx.fillStyle = "hsla(150, 100%, 70%, 1)";
ctx.fill();
実行結果
サンプルコード(jsdo.it)
明らかにおかしいです。
これは本来奥にあるはずの面を関係なしに面1~6の順に描画してしまっている為です。
正確に描画するには奥にあるか手前にあるかを面ごとに判定する必要があります。
このように面ごとにソートする方法をZソートと言います。
奥行き判定する際、手前か奥かというとz座標を使うことを考えてしまいます。
しかしz座標はカメラに対する順序ではないのでオブジェクトの裏に回り込んだ際などに描画が崩れてしまいます。
判定に使用するのはアフィン変換で出力したパースペクティブ値です。
instance.affineOut.p;
各面に使用されている頂点のパースペクティブ値を平均してsort関数にかけましょう。
面ごとにどの頂点を使用するかを保持する配列を作ります。
そのまま配列をsortするとどの面が何色か判断できないので一緒に面の色も持たせます。
var f = new Array();
f[0] = {
color : "hsla(0, 100%, 70%, 1)",
verticies : [0,1,2,3]
};
f[1] = {
color : "hsla(30, 100%, 70%, 1)",
verticies : [1,5,6,2]
};
f[2] = {
color : "hsla(60, 100%, 70%, 1)",
verticies : [5,4,7,6]
};
f[3] = {
color : "hsla(90, 100%, 70%, 1)",
verticies : [4,0,3,7]
};
f[4] = {
color : "hsla(120, 100%, 70%, 1)",
verticies : [4,5,1,0]
};
f[5] = {
color : "hsla(150, 100%, 70%, 1)",
verticies : [3,2,6,7]
};
各面が使用する頂点のパースペクティブ値を平均化して比較するsort関数を作成します。
これで面の配列が視点からの距離に則した順序に並び替えられます。
f.sort(
function(a, b) {
var pA = 0;
for(var i=0; i<a.verticies.length; i++) {
pA += v[a.verticies[i]].affineOut.p;
if(i == a.verticies.length-1) {
pA /= 4;
};
};
var pB = 0;
for(var i=0; i<b.verticies.length; i++) {
pB += v[b.verticies[i]].affineOut.p;
if(i == b.verticies.length-1) {
pB /= 4;
};
};
if (pA < pB) {
return -1;
};
if (pA > pB) {
return 1;
};
return 0;
}
);
並び替えた面を描画します。
for(var i=0; i<f.length; i++) {
ctx.beginPath();
for(var j=0; j<f[i].verticies.length; j++) {
if(j==0) {
ctx.moveTo(v[f[i].verticies[j]].affineOut.x, v[f[i].verticies[j]].affineOut.y);
} else {
ctx.lineTo(v[f[i].verticies[j]].affineOut.x, v[f[i].verticies[j]].affineOut.y);
};
};
ctx.closePath();
ctx.fillStyle = f[i].color;
ctx.fill();
};実行結果
サンプルコード(jsdo.it)
正確に描画されました。
ここまで組めれば後は頂点位置を変更して様々な形が組めるようになっています。
更にこうしたオブジェクトも形ごとにクラス化しておくと大量のオブジェクトを動かす際に便利です。