В этом уроке я обьясню вам, как находить нормали полигонов.

Все математические функции мы вынесем в отдельный класс: 3dmath.h/.cpp
Отныне вы сможете простым подключением этих файлов использовать их функции в любых своих проектах.

Файлы 3dmath.h/.cpp будут содержать математические функции, не только для этого урока, но и на будущее. Математика — необходимая часть програмирования игр, и в особенности трёхмерных. Если вы не чувствуете себя уверенно в основах алгебры и геометрии, займитесь ими, пока не начнете ориентироватся. Вы не сможете стать эффективным 3д программистом без знания этих основ. Однако не гордитесь, если вы их знаете. Остается всё ещё очень много вещей помимо основ 😉

Итак, новый файл 3dmath.h

#ifndef _3DMATH_H
#define _3DMATH_H// Эта структура используется для хранения 3д точек и векторов. Она использовалась
// в наших уроках по камере, вернитесь назад, чтобы узнать подробности.
struct CVector3
{
public:
float x, y, z;
};

// Возвращает вектор, перпендикулярный двум переданным векторам (плоскости)
CVector3 Cross(CVector3 vVector1, CVector3 vVector2);

// Возвращает вектор между 2мя точками.
CVector3 Vector(CVector3 vPoint1, CVector3 vPoint2);

// Возвращает величину нормали или любого другого вектора
float Magnitude(CVector3 vNormal);

// Возвращает нормализованный вектор (его длинна становится равной 1)
CVector3 Normalize(CVector3 vNormal);

// Возвращает нормаль полигона (направление, куда повернут полигон)
CVector3 Normal(CVector3 vTriangle[]);

#endif

 

Файл 3dmath.cpp:

#include «3dmath.h»
#include <math.h>// * Нахождение нормали полигона *

// Чтобы найти нормаль полигона, нам нужно найти результат cross-a от двух
// векторов этого полигона. В общем, это всё, что нам нужно для получения направлений
// двух сторон треугольника. В конце концов, вектор — это только направление и длинна.
// Длинна вектора в нашем случае не важна. Нам нужно только узнать направление.
// Итак, имея 2 вектора треугольника, мы можем найти вектор, стоящий перпендикулярно
// к полигону.
// Теперь, в зависимости от порядка следования вершин, нормаль будет расположена с
// какой-то из сторон полигона. Вам остаётся только решить, в каком порядке отрисовывать
// вершины — ВСЕГДА запоминайте это.
// Обычно полигоны отрисовываются только с одной стороны. Никому не нужно второй раз
// отрисовывать сторону, которую не видно. Задумайтесь, если у вас есть какая-нибуть 3д
// модель, нужно ли вам отрисовывать внутренние стороны её полигонов? Конечно нет,
// это безсмысленно.
//

/////////////////////////////////////// CROSS \\\\\*
/////
/////   Возвращает вектор, перпендикулярный 2м переданным.
/////
/////////////////////////////////////// CROSS \\\\\*

CVector3 Cross(CVector3 vVector1, CVector3 vVector2)
{
CVector3 vNormal;                       // результирующий вектор

// Еще раз, если у нас есть 2 вектора (2 стороны полигона), у нас есть плоскость.
// cross находит вектор, перпендикулярный плоскости, составляемой 2мя векторами.
// Формула в принципе проста, но сложна для запоминания:

// Значение X для векторы вычисляется так:  (V1.y * V2.z) — (V1.z * V2.y)
vNormal.x = ((vVector1.y * vVector2.z) (vVector1.z * vVector2.y));

// Значение Y для векторы вычисляется так:  (V1.z * V2.x) — (V1.x * V2.z)
vNormal.y = ((vVector1.z * vVector2.x) (vVector1.x * vVector2.z));

// Значение Z для векторы вычисляется так:  (V1.x * V2.y) — (V1.y * V2.x)
vNormal.z = ((vVector1.x * vVector2.y) (vVector1.y * vVector2.x));

return vNormal;  // Возвращаем результат (направление, куда направлен полигон — нормаль)
}

/////////////////////////////////////// VECTOR \\\\\*
/////
/////   Возвращает вектор между 2мя точками.
/////
/////////////////////////////////////// VECTOR \\\\\*

CVector3 Vector(CVector3 vPoint1, CVector3 vPoint2)
{
CVector3 vVector = {0};

// Чтобы получить вектор между 2 точками (направление), нужно вычесть вторую
// точку из первой.

vVector.x = vPoint1.x vPoint2.x;
vVector.y = vPoint1.y vPoint2.y;
vVector.z = vPoint1.z vPoint2.z;

// Теперь возвращаем полученный результат

return vVector;
}

/////////////////////////////////////// MAGNITUDE \\\\\*
/////
/////   возвращает величину нормали
/////
/////////////////////////////////////// MAGNITUDE \\\\\*

float Magnitude(CVector3 vNormal)
{
return (float)sqrt( (vNormal.x * vNormal.x) +
(vNormal.y * vNormal.y) +
(vNormal.z * vNormal.z) );
}

/////////////////////////////////////// NORMALIZE \\\\\*
/////
/////   возвращает нормализованный вектор (с длинной 1)
/////
/////////////////////////////////////// NORMALIZE \\\\\*

CVector3 Normalize(CVector3 vNormal)
{
float magnitude = Magnitude(vNormal);

vNormal.x /= magnitude;
vNormal.y /= magnitude;
vNormal.z /= magnitude;

return vNormal;
}

/////////////////////////////////////// NORMAL \\\\\*
/////
/////   Возвращает нормаль полигона
/////
/////////////////////////////////////// NORMAL \\\\\*

CVector3 Normal(CVector3 vTriangle[])
{
CVector3 vVector1 = Vector(vTriangle[2], vTriangle[0]);
CVector3 vVector2 = Vector(vTriangle[1], vTriangle[0]);

// В функцию передаются три вектора — треугольник. Мы получаем vVector1 и vVector2 — его
// стороны. Теперь, имея 2 стороны треугольника, мы можем получить из них cross().
// (*ЗАМЕЧАНИЕ*) Важно: первым вектором мы передаём низ треугольника, а вторым — левую
// сторону. Если мы поменяем их местами, нормаль будет повернута в противоположную
// сторону. В нашем случае мы приняли решение всегда работать против часовой.

CVector3 vNormal = Cross(vVector1, vVector2);

// Теперь, имея направление нормали, осталось сделать последнюю вещь. Сейчас её
// длинна неизвестна, она может быть очень длинной. Мы сделаем её равной 1, это
// называется нормализация. Чтобы сделать это, мы делим нормаль на её длинну.
// Ну а как найти длинну? Мы используем эту формулу: magnitude = sqrt(x^2 + y^2 + z^2)

vNormal = Normalize(vNormal);

// Теперь вернём «нормализованную нормаль» =)
// (*ПРИМЕЧАНИЕ*) если вы хотите увидеть, как работает нормализация, закомментируйте
// предидущую линию. Вы увидите, как длинна нормаль до нормалицации. Я стого рекомендую
// всегда использовать эту функцию. И запомните, неважно, какова длинна нормали
// (конечно, кроме (0,0,0)), если мы её нормализуем, она всегда будет равна 1.

return vNormal;
}

 

Теперь файл main.cpp; мы используем в нём описанные только что функции.

// Для начала после инклуда main.h подключим новый файл:
#include «3dmath.h»// Участок ниже — важен. Мы создаем массив трех векторов и сохраняем в него треугольник.
// Обратите внимание, мы инициализируем сначала левую точку треугольника, потом
// правую, потом верхнюю. Против часовой стрелки. Это важно, надо запомнить этот порядок.
// Иначе начнутся проблемы с направлением наших нормалей. Просто примите такой порядок
// как стандарт.
// Конечно, вы можете при желании изменить направление нормалей
// в OpenGL (см. glFrontFace() glCullFace().
// Разместите эту строку после включения хидеров:

CVector3 vTriangle[3] = { {1,  0,   0},    {1,    0,   0},  {0,   1,   0} };

// Теперь перейдём к функции WinProc(). Чтобы сделать урок нагляднее,
// напишем обработку дополнительных клавиш:
// Если нажато «LEFT» или «RIGHT», двигаем треугольник
// и нормаль. Заметьте, что нормаль следует за центром треугольника, когда тот
// движется.

case WM_KEYDOWN:
switch(wParam)
{
case VK_ESCAPE:
PostQuitMessage(0);
break;

case VK_LEFT:
vTriangle[2].z -= 0.01f;
vTriangle[2].y -= 0.01f;
break;

case VK_RIGHT:
vTriangle[2].z += 0.01f;
vTriangle[2].y += 0.01f;
break;
}

break;
///////////////////////////////////

// И последний этап, переходим к отрисовке:
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

// Изменяем позицию камеры, чтобы лучше видеть нормаль под углом. Смещаемся немного
// влево и вперёд от треугольника. И приподнимаем, чтобы смотреть немного сверху.

//      Position          View       Up Vector
gluLookAt(2.5f, 0, 0.5f,  0, 0.5f, 0,   0, 1, 0);

// Ниже рисуем треугольник, передавая данные из массива векторов.

glBegin (GL_TRIANGLES);

glColor3ub(255, 0, 0);
glVertex3f(vTriangle[0].x, vTriangle[0].y, vTriangle[0].z);

glColor3ub(0, 255, 0);
glVertex3f(vTriangle[1].x, vTriangle[1].y, vTriangle[1].z);

glColor3ub(0, 0, 255);
glVertex3f(vTriangle[2].x, vTriangle[2].y, vTriangle[2].z);
glEnd();

// Теперь, отобразив треугольник, давайте отобразим линию, идущую из него под
// углом 90 градусов, чтобы показать, на что похожа нормаль. Запомните, у нормали НЕТ
// позиции, только направление, но просто чтобы визуализировать, нарисуем её исходящей
// из центра треугольника.

// Получим нормаль треугольника, передав его массив.
CVector3 vNormal = Normal(vTriangle);

// Теперь просто для удобства найдем центр треугольника.
// Мы знаем, что середина по X = 0, так что её нам находить не надо.
CVector3 vCenter = {0};

// Ищем центр треугольника.
// Чтобы найти значение Y, просто делим верхнюю точку Y на 2, так как отсчет идёт от 0.
// Сделаем то же для Z, просто делим значение Z верхней точки на 2, т.к. отсчет идет от
// нуля по оси Z, и только верхняя точка треугольника будет двигатся.
// Если бы треугольник не был так просто расположен, пришлось бы сделать всё немного
// сложнее. Чтобы найти центр обьекта, сложите все значения X, Y, и Z, затем
// разделите каждое
// на количество вершин, и это даст вам центр. Например, после получения итога (total):
// total.x /= totalVertices; total.y /=totalVertices; total.z /= totalVertices;
// Я решил не делать отдельной функции для наших рассчетов, но вам — придется 😉

vCenter.y = vTriangle[2].y / 2;
vCenter.z = vTriangle[2].z / 2;

// Давайте нарисуем линию из центра треугольника.
// Первая точка будет центром, который мы только что вычислили.
// Следующая будет центром + нормаль треугольника.

glBegin (GL_LINES);

glColor3ub(255, 255, 0);
// Первая точка — центр треугольника:
glVertex3f(vCenter.x, vCenter.y, vCenter.z);
// Вторая — другой конец нормали:
glVertex3f(vCenter.x + vNormal.x, vCenter.y + vNormal.y, vCenter.z + vNormal.z);
glEnd();
// Это всё, теперь используйте клавиатуру, чтобы увидеть нормаль в движении.
SwapBuffers(g_hDC);
}

 

Вот и всё! Это основы нахождения нормалей. Ни о чем не приходится заботится, функции, написанные один раз, дальше всё делают сами. Нашу мы назвали «Normal()».
Не важно, сколько у полигона вершин, хоть 20 — всё, что нужно, это 3 его точки (первые 3). Этого достаточно, чтобы найти плоскость. Потом находим от неё нормаль.

Исходные коды к уроку