1/jekyll_site/ru/2023/01/15/spinning-spatial-cross.md
2023-12-17 07:55:25 +03:00

14 KiB
Raw Blame History

title description sections tags scripts styles canonical_url url_translated title_translated date
Вращаем пространственный крест Пишем алгоритм для поворота объёмной фигуры на угол вокруг своего центра по всем трём осям сразу. В предыдущем примере мы вращали куб в пространстве...
Объёмные фигуры
Матрица поворота
Экспериментальная модель
javascript
canvas
геометрия
матрица
графика
изображение
картинка
квадрат
куб
/js/classes-point-cube.js
/js/spinning-spatial-cross.js
/js/spinning-spatial-cross2.js
/css/pomodoro1.css
/ru/2023/01/15/spinning-spatial-cross.html /en/2023/01/16/spinning-spatial-cross.html Spinning spatial cross 2023.01.15

Пишем алгоритм для поворота объёмной фигуры на угол вокруг своего центра по всем трём осям сразу. В предыдущем примере мы [вращали куб в пространстве]({{ '/ru/2023/01/10/spinning-cube-in-space.html' | relative_url }}) — теперь кубиков будет много, алгоритм будет почти такой же и формулы будем использовать те же. Рисуем два варианта фигуры: пространственный крест и крест-куб в двух типах проекций, рассматриваем разницу.

Тестирование экспериментального интерфейса: [Объёмный тетрис]({{ '/ru/2023/01/21/volumetric-tetris.html' | relative_url }}).

Пространственный крест

Параллельная проекция

Ваш браузер не поддерживает Canvas

Перспективная проекция

Ваш браузер не поддерживает Canvas

Крест-куб

Параллельная проекция

Ваш браузер не поддерживает Canvas

Перспективная проекция

Ваш браузер не поддерживает Canvas

Параллельная проекция — все кубики одинакового размера.

Перспективная проекция — кубики выглядят уменьшающимися вдалеке.

Экспериментальная модель

Слегка усложнённая версия из предыдущего примера — теперь кубиков много. В дополнение к предыдущим настройкам можно поменять: вариант фигуры — пространственный крест или крест-куб, направление сортировки граней — линейная перспектива или обратная перспектива и прозрачность стенок кубиков.

Ваш браузер не поддерживает Canvas

Вращение по осям:
Центр на экране наблюдателя:
150
150
125
Удалённость центра проекции:
300
Прозрачность кубиков:
20%
Вариант фигуры: Перспективная проекция:

Описание алгоритма

Подготавливаем матрицу из нулей и единиц, где единица означает кубик в определенном месте фигуры. Затем обходим эту матрицу и заполняем массив кубиков с соответствующими координатами вершин. После этого запускаем вращение по всем трём осям сразу. На каждом шаге обходим массив кубиков и получаем проекции их граней. Затем сортируем массив граней по удалённости от центра проекции, обходим этот массив и выкидываем из него одинаковые пары — это есть смежные стенки между соседними кубиками внутри фигуры. После этого полупрозрачным цветом рисуем грани кубиков — сначала дальние и затем ближние, чтобы через ближние грани было видно дальние.

Реализация на JavaScript

{% include classes-point-cube-ru.md -%}

Создаём объекты по шаблонам и рисуем их проекции на плоскости.

'use strict';
// матрицы-шаблоны для кубиков
const shape1 = [ // пространственный крест
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]]];
const shape2 = [ // крест-куб
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
  [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
  [[1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1], [1,0,0,0,1], [1,1,1,1,1]],
  [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]]];
// размер кубика, количество кубиков в ряду, отступ
const size = 40, row = 5, gap = 50;
// массивы для кубиков
const cubes1 = [], cubes2 = [];
// обходим матрицы, заполняем массивы кубиками
for (let x=0; x<row; x++)
  for (let y=0; y<row; y++)
    for (let z=0; z<row; z++) {
      if (shape1[x][y][z]==1)
        cubes1.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
      if (shape2[x][y][z]==1)
        cubes2.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
    }
// центр фигуры, вокруг него будем выполнять поворот
const t0 = new Point(150,150,150);
// удалённость центра проекции
const d = 300;
// положение экрана наблюдателя
const tv = new Point(150,150,125);
// угол поворота в градусах
const deg = {x:1,y:1,z:1};
// рисовать будем по две картинки для каждой фигуры
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
const canvas3 = document.getElementById('canvas3');
const canvas4 = document.getElementById('canvas4');
// обновление изображения
function repaint() {
  // пространственный крест
  processFigure(cubes1,canvas1,canvas2);
  // крест-куб
  processFigure(cubes2,canvas3,canvas4);
}
// поворачиваем фигуру и получаем проекции
function processFigure(cubes,cnv1,cnv2) {
  // массивы проекций граней кубиков
  let parallel = [], perspective = [];
  // поворачиваем кубики и получаем проекции
  for (let cube of cubes) {
    cube.rotate(deg, t0);
    parallel = parallel.concat(cube.projection('parallel',tv,d));
    perspective = perspective.concat(cube.projection('perspective',tv,d));
  }
  // смежные стенки между соседними кубиками не рисуем
  noAdjacent(parallel);
  noAdjacent(perspective);
  // сортируем грани разных кубиков по удалённости и внутри одного кубика по наклону
  parallel.sort((a,b)=>Math.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
  // сортируем грани по удалённости от центра проекции
  perspective.sort((a,b)=>b.dist-a.dist);
  // рисуем параллельную проекцию
  drawFigure(cnv1, parallel);
  // рисуем перспективную проекцию
  drawFigure(cnv2, perspective);
}
// смежные стенки между соседними кубиками не рисуем
function noAdjacent(array) {
  // сортируем грани по удалённости
  array.sort((a,b) => b.dist-a.dist);
  // удаляем смежные стенки между кубиками
  for (let i=0, j=1; i<array.length-1; j=++i+1)
    while (j<array.length && Cube.pEquidistant(array[i],array[j]))
      if (Cube.pAdjacent(array[i],array[j])) {
        array.splice(j,1);
        array.splice(i,1);
        i--; j=array.length;
      } else j++;
}
// рисуем фигуру по точкам из массива
function drawFigure(canvas, proj, alpha=0.8) {
  const context = canvas.getContext('2d');
  // очищаем весь холст целиком
  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 = 1.9;
    context.lineJoin = 'round';
    context.fillStyle = 'rgba(200,230,201,'+alpha+')';
    context.strokeStyle = 'rgba(102,187,106,'+(0.2+alpha)+')';
    context.fill();
    context.stroke();
  }
}
// после загрузки страницы, задаём интервал обновления изображения
document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));