Используя средства html-canvas, выводится полигональная модель тора из трапеций (рис. 1).
Рис. 1. Тор из трапеций
При генерации тора его центр находится в начале системы координат. Ось тора совпадает с осью Y.
При отображении модели выполняется ее сдвиг в центр окна вывода:
cw = canvas.width;
ch = canvas.height;
shiftX = 0.5 * cw; // Смещение по X
shiftY = 0.5 * ch; // Смещение по Y
ctx.translate(shiftX, shiftY);
Вдобавок тор поворачивается относительно осей X и Y.
rotateX3D(45, obj.verts)
rotateY3D(-15, obj.verts)
Функции rotateX3D и rotateY3D см. ниже в коде файла torusMesh.js.
Система координат, в которой задаются вершины тора, является левосторонней (рис. 2).
Рис. 2. Левосторонняя система координат
В торе, показанном на рис. 3, грани при ее создании задается красный цвет, если средняя Z-координата его вершин больше нуля, и зеленый - в противном случае.
Рис. 3. Тор в левосторонней системе координат
Код задания цвета грани:
fc = obj.faces[i]; // Грань тора
// Средняя Z-координата вершин грани
zAvg = (obj.verts[fc[0]][2] + obj.verts[fc[1]][2] + obj.verts[fc[2]][2] + obj.verts[fc[3]][2]) / 4.0;
if (zAvg > 0)
obj.colors[i] = 'rgb(255, 0, 0)';
else
obj.colors[i] = 'rgb(0, 255, 0)';
Порядок вывода граней определяется Z-координатами их вершин.
В приводимой ниже программе для каждой грани вычисляется средняя значение Z-координат вершин грани. Затем грани сортируются по убыванию этой величиины.
Таким образом, чем больше усредненная Z-координата вершин грани, тем раньше будет выполнен ее вывод.
Массив отсортированных граней формиируется следующим кодом:
Первоначально выполняется поворот тора на 45° относительно оси X и на -15° относительно Y.
Далее с интервалом в 100 миллисекунд тор поворачивается на rotAng градусов относительно оси X.
После полного поворота суммарный угол поворота обнуляется, а координатам вершин тора возвращаются начальные значения.
Они хранятся свойством points созданного объекта.
rotateX3D(rotAng, obj.verts);
rotAngSum += rotAng;
if (rotAngSum > 360) {
for (i = 0; i < obj.verts.length; i++) {
obj.verts[i][0] = obj.points[i][0];
obj.verts[i][1] = obj.points[i][1];
obj.verts[i][2] = obj.points[i][2];
}
rotateX3D(rotXStart, obj.verts);
rotateY3D(rotYStart, obj.verts);
rotAngSum = 0;
}
Выводится следующим кодом:
<html>
<head>
<script type = "text/javascript" src="torusMesh.js"></script>
<script type = "text/javascript"> obj = new createTorus(30, 8, 120, 16); </script>
<script type = "text/javascript" src = "torusMain.js"></script>
</head>
<body>
<canvas id = "scene" height = "400" width = "600"></canvas>
</body>
</html>
// Файл torusMesh.js
pi = Math.PI;
// Цвета граней
function facesColors(obj) {
obj.colors = new Array();
for (i = 0; i < obj.faces.length; i++) {
r = Math.round(Math.random() * 255);
g = Math.round(Math.random() * 255);
b = Math.round(Math.random() * 255);
obj.colors[i] = 'rgb(' + r + ',' + g + ',' + b + ')';
showZDir = true;
if (showZDir) {
fc = obj.faces[i];
// Средняя Z-координата вершин грани
zAvg = (obj.points[fc[0]][2] + obj.points[fc[1]][2] + obj.points[fc[2]][2] + obj.points[fc[3]][2]) / 4.0;
if (zAvg > 0)
obj.colors[i] = 'rgb(255, 0, 0)';
else
obj.colors[i] = 'rgb(0, 255, 0)';
}
}
}
function createTorus(r1, n1, r2, n2) {
// Центр тора в начале координат - точка (0, 0, 0)
// Ось тора совпадает с осью Y
// r1 - радиус сечения тора
// n1 - число разбиений сечения тора
// r2 - радиус тора
// n2 - число сечений тора
theta = 2 * pi / n1;
theta2 = 2 * pi / n2;
torusVerts = [];
for (j = 0; j < n2; j++) {
tj = theta2 * j;
cx = Math.cos(tj);
sn = Math.sin(tj);
c = [r2 * cx, 0, r2 * sn];
for (i = 0; i < n1; i++) {
ti = theta * i;
dx = r1 * Math.cos(ti);
dy = r1 * Math.sin(ti);
torusVerts.push([c[0] + dx * cx, c[1] + dy, c[2] + dx * sn]);
}
}
faces = [];
for (j = 0; j < n2 - 1; j++) {
s = j * n1;
for (i = 0; i < n1 - 1; i++) {
si = s + i;
faces.push([si, si + 1, si + n1 + 1, si + n1]);
}
faces.push([s, s + n1 - 1, s + 2 * n1 - 1, s + n1]);
}
s = (n2 - 1) * n1;
for (i = 0; i < n1 - 1; i++) {
si = s + i;
faces.push([si, si + 1, i + 1, i]);
}
fc = faces[faces.length - 1];
faces.push([fc[1], fc[2], 0, s]);
this.points = torusVerts;
this.verts = torusVerts;
this.faces = faces;
facesColors(this);
}
function rotateY3D(theta, verts) {
ang = pi * theta / 180;
ct = Math.cos(ang);
st = Math.sin(ang);
for (i = 0; i < verts.length; i++) {
x = verts[i][0];
y = verts[i][1];
z = verts[i][2];
verts[i] = [ct * x + st * z, y, -st * x + ct * z];
}
}
function rotateX3D(theta, verts) {
ang = pi * theta / 180;
ct = Math.cos(ang);
st = Math.sin(ang);
for (i = 0; i < verts.length; i++) {
x = verts[i][0];
y = verts[i][1];
z = verts[i][2];
verts[i] = [x, ct * y - st * z, st * y + ct * z];
}
}
// Файл torusMain.js
var ctx, cw, ch, shiftX, shiftY;
var rotAng = 1, rotAngSum = 0;
var rotXStart = 45;
var rotYStart = -15;
// Инициализация
function sceneInit() {
// Подготовка сцены
canvas = document.getElementById('scene');
ctx = canvas.getContext('2d');
ctx.strokeStyle = 'rgb(0,0,0)'; // Цвет линии (черный)
ctx.lineWidth = 0.5; // Толщина линии
// Начльное положение тора
rotateX3D(rotXStart, obj.verts);
rotateY3D(rotYStart, obj.verts);
cw = canvas.width;
ch = canvas.height;
shiftX = 0.5 * cw; // Смещение по X
shiftY = 0.5 * ch; // Смещение по Y
ctx.translate(shiftX, shiftY);
// 100 миллисекунд - частота обновлений сцены
setInterval(drawScene, 100);
}
// Вывод сцены
function drawScene() {
rotateX3D(rotAng, obj.verts);
rotAngSum += rotAng;
if (rotAngSum > 360) {
for (i = 0; i < obj.verts.length; i++) {
obj.verts[i][0] = obj.points[i][0];
obj.verts[i][1] = obj.points[i][1];
obj.verts[i][2] = obj.points[i][2];
}
rotateX3D(rotXStart, obj.verts);
rotateY3D(rotYStart, obj.verts);
rotAngSum = 0;
}
// Очистка области вывода
ctx.translate(-shiftX, -shiftY);
ctx.clearRect(0, 0, cw, ch); // Очистка области вывода
ctx.translate(shiftX, shiftY);
// arrSortedFaces - массив граней, отсортированных по глубине
// Элемент массива - это структура с двумя полями: ind и z_avg
// ind - это номер грани в массиве obj.faces[]
// z_avg - это средняя Z-координата вершин грани
var arrSortedFaces = new Array();
for(i = 0; i < obj.faces.length; i++) {
fc = obj.faces[i];
zAvg = (obj.verts[fc[0]][2] + obj.verts[fc[1]][2] + obj.verts[fc[2]][2] + obj.verts[fc[3]][2]) / 4.0;
arrSortedFaces[i] = {"ind":i, "z_avg":zAvg};
}
// Сортировка граней по глубине в левосторонней системе координат
arrSortedFaces.sort(function (a, b) { return b.z_avg - a.z_avg });
// Вывод объекта
for (i = 0; i < obj.faces.length; i++) {
// Начало вывода
ctx.beginPath();
// Выводимая грань
fc = obj.faces[arrSortedFaces[i].ind];
// Вывод грани (трапеции)
for (j = 0; j < fc.length; j++) {
vert = fc[j]; // Вершина грани
ctx.lineTo(obj.verts[vert][0], obj.verts[vert][1]);
}
ctx.closePath();
ctx.fillStyle = obj.colors[i];
ctx.stroke(); // Вывод линий
ctx.fill(); // Заливка граней
}
}
window.onload = sceneInit;