Статьи

Треугольная сетка для 3D-объектов в HTML5

 Сегодняшний урок — это мост между двухмерной графикой в ​​формате html5 и действительно трехмерной (с использованием WebGL). Сегодня я покажу, как рисовать трехмерные объекты, используя полигональную сетку. Многоугольная сетка или неструктурированная сетка — это набор вершин, ребер и граней, который определяет форму многогранного объекта в трехмерной компьютерной графике и твердотельном моделировании. Грани обычно состоят из треугольников, четырехугольников или других простых выпуклых многоугольников, поскольку это упрощает рендеринг, но также может состоять из более общих вогнутых многоугольников или многоугольников с отверстиями.

Чтобы понять, о чем идет речь, рекомендую ознакомиться с базой, описанной в википедии .
Для демонстрации мы подготовили простые трехмерные объекты — куб и многомерную сферу (с переменным числом граней).

Live Demo
скачать в упаковке

Если вы готовы — начнем!


Шаг 1. HTML

Как обычно (для всех демонстраций на основе canvas) у нас есть очень простая html-разметка (с одним объектом canvas внутри):

<html lang="en" >
    <head>
        <meta charset="utf-8" />
        <meta name="author" content="Script Tutorials" />
        <title>Triangle mesh for 3D objects in HTML5 | Script Tutorials</title>

        <!-- add styles -->
        <link href="css/main.css" rel="stylesheet" type="text/css" />

        <!-- add script -->
        <script src="js/meshes.js"></script>
        <script src="js/transform.js"></script>
        <script>
            //var obj = new cube();
            //var obj = new sphere(6);
            var obj = new sphere(16);
        </script>
        <script src="js/main.js"></script>
    </head>
    <body>
        <div class="container">
            <canvas id="scene" height="500" width="700" tabindex="1"></canvas>
            <div class="hint">Please use Up / Down keys to change opacity</div>
        </div>
    </body>
</html>

Я извлек инициализацию сгенерированного объекта здесь, смотрите:

<script>
    //var obj = new cube();
    //var obj = new sphere(6);
    var obj = new sphere(16);
</script>

Это означает, что если нам нужно отобразить куб — вам нужно раскомментировать первую строку, если вы хотите отобразить сферу с 6 гранями — выберите второй вариант.

Шаг 2. JS

Существует три файла JS (main.js, meshes.js и transform.js), мы опубликуем два из них, третий (transform.js) содержит только математические функции (для поворота, масштабирования, перевода и проецирования объектов). , Это будет доступно в нашем пакете. Итак, давайте рассмотрим код первого JavaScript:

JS / meshes.js

// get random color
function getRandomColor() {
    var letters = '0123456789ABCDEF'.split('');
    var color = '#';
    for (var i = 0; i < 6; i++ ) {
        color += letters[Math.round(Math.random() * 15)];
    }
    return color;
}

// prepare object
function prepareObject(o) {
    o.colors = new Array();

    // prepare normals
    o.normals = new Array();
    for (var i = 0; i < o.faces.length; i++) {
        o.normals[i] = [0, 0, 0];

        o.colors[i] = getRandomColor();
    }

    // prepare centers: calculate max positions
    o.center = [0, 0, 0];
    for (var i = 0; i < o.points.length; i++) {
        o.center[0] += o.points[i][0];
        o.center[1] += o.points[i][1];
        o.center[2] += o.points[i][2];
    }

    // prepare distances
    o.distances = new Array();
    for (var i = 1; i < o.points.length; i++) {
        o.distances[i] = 0;
    }

    // calculate average center positions
    o.points_number = o.points.length;
    o.center[0] = o.center[0] / (o.points_number - 1);
    o.center[1] = o.center[1] / (o.points_number - 1);
    o.center[2] = o.center[2] / (o.points_number - 1);

    o.faces_number = o.faces.length;
    o.axis_x = [1, 0, 0];
    o.axis_y = [0, 1, 0];
    o.axis_z = [0, 0, 1];
}

// Cube object
function cube() {

    // prepare points and faces for cube
    this.points=[
        [0,0,0],
        [100,0,0],
        [100,100,0],
        [0,100,0],
        [0,0,100],
        [100,0,100],
        [100,100,100],
        [0,100,100],
        [50,50,100],
        [50,50,0],
    ];

    this.faces=[
        [0,4,5],
        [0,5,1],
        [1,5,6],
        [1,6,2],
        [2,6,7],
        [2,7,3],
        [3,7,4],
        [3,4,0],
        [8,5,4],
        [8,6,5],
        [8,7,6],
        [8,4,7],
        [9,5,4],
        [9,6,5],
        [9,7,6],
        [9,4,7],
    ];

    prepareObject(this);
}

// Sphere object
function sphere(n) {
    var delta_angle = 2 * Math.PI / n;

    // prepare vertices (points) of sphere
    var vertices = [];
    for (var j = 0; j < n / 2 - 1; j++) {
        for (var i = 0; i < n; i++) {
            vertices[j * n + i] = [];
            vertices[j * n + i][0] = 100 * Math.sin((j + 1) * delta_angle) * Math.cos(i * delta_angle);
            vertices[j * n + i][1] = 100 * Math.cos((j + 1) * delta_angle);
            vertices[j * n + i][2] = 100 * Math.sin((j + 1) * delta_angle) * Math.sin(i * delta_angle);
        }
    }
    vertices[(n / 2 - 1) * n] = [];
    vertices[(n / 2 - 1) * n + 1] = [];

    vertices[(n / 2 - 1) * n][0] = 0;
    vertices[(n / 2 - 1) * n][1] =  100;
    vertices[(n / 2 - 1) * n][2] =  0;

    vertices[(n / 2 - 1) * n + 1][0] = 0;
    vertices[(n / 2 - 1) * n + 1][1] = -100;
    vertices[(n / 2 - 1) * n + 1][2] = 0;

    this.points = vertices;

    // prepare faces
    var faces = [];
    for (var j = 0; j < n / 2 - 2; j++) {
        for (var i = 0; i < n - 1; i++) {
            faces[j * 2 * n + i] = [];
            faces[j * 2 * n + i + n] = [];

            faces[j * 2 * n + i][0] = j * n + i;
            faces[j * 2 * n + i][1] = j * n + i + 1;
            faces[j * 2 * n + i][2] = (j + 1) * n + i + 1;
            faces[j * 2 * n + i + n][0] = j * n + i;
            faces[j * 2 * n + i + n][1] = (j + 1) * n + i + 1;
            faces[j * 2 * n + i + n][2] = (j + 1) * n + i;
        }

        faces[j * 2 * n + n - 1] = [];
        faces[2 * n * (j + 1) - 1] = [];

        faces[j * 2 * n + n - 1  ][0] = (j + 1) * n - 1;
        faces[j * 2 * n + n - 1  ][1] = (j + 1) * n;
        faces[j * 2 * n + n - 1  ][2] = j * n;
        faces[2 * n * (j + 1) - 1][0] = (j + 1) * n - 1;
        faces[2 * n * (j + 1) - 1][1] = j * n + n;
        faces[2 * n * (j + 1) - 1][2] = (j + 2) * n - 1;
    }
    for (var i = 0; i < n - 1; i++) {
        faces[n * (n - 4) + i] = [];
        faces[n * (n - 3) + i] = [];

        faces[n * (n - 4) + i][0] = (n / 2 - 1) * n;
        faces[n * (n - 4) + i][1] = i;
        faces[n * (n - 4) + i][2] = i + 1;
        faces[n * (n - 3) + i][0] = (n / 2 - 1) * n + 1;
        faces[n * (n - 3) + i][1] = (n / 2 - 2) * n + i + 1;
        faces[n * (n - 3) + i][2] = (n / 2 - 2) * n + i;
    }

    faces[n * (n - 3) - 1] = [];
    faces[n * (n - 2) - 1] = [];

    faces[n * (n - 3) - 1][0] = (n / 2 - 1) * n;
    faces[n * (n - 3) - 1][1] = n - 1;
    faces[n * (n - 3) - 1][2] = 0;
    faces[n * (n - 2) - 1][0] = (n / 2 - 1) * n + 1;
    faces[n * (n - 2) - 1][1] = (n / 2 - 2) * n;
    faces[n * (n - 2) - 1][2] = (n / 2 - 2) * n + n - 1;

    this.faces=faces;

    prepareObject(this);
}

В самом начале мы должны подготовить все точки и грани нашего объекта. Есть 2 функции: куб (который генерирует начальные массивы для простого объекта куба) и сфера (для генерации сферы). Как видите — вычислить все точки и грани для многомерной сферы гораздо сложнее. Как только мы получим все эти точки и поверхности, мы должны вычислить другие параметры (например, нормали, расстояния, абсолютный центр и три оси).

JS / main.js

// inner variables
var canvas, ctx;
var vAlpha = 0.5;
var vShiftX = vShiftY = 0;
var distance = -700;
var vMouseSens = 0.05;
var iHalfX, iHalfY;

// initialization
function sceneInit() {
    // prepare canvas and context objects
    canvas = document.getElementById('scene');
    ctx = canvas.getContext('2d');

    iHalfX = canvas.width / 2;
    iHalfY = canvas.height / 2;

    // initial scale and translate
    scaleObj([3, 3, 3], obj);
    translateObj([-obj.center[0], -obj.center[1], -obj.center[2]],obj);
    translateObj([0, 0, -1000], obj);

    // attach event handlers
    document.onkeydown = handleKeydown;
    canvas.onmousemove = handleMousemove;

    // main scene loop
    setInterval(drawScene, 25);
}

// onKeyDown event handler
function handleKeydown(e) {
    kCode = ((e.which) || (e.keyCode));
    switch (kCode) {
        case 38: vAlpha = (vAlpha <= 0.9) ? (vAlpha + 0.1) : vAlpha; break; // Up key
        case 40: vAlpha = (vAlpha >= 0.2) ? (vAlpha - 0.1) : vAlpha; break; // Down key
    }
}

// onMouseMove event handler
function handleMousemove(e) {
    var x = e.pageX - canvas.offsetLeft;
    var y = e.pageY - canvas.offsetTop;

    if ((x > 0) && (x < canvas.width) && (y > 0) && (y < canvas.height)) {
        vShiftY = vMouseSens * (x - iHalfX) / iHalfX;
        vShiftX = vMouseSens * (y - iHalfY) / iHalfY;
    }
}

// draw main scene function
function drawScene() {
    // clear canvas
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    // set fill color, stroke color, line width and global alpha
    ctx.strokeStyle = 'rgb(0,0,0)';
    ctx.lineWidth = 0.5;
    ctx.globalAlpha= vAlpha;

    // vertical and horizontal rotate
    var vP1x = getRotationPar([0, 0, -1000], [1, 0, 0], vShiftX);
    var vP2x = getRotationPar([0, 0, 0], [1, 0, 0], vShiftX);
    var vP1y = getRotationPar([0, 0, -1000], [0, 1, 0], vShiftY);
    var vP2y = getRotationPar([0, 0, 0], [0, 1, 0], vShiftY);
    rotateObj(vP1x, vP2x, obj);
    rotateObj(vP1y, vP2y, obj);

    // recalculate distances
    for (var i = 0; i < obj.points_number; i++) {
        obj.distances[i] = Math.pow(obj.points[i][0],2) + Math.pow(obj.points[i][1],2) + Math.pow(obj.points[i][2], 2);
    }

    // prepare array with face triangles (with calculation of max distance for every face)
    var iCnt = 0;
    var aFaceTriangles = new Array();
    for (var i = 0; i < obj.faces_number; i++) {
        var max = obj.distances[obj.faces[i][0]];
        for (var f = 1; f < obj.faces[i].length; f++) {
            if (obj.distances[obj.faces[i][f]] > max)
                max = obj.distances[obj.faces[i][f]];
        }
        aFaceTriangles[iCnt++] = {faceVertex:obj.faces[i], faceColor:obj.colors[i], distance:max};
    }
    aFaceTriangles.sort(sortByDistance);

    // prepare array with projected points
    var aPrjPoints = new Array();
    for (var i = 0; i < obj.points.length; i++) {
        aPrjPoints[i] = project(distance, obj.points[i], iHalfX, iHalfY);
    }

    // draw an object (surfaces)
    for (var i = 0; i < iCnt; i++) {

        ctx.fillStyle = aFaceTriangles[i].faceColor;

        // begin path
        ctx.beginPath();

        // face vertex index
        var iFaceVertex = aFaceTriangles[i].faceVertex;

        // move to initial position
        ctx.moveTo(aPrjPoints[iFaceVertex[0]][0], aPrjPoints[iFaceVertex[0]][1]);

        // and draw three lines (to build a triangle)
        for (var z = 1; z < aFaceTriangles[i].faceVertex.length; z++) {
            ctx.lineTo(aPrjPoints[iFaceVertex[z]][0], aPrjPoints[iFaceVertex[z]][1]);
        }

        // close path, strole and fill a triangle
        ctx.closePath();
        ctx.stroke();
        ctx.fill();
    }
}

// sort function
function sortByDistance(x, y) {
    return (y.distance - x.distance);
}

// initialization
if (window.attachEvent) {
    window.attachEvent('onload', sceneInit);
} else {
    if (window.onload) {
        var curronload = window.onload;
        var newonload = function() {
            curronload();
            sceneInit();
        };
        window.onload = newonload;
    } else {
        window.onload = sceneInit;
    }
}

Что ж, пришло время вернуться к функциональности нашей главной страницы. Как только страница загружена, мы выполняем основную инициализацию (функция sceneInit). Мы создаем объекты canvas и context, затем выполняем начальное масштабирование и переводим наш объект, который мы создали в самом начале (куб или сфера). Затем мы присоединяем обработчики событий onkeydown и onmousemove и устанавливаем таймер для рисования нашей главной сцены (функция drawScene). Не забывайте, что мы можем изменить параметр globalAlpha, нажимая кнопки вверх / вниз.