Сегодняшний урок — это мост между двухмерной графикой в формате 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, нажимая кнопки вверх / вниз.