§ Цель статьи

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

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

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

    return {
        a: vec3(a[0], a[1], a[2], a[3] || 0, a[4] || 0),
        b: vec3(b[0], b[1], b[2], b[3] || 0, b[4] || 0),
        c: vec3(c[0], c[1], c[2], c[3] || 0, c[4] || 0),
    };
}
Сам же вектор реализован так
function vec3(x, y, z, u = 0, v = 0) {
    return {
        x: x, y: y, z: z,
        u: u, v: v
    };
}
И вот теперь — самое главное. Расчет плоскости. В качестве параметра T передается результат функции create3. Параметр P представляет из себя функцию vec3(x,y,z) — что представляет собой положение камеры в пространстве. Для простоты я ставлю камеру в (0,0,0).
compute3(T, P) {

    // Разности между точками в пространстве
    let AB = vec3(T.b.x - T.a.x, T.b.y - T.a.y, T.b.z - T.a.z);
    let AC = vec3(T.c.x - T.a.x, T.c.y - T.a.y, T.c.z - T.a.z);
    let AP = vec3(P.x   - T.a.x, P.y   - T.a.y, P.z   - T.a.z);

    return {

        // Параметры для дальнейших расчетов
        U: vec3(
            AC.y * AP.z - AC.z * AP.y,
            AC.z * AP.x - AC.x * AP.z,
            AC.x * AP.y - AC.y * AP.x
        ),
        V: vec3(
            AB.z * AP.y - AB.y * AP.z,
            AB.x * AP.z - AB.z * AP.x,
            AB.y * AP.x - AB.x * AP.y
        ),
        D: vec3(
            AB.z * AC.y - AB.y * AC.z,
            AB.x * AC.z - AB.z * AC.x,
            AB.y * AC.x - AB.x * AC.y
        ),
        // Вектор нормали
        N: vec3(
            AB.y * AC.z - AB.z * AC.y,
            AB.z * AC.x - AB.x * AC.z,
            AB.x * AC.y - AB.y * AC.x
        ),
        // Текстурные координаты
        tu: vec3(T.a.u, T.b.u - T.a.u, T.c.u - T.a.u),
        tv: vec3(T.a.v, T.b.v - T.a.v, T.c.v - T.a.v),
        // Сохранить для быстрого обновления
        a: T.a, AB: AB, AC: AC, AP: AP,
    };
}
Между прочим, если требуется быстро обновить позицию камеры, то вовсе необязательно снова вызывать функцию полного пересчета. Достаточно сделать несколько вычислений.
Здесь R - это объект, полученный из compute3, а P - это вектор нового положения начального положения луча (или камеры) в пространстве.
update3(R, P) {

    let AP  = vec3(P.x - R.a.x, P.y - R.a.y, P.z - R.a.z);
    let [AB, AC] = [R.AB, R.AC];

    R.U = vec3(
        AC.y * AP.z - AC.z * AP.y,
        AC.z * AP.x - AC.x * AP.z,
        AC.x * AP.y - AC.y * AP.x
    );

    R.V = vec3(
        AB.z * AP.y - AB.y * AP.z,
        AB.x * AP.z - AB.z * AP.x,
        AB.y * AP.x - AB.x * AP.y
    );

    R.AP = AP;
}

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

Вот теперь можно и перейти к описанию рутины, по которой происходит поиск точки пересений и текстурных координат.
// Расчет
let T = {
    ok: 0,
    u:  R.U.x*d.x + R.U.y*d.y + R.U.z*d.z,
    v:  R.V.x*d.x + R.V.y*d.y + R.V.z*d.z,
    d:  R.D.x*d.x + R.D.y*d.y + R.D.z*d.z,
    t:  R.N.x*R.AP.x + R.N.y*R.AP.y + R.N.z*R.AP.z,
    tx: 0,
    ty: 0
};

// Пересечение произошло для плоскости
if (T.d !== 0) {

    // Либо треугольник, либо плоскость (quad)
    let ok = quad ? (T.u <= T.d && T.v <= T.d) : (T.u + T.v <= T.d);

    // Попадает ли?
    if (ok && T.u >= 0 && T.v >= 0 && T.t > 0) {

        T.ok = 1;
        T.u /= T.d;
        T.v /= T.d;
        T.t /= T.d;

        // Рассчитать позицию текстуры
        T.tx = R.tu.x + R.tu.y*T.u + R.tu.z*T.v;
        T.ty = R.tv.x + R.tv.y*T.u + R.tv.z*T.v;
    }
}
Вначале идет расчет параметров u,v,d,t, которые получаются путем скалярного произведения векторов U,V,D с вектором направления луча. Как можно заметить, начальная точка этого луча уже ранее была вычислена в compute3, потому она тут и не требуется.
Если луч не параллелен плоскости, то T.d будет не равно 0. Далее проверяем (u,v) — в зависимости от того, треугольник ли или плоскость пересекается. Если пересечение есть, то уже рассчитываются конечные координаты u,v,t и положение текстурной точки tx,ty.
Ниже показан пример как нарисовать куб. Работает медленно, как и должно.

Вот и всё.

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

Чтобы код не потерять в глубине тысячелетий.
this.cast = new Caster3D();
this.cast.addCube(vec3(-1.5, -0, 3));
this.cast.addPlane([-8,-1,8,0,0],[8,-1,8,255*8,0],[-8,-1,-8,0,255*8]);

this.cls(colorBlack);

for (let y = -100; y < 100; y++)
for (let x = -160; x < 160; x++) {

    let cl = 0;

    let vec = this.cast.camDir(x / 100, y / 100);
    let int = this.cast.find(vec);

    // Поиск объекта на сцене
    if (int.ok) {
        cl = ((Math.floor(int.tx) ^ Math.floor(int.ty)) & 255) << 8;
    }

    this.pset(160 + x, 99 - y, cl);
}