Цей спосіб не дуже рекомендується, але він легкий і швидкий для реалізації простого способу вибору об’єкта. Будь-ласка, не використовуйте його в реальній грі, так як він може спричинити помітне зменшення fps (кількості кадрів в секунду). Якщо у Вас якась симуляція чи швидкодія Вам не принципова, цей спосіб може бути чудовим вибором.

Джерельний код для цього туторіалу доступний тут misc05_picking/misc05_picking_slow_easy.cpp з досить неочевидним іменем.

Основна ідея

Основна ідея цього способу в тому, що б малювати сцену як зазвичай, але замість того, що б використовувати гарні тіні, Ви малюєте кожний меш своїм специфічним і унікальним кольором.

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

Ось приклад:

На цьому знімку екрана кожна мавпа має трішки відмінний колір, що робить можливим їх однозначну ідентифікацію.

Звичайно, скоріше за все Ви не захочете дивитись на зображення з цими дивними кольорами, тому напевне Ви захочете очистити екран і перемалювати все.

Реалізація

Присвоєння ідентифікатора кожному об’єкту

Кожний об’єкт на сцені буде мати свій унікальний колір. Найпростіший спосіб це зробити - дати кожному об’єкту число-ідентифікатор, який конвертувати в колір. Власне колір не має значення, це ж трюк:)

В наведеному коді, створюється 100 об’єктів і зберігаються в std::vector, і ідентифікатор кожного об’єкту це просто індекс в векторі. Якщо у Вас більш складна ієрархія об’єктів, можливо є сенс використати std::map і зберігати в них зв’язок між об’єктами і їх ідентифікаторами.

Виявлення клацання мишки

В цьому простому прикладі обирання виконується в кожному кадрі, коли ліва кнопка миші натиснута:

		if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT)){

В реальній програмі Ви будете робити це, коли користувач відпускає кнопку, отже, скоріше за все Вам потрібно буде зберегти bool wasLeftMouseButtonPressedLastFrame, чи, що краще, використовувати glfwSetMouseButtonCallback() (читайте документацію GLFW).

Перетворення ідентифікатора в спеціальний колір

Так як ми збираємося малювати кожний меш своїм кольором, перший крок - це обчислити цей колір. Самий простий спосіб це розділити ідентифікатор на байти і помістити в червоний, синій і зелений колір:

// Перетворимо "i", цілочисельний ідентифікатор, в RGB колір
int r = (i & 0x000000FF) >>  0;
int g = (i & 0x0000FF00) >>  8;
int b = (i & 0x00FF0000) >> 16;

Це може Вас налякати, але це стандартна маніпуляція бітами. В результаті у Вас буде три цілих числа в діапазоні 0..255. За допомогою цього методу Ви зможете закодувати 255^3 = 16 мільйонів мешів, що повинно бути достатнім для більшості випадків.

Малювання сцени з цими кольорами

Тепер нам потрібен шейдер, що б малювати цим кольором. Це дуже просто. Цей шейдер нічого не робить:

#version 330 core

// Вхідні дані вершини, різні для всіх запусків цього шейдеру
layout(location = 0) in vec3 vertexPosition_modelspace;

// Значення, що константні для цілого меша.
uniform mat4 MVP;

void main(){

    // Вихідна позиція в просторі обрізання, : MVP * position
    gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

}

І фрагментний шейдер просто записує бажаний колів в буфер фрейму:

#version 330 core

// Ouput data
out vec4 color;

// Значення, що константні для цілого меша.
uniform vec4 PickingColor;

void main(){

    color = PickingColor;

}

Легко !

Єдиний трюк в тому, що Ви повинні надсилати колір як дійсне число (в діапазоні 0..1), але у Вас цілі числа (в діапазоні 0..255), отже потрібно поділити перед викликом glUniformXX():

// OpenGL очікує колір в діапазоні 0..1 отже поділимо на 255
glUniform4f(pickingColorID, r/255.0f, g/255.0f, b/255.0f, 1.0f);

І тепер можна малювати, як зазвичай (glBindBuffer, glVertexAttribPointer, glDrawElements) і Ви отримаєте дивне зображення.

Отримання кольору під мишкою

Коли Ви намалювали всі Ваші меші (напевне, використовуючи цикл), Вам потрібно викликати функцію glReadPixels(), яка отримає вихідний піксель. Але для правильної роботи цієї функції, потрібно декілька додаткових функцій.

По перше, Ви повинні викликати glFlush(), вона попросить драйвер OpenGL відіслати всі команди, що очікують в черзі (включаючи Ваші останні glDrawXX) до GPU. Це зазвичай не робиться автоматично, тому що команди відправляються пачками і не відразу (це значить, що коли Ви викликаєте glDrawElements(), нічого в цей момент не малюється. Воно скоріше за все намалюється через декілька мілісекунд). Ця операція дуже ПОВІЛЬНА.

Далі, Ви повинні викликати glFinish(), яка буде чекати, поки все дійсно намалюється. Ця функція відрізняється від glFlush() тим, що glFlush() просто посилає команду, glFinish() чекає поки ця команда буде виконана. Ця операція ДУЖЕ ПОВІЛЬНА.

Також потрібно налаштувати, як glReadPixels буде вести себе з вирівнюванням пам’яті. Це трішки за межами туторіалу, але просто додайте наступний виклик glPixelStorei(GL_UNPACK_ALIGNMENT, 1).

І нарешті, можна викликати glReadPixels! Ось повний код:

// Чекаємо, поки всі команди відмальовки будуть дійсно виконані.
// Це неймовірно супер повільно ! 
// Насправді, між викликом glDrawElements() та тим, коли зображення буде на екрані
// проходить дуже багато часу
glFlush();
glFinish(); 

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

// Прочитаємо піксель в центрі екрану.
// Можна також дізнатись позицію курсора функцією glfwGetMousePos().
// Це теж супер-мега-ультра повільно навіть для одного пікселю, 
// тому що це буфер кадру, який знаходиться на відеокарті.
unsigned char data[4];
glReadPixels(1024/2, 768/2,1,1, GL_RGBA, GL_UNSIGNED_BYTE, data);

Ваш колір тепер зберігається в масиві data. Ось тут видно, що ID меша 19.

Перетворення кольорі назад в ідентифікатор

Тепер можна відновити Ваш ідентифікатор об’єкта з буфера data. Код повністю протилежний до перетворення ідентифікатору в колір:

// Перетворення кольору в ідентифікатор
int pickedID = 
	data[0] + 
	data[1] * 256 +
	data[2] * 256*256;

Використання цього ідентифікатора

Тепер Ви можете використовувати цей ідентифікатор для будь-чого. В цьому випадку оновлюється текст в UI. Але в Вашому випадку може бути все, що завгодно.

if (pickedID == 0x00ffffff){ // Повністю білий, це фон!
	message = "background";
}else{
	std::ostringstream oss; // C++ рядки дивні
	oss << "mesh " << pickedID;
	message = oss.str();
}

(від перекладача - в с++11 і новіших можна записати код компактніше

if (pickedID == 0x00ffffff){ // Повністю білий, це фон!
	message = "background";
}else{
	message = std::to_string(pickedID);
}

)

Переваги та недоліки

Переваги :

  • Просто, легко реалізувати
  • Не потрібні додаткові бібліотеки чи знання спеціальної математики

Мінуси :

  • Використання glFlush(), glFinish(), glReadPixels() відомі своєю повільною роботою, тому що вони заставляють процесор чекати відеокарту, а це руйнує швидкодію.
  • Вам зазвичай не потрібно знати, по якому саме трикутнику натиснули мишкою, яка тут нормаль і тому подібне.

Висновки

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

Якщо Ви обрали цю техніку і Вам потрібно обрати декілька точок в одному кадрі, Ви можете зробити це одночасно, Вам не потрібно малювати 5 раз зображення, що б взяти 5 точок!