Этот урок был написан, чтобы показать способ выбирать мышью 3D объекты.
Коротко: сначала мы получаем координаты (экранные) курсора мыши в момент клика, затем проверяем, нет ли в этом регионе проекции на экран какого-либо 3д объекта.
Эта технология может быть очень полезна для всяческих редакторов уровней и/или интерфейсов. Основная идея — перед отрисовкой потенциально выбираемого объекта присваиваем ему ID.
Исходные коды взяты из урока «Загрузка текстур».
Редактируем файл main.cpp:
#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;