§ Цель статьи

У меня очень давно есть мысль, которую все мечтаю реализовать, это просто нарисовать обычный куб. Ничего особенного вроде как. Обычный и куб. Делается это легко, вроде как, но все равно, поскольку я человек, который любит все с нуля писать, то это будет не так просто. Конечно, можно взять Блендер и не удалять оттуда обычный куб и тогда будет обычный куб, да еще в и квадрате, но, мне же нужно страдать и изобретать велосипеды.

§ Конструкция куба

Так, что из себя представляет куб? У него 6 сторон (граней), 8 точек и 12 ребер. Вот именно это и надо сделать. Я пойду по пути бросания лучей из камеры и попадания на границу этого шикарного куба в пространстве. Кстати, тени тоже я сделаю. Для этого придется просто бросать лучи из других точек.
Первым делом, мне потребуется код для пересечения некоторой линии с плоскостью. В этой статье я уже рассматривал, как вычислять пересечения. Говоря проще, есть плоскость, заданная через точки ABC или через два вектора \vec{AB} и \vec{AC} и некоторая прямая \vec{OA}
Куб будет располагаться по следующим координатам:
1let vertex = [
2    [-1,  1,  1], // 0 Левый  верхний спереди
3    [ 1,  1,  1], // 1 Правый верхний спереди
4    [ 1, -1,  1], // 2 Правый нижний  спереди
5    [-1, -1,  1], // 3 Левый  нижний  спереди
6    [-1,  1, -1], // 4
7    [ 1,  1, -1], // 5
8    [ 1, -1, -1], // 6
9    [-1, -1, -1]  // 7
10];
Но этого мало, конечно, необходимо указать все грани этого куба:
1let faces = [
2    [1, 0, 3, 2], // Перед
3    [0, 1, 5, 4], // Верх
4    [4, 5, 6, 7], // Зад
5    [2, 3, 7, 6], // Низ
6    [0, 4, 7, 3], // Слева
7    [5, 1, 2, 6], // Справа
8];
Порядок обхода вершин имеет значение. Необходимо, чтобы обход был по часовой стрелке.
01 23 45 76
Поскольку плоскость располагается достаточно симметрично, будет хватать взять лишь только первые 3 точки оттуда. Но крайне важно брать именно перпендикулярные друг другу ребра, так что можно будет добавлять плоскости по трем точкам, к примеру точки (1,0,2) для того, чтобы описать переднюю грань куба.
Теперь самое интересное, это нахождение точек пересечения с каждым ребром куба. Допустим, что у меня уже есть функция, которая рассчитывает расстояние от точки до плоскости, и не просто рассчитывает, а еще указывает, куда конкретно попала линия.

§ Угол зрения FOV

Собственно, суть в том, чтобы найти точки пересечения исходящего луча из камеры, вектор которой равен:
vec3(dx*fov, dy*fov, 1.0)
Здесь FOV (Field Of View) - это коэффициент угла зрения. Если FOV=1, то при dx=-1..1 дается 90 градусов обзора по горизонтали и вертикали. Просто из-за того что dx=-159 до 159 и dy=-99 до 99, то приходится делать fov=1/100, например, чтобы приравнять dy=-1..1 хотя бы. По причине того, что экран не квадратный, то dx на краях будет в 1.6 раз больше, но куда деваться, ничего не поделаешь. Если же обе стороны делать пропорциональными например, dx=-1..1 то будет заметно "сжатие" экрана и все будет расплющено.

§ Расчет параметров плоскости

Плоскость в пространстве, как я и говорил, можно задавать через три точки, например, точки a,b,c. Теперь я сразу выложу код, как рассчитываются коэффициенты для этих точек.
Опишу первым делом, как задаются точки у меня. Точки передаются как массив 3 или 5. Если массив состоит из 5 значений, то это передача параметров (u,v)
1create3(a, b, c) {
2
3    return {
4        a: vec3(a[0], a[1], a[2], a[3] || 0, a[4] || 0),
5        b: vec3(b[0], b[1], b[2], b[3] || 0, b[4] || 0),
6        c: vec3(c[0], c[1], c[2], c[3] || 0, c[4] || 0),
7    };
8}
Сам же вектор реализован так
1function vec3(x, y, z, u = 0, v = 0) {
2    return {
3        x: x, y: y, z: z,
4        u: u, v: v
5    };
6}
И вот теперь — самое главное. Расчет плоскости. В качестве параметра T передается результат функции create3. Параметр P представляет из себя функцию vec3(x,y,z) — что представляет собой положение камеры в пространстве. Для простоты я ставлю камеру в (0,0,0).
1compute3(T, P) {
2
3    // Разности между точками в пространстве
4    let AB = vec3(T.b.x - T.a.x, T.b.y - T.a.y, T.b.z - T.a.z);
5    let AC = vec3(T.c.x - T.a.x, T.c.y - T.a.y, T.c.z - T.a.z);
6    let AP = vec3(P.x   - T.a.x, P.y   - T.a.y, P.z   - T.a.z);
7
8    return {
9
10        // Параметры для дальнейших расчетов
11        U: vec3(
12            AC.y * AP.z - AC.z * AP.y,
13            AC.z * AP.x - AC.x * AP.z,
14            AC.x * AP.y - AC.y * AP.x
15        ),
16        V: vec3(
17            AB.z * AP.y - AB.y * AP.z,
18            AB.x * AP.z - AB.z * AP.x,
19            AB.y * AP.x - AB.x * AP.y
20        ),
21        D: vec3(
22            AB.z * AC.y - AB.y * AC.z,
23            AB.x * AC.z - AB.z * AC.x,
24            AB.y * AC.x - AB.x * AC.y
25        ),
26        // Вектор нормали
27        N: vec3(
28            AB.y * AC.z - AB.z * AC.y,
29            AB.z * AC.x - AB.x * AC.z,
30            AB.x * AC.y - AB.y * AC.x
31        ),
32        // Текстурные координаты
33        tu: vec3(T.a.u, T.b.u - T.a.u, T.c.u - T.a.u),
34        tv: vec3(T.a.v, T.b.v - T.a.v, T.c.v - T.a.v),
35        // Сохранить для быстрого обновления
36        a: T.a, AB: AB, AC: AC, AP: AP,
37    };
38}
Между прочим, если требуется быстро обновить позицию камеры, то вовсе необязательно снова вызывать функцию полного пересчета. Достаточно сделать несколько вычислений.
Здесь R - это объект, полученный из compute3, а P - это вектор нового положения начального положения луча (или камеры) в пространстве.
1update3(R, P) {
2
3    let AP  = vec3(P.x - R.a.x, P.y - R.a.y, P.z - R.a.z);
4    let [AB, AC] = [R.AB, R.AC];
5
6    R.U = vec3(
7        AC.y * AP.z - AC.z * AP.y,
8        AC.z * AP.x - AC.x * AP.z,
9        AC.x * AP.y - AC.y * AP.x
10    );
11
12    R.V = vec3(
13        AB.z * AP.y - AB.y * AP.z,
14        AB.x * AP.z - AB.z * AP.x,
15        AB.y * AP.x - AB.x * AP.y
16    );
17
18    R.AP = AP;
19}

§ Поиск пересечений

Вот теперь можно и перейти к описанию рутины, по которой происходит поиск точки пересений и текстурных координат.
1// Расчет
2let T = {
3    ok: 0,
4    u:  R.U.x*d.x + R.U.y*d.y + R.U.z*d.z,
5    v:  R.V.x*d.x + R.V.y*d.y + R.V.z*d.z,
6    d:  R.D.x*d.x + R.D.y*d.y + R.D.z*d.z,
7    t:  R.N.x*R.AP.x + R.N.y*R.AP.y + R.N.z*R.AP.z,
8    tx: 0,
9    ty: 0
10};
11
12// Пересечение произошло для плоскости
13if (T.d !== 0) {
14
15    // Либо треугольник, либо плоскость (quad)
16    let ok = quad ? (T.u <= T.d && T.v <= T.d) : (T.u + T.v <= T.d);
17
18    // Попадает ли?
19    if (ok && T.u >= 0 && T.v >= 0 && T.t > 0) {
20
21        T.ok = 1;
22        T.u /= T.d;
23        T.v /= T.d;
24        T.t /= T.d;
25
26        // Рассчитать позицию текстуры
27        T.tx = R.tu.x + R.tu.y*T.u + R.tu.z*T.v;
28        T.ty = R.tv.x + R.tv.y*T.u + R.tv.z*T.v;
29    }
30}
Вначале идет расчет параметров u,v,d,t, которые получаются путем скалярного произведения векторов U,V,D с вектором направления луча. Как можно заметить, начальная точка этого луча уже ранее была вычислена в compute3, потому она тут и не требуется.
Если луч не параллелен плоскости, то T.d будет не равно 0. Далее проверяем (u,v) — в зависимости от того, треугольник ли или плоскость пересекается. Если пересечение есть, то уже рассчитываются конечные координаты u,v,t и положение текстурной точки tx,ty.
Ниже показан пример как нарисовать куб. Работает медленно, как и должно.

Вот и всё.

§ Разве что код

Чтобы код не потерять в глубине тысячелетий.
1this.cast = new Caster3D();
2this.cast.addCube(vec3(-1.5, -0, 3));
3this.cast.addPlane([-8,-1,8,0,0],[8,-1,8,255*8,0],[-8,-1,-8,0,255*8]);
4
5this.cls(colorBlack);
6
7for (let y = -100; y < 100; y++)
8for (let x = -160; x < 160; x++) {
9
10    let cl = 0;
11
12    let vec = this.cast.camDir(x / 100, y / 100);
13    let int = this.cast.find(vec);
14
15    // Поиск объекта на сцене
16    if (int.ok) {
17        cl = ((Math.floor(int.tx) ^ Math.floor(int.ty)) & 255) << 8;
18    }
19
20    this.pset(160 + x, 99 - y, cl);
21}