
Так как я не сильно разбираюсь в математике и в геометрии, буду использовать подход графического 3D-программирования, потому что подход, применяющийся там, хорошо подходит и для проецирования четырёхмерных пространств. Поэтому для чтения этого материала рекомендую иметь хотя бы базовые знания о том, как работают матрицы преобразования и как с их помощью точка из трёхмерного пространства преобразуется в точку на экране. Код будем писать на JavaScript с использованием WebGL и библиотеки matrix-gl.
Чтобы понять, как проецировать четырёхмерное пространство на двухмерную плоскость, рассмотрим аналогичную задачу. Допустим, есть некоторый объект в двухмерном пространстве — как показать ему трёхмерное пространство? Сделаем изображение трехмерного пространства получим двухмерную плоскость. Объект в двухмерном пространстве может видеть всего лишь линию перед собой — это как если бы мы стали в центре прохода через стену и посмотрели вдоль стены. Поэтому посмотреть на изображение объект не сможет, но мы можем поместить этот объект внутрь этого изображения, так как изображение двухмерное. Теперь этот объект, перемещаясь по нему, может исследовать это изображение.
Поступим так же и для четырёхмерного пространства: сделаем трёхмерное изображение четырехмерного пространства, а потом поместим туда камеру. Теперь возникает вопрос: как сделать трёхмерное изображение четырёхмерного пространства? У нас будут четыре компоненты — x, y, z, w. Возьмём последнюю компоненту и избавимся от неё с помощью матриц преобразования и деления, так же, как мы делаем это в трёхмерной графике. В итоге останется три компоненты, из которых можно построить трёхмерное пространство. Только вот матрицы для трехмерных преобразований там не подходят, поэтому они должны быть расширены для работы с четырьмя компонентами:
Вот как будут выглядеть матрицы перемещения и масштабирования.
function translate4D(x, y, z, w) {
return [
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
x, y, z, w, 1
];
}
function scale4D(x, y, z, w) {
return [
x, 0, 0, 0, 0,
0, y, 0, 0, 0,
0, 0, z, 0, 0,
0, 0, 0, w, 0,
0, 0, 0, 0, 1
];
}
Матрица вращения будет немного отличаться от трёхмерной версии, так как вращение в четырёхмерном пространстве не может происходить вокруг только одной оси, только вокруг плоскости, то есть по двум осям. Числа, которые мы передаём в r1 и r2 — это номера этих осей где x=0, y=1, z=2, w=3, которые вместе определяют плоскость вращения.
function rotate4D(r1, r2, angle) {
let _ = (i, j) => i * 5 + j;
let m = initMatrix4D();
m[_(r1, r1)] = m[_(r2, r2)] = Math.cos(angle);
m[_(r1, r2)] = -(m[_(r2, r1)] = Math.sin(angle));
return m;
}
С проекцией всё тоже достаточно просто:
function projection4D(near, far, fovy) {
let c = 1 / Math.tan(fovy / 2);
let a = (far + near) / (far - near);
let b = -2 * near * far / (far - near);
return [
c, 0, 0, 0, 0,
0, c, 0, 0, 0,
0, 0, c, 0, 0,
0, 0, 0, a, 1,
0, 0, 0, b, 0
]
}
Отличие от трёхмерной проекции только в добавлении еще одного коэффициента c
. Также мы не заботимся о соотношении сторон, так как оно нам в данном случае не нужно.
Теперь посмотрим на матрицу камеры. Суть матрицы камеры в том, что она перемещает все объекты так, как если бы они перемещались относительно камеры при её движении. Например, если нужно сдвинуть камеру влево, все объекты двигаем вправо; если нужно повернуть камеру налево, то всю сцену вращаем вправо. Это легко реализуется с помощью обратной матрицы к матрицам вращения и перемещения.
function camera4D(r1, r2, angle, x, y, z, w) {
return invMatrixN(MultiplyMatr5(rotate4D(r1, r2, angle),
translate4D(x, y, z, w)));
}
Камера немного отличается от привычной нам камеры lookAt
, но при желании её можно реализовать похожим образом.
Когда мы умножаем все эти матрицы на вектор, на выходе получаем вектор из пяти элементов: x, y, z, w и v. Первые четыре элемента (x, y, z, w) мы делим на v
.Теперь компонент w
хранит значение глубины, которое обычно используется для теста глубины — то есть для определения, какой элемент нужно отобразить при перекрытии нескольких объектов. Мы не будем использовать это значение, так как реализация теста глубины довольно сложна, и в нашем случае не особо нужна, поскольку мы хотим отобразить прозрачный тессеракт, у которого должный быть видны только ребра.
Однако есть ещё одна проблема, связанная с отрицательными значениями w
: по идее, мы не можем видеть объекты, находящиеся за камерой. Поэтому мы будем смещать тессеракт вперёд относительно камеры, чтобы избежать появления странных линий при визуализации.
Пара слов про код, формирующий тессеракт.
let points = [];
let relations = [];
function createRelations(n, arr) {
if (n == 1) {
let a = [-1, ...arr, 1]; //last 1 is w in 3D
let b = [1, ...arr, 1];
points.push(a, b);
let rel = [points.length - 1, points.length - 2]
relations.push(rel);
return rel;
}
n--;
let a = createRelations(n, [-1, ...arr]);
let b = createRelations(n, [1, ...arr]);
for (let i = 0; i < a.length; i++) {
relations.push([a[i], b[i]]);
}
return [...a, ...b];
}
function createMeshCube4D() {
points = [];
relations = [];
createRelations(4, []);
return {
points,
relations
}
}
Этот код создаёт вершины (points
) и рёбра (relations
) для построения 4-мерного куба — тессеракта. Идея в том, что мы начинаем с двух точек (линию), затем объединяем две линии в квадрат, два квадрата в куб, и, наконец, два куба в тессеракт.
Когда мы смотрим на фигуру в четырехмерном пространстве, у которой видны лишь ребра, сложно разобрать, какая линия находится впереди а какая сзади и это сильно запутывает восприятие. Мы могли бы использовать полупрозрачные плоскости для решения этой проблемы, но мы пойдем другим путем. Мы будем задавать каждой вершине яркость чем она ближе к камере относительно центра тем она ярче будет. Для этого будем ориентироваться на центр тессеракта и значение координаты z каждой вершины.
const vertexSource = `
precision lowp float;
attribute vec4 aVertexPosition;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform vec4 uCenter;
varying float color;
const float size = 2.;
void main(void) {
gl_PointSize = 3.0;
vec4 vertex = uModelViewMatrix * aVertexPosition;
vec4 center = uModelViewMatrix * uCenter;
color = (vertex.z - center.z + size / 2.) * 0.5 / size + 0.5;
gl_Position = uProjectionMatrix * vertex;
}
`;
gl.uniform4fv(programInfo.uniformLocations.centerCoords, calcPoint(0, 0, 0, 0));
Здесь size — это максимально возможная ширина фигуры по оси z. Мы указали его равным 2, хотя для куба он будет немного больше. На практике лучше передавать это значение через uniform переменные так как для каждой фигуры может быть свое значение ширины. Для центра фигуры мы выполняем те же вычисления, что и для остальных точек, кроме умножения на трёхмерную матрицу проекции — то есть центр умножаем только на матрицу модели-вида и берём у него координату z
У проекции тессеракта и куба есть похожие свойства. Например вот сравнение того как работает глубина в четырехмерном пространстве и в трехмерном пространстве.

Куб сверху проецируется на плоскость как два прямоугольника, где меньший расположен по центру внутри большего. Если чуть сдвинуть куб вниз, становится видно, что проекция дальнего прямоугольника ближе к центру, чем переднего. Аналогично с тессерактом: вложенный куб находится дальше по четвёртой оси w и потому кажется меньшим и вложенным; при сдвиге вниз также заметно, что куб, находящийся дальше по оси w, ближе к центру — точке (0, 0, 0) в трёхмерной проекции четырёхмерного пространства.


По данной ссылке на codepen находится живой пример проекции, а на github по ссылке можно найти исходный код проекта.
amvasiljev
Осторожней с этим))
Напомнило «And He Built a Crooked House». Хайнлайн Р.