12 KiB
title | description | sections | tags | scripts | styles | canonical_url | url_translated | title_translated | date | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Вращаем куб в пространстве | Рассматриваем разницу между параллельной и перспективной проекцией. Обе широко используются на практике для различных целей. В предыдущем примере мы вращали... |
|
|
|
|
/ru/2023/01/10/spinning-cube-in-space.html | /en/2023/01/11/spinning-cube-in-space.html | Spinning cube in space | 2023.01.10 |
Рассматриваем разницу между параллельной и перспективной проекцией. Обе широко используются на практике для различных целей. В предыдущем примере мы [вращали квадрат на плоскости]({{ '/ru/2023/01/05/spinning-square-on-plane.html' | relative_url }}) — переходим в трёхмерное пространство. Теперь, чтобы отобразить на плоскости экрана поворот трёхмерного объекта, нужно сначала создать трёхмерный объект, повернуть его на угол, срисовать с него проекцию и отобразить на экране уже проекцию.
Усложнённая модель, много кубиков: [Вращаем пространственный крест]({{ '/ru/2023/01/15/spinning-spatial-cross.html' | relative_url }}).
Ваш браузер не поддерживает Canvas
Ваш браузер не поддерживает Canvas
Параллельная проекция — центр проекции бесконечно удалён от плоскости экрана наблюдателя, размеры предметов выглядят одинаковыми.
Перспективная проекция — параллельные линии сходятся в центре перспективы, предметы выглядят уменьшающимися вдалеке.
Экспериментальная модель
Размер куба 200, размер холста 300, начало координат находится в верхнем левом углу. Центр фигуры
в середине холста. Ось X
направлена вправо, ось Y
направлена вниз, ось Z
направлена вдаль.
Выполняется поворот последовательно по всем трём осям: сначала по оси X
, затем по оси Y
и затем
по оси Z
. Настройками модели можно управлять, например можно отключать лишнее вращение по осям и
изменять положение центра проекции на экране наблюдателя.
Ваш браузер не поддерживает Canvas
Поворот точки в пространстве
Рассчитываем новые координаты точки по формулам матрицы поворота для трёхмерного пространства.
Поворачиваем точку t
относительно точки t0
— получаем точку t'
.
Поворот по оси X
.
{% include image_svg.html src="/img/column-vector3dx.svg" style="width: 242.619pt; height: 59.0768pt;" alt="&x'=x,&\&y'=y_0+(y-y_0)cos\varphi-(z-z_0)sin\varphi,&\&z'=z_0+(y-y_0)sin\varphi+(z-z_0)cos\varphi.&\" %}
Поворот по оси Y
.
{% include image_svg.html src="/img/column-vector3dy.svg" style="width: 246.251pt; height: 59.0768pt;" alt="&x'=x_0+(x-x_0)cos\varphi-(z-z_0)sin\varphi,&\&y'=y,&\&z'=z_0+(x-x_0)sin\varphi+(z-z_0)cos\varphi.&\" %}
Поворот по оси Z
.
{% include image_svg.html src="/img/column-vector3dz.svg" style="width: 246.793pt; height: 55.4753pt;" alt="&x'=x_0+(x-x_0)cos\varphi-(y-y_0)sin\varphi,&\&y'=y_0+(x-x_0)sin\varphi+(y-y_0)cos\varphi,&\&z'=z.&\" %}
Проекция точки
Экспериментальные формулы с возможностью смещения центра проекции d0
на экране наблюдателя tv
.
Отображаем точку пространства t
на плоскость экрана — получаем точку t'
.
Параллельная проекция.
{% include image_svg.html src="/img/oblique-projection.svg" style="width: 123.97pt; height: 37.2836pt;" alt="&x'=x,&\&y'=y+(y_v-z)/4.&\" %}
Перспективная проекция.
{% include image_svg.html src="/img/central-projection.svg" style="width: 231.924pt; height: 37.2836pt;" alt="&x'=x_v+d_0\cdot(x-x_v)/(z-z_v+d_0),&\&y'=y_v+d_0\cdot(y-y_v)/(z-z_v+d_0).&\" %}
Расстояние от точки до центра проекции.
{% include image_svg.html src="/img/euclidean-distance.svg" style="width: 319.911pt; height: 17.9328pt;" alt="d(t,d_0)=\sqrt{(x-x_v)^2+(y-y_v)^2+(z-z_v+d_0)^2}." %}
Сортировка граней
При создании кубика, вершины каждой грани задаём по часовой стрелке. При получении проекции, подставляем в уравнение прямой три подряд идущие вершины, чтобы определить наклон грани и удалённость её от плоскости проекции.
Уравнение прямой, проходящей через две точки.
{% include image_svg.html src="/img/linear-equation.svg" style="width: 137.171pt; height: 35.3194pt;" alt="{(x-x_1)\over(y-y_1)}={(x_2-x_1)\over(y_2-y_1)}." %}
Описание алгоритма
Сначала обходим вершины куба и поворачиваем их на угол относительно центральной точки. Затем обходим грани куба и получаем проекции входящих в них вершин. После этого сортируем проекции граней по удалённости. Затем рисуем проекции на плоскости — соединяем точки линиями. Рисуем полупрозрачным цветом сперва дальние грани и поверх них ближние, чтобы сквозь ближние грани было видно дальние.
На каждом шаге отображения фигуры повторяем сортировку граней по удалённости, так как с изменением угла поворота, координаты смещаются, и ближние грани становятся дальними.
Реализация на JavaScript
{% include classes-point-cube-ru.md -%}
Создаём объект и рисуем две проекции на плоскости.
'use strict';
// рисовать будем сразу две картинки,
// объект будет один, а проекций будет много
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
// создаём объект
const cube = new Cube(50,50,50,200);
// центр фигуры, вокруг него будем выполнять поворот
const t0 = new Point(150,150,150);
// удалённость центра проекции
const d = 300;
// положение экрана наблюдателя
const tv = new Point(150,150,80);
// угол поворота в градусах
const deg = {x:0,y:1,z:0};
// поворот фигуры и обновление изображения
function repaint() {
cube.rotate(deg, t0);
// рисуем параллельную проекцию
drawFigure(canvas1, cube.projection('parallel', tv));
// рисуем перспективную проекцию
drawFigure(canvas2, cube.projection('perspective', tv, d));
}
// рисуем фигуру по точкам из массива
function drawFigure(canvas, proj) {
let context = canvas.getContext('2d');
// сортируем грани по их наклону
proj.sort((a,b) => b.clock-a.clock);
// очищаем весь холст целиком
context.clearRect(0, 0, canvas.width, canvas.height);
// обходим массив граней куба
for (let i = 0; i < proj.length; i++) {
// обходим массив точек и соединяем их линиями
context.beginPath();
for (let j = 0; j < proj[i].length; j++) {
if (j == 0) {
context.moveTo(proj[i][j].x, proj[i][j].y);
} else {
context.lineTo(proj[i][j].x, proj[i][j].y);
}
}
context.closePath();
// рисуем грань куба вместе с рёбрами
context.lineWidth = 2.2;
context.lineJoin = 'round';
context.fillStyle = '#fff9';
context.strokeStyle = '#222';
context.fill();
context.stroke();
}
}
// после загрузки страницы, задаём интервал обновления изображения
document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));