Все части

В этой части мы разберем как и зачем строить модель при помощи треугольников. А также научимся не рисовать те части модели, которые не смотрят в камеру и не должны быть видны.

Для начала возьмите код с усеченной пирамидой из прошлой части бонусного раздела ( 25.01.22 в прошлую часть я внес исправления ошибок в код и текст, и если вы скачивали код раньше, нужно его сказать заново, какие именно правки указаны в конце прошлой части ). Всю пирамиду мы построили при помощи вершин ( массив vertices ) и ребер ( массив edges ). У этого подхода есть минус, у нас в коде у фигуры нету граней, т.е., у пирамиды неясно какие ребра ( или просто линии ) нужно соединить, чтобы в коде стало понятно что это грань, давайте посмотрим на такой пример

Индексы вершин усеченной пирамиды
Индексы вершин усеченной пирамиды

По картинке видно что верхняя грань пирамиды это 0, 1, 2, 3, лицевая грань: 3, 2, 6, 7 и т.д. Т.е. визуально мы эти грани различаем, но в коде у нас это никак не определено, т.к. мы рисуем фигуру линиями ( ребра ). Вы можете подумать, зачем нам вообще нужны грани, ведь и так красиво. Вопрос хороший и тут есть много причин, мы разберем некоторые из них.

Нам нужно знать грани, чтобы закрашивать модель в цвет, освещать её, или накладывать текстуры ( это будет в след. частях ). Ведь мы сейчас можем поменять цвет линии которой рисуем. А для того чтобы что-то закрасить, у нас должна быть замкнутая фигура в коде, которую будем заливать цветом.

Также грани нужны чтобы удалять невидимые части фигуры и немного оптимизировать скорость вывода и модель на экране была более реалистична ( посмотрим в этой части ).

Многие из моделей которые делают в других редакторах, состоят не просто из линий как наша текущая пирамида, а из множества граней ( полигонов ), которые стоят рядом и создают фигуру. Умение работать с такими моделями позволит нам в коде не создавать модели вручную ( их вершины и полигоны ), а загружать их из файлов, который были экспортированы из 3D-редакторов или скачаны из сайтов с 3D-моделями.

Давайте разберемся как нашу текущую модель и код переписать так, чтобы она имела понятия полигона. Первое что нам нужно это придумать систему, по которой мы будем выводить любую другую фигуру, а не только пирамиду. Ведь мы же не будем писать отдельную программу для пирамиды, сферы, или игрового персонажа… Например, смотря на пирамиду, мы видим что она состоит из 6 четырехугольных граней, у которых есть общие вершины. Все эти четырехугольники мы видим на картинке выше, там где пронумерованы вершины. Эти четырехугольники можно еще назвать четырехугольными полигонами. Но в 3D программировании на первых этапах нам проще использовать треугольные полигоны ( треугольники ), потому что такие полигоны будут в моделях, которые мы будем загружать из файлов. Плюс треугольники создают одну плоскость, в отличии от четырехугольника, и нам по треугольнику будет проще считать нормали ( узнаем что это в этой части ) и проще текстурировать ( разберем в следующих частях ). Вот пример треугольного и четырехугольного полигона:

Слева - треугольный полигон, справа - четырехугольный.
Слева - треугольный полигон, справа - четырехугольный.

Из картинки можно увидеть что четырехугольный полигон, может создавать 2 плоскости. С треугольником такого не произойдет.

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

Первое что нужно понять, что вершины никуда не деваются, все те же 8 штук, только теперь мы их соединим не просто линиями независимыми друг от друга, а каждую линию будем объединять в треугольник, так, чтобы получилась желаемая нам фигура. Принцип объединения такой-же как и был в массиве edges, т.е. при помощи индексов вершин, только теперь в массиве у нас будут не пары вершин ( пара т.к. у каждой линии есть начала и конец ), а по 3 вершины, например, чтобы описать 1 треугольник нам нужно указать 3 вершины, соединив которые мы получим треугольник. Мы уже упоминали что для построения усеченной пирамиды нам нужно 6 четырехугольников, а вот треугольников нам нужно в 2 раза больше, т.к. для вывода одного четырехугольника ( четырехугольного полигона ) нужно 2 треугольника. На картинке ниже в верхней части мы выводим 1 сторону пирамиды при помощи линий ( как у нас сейчас ), а ниже при помощи треугольников ( так сделаем ). В результате и там и там у нас визуально получился четырехугольник ( например, грань пирамиды ). Но в случае треугольников, у нас еще есть линия по диагонали, т.к. по сути это 2 приставленных к друг другу треугольника. Эта линия видна только потому что мы рисуем линиями. Когда мы научимся закрашивать в цвет, а потом и текстурировать фигуры и отключим линии, их не будет видно, и все будет выглядеть красиво, без ненужных стыков:

Сверху прямоугольник из линий, снизу - из треугольников.
Сверху прямоугольник из линий, снизу - из треугольников.

Кажется что использование треугольников – избыточно. Конкретно в этом случае так и есть, т.к. пирамида состоит из четырехугольников и тут они конечно больше подходят. Но вспомните о преимуществах, которые описаны выше. А также о том, что мы не будем все время выводить одни пирамиды, будут другие, более сложные фигуры, где треугольники уже покажутся лучшим выбором и чтобы в коде все было просто и все фигуры выводились одинаково, мы все их будем выводить при помощи треугольников, хоть это и будет иногда не самым идеальным вариантом.

Давайте закомментируем массив edges и заменим его на массив indices, в котором будут подмассивы по 3 индекса:

const indices = [
    [0, 1, 2], // 0
    [0, 2, 3], // 1
    [4, 6, 5], // 2
    [4, 7, 6], // 3
    [0, 5, 1], // 4
    [0, 4, 5], // 5
    [1, 5, 2], // 6
    [6, 2, 5], // 7
    [3, 2, 6], // 8
    [3, 6, 7], // 9
    [3, 4, 0], // 10
    [4, 3, 7], // 11
];

В этом массиве каждая строчка ( подмассив из 3х элементов ) – это 3 индекса из массива вершин ( vertices ) которые если мы соединим линиями между собой – получим треугольник. Таким образом мы описали все стороны усеченной пирамиды, их у нее 6, но они прямоугольные, и для каждого прямоугольника приходится использовать по 2 треугольника.

Теперь нужно переписать код отрисовки линий, т.к. теперь мы рисуем не по одной линии используя массив edges, а сразу будем выводить по треугольнику ( массив indices ).

Давайте закомментируем целиком цикл работы с edges:

//for (let i = 0, l = edges.length; i < l; i++) {
//  весь внутренний код цикла тоже комментируем...
//}

И вместо него будем перебирать массив indices:

for (let i = 0, l = indices.length; i < l; i++) {
}

Внутри этого цикла при помощи записи indices[i] мы можем достать строчку с 3мя индексами конкретного выводимого треугольника, эти индексы указывают на вершины в массиве sceneVertices ( т.к. мы сохранили порядок вершин из оригинального массива indices ). Поэтому при помощи такой записи мы можем достать координаты всех вершин:

// Индексы конкретного треугольника
const e = indices[i];
// Координаты каждой вершины ( объекты класса Vector )
let v1 = sceneVertices[e[0]];
let v2 = sceneVertices[e[1]];
let v3 = sceneVertices[e[2]];

И последним шагом перехода на треугольники — отрисовка 3х линий по 3м точкам:

// Линия от v1 к v2
drawer.drawLine(
  v1.x,
  v1.y,
  v2.x,
  v2.y,
  0, 0, 255
);
// Линия от v2 к v3
drawer.drawLine(
  v2.x,
  v2.y,
  v3.x,
  v3.y,
  0, 0, 255
);
// Линия от v1 к v3
drawer.drawLine(
  v1.x,
  v1.y,
  v3.x,
  v3.y,
  0, 0, 255
);

В каком порядке рисовать линии не имеет значения, главное чтобы все 3 точки были соединены между собой и на экране получился треугольник.

Для того чтобы видеть пирамиду в динамике, давайте вернем ей вращение по оси Y, сразу после вращени по X ( хоть там сейчас и 0 стоит для X ):

matrix = Matrix.multiply(
  Matrix.getRotationY(angle += 1),
  matrix
);

Если все сделали правильно, то у вас должна быть на экране вращающаяся пирамида состоящая из треугольников:

Модель усеченной пирамиды из треугольников
Модель усеченной пирамиды из треугольников

Как видите, она отличается от предыдущей наличием диагональных линий, как я уже упоминал ранее это потому что каждый четырехугольник в пирамиде состоит из рядом стоящих 2 треугольников. Такой артефакт мы позднее уберем, в следующих частях.

Теперь давайте посмотрим еще раз что не так. У пирамиды которая получилась как и всех моделей выводимых ранее видны сразу все её грани ( технически мы видим линии ребер, т.к. их рисуем ), ранее это нас устраивало, т.к. нужно было видеть что происходит с каждой стороны, настраивать перспективу и т.д.

Предлагаю сделать оптимизацию и скрыть те грани фигуры, которые нам не должны быть видны. Это даст более реалистичную картинку ( сделает пирамиду непрозрачной ), также мы немного ускорим приложение, т.к. не будем рисовать линии тех треугольников, что сейчас не должны быть видны.

Для того чтобы эти треугольники скрыть, давайте подумаем как вообще это можно сделать. Сейчас когда мы используем треугольники, мы знаем что у него есть одна плоскость с 2мя сторонами, если бы мы вырезали треугольник из бумаги, то можно было бы его покрутить то одно стороной то другой. В жизни мы конечно видим треугольник независимо от того какой стороной он к нам повернут, но в 3D графике мы можем настроить так, чтобы была видна только одна сторона. Как определить какая сторона должна быть лицевой и видимой? Вообще обе стороны имеют право на звание лицевой и тут свобода выбора, поэтому мы как разработчики этой модели сами должны решить, какая сторона какой будет. Сейчас мы не оперируем понятием сторона в коде, у нас есть только индексы 3х вершин, по которым мы рисуем линии. Для того чтобы это понятие появилось нам поможет очень полезная штука — нормаль. Нормаль это вектор перпендикулярный треугольнику и откуда исходит нормаль, та сторона и считается лицевой:

Нормаль видимой грани.
Нормаль видимой грани.

Синяя стрелка это вектор нормали для какого-то треугольника. Поскольку выше упоминалось что сторон у треугольника 2, то и нормали тоже может быть 2, но нам нужна только 1 нормаль, та которая будет исходить из видимой стороны треугольника. когда мы будем считать нормаль ( ниже покажу как это делать ) то мы будем считать ее при помощи уже имеющихся у нас 3х вершин треугольника и порядок расстановки вершин будет влиять на то в какую сторону указывает нормаль. Например, если бы точки a, b, c дали бы нам одну нормаль, то c, b, a дали бы уже другую, возможно, в противоположную сторону. Следить за массивом indices вручную очень сложно, ведь у нас очень простая фигура и там уже 12 записей по 3 индекса. Поэтому все это за нас уже давно делают 3D редакторы, когда мы экспортируем модели из них, они уже расставляют вершины в правильном всегда одинаковом порядке, так что мы сможем по ним посчитать правильные нормали. Но пока мы с 3D-редакторов еще не умеем что либо загружать, я правильную расстановку вершин в массиве indices уже сделал заранее, тот массив что мы использовали выше расставлен так, что все индексы вершин в нем дают нам правильные нормали.

Для того чтобы посчитать сами нормали нам никаких новых методов добавлять не придется, более того, мы уже делали их в прошлой части в секции бонуса, когда считали вектор для движения в сторону по отношению к положению к камере.

Для того чтобы посчитать нормаль. нам нужно получить 2 вектора треугольника, именно эти вектора образуют плоскость, перпендикуляр к которой мы хотим получить — это и будет нормалью, чтобы получить из 3х точек 2 вектора, нам нужно применить уже известную функцию substruct, ведь если вычесть одну точку из другой, мы получим вектор направления от одной точки к другой. Добавьте следующий код ниже получения v1, v2, v3 в цикле перебора indices:

let t1 = Vector.substruct(v1, v2);
let t2 = Vector.substruct(v2, v3);

И теперь вектора t1 и t2 образуют плоскость, если мы перемножим их при помощи векторного произведения векторов, то получим вектор перпендикулярный обоим векторам — это и будет нашей нормалью:

let normal = Vector.crossProduct(t1, t2).normalize();

Порядок вычитания точек играет роль, в примере выше если бы я отнимал v2 и v1 ( вот так: let t1 = Vector.substruct(v2, v1); ) то вектор был бы у меня в другом направлении, и это могло бы дать нормаль в другом направлении, в таком случае я увижу внутреннюю часть пирамиды. Вообще тут строго запоминать какие вектора от каких отнимать и не нужно, вы всегда можете подобрать порядок вычитания так — как вам нужно. Получили не тот результат и фигура наизнанку, поменяйте вектора местами и получить то что вам нужно. Ведь тут нельзя говорить что если нормаль повернута в другую сторону то это неправильно, т.к. вполне может быть что мы хотим такое отображение для игры, например, если нам нужно сделать простенький горизонт в игре, для этого мы можем поместить сферу вокруг нашей камеры и наложить текстуру на внутреннюю сторону сферы, т.к. мы видим её изнутри, и тут нормали, возможно, нам придется направить в другую сторону переставив вектора местами или любым другим способом. В тоже время эту же модель сферы мы можем переиспользовать для других объектов в игре и там уже нам нужно отрисовывать наружную часть сферы, где нормали повернуты в противоположную сторону от тех что нужны были для горизонта. Таким образом для одной модели нам может понадобиться считать нормали направленные как в одну сторону, так и в другую, зависимо от того что мы хотим получить на экране. А может нам и вовсе не нужно делать какую-то часть треугольника невидимой, если мы хотим чтобы были видны обе его стороны.

Нормали нужны далеко не для того чтобы можно было посчитать какие полигоны ( треугольники ) модели видны, с помощью нормалей, например, можно освещать модели, но это мы сделаем в следующих частях.

Теперь, когда у нас есть нормаль треугольника, нужно посчитать должен ли этот треугольник быть видимым по отношению к камере ( с нашего ракурса ). Для этого мы можем воспользоваться новой операцией над векторами: скалярным произведением векторов. Для этой операции нам нужно 2 вектора и в результате этой операции мы получим число, которое на первый взгляд не понятно как применить к нашей задаче. Определение результата скалярного произведения векторов звучит так: результатом называется число, равное произведению длин этих векторов на косинус угла между ними. Что за длины, что за косинус и как нам все это поможет? Давайте разбираться, добавим код этого самого скалярного произведения в класс Vector:

static scalarProduct(a, b) {
  return a.x * b.x + a.y * b.y + a.z * b.z
}

Код не сложный, а вот определение результата так себе. На самом деле нам в результате этой операции само число и не нужно, нам нужен лишь знак этого числа. Ведь в формулировке говорится что результат это длины векторов и косинус угла между ними перемноженные между собой, вот длины векторов нам не особо нужны, они всегда положительные, а вот косинус угла как раз интересный, т.к. если вспомним чуть математику или просто брутфорсом начнем подставлять углы в косинус, то увидим, что косинус угла 90 градусов = 0, а косинус 89 градусов равен примерно 0.017, а косинус 91 градуса -0.017. Т.е. если угол который мы передадим в косинус более 90 градусов то мы будем получать отрицательные числа, а если менее — положительные. Таким образом, если результат скалярного произведения векторов окажется положительным, то значит что угол между ними ( в нашем случае между вектором камеры и нормалью треугольника ) менее 90 градусов и такой треугольник нам нужно рисовать, а если отрицательный - не нужно. Т.е. мы не зная углов между камерой и нормалью все равно можем принимать решения по отрисовке, т.к. в результате хоть и нету угла, но есть косинус угла, который даст нам правильный знак в результате скалярного произведения. Давайте попробуем это теперь применить в коде, ниже строчки в которой мы посчитали нормаль, добавим скалярное произведения вектора камеры на нормаль:

let res = Vector.scalarProduct(cameraDirection, normal)

И следующей строчкой, поместим всю отрисовку пирамиды в проверку:

// res положительный, если угол между камерой и нормалью будет менее 90 градусов
if (res > 0) {
  // внутри этого if находятся все 3 вызова drawer.drawLine
}

Если все сделали правильно, у Вас должна получиться пирамида, у которой видна только часть смотрящая в камеру, а остальные грани отброшены условием.:

Такое отсечение невидимых граней еще называют отбраковкой обратной стороны ( или backface culling ). Для теста, можете заменить в условии res > 0 на res < 0 и увидите внутреннюю часть пирамиды, т.к. теперь мы рисуем отвернутые от камеры треугольники.

На этом часть заканчивается, но приключение продолжается! В этот раз мы научились выводить модель при помощи треугольников. Посмотрели как считать нормали и как при помощи этих нормалей скрывать невидимую часть фигуры, воспользовавшись новой операцией, скалярным произведением векторов.

В следующей части мы посмотрим как можно закрашивать треугольники в интересующий нас цвет, научимся отсекать слишком близкие и далекие пиксели ( используем near / far параметры матрицы перспективы ) а также познакомимся с z-буфером, при помощи которого будем рисовать пиксели нескольких моделей на экране так, чтобы они правильно перекрывали друг друга. И даже сделаем первую игру, используя уже имеющиеся знания и наработки.

Код всего приложения

const ctx = document.getElementById('surface').getContext('2d');
const imageData = ctx.createImageData(800, 600);

class Vector {
  x = 0;
  y = 0;
  z = 0;
  w = 1;

  constructor(x, y, z, w = 1) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.w = w;
  }

  static substruct(v1, v2) {
    return new Vector(
      v1.x - v2.x,
      v1.y - v2.y,
      v1.z - v2.z,
      v1.w - v2.w
    );
  }

  static scalarProduct(a, b) {
    return a.x * b.x + a.y * b.y + a.z * b.z
  }

  static crossProduct(a, b) {
    return new Vector(
      a.y * b.z - a.z * b.y,
      a.z * b.x - a.x * b.z,
      a.x * b.y - a.y * b.x
    );
  }

  static add(v1, v2) {
    return new Vector(
      v1.x + v2.x,
      v1.y + v2.y,
      v1.z + v2.z
    );
  }

  multiplyByScalar(s) {
    this.x *= s;
    this.y *= s;
    this.z *= s;

    return this;
  }

  getLength() {
    return Math.sqrt(
      this.x * this.x + this.y * this.y + this.z * this.z
    );
  }

  normalize() {
    const length = this.getLength();

    this.x /= length;
    this.y /= length;
    this.z /= length;

    return this;
  }
}

class Matrix {
  static getLookAt(eye, target, up) {
    const vz = Vector.substruct(eye, target).normalize();
    const vx = Vector.crossProduct(up, vz).normalize();
    const vy = Vector.crossProduct(vz, vx).normalize();

    return Matrix.multiply(
      Matrix.getTranslation(-eye.x, -eye.y, -eye.z),
      [
        [vx.x, vx.y, vx.z, 0],
        [vy.x, vy.y, vy.z, 0],
        [vz.x, vz.y, vz.z, 0],
        [0, 0, 0, 1]
      ]);
  }

  static getPerspectiveProjection(fovy, aspect, n, f) {
    const radians = Math.PI / 180 * fovy;
    const sx = (1 / Math.tan(radians / 2)) / aspect;
    const sy = (1 / Math.tan(radians / 2));
    const sz = (f + n) / (f - n);
    const dz = (-2 * f * n) / (f - n);

    return [
      [sx, 0, 0, 0],
      [0, sy, 0, 0],
      [0, 0, sz, dz],
      [0, 0, -1, 0]
    ];
  }

  static multiply(a, b) {
    const m = [
      [0, 0, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0]
    ];

    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        m[i][j] = a[i][0] * b[0][j] +
          a[i][1] * b[1][j] +
          a[i][2] * b[2][j] +
          a[i][3] * b[3][j];
      }
    }

    return m;
  }

  static getRotationX(angle) {
    const rad = Math.PI / 180 * angle;

    return [
      [1, 0, 0, 0],
      [0, Math.cos(rad), -Math.sin(rad), 0],
      [0, Math.sin(rad), Math.cos(rad), 0],
      [0, 0, 0, 1]
    ];
  }

  static getRotationY(angle) {
    const rad = Math.PI / 180 * angle;

    return [
      [Math.cos(rad), 0, Math.sin(rad), 0],
      [0, 1, 0, 0],
      [-Math.sin(rad), 0, Math.cos(rad), 0],
      [0, 0, 0, 1]
    ];
  }

  static getRotationZ(angle) {
    const rad = Math.PI / 180 * angle;

    return [
      [Math.cos(rad), -Math.sin(rad), 0, 0],
      [Math.sin(rad), Math.cos(rad), 0, 0],
      [0, 0, 1, 0],
      [0, 0, 0, 1]
    ];
  }

  static getTranslation(dx, dy, dz) {
    return [
      [1, 0, 0, dx],
      [0, 1, 0, dy],
      [0, 0, 1, dz],
      [0, 0, 0, 1]
    ];
  }

  static getScale(sx, sy, sz) {
    return [
      [sx, 0, 0, 0],
      [0, sy, 0, 0],
      [0, 0, sz, 0],
      [0, 0, 0, 1]
    ];
  }

  static multiplyVector(m, v) {
    return new Vector(
      m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z + m[0][3] * v.w,
      m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z + m[1][3] * v.w,
      m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z + m[2][3] * v.w,
      m[3][0] * v.x + m[3][1] * v.y + m[3][2] * v.z + m[3][3] * v.w
    );
  }
}

class Drawer {
  surface = null;
  width = 0;
  height = 0;

  constructor(surface, width, height) {
    this.surface = surface;
    this.width = width;
    this.height = height;
  }

  drawPixel(x, y, r, g, b) {
    x += this.width / 2;
    y = -(y - this.height / 2);
    const offset = (this.width * y + x) * 4;

    if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
      this.surface[offset] = r;
      this.surface[offset + 1] = g;
      this.surface[offset + 2] = b;
      this.surface[offset + 3] = 255;
    }
  }

  drawLine(x1, y1, x2, y2, r = 0, g = 0, b = 0) {
    const round = Math.trunc;
    x1 = round(x1);
    y1 = round(y1);
    x2 = round(x2);
    y2 = round(y2);

    const c1 = y2 - y1;
    const c2 = x2 - x1;

    const length = Math.max(
      Math.abs(c1),
      Math.abs(c2)
    );

    const xStep = c2 / length;
    const yStep = c1 / length;

    for (let i = 0; i <= length; i++) {
      this.drawPixel(
        Math.trunc(x1 + xStep * i),
        Math.trunc(y1 + yStep * i),
        r, g, b
      );
    }
  }

  clearSurface() {
    const surfaceSize = this.width * this.height * 4;
    for (let i = 0; i < surfaceSize; i++) {
      this.surface[i] = 0;
    }
  }
}

let cameraDirection = new Vector(0, 0, -1, 0);
let cameraPos = new Vector(0, 0, 0);

const drawer = new Drawer(
  imageData.data,
  imageData.width,
  imageData.height
);

// Cube vertices
const vertices = [
  new Vector(-0.5, 1, 0.5), // 0 вершина
  new Vector(-0.5, 1, -0.5), // 1 вершина
  new Vector(0.5, 1, -0.5), // 2 вершина
  new Vector(0.5, 1, 0.5), // 3 вершина
  new Vector(-1, -1, 1), // 4 вершина
  new Vector(-1, -1, -1), // 5 вершина
  new Vector(1, -1, -1), // 6 вершина
  new Vector(1, -1, 1) // 7 вершина
];

const indices = [
  [0, 1, 2], // 0
  [0, 2, 3], // 1

  [4, 6, 5], // 2
  [4, 7, 6], // 3

  [0, 5, 1], // 4
  [0, 4, 5], // 5

  [1, 5, 2], // 6
  [6, 2, 5], // 7

  [3, 2, 6], // 8
  [3, 6, 7], // 9

  [3, 4, 0], // 10
  [4, 3, 7], // 11
];

let angle = 0;
setInterval(() => {
  let matrix = Matrix.getRotationX(0);

  matrix = Matrix.multiply(
    Matrix.getRotationY(angle += 1),
    matrix
  );

  matrix = Matrix.multiply(
    Matrix.getScale(100, 100, 100),
    matrix
  );

  matrix = Matrix.multiply(
    Matrix.getTranslation(0, 0, -300),
    matrix
  );

  matrix = Matrix.multiply(
    Matrix.getLookAt(
      cameraPos,
      Vector.add(cameraPos, cameraDirection),
      new Vector(0, 1, 0)
    ),
    matrix
  );

  matrix = Matrix.multiply(
    Matrix.getPerspectiveProjection(
      90, 800 / 600,
      -1, -1000),
    matrix
  );

  const sceneVertices = [];
  for (let i = 0; i < vertices.length; i++) {
    let vertex = Matrix.multiplyVector(
      matrix,
      vertices[i]
    );

    vertex.x = vertex.x / vertex.w * 400;
    vertex.y = vertex.y / vertex.w * 300;

    sceneVertices.push(vertex);
  }

  drawer.clearSurface();

  for (let i = 0, l = indices.length; i < l; i++) {
    const e = indices[i];

    let v1 = sceneVertices[e[0]]
    let v2 = sceneVertices[e[1]]
    let v3 = sceneVertices[e[2]]

    let t1 = Vector.substruct(v1, v2)
    let t2 = Vector.substruct(v2, v3)

    let normal = Vector.crossProduct(t1, t2).normalize()

    let res = Vector.scalarProduct(cameraDirection, normal)

    if (res > 0) {
      drawer.drawLine(
        v1.x,
        v1.y,
        v2.x,
        v2.y,
        0, 0, 255
      );

      drawer.drawLine(
        v2.x,
        v2.y,
        v3.x,
        v3.y,
        0, 0, 255
      );

      drawer.drawLine(
        v1.x,
        v1.y,
        v3.x,
        v3.y,
        0, 0, 255
      );
    }
  }

  ctx.putImageData(imageData, 0, 0);
}, 100);

Комментарии (3)


  1. shaman4d
    25.01.2022 16:48
    +1

    Спасибо! Я ждал и надеялся на новый урок.


    1. da-nie
      25.01.2022 17:39
      +1

      Найдите книжку Шикин, Боресков «Компьютерная графика. Полигональные модели». Там всё необходимое есть.


  1. Roma_letchik
    26.01.2022 14:03
    +2

    Очень полезный материал. Добавьте в начало ссылки на предыдущие части.