Выбор объектов средствами OpenGL.
Автор: terror
Введение
Буфер выбора
Буфер цвета
Введение
Само понятие выбора объектов больше подходит графическим редакторам. Обычно в играх нет смысла делать такую возможность. Исключением, конечно же, являются стратегии, там данная технология жизненно необходима. В стрелялках тоже иногда используется выбор объектов, но чаще до них нужно просто дотронуться. Поэтому чаще всего эта технология используется в редакторах. Необходимость выбрать примитив и делать над ним дальнейшие операции одно из первых условий нормальной работы графического редактора. Вы скорее всего не найдете программ, где идет работа с графикой и при этом нет возможности выбирать объекты. Также выбор можно делать и в трехмерных графических интерфейсах. Если вы используете в качестве графической библиотеки OpenGL, то с её помощью можно сделать выбор объектов довольно качественно и быстро.
Далее я попытаюсь рассмотреть два способа выбора объектов исключительно средствами OpenGL.
Первый способ это, конечно же, буфер выбора в OpenGL.
Его достоинства в том, что это, пожалуй, единственный способ выбора объектов, где есть хоть какая-то надежда, что он более менее аппаратный. Поэтому и работает, возможно, быстрее других. Для OpenGL это также является неким стандартом выбора объектов. При этом минусов у этого способа достаточно. Чего только стоит отсутствие теста на пересечение с другими, не выделяющимися объектами. То есть для OpenGL нужно создать такие условия, при которых на сцене все объекты должны выделяться, тогда все будет работать корректно.
Второй способ это буфер цвета для объектов.
Достоинства в том, что это уже чисто аппаратный способ выбора. Хотя и не всегда это может быть достоинством, особенно если видео карта слабенькая. Такой способ рекомендуется для всего и вся. То есть, и количество объектов может быть огромным и их детализация тоже, и все будет работать быстро и без проблем. И, разумеется, нет никаких глюков при проверке пересечения с другими объектами, которых нельзя выбрать.
Об этом и многом другом читайте дальше.
Буфер выбора
Не совсем понятна логика инженеров-разработчиков OpenGL, зачем чисто в графической библиотеке понадобилось делать функции косвенно относящиеся к графике. Честно говоря, лучше бы сделали функции расчета lightmap'ов чем выбор объектов.
Ну, коли сделали, так сделали, никуда не денешься от этого. Фишка эта есть, а если она есть значит, нужно ее использовать по полной программе.
Смысл выбора объектов заключается в том, что имея двумерные координаты мышки, мы должны выбрать объект в трехмерном мире. Как не сложно догадаться, нужно делать какие-то переводы из 2D в 3D или обратно. Координата мышки представляет собой два числа, то есть точку. Координатная система окна в нашем случае почти ничем не отличается от координатной системы Windows, но за одним исключением - координата Y должна быть перевернута, то есть значение 600 соответствует значению 0, значение 583 соответствует значению 17. Разумеется, это только в том случае, если высота рабочей области будет 600. Другими словами из высоты (height) рабочего окна нужно вычесть значение Y координаты курсора мышки.
Для того чтобы буфер выбора имел силу, нужно чтобы все объекты имели свои идентификаторы. Без них OpenGL не будет знать, что чему должно соответствовать в вашей программе, а конкретнее - в вашем трехмерном мире. Идентификация (далее имя) объекта - это процесс, при котором OpenGL получает уникальное имя объекта. Данное действие похоже на то, что мы называем дисплейными списками (Display Lists) или текстурные объекты (Texture Objects). Но во всех этих случаях библиотека с помощью специальных для этого функций может сама генерировать имя, а в нашем случае его придется или задавать в ручную или писать функцию по их генерации.
Все поименованные объекты находятся в стеке имен. Для того чтобы начать работу с буфером выбора нужно очистить стек имен. Делает это функция glInitNames(). Эта и многие следующие операции обычно выполняются на этапе нажатия кнопки мышки, то есть потеря в производительности не должна быть столь существенна. Если же есть необходимость в динамической проверке выбора, то стоит поискать другие методы выбора объектов и взять их них самый быстрый. Пускай он будет большим, сложным в реализации, но самое главное он должен быть самым быстрым.
Есть несколько способов задачи имен для объектов.
Первый способ задачи имен заключается в том, что мы устанавливаем нулевой объект, а затем загружаем все имена через glLoadName(). То есть это выглядит так:
glInitNames(); glPushName( 0); glLoadName( OBJECT_ID_1); // Рисуем объект (без текстур, освещения и прочего) glLoadName( OBJECT_ID_2); // Рисуем другой объект (без текстур, освещения и прочего) // И так далее в зависимости от того, сколько объектов участвуют в задании имен.
В этой переменной OBJECT_ID_1 (в нашем случае это макрос) находится уникальный идентификатор объекта. И все другие объекты, записывающиеся в буфер выбора должны иметь уникальные значения.
Второй способ заключается в использовании функций glPushName()/glPopName(). Но у него есть один малюсенький недостаток. Если вы используете эти команды на старых видео картах типа VooDoo, при этом используя старые драйвера (про новые я ничего сказать не могу), то FPS падает до абсолютно ничтожных отметок. Сейчас у меня нет возможности проверить это еще раз, но насколько я помню FPS падал до единицы...
И тем не менее:
glInitNames(); glPushName( OBJECT_ID_1); // Рисуем объект (без текстур, освещения и прочего) glPopName( ); glPushName( OBJECT_ID_2); // Рисуем другой объект (без текстур, освещения и прочего) glPopName( ); // И так далее в зависимости от того, сколько объектов участвуют в задании имен.
То есть, вталкиваем один объект (glPushName) и передаем его вершины. Запомните - текстурные координаты, нормали, освещение, все это не интересует OpenGL на данном этапе, поэтому не следует ничего этого передавать, хотя бы в целях увеличения производительности.
А затем выталкиваем (glPopName). Все, объект записан. И так пробегаемся по всем объектам, которые можно выбирать. Но все-таки лучше пользуйтесь первым методом.
Также хотелось бы отметить, что запись объектов следует осуществлять только во время нажатия кнопки мышки. Если требуется в реальном времени делать выбор объектов, то, как я уже говорил - лучше найти другой способ выбора объектов, сравнить его с буфером выбора и выбрать самый быстрый по скорости.
Мы подошли к самому главному - выбор объектов. Итак, что нам нужно для выбора объекта:
1. Оконные координаты мышки.
2. Матрица рабочей области (Viewport Matrix).
3. Буфер записей нажатий ( Hit Records ) или другими словами - буфер выбора.
4. Ну а самое главное - корректно выполненные предыдущие действия.
Теперь разберем функцию выбора, а именно первую часть этой функции.
glSelectBuffer (PICK_OBJECTS, selBuffer ); glGetIntegerv ( GL_VIEWPORT, viewport ); glMatrixMode ( GL_PROJECTION ); glPushMatrix( ); glRenderMode ( GL_SELECT ); glLoadIdentity( ); gluPickMatrix ( x, viewport[3] - y, 2, 2, viewport ); gluPerspective ( 60.0, ( double)ViewWidth/( double)ViewHeight, 1.0, 1000.0 ); glMatrixMode ( GL_MODELVIEW ); PushObjects( ); Found = glRenderMode ( GL_RENDER ); glMatrixMode ( GL_PROJECTION ); glPopMatrix( ); glMatrixMode ( GL_MODELVIEW );
Функция glSelectBuffer() должна дать OpenGL сведения о буфере выбора. Не то чтобы сведения, сколько куда их записывать. Вообще каждый объект в буфере выбора имеет свои четыре поля. Для того чтобы буфер выбора работал и делал положенный ему результат нам понадобиться только второе поле и последнее, то бишь четвертое. Второе поле хранит в себе минимальное значение глубины объекта. В OpenGL глубина определяется clamp числом, то есть числом лежащим в интервале от 0 до 1. А также идентификатор объекта (четвертое поле). Также имеется первое и третье поле. В первом поле хранится число объектов на момент нажатия, то есть общее число объектов под курсором мышки, даже если они перекрывают друг друга. И третье поле это максимальная глубина (оконная координата Z) объекта.
А теперь все сразу:
Первое поле - количество объектов под курсором на момент нажатия.
Второе поле - минимальная Z глубина объекта (экранная Z координата).
Третье поле - максимальная Z глубина объекта (экранная Z координата).
Четвертое поле - идентификатор объекта.
Поэтому размер буфера задается так: (4 * КоличествоОбъектов).
Дальше принцип работы такой. Мы должны перейти в 2D режим, запомнив текущую матрицу и установить режим glRenderMode() в GL_SELECT. Этот режим позволяет рисовать нам плоские объекты при этом не портить FrameBuffer, при этом вся система буфера выбора начинает работать в "активном режиме", то есть вести запись в буфер выбора. Затем идет функция gluPickMatrix(), здесь нужно остановиться подробнее. Принцип работы этой функции заключается в том, что она создает матрицу проекции вокруг курсора. Это позволяет рисовать объекты в этом регионе (регион будет задан параметрами функции gluPickMatrix()). Следовательно, если объект в этом регионе рисуется, то буфер выбора запоминает его и записывает в массив, который мы передали в glSelectBuffer(). Первые два параметра функции gluPickMatrix() - это координаты мышки, только обратите внимание, что координата Y должна быть перевернута, то есть начало координат для Y идет не сверху окна, а снизу. Вторые два параметра ширина и высота области, начиная от начальной точки (первых двух параметров). Обычно их задают двойкой. Последний параметр это матрица рабочего окна (Viewport Matrix).
Далее мы вызываем стандартную для приложения перспективу. Это нужно для того чтобы создать проекционную матрицу для региона, который мы указали в функции gluPickMatrix(). Затем переходим в матрицу модели glMatrixMode(GL_MODELVIEW) и рисуем наши объекты в режиме выбора. Это двусмысленная фраза, с одной стороны мы действительно рисуем объекты в режиме выбора GL_SELECT, с другой стороны мы их рисуем через glInitNames()/LoadName() и так далее. Рисуем, как я уже говорил, без всяких текстур и прочего лишнего, только вершины. Как уже было замечено ранее, мы находимся в режиме GL_SELECT, поэтому все что мы нарисуем, не отразится на буфере кадра (FrameBuffer). Затем осталось вызвать функцию glRenderMode() с параметром GL_RENDER. Функция вернет количество объектов находящихся под тем регионом, который мы указали в функции gluPickMatrix().
Это была первая часть функции получения выбранного объекта.
Часть вторая.
В первой части мы узнали количество объектов под курсором мышки. Если это количество равно нулю, то программу можно дальше не выполнять, это значит что никаких объектов выбрано не было. Как я уже заметил минус буфера выбора в том, что для него идеальное состояние это когда на сцене можно выбрать любой объект.
Как мы уже заметили, OpenGL возвращает количество объектов под курсором, то есть он не может возвратить тот на который мы нажали. Но это и не нужно, дело в том, что если понадобиться делать выбор всех объектов под курсором, то это будет невозможно. Потому OpenGL предлагает нам универсальность, а за нее нужно платить. Поэтому сортировку мы будем производить сами. Сортировка в данном случае будет идти по Z координате каждого объекта попавшего в выбор. Значение Z записывается в массив, который мы указали функцией glSelectBuffer(). И как уже было сказано, каждый объект имеет в своем распоряжении четыре поля. Во втором поле находится минимальная Z глубина объекта.
if (Found > 0 ) { uint Depth = selBuffer[1]; int selObject = selBuffer[3]; for ( int i = 1; i < Found; i++ ) { if ( selBuffer[( i * 4) + 1] < Depth ) { Depth = selBuffer[( i * 4) + 1]; selObject = selBuffer[( i * 4) + 3]; } } return selObject; }
Так выглядит вторая часть функции. В начале мы записываем минимальный Z первого объекта (стандартный поиск минимального элемента массива, чтобы было с чем сравнивать). А затем идентификатор. Хочу напомнить, что нумерация массивов начинается с нуля, поэтому и данные идут 0-3, 4-7, 8-11 и так далее. Далее мы проходимся по всем объектам найденным функцией glRenderMode(GL_RENDER). Дальнейшая запись не очень трудна в понимании - обычный поиск минимального элемента в массиве. Так как объект был найден (Found больше нуля), то в selObject запишется ближайший выбранный объект.
Вот и все. Еще раз хочу отметить одну вещь (кто внимательно читал, то я это уже говорю третий раз) - идеальное стечение обстоятельств для буфера выбора это когда на сцене все объекты представляют возможность быть выбранными. То есть полностью замкнутое пространство из объектов доступных к выбору. Иначе проверка на Z тест будет глупить. Откройте пример к этой статье, и сквозь стол попробуйте выбрать объект. В идеале для разработчика этого быть не должно. В идеале для буфера выбора стол тоже должен быть объектом выбора. И как я уже говорил, только графические редакторы располагают такой возможностью.
Читайте дальше. Выбор объектов через буфер цвета.
25 августа 2003 (Обновление: 4 июля 2009)