OpenGL: Выбор 3D объекта

Этот урок был написан, чтобы показать способ выбирать мышью 3D объекты.
Коротко: сначала мы получаем координаты (экранные) курсора мыши в момент клика, затем проверяем, нет ли в этом регионе проекции на экран какого-либо 3д объекта.
Эта технология может быть очень полезна для всяческих редакторов уровней и/или интерфейсов. Основная идея — перед отрисовкой потенциально выбираемого объекта присваиваем ему ID.

Исходные коды взяты из урока «Загрузка текстур».
Редактируем файл main.cpp:

// Обьявим глобальные ID для планет:
#define SUN 100     // ID обьекта для Солнца
#define EARTH   101     // ID обьекта для Земли
#define PLUTO   102     // ID обьекта для Плутона// Изменим количество загружаемых текстур до трех:
TextureImage textures[3];// Добавляем глобальные переменные вращения планет:
float SunRotation   = 90;
float EarthRotation = 90;
float PlutoRotation = 90;///////////////////////////////////

// В функции Init добавим загрузку трёх текстур:
Texture->LoadTexture(IL_BMP, «Sun.bmp», &textures[0]);
Texture->LoadTexture(IL_BMP, «Earth.bmp», &textures[1]);
Texture->LoadTexture(IL_BMP, «Pluto.bmp», &textures[2]);

///////////////////////////////////

// Изменяем функцию RenderScene (), в которой будет происходить вся отрисовка:

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

gluLookAt(0, 3, 6,     0, 0, 0,     0, 1, 0);

glInitNames();  // Эта функция очищает массив имён, так что перед рендером мы не имеем ни одного ID

GLUquadricObj *pObj = gluNewQuadric();      // Создадим новый quadric-обьект

gluQuadricTexture(pObj, true);          // Включим использование текст. координат в quadric-обьекте

// Наложим текстуру на сферу солнца
glBindTexture(GL_TEXTURE_2D, textures[0].texID);

// Сейчас мы вызовем функцию glPushName (). Нам нужно передать в неё ID, чтобы позже его проварять.
// Вот как это работает. Мы вызываем glPushName () и передаём в него ID обьекта. Функция заносит
// ID в стек имён. Потом мы отрисовываем любые примитивы и вызываем ф-ю glPopName, которая остановит
// привязку обьектов к данному ID. Наше ID «SUN» мы привяжем к обьекту, отрисованному ниже:

glPushName(SUN);        // Привязываем все следующие примитивы к ID «SUN»

// Теперь переходим в новую матрицу, чтобы изменения не затронули остальные обьекты.
// Сначала перемещаем сферу в начало координат (0,0,0), потом вращаем вокруг оси Y.
// Это даст эффект вращения. Потом отрисовываем самую большую нашу сферу с наложенной
// текстурой солнца.

glPushMatrix();     // Входим в новую матрицу
glTranslatef(0, 0, 0);          // Перемещаем сферу
glRotatef(SunRotation, 0, 1.0, 0);  // Вращаем по оси Y
gluSphere(pObj, 0.5f, 20, 20);      // Рисуем сферу с радиусом 0.5
glPopMatrix();      // Заканчиваем работу в этой матрице

// Теперь, закончив рендер солнца, нужно закрыть пространство имени. Для этого
// вызываем glPopName (). Теперь ничего не будет ассоциировано с этим ID.

glPopName();    // Заканчиваем работу с ID «SUN»

// Далее накладываем текстуру земли
glBindTexture(GL_TEXTURE_2D, textures[1].texID);

// Еще раз: нам нужно создать ID обьекта для земли, так что передаем в glPushName ID «EARTH».
// Теперь, после отрисовки, следующая сфера будет ассоциирована с этим ID.

glPushName(EARTH);  // Привязываем ID «EARTH»

// Ещё раз, сначала нам нужно войти в новую матрицу, чтобы не затронуть остальные сферы.
// Вращаем сферу на её текущее значение вращения ПРЕЖДЕ, чем перемещаем её.
// Таким образом она будет вращатся вокруг солнца. Затем ещё раз вращаем по
// оси Y для вращения по собственной оси.

glPushMatrix();         // Входим в новую систему координат
glRotatef(EarthRotation / 3, 0, 1.0, 0);    // Вращаем сферу вокруг солнца
glTranslatef(2, 0, 0);             // Перемещаем влево
glRotatef(EarthRotation, 0, 1.0, 0);        // Вращаем по оси Y
gluSphere(pObj, 0.2f, 20, 20);          // Рисуем сферу с радиусом 0.2
glPopMatrix();          // Выходим из текущей системы координат

// Закончили присваивание ID «EARTH», скажем об этом OpenGL:
glPopName();

// Накладываем текстуру Плутона:
glBindTexture(GL_TEXTURE_2D, textures[2].texID);

// Передаём ID «PLUTO»:
glPushName(PLUTO);

// Так же, как делали с Землей, сначала вращаем Плутон вокруг солнца, затем
// перемещаем, и наконец вращаем вокруг своей оси.

// Передадим пустой код glBegin () и glEnd (), т.к. используем quadric-обьекты.
// Если этого не сделать, на некоторых видеокартах программа работать не будет.
//glBegin (GL_LINES);
//glEnd ();

glPushMatrix();     // Новая матрица
glRotatef(PlutoRotation / 2, 0, 1.0, 0);        // Вращение вокруг солнца
glTranslatef(3, 0, 0);              // Перемещение
glRotatef(PlutoRotation, 0, 1.0, 0);        // Вращение вокруг оси
gluSphere(pObj, 0.1f, 20, 20);          // Рисуем сферу с радиусом 0.1
glPopMatrix();      // Выходим из матрицы

// Выходим из пространства имён:
glPopName();

SwapBuffers(g_hDC);

gluDeleteQuadric(pObj);  // Освободим память, занятую Quadric-обьектом

// И увеличим значения вращения каждой сферы:

SunRotation   += 0.2f;
EarthRotation += 0.5f;
PlutoRotation += 0.6f;
}

//////////////////////////////////////////////////////////////
//
// Теперь нам нужно написать функцию, получающую ID кликнутого обьекта:

int RetrieveObjectID(int x, int y)
{
int objectsFound = 0;   // Общее количество кликнутых обьектов
int viewportCoords[4] = {0};    // Массив для хранения экранных координат

// Переменная для хранения ID обьектов, на которые мы кликнули.
// Мы делаем массив в 32 элемента, т.к. OpenGL также сохраняет другую
// информацию, которая нам сейчас не нужна. Для каждого обьекта нужно
// 4 слота.
unsigned int selectBuffer[32] = {0};

// glSelectBuffer регистрирует массив как буфер выбора обьектов. Первый параметр — размер
// массива. Второй — сам массив для хранения информации.

glSelectBuffer(32, selectBuffer);   // Регистрируем буфер для хранения выбранных обьектов

// Эта функция возвращает информацию о многих вещах в OpenGL. Мы передаём GL_VIEWPOR,
// чтобы получить координаты экрана. Функция сохранит их в переданном вторым параметром массиве
// в виде top,left,bottom,right.
glGetIntegerv(GL_VIEWPORT, viewportCoords); // Получаем текущие координаты экрана

// Теперь выходим из матрицы GL_MODELVIEW и переходим в матрицу GL_PROJECTION.
// Это даёт возможность использовать X и Y координаты вместо 3D.

glMatrixMode(GL_PROJECTION);    // Переходим в матрицу проекции

glPushMatrix();         // Переходим в новые экранные координаты

// Эта функция делает так, что фреймбуфер не изменяется при рендере в него, вместо этого
// происходит запись имён (ID) примитивов, которые были бы отрисованы при режиме
// GL_RENDER. Информация помещается в selectBuffer.

glRenderMode(GL_SELECT);    // Позволяет рендерить обьекты без изменения фреймбуфера

glLoadIdentity();       // Сбросим матрицу проекции

// gluPickMatrix позволяет создавать матрицу проекции около нашего курсора. Проще говоря,
// рендерится только область, которую мы укажем (вокруг курсора). Если обьект рендерится
// в этой области, его ID сохраняется (Вот он, смысл всей функции).
// Первые 2 параметра — X и Y координаты начала, следующие 2 — ширина и высота области
// отрисовки. Последний параметр — экранные координаты. Заметьте, мы вычитаем 'y' из
// НИЖНЕЙ экранной координаты. Мы сделали это, чтобы перевернуть Y координаты.
// В 3д-пространстве нулевые y-координаты начинаются внизу, а в экранных координатах
// 0 по y находится вверху. Также передаём регион 2 на 2 пиксела для поиска в нём обьекта.
// Это может быть изменено как вам удобнее.

gluPickMatrix(x, viewportCoords[3] y, 2, 2, viewportCoords);

// Далее просто вызываем нашу нормальную функцию gluPerspective, точно так же, как
// делали при инициализации.

gluPerspective(45.0f,(float)g_rRect.right/(float)g_rRect.bottom,0.1f,150.0f);

glMatrixMode(GL_MODELVIEW); // Возвращаемся в матрицу GL_MODELVIEW

RenderScene();          // Теперь рендерим выбранную зону для выбора обьекта

// Если мы вернёмся в нормальный режим рендеринга из режима выбора, glRenderMode
// возвратит число обьектов, найденных в указанном регионе (в gluPickMatrix ()).

objectsFound = glRenderMode(GL_RENDER); // Вернемся в режим отрисовки и получим число обьектов

glMatrixMode(GL_PROJECTION);    // Вернемся в привычную матрицу проекции
glPopMatrix();              // Выходим из матрицы

glMatrixMode(GL_MODELVIEW);     // Вернемся в матрицу GL_MODELVIEW

// УФФ! Это было немного сложно. Теперь нам нужно выяснить ID выбранных обьектов.
// Если они есть — objectsFound должно быть как минимум 1.

if (objectsFound > 0)
{
// Если мы нашли более 1 обьекта, нужно проверить значения глубины всех
// выбоанных обьектов. Обьект с МЕНЬШИМ значением глубины — ближайший
// к нам обьект, значит и щелкнули мы на него. В зависимости от того, что
// мы программируем, нам могут понадобится и ВСЕ выбранные обьекты (если
// некоторые были за ближайшим), но в этом уроке мы позаботимся только о
// переднем обьекте. Итак, как нам получить значение глубины? Оно сохранено
// в буфере выбора (selectionBuffer). Для каждого обьекта в нем 4 значения.
// Первое — "число имен в массиве имен на момент события, далее минимум и
// максимум значений глубины для всех вершин, которые были выбраны при прошлом
// событии, далее по содержимое массива имен, нижнее имя — первое;
// («the number of names in the name stack at the time of the event, followed
// by the minimum and maximum depth values of all vertices that hit since the
// previous event, then followed by the name stack contents, bottom name first.») — MSDN.
// Единстве, что нам нужно — минимальное значение глубины (второе значение) и
// ID обьекта, переданного в glLoadName () (четвертое значение).
// Итак, [0-3] — данные первого обьекта, [4-7] — второго, и т.д...
// Будте осторожны, так как если вы отображаете на экране 2Д текст, он будет
// всегда находится как ближайший обьект. Так что убедитесь, что отключили вывод
// текста при рендеринге в режиме GL_SELECT. Я для этого использую флаг, передаваемый
// в RenderScene (). Итак, получим обьект с минимальной глубиной!

// При старте установим ближайшую глубину как глубину первого обьекта.
// 1 — это минимальное Z-значение первого обьекта.
unsigned int lowestDepth = selectBuffer[1];

// Установим выбранный обьект как первый при старте.
// 3 — ID первого обьекта, переданный в glLoadName ().
int selectedObject = selectBuffer[3];

// Проходим через все найденные обьекты, начиная со второго (значения первого
// мы присвоили изначально).
for(int i = 1; i < objectsFound; i++)
{
// Проверяем, не ниже ли значение глубины текущего обьекта, чем предидущего.
// Заметьте, мы умножаем i на 4 (4 значения на каждый обьект) и прибавляем 1 для глубины.
if(selectBuffer[(i * 4) + 1] < lowestDepth)
{
// Установим новое низшее значение
lowestDepth = selectBuffer[(i * 4) + 1];

// Установим текущий ID обьекта
selectedObject = selectBuffer[(i * 4) + 3];
}
}

// Вернем выбранный обьект
return selectedObject;
}

// Если не щелкнули ни на 1 обьект, вернём 0
return 0;
}

////////////////////////////////////////////////////////////
//
// И последнее: добавим новый код в обработку событий:

// Если нажата левая кнопка мыши, нужно обработать X и Y координаты клика
// и проверить, не найдется ли по этим координатам обьект. Запомните, LOWORD и
// HIWORD значения lParam — это x и y координаты мыши. Вызываем RetrieveObjectID (),
// чтобы получить обьект, на который мы кликнули.

case WM_LBUTTONDOWN:                    // Если нажата ЛКМ

int objectID;

// передаём координаты курсора в функцию, отыскивающую обьект
objectID = RetrieveObjectID(LOWORD(lParam), HIWORD(lParam));

// Теперь просто делаем switch по нужным обьектам:

switch(objectID)        // Проверим objectID
{
case SUN:           // Мы щелкнули на Солнце!
MessageBox(NULL, «Солнце!», «Click», MB_OK);
break;
case EARTH:         // Мы щелкнули на Землю!
MessageBox(NULL, «Земля!», «Click», MB_OK);
break;
case PLUTO:         // Мы щелкнули на Плутон!
MessageBox(NULL, «Плутон!», «Click», MB_OK);
break;
}
break;

 

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

Понравилась статья? Поделиться с друзьями: