Tech things and such
Article / Note
2017/01/11

Dessin en perspectives sur canvas HTML

Calcul de projection en perspective sur canvas 2D avec une caméra virtuelle. Génération de terrain 3D à partir d'une "depth map" obtenue par applications successives de bruits à différentes fréquences spatiales.

rotation auto
mode avion
Vitesse

cam x =
cam y =
cam z =
cam rotation x =
cam rotation y =
cam rotation z =
cam fov =



Voici le constructeur de caméra virtuelle. Les angles thetax, thetay et thetaz représentent l'orientation de la caméra en coordonnées globales. L'objet est pourvu d'une méthode de précalcul des sinus et cosinus de ces angles car ils seront beaucoup utilisés dans l'algorithme de projection.

function Camera(x,y,z,thetax, thetay, thetaz) {
    this.x = x;
    this.y = y;
    this.z = z;

    this.thetax = thetax;
    this.thetay = thetay;
    this.thetaz = thetaz;
    this.eyex = 0;
    this.eyey = 0;
    this.eyez = 1;
    this.cx = 0;   // storage of cos and sin of angles
    this.cy = 0;
    this.cz = 0;
    this.sx = 0;
    this.sy = 0;
    this.sz = 0;

    this.computeTrigo = function() {   // precalculate cos and sin
        this.cx = Math.cos(this.thetax);
        this.cy = Math.cos(this.thetay);
        this.cz = Math.cos(this.thetaz);
        this.sx = Math.sin(this.thetax);
        this.sy = Math.sin(this.thetay);
        this.sz = Math.sin(this.thetaz);
    };
}

Ci-dessous la fonction de projection à proprement dit. Chaque point 3D de chaque objet sera transformé par cette fonction en un point 2D dans les coordonnées du canvas (d'où le facteur d'échelle appliqué sur les coordonnées x et y retournées.

function projection(pt) {
    var x = pt.x - cam.x;
    var y = pt.y - cam.y;
    var z = pt.z - cam.z;

    var cx = cam.cx;
    var cy = cam.cy;
    var cz = cam.cz;
    var sx = cam.sx;
    var sy = cam.sy;
    var sz = cam.sz;

    var dx = cy*(sz*y + cz*x) - sy*z;
    var dy = sx*(cy*z + sy*(sz*y + cz*x)) + cx*(cz*y - sz*x);
    var dz = cx*(cy*z + sy*(sz*y + cz*x)) - sx*(cz*y - sz*x);

    var bx = cam.eyez*dx/dz - cam.eyex;
    var by = cam.eyez*dy/dz - cam.eyey;

    return {x:bx*W/2 + W/2, y:by*W/2 + W/2};
}

Nous disposons d'une fonction de rotation qui fait tourner le point passé en argument selon un angle et un axe caractérisé par un vecteur 3D. Lorsque la checkbox est activée, tous les points de tous les objets sont transformés par cette fonction.

function rotate(pt, angle, ux, uy, uz) {
    var c = Math.cos(angle);
    var s = Math.sin(angle);

    var newx = pt.x*(ux*ux*(1-c)+c)       + pt.y*(ux*uy*(1-c)-uz*s)     + pt.z*(ux*uz*(1-c)+uy*s);
    var newy = pt.x*(ux*uy*(1-c)+uz*s)    + pt.y*(uy*uy*(1-c)+c)        + pt.z*(uy*uz*(1-c)-ux*s);
    var newz = pt.x*(ux*uz*(1-c)-uy*s)    + pt.y*(uy*uz*(1-c)+ux*s)     + pt.z*(uz*uz*(1-c)+c);

    pt.x = newx;
    pt.y = newy;
    pt.z = newz;
}

Enfin, voici la fonction qui génère le terrain "fractal".

Un premier canvas offscreen est rempli de pixels aléatoirement blancs ou noirs (bruit). Sur l'intégralité d'un deuxième canvas offscreen, on colle une très petite portion du bruit (agrandi), puis une portion plus grande légèrement transparente, puis une portion encore plus grande et plus transparente, etc. On obtient ainsi une sorte de bruit dont les l'amplitude dépend de la fréquence spatiale (basse fréquence spatiale = grande amplitude et inversement).

Enfin, une grille de point 3D est créée avec les valeurs des pixels prises comme hauteurs.

function perlin(N, freq) {
    var noiseCanvas = document.createElement('canvas');
    noiseCanvas.width = N;
    noiseCanvas.height = N;
    var noiseCtx = noiseCanvas.getContext("2d");
    var noiseImageData = noiseCtx.getImageData(0, 0, N, N);
    var noisePixels = noiseImageData.data;
    for(var i = 0; i < 4*N*N ; i=i+4) {
        var bw = Math.floor(255 * (Math.random() > 0.5));
        noisePixels[i] = bw;
        noisePixels[i+1] = bw;
        noisePixels[i+2] = bw;
        noisePixels[i+3] = 255;
    }
    noiseCtx.putImageData(noiseImageData, 0, 0);

    var perlinCanvas = document.createElement('canvas');
    perlinCanvas.width = N;
    perlinCanvas.height = N;
    var perlinCtx = perlinCanvas.getContext("2d");
    var F = 2;
    perlinCtx.save();
    for (var size = 2; size <= N; size *= 2) {
        var x = (Math.random() * (N - size)) | 0,
            y = (Math.random() * (N - size)) | 0;
        perlinCtx.globalAlpha = F / size;
        perlinCtx.drawImage(noiseCanvas, x, y, size, size, 0, 0, N, N);
    }
    perlinCtx.restore();

    var perlinImageData = perlinCtx.getImageData(0, 0, N, N);
    var perlinPixels = perlinImageData.data;

    var points = [];
    var lines = [];

    for(var z = 0 ; z < N ; z++) {
        var line = [];
        for(var x = 0; x < N ; x++) {
            var i = (z*N+x)*4;
            var y = (perlinPixels[i] / 255 - 0.5)/5;
            var pt = new Point(x/N - 0.5, y, z/N - 0.5);
            points.push(pt);
            line.push(x+N*z);
        }
        lines.push(line);
    }
    for(var x = 0 ; x < N ; x++) {
        var line = [];
        for(var z = 0; z < N ; z++) {
            var i = (z*N+x)*4;
            line.push(x+N*z);
        }
        lines.push(line);
    }
    return {points:points, lines:lines};
}
>> Réagir à cet article