3Dグラフィックの基礎


3Dグラフィックの基礎を JavaScript で実践

波紋のシミュレーションをワイヤフレームで表示します。
視点変換、透視変換の基礎が学べます。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>3D</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>

var SCREEN_X = 1280;
var SCREEN_Y = 720;

var CX = SCREEN_X/2;    // 中央の位置を記録
var CY = SCREEN_Y/2;    // 中央の位置を記録
var L = 0.55;   // スクリーンと目までの距離[m]
var STOR = 1280/0.34;   // スクリーンとリアルの比率(34cmで1280px)

var POINT_X_NUM = 64;
var POINT_Y_NUM = 64;

var eye = {};
eye.p = {x: 5, y: 10, z: -18};  // 目の位置
eye.v = {x: -eye.p.x, y: -eye.p.y, z: -eye.p.z};    // 視線方向
eye.l = {x: -1, y: -1, z: 0};   // 光の方向

var canvas;
var ctx;
var p;  // 点の配列(x, y, z, vx, vy, vz, sx, sy)
var poly;   // ポリゴンの配列
var timer;  // インターバルのハンドル
var hamon;

$(document).ready(function(){
    canvas = $('#canvas')[0];
    canvas.width = SCREEN_X;
    canvas.height = SCREEN_Y;
    ctx = canvas.getContext('2d');

    p = [];
    poly = [];

    for(var i = 0; i < POINT_Y_NUM; i++){
        for(var j = 0; j < POINT_X_NUM; j++){
            var bp = i*POINT_Y_NUM + j;
            p[bp] = {
                x: j*10/POINT_X_NUM - 5,
                y: 0.0,
                z: 5 - i*10/POINT_Y_NUM,
                vx: 0.0,
                vy: 0.0,
                vz: 0.0,
                sx: 0,
                sy: 0
            };
        }
    }

    var n = 0;
    for(var i = 0; i < POINT_Y_NUM - 1; i++){
        for(var j = 0; j < POINT_X_NUM - 1; j++){
            var bp = i*POINT_Y_NUM + j;
            poly[n] = [bp, bp + POINT_Y_NUM + 1, bp + 1];
            n++;
            poly[n] = [bp, bp + POINT_Y_NUM    , bp + POINT_Y_NUM + 1];
            n++;
        }
    }


    var NUM = 16;
    hamon = [];
    for(var i = 0; i < NUM; i++){
        hamon[i] = {
            x: 5*Math.cos(2*Math.PI/NUM*i),
            z: 5*Math.sin(2*Math.PI/NUM*i),
            r: 0.05,
            i: 0
        };
    }

    calcViewG();
    var t = 0;
    timer = setInterval(function(){
        t += 0.1;
        if(t >= 2*Math.PI){
            t -= 2*Math.PI;
        }

        for(var i = 0; i < POINT_Y_NUM; i++){
            for(var j = 0; j < POINT_X_NUM; j++){
                bp = i*POINT_Y_NUM + j;
                p[bp].y = 0;
                for(var k in hamon){
                    var h = hamon[k];
                    var r = Math.sqrt((h.x - p[bp].x)*(h.x - p[bp].x) + (h.z - p[bp].z)*(h.z - p[bp].z));
                    p[bp].y += h.r*Math.sin(t - 5*r + h.i);
                }
            }
        }

        reDraw();
    }, 1);
});




function reDraw(){
    // 視点変換
    var g = eye.g;
    for(var i in p){
        p[i].vx = p[i].x*g[0][0] + p[i].y*g[0][1] + p[i].z*g[0][2] + g[0][3];
        p[i].vy = p[i].x*g[1][0] + p[i].y*g[1][1] + p[i].z*g[1][2] + g[1][3];
        p[i].vz = p[i].x*g[2][0] + p[i].y*g[2][1] + p[i].z*g[2][2] + g[2][3];
    }

    // 透視変換
    for(var i in p){
        p[i].sx = parseInt(CX + STOR*L*p[i].vx/p[i].vz);
        p[i].sy = parseInt(CY - STOR*L*p[i].vy/p[i].vz);
    }

    // キャンバス初期化
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 描画
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = "black";
    ctx.lineWidth = 1;

    // 横線
    for(var i = 0; i < POINT_Y_NUM; i++){
        for(var j = 0; j < POINT_X_NUM; j++){
            bp = i*POINT_Y_NUM + j;
            if(j == 0){
                ctx.beginPath();
                ctx.moveTo(p[bp].sx, p[bp].sy);
            }else{
                ctx.lineTo(p[bp].sx, p[bp].sy);
            }
        }
        ctx.stroke();
    }

    // 縦線
    for(var j = 0; j < POINT_X_NUM; j++){
        for(var i = 0; i < POINT_Y_NUM; i++){
            bp = i*POINT_Y_NUM + j;
            if(i == 0){
                ctx.beginPath();
                ctx.moveTo(p[bp].sx, p[bp].sy);
            }else{
                ctx.lineTo(p[bp].sx, p[bp].sy);
            }
        }
        ctx.stroke();
    }
}


// 視線左回転
function turnLeft(){
    var cosT = Math.cos(-0.1);
    var sinT = Math.sin(-0.1);
    var x = eye.p.x;
    var y = eye.p.z;
    eye.p.x = x*cosT - y*sinT;
    eye.p.z = y*cosT + x*sinT;
    eye.v.x = -eye.p.x;
    eye.v.z = -eye.p.z;
    calcViewG();
}

// 視線右回転
function turnRight(){
    var cosT = Math.cos(0.1);
    var sinT = Math.sin(0.1);
    var x = eye.p.x;
    var y = eye.p.z;
    eye.p.x = x*cosT - y*sinT;
    eye.p.z = y*cosT + x*sinT;
    eye.v.x = -eye.p.x;
    eye.v.z = -eye.p.z;
    calcViewG();
}



/**
 * 行列計算
 */
function gyoretsu(a, b){
    var r = [];

    for(var y = 0; y < 4; y++){
        r[y] = [];
        for(var x = 0; x < 4; x++){
            r[y][x] = a[0][x]*b[y][0]
                    + a[1][x]*b[y][1]
                    + a[2][x]*b[y][2]
                    + a[3][x]*b[y][3];
        }
    }

    return r;
}


/**
 * 視点行列計算
 */
function calcViewG(){
    // 光源ベクトルを単位ベクトルに
    var r = Math.sqrt(eye.l.x*eye.l.x + eye.l.y*eye.l.y + eye.l.z*eye.l.z);
    eye.l.x = eye.l.x/r;
    eye.l.y = eye.l.y/r;
    eye.l.z = eye.l.z/r;

    // 平行移動
    var g = [
        [1, 0, 0, -eye.p.x],
        [0, 1, 0, -eye.p.y],
        [0, 0, 1, -eye.p.z],
        [0, 0, 0,        1]
    ];

    // y軸回転で視線をz軸方向に
    r = Math.sqrt(eye.v.x*eye.v.x + eye.v.z*eye.v.z);
    var cosT = eye.v.z/r;
    var sinT = eye.v.x/r;
    g = gyoretsu(g, [
        [cosT, 0, -sinT, 0],
        [   0, 1,     0, 0],
        [sinT, 0,  cosT, 0],
        [   0, 0,     0, 1]
    ]);

    // x軸回転で視線をz軸と平行に
    var ez = r;
    r = Math.sqrt(ez*ez + eye.v.y*eye.v.y);
    cosT = ez/r;
    sinT = -eye.v.y/r;
    eye.g = gyoretsu(g, [
        [   1,     0,    0, 0],
        [   0,  cosT, sinT, 0],
        [   0, -sinT, cosT, 0],
        [   0,     0,    0, 1]
    ]);
}


</script>
</head>
<body>
<div style="width: 1280px; margin: auto">
<h1 style="float:left">3D</h1>
<button id="left" style="float:left" onclick="turnLeft();">←</button>
<button id="right" style="float:left" onclick="turnRight();">→</button><br />
<button id="stop" style="float:left" onclick="clearInterval(timer);">停止</button>
<div style="clear;both"></div>

<canvas id="canvas" style="width:100%; height: 720px;border:solid 1px black"></canvas>
</div>
</body>
</html>