Как найти нормаль треугольника

 
Drimmer
 
(2003-01-08 20:42)
[0]

Сабж..как?.помогите… совсем забыл геометрию=)


 
Ketmar
 
(2003-01-08 21:02)
[1]

Vector Product – дает перпендикуляр. формулу, если сами не найдете – завтра… или завтра кину линк на EMLib.

Satanas Nobiscum! 08-Jan-XXXVIII A.S.


 
Ev_genus
 
(2003-01-09 00:11)
[2]



Пусть треугольник задан тремя точками

A(x1, y1, z1)

B(x2, y2, z2)

C(x3, y3, z3)

n=AB*AC=

| i j k |

| x2-x1 y2-y1 z2-z1 | =

| x3-x1 y3-y1 z3-z1 |

=i*((y2-y1)*(z3-z1)-(z2-z1)*(y3-y1))-

-j*((x2-x1)*(z2-z1)-(z2-z1)*(x3-x1))+

+k*((x2-x1)*(y3-y1)-(y2-y1)*(x3-x1))

где i=(1, 0, 0) j=(0, 1, 0) k=(0, 0, 1) - орты координатных осей

следовательно

nx=(y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)

ny=(z2-z1)*(x3-x1)-(x2-x1)*(z2-z1)

nz=(x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)

нормаль считается с точностью до знака, как правая тройка векторов.



I am in a graphics programming class and I am doing the written homework, not programming, so I hope this is appropriate for this site. I have this problem:

Compute the unit normal for the triangles specified by each of the
following vertex sets (assume the triangles are facing away from the
origin):

I took linear algebra over a year ago, my teacher then said that he wouldn’t teach cross products because only the Computer Science people in the class would need it and it would be covered when they needed it (it wasn’t because they assumed the linear algebra teacher did it), and I have checked two dozen explanations and they are all way over my head.

This problem has three different problems within it, so if someone could walk me through how to solve a single one that didn’t involve tons of variables and Greek letters it would be greatly appreciated.

Part A of this problem has these three coordinates as the points of the triangle: [1, 1, 1]; [1, -1, 1]; [1, 0, -1]. I tried cobbling together different formulas and explanations and I got that the normal vector is [4, 0, 0], but that doesn’t seem right since I know enough to know that this triangle doesn’t lie on the y-z plane. The only other thing I have is the formula:

(A x B) / | A x B |

I know that A and B are two random sides of the triangle represented as a vector, and calculated by subtracting V2 and V1 for A and V3 and V1 for B, but I don’t understand what exactly it is telling me to do.

Время на прочтение
14 мин

Количество просмотров 5.3K

Все части

  • Часть 1: пиксели и линии
    ( рендеринг, поверхность, пиксель, линия )

  • Часть 2: оно трехмерное
    ( система координат, точка, вектор, матрица, вершина, индекс, конвейер визуализации )

  • Часть 3: чем дальше в лес, тем меньше дом
    ( камера при помощи матриц вида и перспективы )

  • Часть 4: треугольник невидимка
    ( треугольный полигон, нормаль )

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

Для начала возьмите код с усеченной пирамидой из прошлой части бонусного раздела ( 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);

Метод координат — весьма эффективный и универсальный способ нахождения любых углов или расстояний между стереометрическими объектами в пространстве. Если Ваш репетитор по математике имеет высокую квалификацию, то он должен это знать. В противном случае я бы советовал для «С» части сменить репетитора. Моя подготовка к ЕГЭ по математике С1-С6 обычно включает разбор основных алгоритмов и формул, описанных ниже.

Угол между прямыми а и b

Метод координат - угол между прямыми

Углом между прямыми в пространстве называется угол между любыми параллельными им пересекающимися прямыми. Этот угол равен углу между направляющими векторами данных прямых (или дополняет его до 180 град).

Какой алгоритм использует репетитор по математике для поиска угла?

1) Выбираем любые вектора overrightarrow{AB} и overrightarrow{CD}, имеющие направления прямых а и b (параллельные им).
2) Определяем координаты векторов overrightarrow{AB}(x_1;y_1) и overrightarrow{CD}(x_2;y_2) по соответствующим координатам их начал и концов (от координат конца вектора нужно отнять координаты начала).
3) Подставляем найденный координаты в формулу:
Cos (widehat{AB,CD}) =left vert Cos(widehat{ overrightarrow{AB},overrightarrow{CD}}) right vert =left vert dfrac{x_1x_2+y_1y_2}{sqrt{x_1^2+y_1^2} cdot sqrt{x_2^2+y_2^2}} right vert . Для нахождения самого угла, нужно найти арккосинус полученного результата.

Нормаль к плоскости

Нормалью vec{n} к плоскости называется любой вектор, перпендикулярный к этой плоскости.
Как найти нормаль? Для поиска координат нормали достаточно узнать координаты любых трех точек M, N и K, лежащих в данной плоскости. По этим координатам находим координаты векторов overrightarrow{MN} и overrightarrow{MK} и требуем выполнения условий overrightarrow{n} perp overrightarrow{MN} и overrightarrow{n} perp overrightarrow{MK}. Приравнивая скалярные произведение векторов к нулю, составляем систему уравнений с тремя переменными, из которой можно найти координаты нормали.

Замечание репетитора по математике: Совсем не обязательно решать систему полностью, ибо достаточно подобрать хотя бы одну нормаль. Для этого можно подставить вместо какой-нибудь из ее неизвестных координат любое число (например единицу) и решить систему двух уравнений с оставшимися двумя неизвестными. Если она решений не имеет, то это значит, что в семействе нормалей нет той, у которой по выбранной переменной стоит единица. Тогда подставьте единицу вместо другой переменной (другой координаты) и решите новую систему. Если опять промахнетесь, то Ваша нормаль будет иметь единицу по последней координате, а сама она окажется параллельной какой-нибудь координатной плоскости (в таком случае ее легко найти и без системы).

Угол между прямой и плоскостью

Угол между прямой и плоскостьюДопустим, что нам заданы прямая и плоскость координатами направляющего вектора overrightarrow{AB}(x_1;y_1) и нормали overrightarrow{n}(x_2;y_2)
Угол psi между прямой и плоскость вычисляется по следующей формуле:
Sin psi = left vert Cos(widehat{ overrightarrow{n},overrightarrow{AB}}) right vert = left vert dfrac{x_1x_2+y_1y_2}{sqrt{x_1^2+y_1^2} cdot sqrt{x_2^2+y_2^2}} right vert

Угол между плоскостями

Пусть overrightarrow{n_1} (x_1;y_1) и overrightarrow{n_1} (x_1;y_1) — две любые нормали к данным плоскостям. Угол между плоскостями Тогда косинус угла boldsymbol{psi} между плоскостями равен модулю косинуса угла между нормалями:

Cos psi = left vert Cos(widehat{ overrightarrow{n_1},overrightarrow{n_2}}) right vert =left vert dfrac{x_1x_2+y_1y_2}{sqrt{x_1^2+y_1^2} cdot sqrt{x_2^2+y_2^2}} right vert

Уравнение плоскости в пространстве

Плоскость, заданная уравнениемТочки, удовлетворяющие равенству A cdot x + B cdot y + C cdot z + D =0 образуют плоскость с нормалью overrightarrow{n}(A;B;C). Коэффициент D отвечает за величину отклонения (параллельного сдвига) между двумя плоскостями с одной и той же заданной нормалью overrightarrow{n}(A;B;C). Для того, чтобы написать уравнение плоскости нужно сначала найти ее нормаль (как это описано выше), а затем подставить координаты любой точки плоскости вместе с координатами найденной нормали в уравнение A cdot x + B cdot y + C cdot z + D =0 и найти коэффициент D.

Расстояние от точки до плоскости

Расстояние от точки до плоскости
Для вычисления расстояния rho(M;alpha) от точки M(x_0;y_0;z_0) до плоскости alpha, заданной уравнением A cdot x + B cdot y + C cdot z + D =0 можно использовать следующую формулу:

rho(M;alpha)=dfrac{|A cdot x_0 + B cdot y_0 + C cdot z_0 + D|}{sqrt{A^2+B^2+C^2}}
В знаменателе стоит длина нормали, а числителе — значение выражения из левой части уравнения плоскости в точке M(x_0;y_o;z_0)

Комментарий репетитора по математике:

Методом координат можно находить не только углы и расстояния в пространстве, но и
1) площади многоугольников (треугольника, параллелограмма), расположенных в заданной плоскости.
2) объемы простейших многогранников (параллелепипедов и пирамид).

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

Средства аналитической геометрии репетитор по математике практически не использует в работе со средним и тем более слабым учеником. И очень жаль, что загруженность среднестатистического сильного школьника не позволяет репетитору провести более-менее серьезную работу на уровне определений из высшей математики и с соответствующей практикой решения задач. Поэтому я часто ограничиваюсь простым сообщением формул и демонстрацией одного – двух примеров их использования. В школьной программе не предусмотрено время для изучения векторных приемов вообще, однако на ЕГЭ Вы имеете право решать задачу С2 любым из известных науке способов. Отсюда мораль: учите координаты. Расширенная подготовка к ЕГЭ по математике с изучением приемов аналитической геометрии даст Вам мощное и универсальное средство для решения огромного класса задач типа С2. Пользуйтесь этой страничкой на здоровье!

Колпаков А.Н. Репетитор по математике Москва (Строгино).

типо того?)
Задан треугольник ABC.
A(x1,y1,z1)
B(x2,y2,z2)
C(x3,y3,z3)
Как найти вектор (координаты) нормали в точке B к этому треугольнику?
Гм. Весь треугольник лежит в одной плоскости. Поэтому направление нормали будет совершенно одинаковым в любой точке. Для того, чтобы его определить, необходимо и достаточно вычислить векторное произведение любых двух векторов, образующих стороны треугольника.
Например, [BA x BC]:

N = {BAy*BCz-BAz*BCy, BAz*BCx-BAx*BCz, BAx*BCy-BAy*BCx} =
= {(y1-y2)*(z3-z2)-(z1-z2)*(y3-y2),
(z1-z2)*(x3-x2)-(x1-x2)*(z3-z2),
(x1-x2)*(y3-y2)-(y1-y2)*(x3-x2)} =
= {y1z3 – y2z3 – y1z2 + y2z2 – z1y3 + z2y3 + z1y2 – z2y2,
z1x3 – z2x3 – z1x2 + z2x2 – x1z3 + x2z3 + x1z2 – x2z2,
x1y3 – x2y3 – x1y2 + x2y2 – y1x3 + y2x3 + y1x2 – y2y2} =
= {y1z3 – y2z3 – y1z2 – z1y3 + z2y3 + z1y2,
z1x3 – z2x3 – z1x2 – x1z3 + x2z3 + x1z2,
x1y3 – x2y3 – x1y2 – y1x3 + y2x3 + y1x2}

Добавить комментарий