§ Цель статьи
У меня очень давно есть мысль, которую все мечтаю реализовать, это просто нарисовать обычный куб. Ничего особенного вроде как. Обычный и куб. Делается это легко, вроде как, но все равно, поскольку я человек, который любит все с нуля писать, то это будет не так просто. Конечно, можно взять Блендер и не удалять оттуда обычный куб и тогда будет обычный куб, да еще в и квадрате, но, мне же нужно страдать и изобретать велосипеды.§ Конструкция куба
Так, что из себя представляет куб? У него 6 сторон (граней), 8 точек и 12 ребер. Вот именно это и надо сделать. Я пойду по пути бросания лучей из камеры и попадания на границу этого шикарного куба в пространстве. Кстати, тени тоже я сделаю. Для этого придется просто бросать лучи из других точек.Первым делом, мне потребуется код для пересечения некоторой линии с плоскостью. В этой статье я уже рассматривал, как вычислять пересечения. Говоря проще, есть плоскость, заданная через точки ABC или через два вектора и и некоторая прямая
Куб будет располагаться по следующим координатам:
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];Порядок обхода вершин имеет значение. Необходимо, чтобы обход был по часовой стрелке.
Поскольку плоскость располагается достаточно симметрично, будет хватать взять лишь только первые 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}