Help us understand the problem. What is going on with this article?

WebGLを使わずにCanvasで描画する似非3D

More than 5 years have passed since last update.

WebGLを使わずにHTML5のcanvasを使ってなんとなく3Dっぽいものを描画する方法を、わかりづらく書いていきます。まぁ、なんとなく3Dっぽくしましょうってことで。

まず、こんな感じのbodyを持ったHTMLを用意します。

triangle.html
<body>
  <canvas id="ese" width="401" height="401" ></canvas>
</body>

そして、こんな感じのJavaScriptを書きます。scriptタグで囲むなりしましょう。

triangle.js
window.addEventListener("load", function(){
    var canvas = document.getElementById("ese");
    var context = canvas.getContext('2d');

    context.beginPath();
    context.moveTo(200, 0);
    context.lineTo(400, 400);
    context.lineTo(0, 400);
    context.lineTo(200, 0);
    context.stroke();
}, false);

そうすると、こんな感じの2Dっぽい三角形が描画されます。

tri.png

ちょっとでかいですが、まぁ気にしない事にします。これをちょっと修正します。verticesという配列を用意します。verticesの各要素には三角形の頂点の座標を配列として保持しています。その配列にはx,y,zの順で座標が入っています。zはこの時点ではまだ使っていません。

そして、faceという配列を用意します。このfaceの各要素は三角形の面を構成する頂点のindexを3つずつ入れてあります。今回はverticesの0番目と1番目と2番目の頂点番号を使った三角形を1つ描画しているのでfaceは1つの要素を持ち、その要素の中には0, 1, 2という数値を持った配列が入っています。

この時点ではfaceを使う利点はありませんが、このあとの為にこの時点で入れておきます。描画される三角形は先ほどのプログラムと同じです。

triangle.js
window.addEventListener("load", function(){
    var canvas = document.getElementById("ese");
    var context = canvas.getContext('2d');

    var vertices = [
        [ 200,    0,  5],
        [ 400,  400,  5],
        [   0,  400,  5],
    ];

    var face = [
        [0, 1, 2]
    ]

    context.beginPath();

    for(var i  = 0; i < face.length; i++){
        var vertex1 = vertices[face[i][0]];
        var vertex2 = vertices[face[i][1]];
        var vertex3 = vertices[face[i][2]];

        context.moveTo(vertex1[0], vertex1[1]);
        context.lineTo(vertex2[0], vertex2[1]);
        context.lineTo(vertex3[0], vertex3[1]);
        context.lineTo(vertex1[0], vertex1[1]);
    }

    context.stroke();
}, false);

ではここでverticesとfaceの内容を以下のように書き変えてみます。すると先ほどのプログラムとは違う形になるはずです。とりあえず4つの三角形を描画していますが、先ほどのfaceを使わなければ4(三角形の数)*3(三角形を構成する頂点の数)の12個の頂点が必要になりますが、faceで使い回すことにより、4つの頂点を定義すればすみます。faceを使わずに12個の頂点を書いていってもいいですが...ここではfaceを使って再利用しておきます。

var vertices = [
    [ 100,  100,  100],
    [ 100,  200,  200],
    [ 200,  100,  200],
    [ 200,  200,  100],
];

var face = [
    [0, 3, 2],
    [0, 2, 1],
    [0, 1, 3],
    [1, 2, 3]
];

これで以下の画像のようになるはずです。ならなかったら何かがおかしいのでしょう(掲載されてるプログラムがおかしい可能性もある)。

tri2.png

では次に、この図形を3次元っぽく動かします。3次元の回転行列というものを使って、図形を回転させます。ただし、この図形の中心を(0, 0, 0)にするために今まで0~200だったのを-100~100に変更しています。一定時間ごとに動かすためにsetIntervalを使いtimeという変数をインクリメントすることで図形を動かしています。回転行列に関してはGoogle先生に聞きましょう!そして、今回も表示しているのはxとyだけで、これらの値は保存しておきたいがためにscreenXとscreenYという変数をあとから用意して、これらの座標で三角形を描画します。

triangle.js
var context = null;

var vertices = [
    [ -100, -100, -100],
    [ -100,  100,  100],
    [  100, -100,  100],
    [  100,  100, -100],
];

var face = [
    [0, 3, 2],
    [0, 2, 1],
    [0, 1, 3],
    [1, 2, 3]
];

var time = 0;

window.addEventListener("load", function(){
    var canvas = document.getElementById("ese");
    context = canvas.getContext('2d');

    setInterval(draw, 33);
}, false);

var draw = function(){
    context.clearRect(0, 0, 400, 400);
    context.beginPath();

    for(var i = 0; i < vertices.length; i++){
        var vertex = vertices[i];

        vertex.screenX = vertex[0] * Math.cos(time) * Math.cos(time) - vertex[1] * Math.sin(time) + vertex[2] * Math.cos(time) * Math.sin(time) + 200;
        vertex.screenY = vertex[0] * Math.sin(time) * Math.cos(time) + vertex[1] * Math.cos(time) + vertex[2] * Math.sin(time) * Math.sin(time) + 200;
    }

    for(var i  = 0; i < face.length; i++){
        var vertex1 = vertices[face[i][0]];
        var vertex2 = vertices[face[i][1]];
        var vertex3 = vertices[face[i][2]];

        context.moveTo(vertex1.screenX, vertex1.screenY);
        context.lineTo(vertex2.screenX, vertex2.screenY);
        context.lineTo(vertex3.screenX, vertex3.screenY);
        context.lineTo(vertex1.screenX, vertex1.screenY);
    }

    context.stroke();
    time += 0.01;
};

今回のプログラムはアニメーションを行っていますがとりあえず静止画で一部分を撮ると以下のようになります。

tri3.png

では、今回のエントリの最後のプログラムを載せます。少し長くなりましたが、この変更によってだいぶ3Dっぽく見えるようになります。まず、今までのプログラムは線しか描画していませんし、3Dっぽく見えても線の前後関係がわからずどちらが手前か分かりません。なので、今回は面を塗りつぶして手前をわかりやすくします。面を塗りつぶすのは奥に表示される面から塗りつぶさないと、手前の面に奥の面の色が見えてしまいわけのわからない形となり3Dっぽく見えなかったりします。なにより違和感MAXです。

ですので、今回はZソートという怪しいソートで一番奥にある面を最初に持ってくるという強硬手段にでます。Zソートと言っても計算したZで並び変えてるだけですが!ついでにfaceの各要素にこっそり面の色情報を混ぜてあります。気を付けてください

triangle.js
var context = null;

var vertices = [
    [ -100, -100, -100],
    [ -100,  100,  100],
    [  100, -100,  100],
    [  100,  100, -100],
];

var faces = [
    [0, 3, 2, "#FF0000"],
    [0, 2, 1, "#00FF00"],
    [0, 1, 3, "#0000FF"],
    [1, 2, 3, "#00FFFF"]
];

var time = 0;

window.addEventListener("load", function(){
    var canvas = document.getElementById("ese");
    context = canvas.getContext('2d');

    setInterval(draw, 33);
}, false);

var draw = function(){
    context.clearRect(0, 0, 400, 400)

    updateVertices(vertices, time);
    zsort(faces);

    for(var i  = 0; i < faces.length; i++){
        drawTriangle(context, vertices, faces[i]);
    }

    time += 0.01;
};

var updateVertices = function(vertices, time){
    for(var i = 0; i < vertices.length; i++){
        var vertex = vertices[i];

        vertex.screenX =  vertex[0] * Math.cos(time) * Math.cos(time) - vertex[1] * Math.sin(time) + vertex[2] * Math.cos(time) * Math.sin(time) + 200;
        vertex.screenY =  vertex[0] * Math.sin(time) * Math.cos(time) + vertex[1] * Math.cos(time) + vertex[2] * Math.sin(time) * Math.sin(time) + 200;
        vertex.screenZ = -vertex[0] * Math.sin(time) + vertex[2] * Math.cos(time);
    }
};

var zsort = function(faces){
    faces.sort(function(a, b){
        var z1 = (vertices[a[0]].screenZ + vertices[a[1]].screenZ + vertices[a[2]].screenZ) / 3.0;
        var z2 = (vertices[b[0]].screenZ + vertices[b[1]].screenZ + vertices[b[2]].screenZ) / 3.0;

        return z1 - z2;
    });
};

var drawTriangle = function(context, vertices, face){
    var vertex1 = vertices[face[0]];
    var vertex2 = vertices[face[1]];
    var vertex3 = vertices[face[2]];

    context.fillStyle = face[3];
    context.beginPath();

    context.moveTo(vertex1.screenX, vertex1.screenY);
    context.lineTo(vertex2.screenX, vertex2.screenY);
    context.lineTo(vertex3.screenX, vertex3.screenY);
    context.lineTo(vertex1.screenX, vertex1.screenY);

    context.closePath();
    context.fill();
    context.stroke();
};

上記のプログラムを実行すると、何か3Dっぽさが増すとい思います。以下の画像のようになりますが、実際には動いていて3Dっぽく見えると思います。

tri4.png

いい加減でわかりづらい似非3Dエントリですが、ソースコードはあっさりしてると思うので、ここまで読んだ君ならきっとわかる!...はずです

d-kami
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away