Уроки з OpenGL із сайту OGLDev - Урок 29 - 3D Вибір

Урок 29 – 3D Вибір

Можливість зіставити клацання миші у вікні з 3D сценою з примітивом (нехай це буде трикутник), якому пощастило спроектуватися в ту ж точку, в якій було клацання миші, називається 3D Вибір (Picking). Це може бути корисно у випадках, коли додатку потрібно відобразити користувальницьке клацання миші (що за своєю природою з 2D) на щось у локальному/світовому просторі об'єкта на сцені. Наприклад, ви можете використовувати це для вибору об'єкта або його частини для майбутніх операцій (видалення та інші). У демо до цього уроку ми рендеруємо набір об'єктів і показуємо як відзначити "вибраний" трикутник червоним, щоб він виділявся.

Для реалізації 3D вибору ми скористаємось здатністю OpenGL, яка була представлена ​​в уроці за картою тіней (#23) – об'єкт буфера кадрів (Framebuffer Object (FBO)). Раніше ми використовували FBO лише для буфера глибини, оскільки нам було цікаво лише порівнювати глибину пікселя з різних позицій. Для 3D вибору ми будемо використовувати буфер глибини і буфер кольору для зберігання індексу відмальованого трикутника.

Секрет 3D вибору дуже простий. Ми будемо прив'язувати індекс кожному трикутнику і одержуватимемо з FS індекс трикутника, на якому знаходиться піксель. Зрештою ми отримаємо буфер "колір", який містить не зовсім колір. Для кожного пікселя, який буде покритий примітивом, ми отримаємо індекс цього примітиву. Під час кліку миші у вікні ми будемо зчитувати цей індекс назад (згідно з позицією миші) і рендерувати обраний трикутник червоним. За допомогою комбінації з буфером глибини під час проходу ми гарантуватимемо, що коли кілька примітивів покривають однаковий піксель, то ми отримаємо індекс найвищого примітиву (найближчого докамері).

Це, двома словами, і є 3D вибір. Перш ніж поринути у код, нам потрібно буде вирішити кілька простих питань. Наприклад, як чинити з безліччю об'єктів? Що робити з численними викликами? Чи хочемо ми збільшувати індекс примітивів від об'єкта до об'єкта так, щоб кожен примітив у сцені отримував унікальний індекс, або починати заново для кожного об'єкта?

Код цього уроці виконує лише основне завдання, яке може бути спрощено за необхідності. Ми будемо рендерувати по три індекси для кожного пікселя:

  1. Індекс об'єкта, якому належить піксель. Кожен об'єкт сцени матиме унікальний індекс.
  2. Індекс виклику малювання всередині об'єкта. Цей індекс обнулятиметься спочатку нового об'єкта.
  3. Індекс примітиву всередині виклику малювання. Цей індекс обнулюватиметься спочатку кожного виклику.

Коли ми зчитуватимемо піксель із буфера, то отримаємо відразу всю трійцю. Потім потрібно перейти назад до конкретного примітиву.

Нам потрібно буде рендерувати сцену двічі. Перший прохід називається "текстура вибору", яка міститиме індекси примітивів, і другий прохід у звичайний буфер кольору. Тому головний цикл рендеру складатиметься з фази вибору та фази рендеру.

Примітка: модель павука, яка використовується в демо, взята з набору Assimp. Вона містить кілька VB, які дають змогу протестувати наш випадок.

Прямо до коду!

Клас PickingTexture представляє FBO, який ми будемо рендерити примітиви. Він інкапсулює вказівник на об'єкт буфера кадрів, об'єкт текстури для запису індексів та об'єкт текстури для буфера глибини. Він ініціалізується з тими самими параметрами, що й у нашого головного вікна і представляє 3 функції.EnableWriting() має бути викликана спочатку фази вибору. Потім ми рендеруємо всі необхідні об'єкти. Наприкінці ми викликаємо DisableWriting() повернення до стандартного буфера кадру. Для читання назад індексу пікселя ми викликаємо ReadPixel() та його екранними координатами. Ця функція повертає структуру з трьома індексами (або індивідуальними номерами (ID)), які було розібрано у розділі теорії. Якщо миша клацнула повз всі об'єкти, всі поля PrimID структури PixelInfo будуть містити 0xFFFFFFFF.

Код вище ініціалізує клас PickingTexture. Ми створюємо FBO і прив'язуємо його до мітки GL_DRAW_FRAMEBUFFER (оскільки ми збираємося малювати в нього). Потім ми генеруємо 2 об'єкти текстури (для інформації про пікселі та глибину). Зауважимо, що внутрішній формат текстури, яка міститиме інформацію про піксель, - GL_RGB32UI. Це означає, що кожен піксель - вектор із 3-х беззнакових цілісних змінних. Цей вибір дозволяє нам дійти до 4-х мільярдів об'єктів, викликів малювання та примітивів (має вистачити більшості сцен…). Крім того, незважаючи на те, що ми ініціалізуємо цю текстуру без даних (останній параметр glTexImage2D - NULL), нам, як і раніше, потрібно вказати відповідний формат і тип (7-й та 8-й параметри). Формат та тип, який відповідають GL_RGB32UI - GL_RGB_INTEGER та GL_UNSIGNED_INT. Нарешті, ми прив'язуємо цю текстуру до позначки GL_COLOR_ATTACHMENT0 у FBO. Так ми позначаємо куди виходитимуть дані із фрагментного шейдера.

Об'єкт текстури для буфера глибини створюється і прив'язується так само, як і в уроці по карті тіней, тому ми не розглядаємо його знову. Після того, як все ініціалізовано, ми перевіряємо статус FBO та повертаємо стандартний буфер перед виходом.

Перш ніж ми почнемо рендерувати у текстуру вибору, нампотрібно увімкнути її для запису. Це означає, що прив'язати FBO до GL_DRAW_FRAMEBUFFER.

Після того, як ми завершимо рендери в текстуру вибору, ми повідомляємо OpenGL, що починаючи з цього моменту ми хочемо рендер в стандартний буфер кадру, передавши 0 в мітку GL_DRAW_FRAMEBUFFER.

Це VS класу PickingTechnique. Цей метод відповідає за рендер пікселя в об'єкт PickingTexture. Як бачите, він дуже простий, оскільки нам потрібно лише перетворити позицію вершини.

FS класу PickingTechnique записує інформацію про пікселі у текстуру вибору. Індекс об'єкта та індекс відображення збігаються для всіх пікселів (в одному виклику), тому вони надходять з uniform-змінних. Для того, щоб отримати індекс примітиву, ми використовуємо вбудовану змінну gl_PrimitiveID. Це індекс примітиву, який автоматично надходить із системи. Зауважимо, що розширення GL_EXT_gpu_shader4 має бути включене на початку шейдера для використання. gl_PrimitiveID можна використовувати тільки для GS PS. Якщо GS увімкнено, і FS хоче використовувати gl_PrimitiveID, то GS повинен записувати gl_PrimitiveID в одну з вихідних змінних, і FS повинен оголосити її з аналогічним ім'ям на вхід. У нашому випадку GS немає, тому ми можемо просто використовувати gl_PrimitiveID.

Система встановлює gl_PrimitiveID у 0 на початку відтворення. Це ускладнить вибір між "фоновими" пікселями та пікселями, які покриті об'єктами (як розібратися в такій ситуації?). Для виправлення цього ми збільшуємо індекс на 1 перед записом на вихід. Це означає, що фоновий піксель може бути відмінний, оскільки індекс дорівнює 0, а до пікселів, покритих об'єктами, індекс починається з 1, як і ID примітиву. Ми побачимо пізніше що компенсує це коли ми будемо використовувати ID примітив для рендеру вказаноготрикутник.

Метод вибору вимагає від програми оновлювати індекс малювання перед кожним викликом. Це створює проблему, оскільки поточний клас міша (у випадку міша з кількома VB) всередині проходить буферами і надсилає окремі виклики для комбінації IB/VB. Це не дає нам шансу для оновлення індексу малювання. Рішення, яке ми застосуємо тут, є інтерфейсом вище. Клас PickingTechnique походить від нього і успадковує методи вище. Функція Mesh::Render() тепер приймає покажчик цього інтерфейс і викликає лише функцію у ньому перед початком нового отрисовки. Це забезпечує прекрасний поділ між класом Mesh та будь-яким методом, який хоче отримати зворотний виклик перед малюванням.

Код вище вказує частину оновленої функції Mesh::Render(). Якщо ми не зацікавлені у зворотному дзвінку для кожного малювання, ми просто передаємо NULL як аргумент функції.

Це реалізація IRenderCallbacks::DrawStartCB() від класу PickingTechnique. Функція Mesh::Render() надає індекс відображення, який передається як uniform-змінна. Зауважимо, що PickingTechnique також має функцію для встановлення індексу об'єкта, але вона викликається безпосередньо головним додатком без механізму вище.

Це головна функція рендеру. Функціонал був розділений на 2 центральні фази, одна для малювання в текстуру вибору, та інша для рендеру об'єктів та обробки клацання миші.

Після фази вибору йде фаза рендеру. Ми налаштовуємо конвеєр так само, як і раніше. Потім йде перевірка чи було клацання миші. Якщо був, ми використовуємо PickingTexture::ReadPixel() для захоплення інформації про пікселі. Так як FS збільшує ID примітиву, то у всіх фонових пікселів ID = 0, а в покритих від 1 і далі. Якщо піксель покритий об'єктом, ми включаємо дуже простий метод, якийпросто повертає червоний колір із FS. Ми оновлюємо об'єкт Pipeline зі світовою позицією вибраного об'єкта, використовуючи інформацію про пікселі. Ми використовуємо нову функцію рендеру класу Mesh, яка приймає ID примітиву і вимагає червоного примітиву (зауважимо, що ми повинні зменшувати ID примітиву, оскільки у класу Mesh відлік йде від 0). Нарешті ми рендері примітиви як завжди.

Цей урок запитує програму відстежувати кліки миші. Функція glutMouseFunc() займається цим. Для неї додалася додаткова функція зворотного виклику в ICallbacks інтерфейс (який успадковує клас головного додатка). Ви можете використовувати переліки, такі як GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON і GLUT_RIGHT_BUTTON для обробки натиснутої кнопки (перший аргумент MouseCB()). Параметр 'State' повідомляє, чи була клавіша натиснута (GLUT_DOWN) або відпущена (GLUT_UP).