Этот урок покажет метод создания кривых Безье. Эти знания вам ОЧЕНЬ пригодятся в будущем, особенно когда вы увидите, насколько эти кривые популярны в играх (начиная с Quake). Например, мы будем использовать их для туннелей, маршрутов движения камеры, и др…
Чтобы продемонстрировать использование кривых безье, мы создадим маленькую сферу, которую сможем двигать вдоль кривой безье клавишами ВЛЕВО и ВПРАВО. Создание кривых Безье само по себе несложно, но поначалу расчеты могут вас напугать. Кривые Безье имеют как минимум три точки.
Первая точка — стартовая точка кривой. Вторая точка — контрольная точка кривой. Контрольная точка «контролирует» искривление кривой. Третья точка — конечная точка кривой. В этом уроке мы будем использовать 4 контрольные точки (включая начальную и конечную). Первая и последняя точка — начало и конец кривой, вторая и третья — для задания формы кривой.
Итак, приступим.
Исходный код взят из первого урока — «инициализация».
Файл main.cpp:
// Сначала обьявим две константы:
#define BALL_SPEED 0.02f // Скорость кругового вращения камеры
#define MAX_STEPS 25.0f // Число шагов, составляющих линию
// Создадим простенький класс для хранения вершин
struct CVector3
{
public:
float x, y, z;
};
float g_CurrentTime = 0.0f; // Текущая позиция сферы на кривой (между 0 и 1)
CVector3 g_vStartPoint = {—4, 0, 0}; // Стартовая точка кривой
CVector3 g_vControlPoint1 = {—2, 3, 2}; // Первая контрольная точка
CVector3 g_vControlPoint2 = { 2, —3, —2}; // Вторая контрольная точка
CVector3 g_vEndPoint = { 4, 0, 0}; // Конечная точка кривой
//////////////////////////////////////////////////////////////////////////////
//
// Расширим инициализацию:
void Init(HWND hWnd)
{
g_hWnd = hWnd;
GetClientRect(g_hWnd, &g_rRect);
InitializeOpenGL(g_rRect.right, g_rRect.bottom);
// Просто чтобы придать сфере больше реализма, включим дефолтное
// освещение и затенение. Сначала включим освещение и источник света,
// затем разрешим использовать цвет.
glEnable(GL_LIGHT0); // Источник света
glEnable(GL_LIGHTING); // Освещение
glEnable(GL_COLOR_MATERIAL); // Разрешим использование цветов
}
///////////////////////////////// POINT ON CURVE \\\\*
/////
///// Новая функция: возврщаем точку XYZ на кривой, в зависимости от «t» (от 0 до 1)
/////
///////////////////////////////// POINT ON CURVE \\\\*
CVector3 PointOnCurve(CVector3 p1, CVector3 p2, CVector3 p3, CVector3 p4, float t)
{
float var1, var2, var3;
CVector3 vPoint = {0.0f, 0.0f, 0.0f};
// Вот соль этого урока. Наже — формула для четырёх точек кривой безье:
// B(t) = P1 * ( 1 — t )^3 + P2 * 3 * t * ( 1 — t )^2 + P3 * 3 * t^2 * ( 1 — t ) + P4 * t^3
//
// Да, согласен, это не слишком простая формула, но зато она очень прямолинейна!
// Если вы были внимательны, вы увидели, что элементы в ней повторяются.
// «t» — время от 0 до 1. Вы можете подумать, что это дестанция протяжения кривой,
// и вы будете правы. P1 — P4 это 4 контрольные точки. Каждая из них имеет ассоциированные
// с ней (x, y, z). Вы заметили, что в функции встречаются несколько выражений (1 — t)?
// Чтобы немного подчистить наш код, мы занесём одинаковые выражения в переменную.
// Это немного поможет не запутатся в повторяющемся коде.
// Занесём в переменную (1-t):
var1 = 1 — t;
// Занесём в переменную (1-t)^3
var2 = var1 * var1 * var1;
// Занесем в переменную t^3
var3 = t * t * t;
// Теперь, обрезав некоторые рассчеты, просто следуем нашей функции.
// Я не буду вдаватся в детали рассчетов кривых безье, поскольку есть места в интернете,
// где всё расписано очень подробно. Ниже я их приведу.
vPoint.x = var2*p1.x + 3*t*var1*var1*p2.x + 3*t*t*var1*p3.x + var3*p4.x;
vPoint.y = var2*p1.y + 3*t*var1*var1*p2.y + 3*t*t*var1*p3.y + var3*p4.y;
vPoint.z = var2*p1.z + 3*t*var1*var1*p2.z + 3*t*t*var1*p3.z + var3*p4.z;
// Теперь у нас есть точка на кривой, вернём её.
return(vPoint);
}
///////////////////////////////// DRAW SPHERE \\\\*
/////
///// Новая функция: рисует сферу с центром в XYZ, и с переданным радиусом
/////
///////////////////////////////// DRAW SPHERE \\\\*
void DrawSphere(float x, float y, float z, float radius)
{
// Создадим сферу с помощью quadric-обьекта.
GLUquadricObj *pSphere = gluNewQuadric();
glPushMatrix();
glTranslatef(x, y, z);
// Рисуем сферу с переданным радиусом и детальностью 15.
gluSphere(pSphere, radius, 15, 15);
glPopMatrix();
gluDeleteQuadric(pSphere);
}
///////////////////////////////// RENDER SCENE \\\\*
/////
///// Отрисовка нашей сцены
/////
///////////////////////////////// RENDER SCENE \\\\*
void RenderScene()
{
static float rotateY = 0.0f; // Статическая переменная для вращения
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
// Ниже мы нарисуем кривую Безье с четырьмя контрольными точками.
// Затем, используя GL_LINES, нарисуем сегменты линий для этой кривой.
// Вы можете использовать клавиши ВЛЕВО/ВПРАВО, чтобы двигать сферу
// вдоль кривой.
gluLookAt(0, 0.5f, 10.0f, 0, 0.5f, 0, 0, 1, 0);
// Выключаем освещение, чтобы четко видеть кривую
glDisable(GL_LIGHTING);
glColor3ub(0, 255, 0);
CVector3 vPoint = {0.0f, 0.0f, 0.0f}; // инициализируем CVector3 для хранения точек
// Просто для динамики и чтобы видеть всё с разных сторон, я вращаю кривую и сферу
glRotatef(rotateY, 0.0f, 1.0f, 0.0f);
rotateY += 0.1f; // Увеличиваем вращение
// Просим OpenGL рендерить линию толщиной в 1.5
glLineWidth(1.5);
// До этого мы ещё не использовали GL_LINE_STRIP, так что давайте я обьясню.
// Вместо того, чтобы передавать первую точку линии, потом вторую, и т.д…
// мы можем сделать «полосу линий» (line strip). Это значит, что мы передаём
// только ОДНУ точку, и OpenGL соединяет их для нас. Если мы просто применим
// GL_LINES, будет отрисована кривая из _разбитых_ сегментов. Полоса линий же
// нарисует одну сплошную кривую. GL_STRIP очень удобна, а к тому же очень быстра.
// Вы можете также делать стрипы квадратов и триугольников.
glBegin(GL_LINE_STRIP);
// Здесь мы проходим вдоль кривой и получаем точки для построения
// полосы линий. Посколько наша функция PointOnCurve() принимает
// 4 точки и временное значение, мы используем цикл, начиная от 0 до
// MAX_STEPS. Так как время 0 — это начало кривой, и 1 — конец,
// мы делим 1 на количество нужных нам шагов. Меньшее значение шагов
// уменьшит число линий. По сути это просто цепочка линий, и
// чем больше шагов, тем более сглаженной будет кривая.
// Проходим по кривой, начиная с нуля и заканчивая на 1 + шаг.
// Так как используем полоску линий, нужно добавить ещё 1 точку как конечную.
for(float t = 0; t <= (1 + (1.0f / MAX_STEPS)); t += 1.0f / MAX_STEPS)
{
// Передаём 4 наших точки. Также передаём «t», текущее время от 0 до 1.
// Если передано 0 — получаем стартовую точку линии, если 1 — конечную.
// Всё, что между ними — точки на линии, например, 0.5 — точка в центре
// кривой.
// Получаем текущую точку на линии
vPoint = PointOnCurve(g_vStartPoint, g_vControlPoint1, g_vControlPoint2, g_vEndPoint, t);
// Рисуем текущую точку на расстоянии «t» вдоль по линии.
glVertex3f(vPoint.x, vPoint.y, vPoint.z);
}
glEnd();
// Теперь, нарисовав кривую, включим освещение, чтобы сферы были немного затенённые.
glEnable(GL_LIGHTING);
// Чтобы двигать сферу вдоль кривой, будем использовать ту же функцию, что и для
// отрисовки кривой, только в «t» будем передавать текущее положение сферы.
// В начале «время» сферы — 0, и это начало кривой. Нажав ПРАВУЮ стрелку, g_CurrentTime
// увеличится, и сфера сдвинется вдоль кривой с заданной скоростью (time+=BALL_SPEED).
// Когда g_CurrentTime достигает 1, останавливаемся. Если 0 — тоже останавливаемся.
// Получаем текущую точку на кривой
vPoint = PointOnCurve(g_vStartPoint, g_vControlPoint1,
g_vControlPoint2, g_vEndPoint, g_CurrentTime);
glColor3ub(255, 0, 0); // Установим цвет в красный
// Рисуем сферу с радиусом 0.2 в текущей точке кривой
DrawSphere(vPoint.x, vPoint.y, vPoint.z, 0.2f);
glColor3ub(255, 255, 0); // Желтый цвет
// Теперь нужно отобразить контрольные точки, чтобы вы лучше поняли, что они делают.
// Мы представим их маленькими желтыми сферами.
// Первая контрольная точка
DrawSphere(g_vControlPoint1.x, g_vControlPoint1.y, g_vControlPoint1.z, 0.1f);
// Вторая
DrawSphere(g_vControlPoint2.x, g_vControlPoint2.y, g_vControlPoint2.z, 0.1f);
SwapBuffers(g_hDC);
}
/////////////////////////////////////////////////////////////////////////////////////////////
//
// Наконец, изменим обработку клавиш:
//
case WM_KEYDOWN:
switch(wParam) {
case VK_ESCAPE:
PostQuitMessage(0);
break;
case VK_LEFT: // Если нажата ВЛЕВО
g_CurrentTime -= BALL_SPEED; // Уменьшаем «время» сферы по кривой
if(g_CurrentTime < 0) // Если меньше нуля
g_CurrentTime = 0; // устанавливаем в 0
break;
case VK_RIGHT: // Если нажата ВПРАВО
g_CurrentTime += BALL_SPEED; // Увеличиваем «время» сферы на кривой
if(g_CurrentTime > 1) // Если превысили 1
g_CurrentTime = 1; // Устанавливаем в 1
break;
}
break;