§ Цель статьи
У меня очень давно есть мысль, которую все мечтаю реализовать, это просто нарисовать обычный куб. Ничего особенного вроде как. Обычный и куб. Делается это легко, вроде как, но все равно, поскольку я человек, который любит все с нуля писать, то это будет не так просто. Конечно, можно взять Блендер и не удалять оттуда обычный куб и тогда будет обычный куб, да еще в и квадрате, но, мне же нужно страдать и изобретать велосипеды.§ Конструкция куба
Так, что из себя представляет куб? У него 6 сторон (граней), 8 точек и 12 ребер. Вот именно это и надо сделать. Я пойду по пути бросания лучей из камеры и попадания на границу этого шикарного куба в пространстве. Кстати, тени тоже я сделаю. Для этого придется просто бросать лучи из других точек.Первым делом, мне потребуется код для пересечения некоторой линии с плоскостью. В этой статье я уже рассматривал, как вычислять пересечения. Говоря проще, есть плоскость, заданная через точки ABC или через два вектора и и некоторая прямая
Куб будет располагаться по следующим координатам:
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], // Справа ];Порядок обхода вершин имеет значение. Необходимо, чтобы обход был по часовой стрелке.
Поскольку плоскость располагается достаточно симметрично, будет хватать взять лишь только первые 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)
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), }; }create3(a, b, c) { Сам же вектор реализован так
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).// Разности между точками в пространстве 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, }; }compute3(T, P) { Между прочим, если требуется быстро обновить позицию камеры, то вовсе необязательно снова вызывать функцию полного пересчета. Достаточно сделать несколько вычислений.
Здесь R - это объект, полученный из
compute3
, а 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; }update3(R, P) {
§ Поиск пересечений
Вот теперь можно и перейти к описанию рутины, по которой происходит поиск точки пересений и текстурных координат.// Расчет 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); }