Туторіал 14 : Рендер в текстуру
Рендер (малювання) в текстуру - це зручний метод для створення різноманітних ефектів. Основна ідея полягає в тому, що Ви малюєте сцену як зазвичай, але в текстуру, яку потім можна буде використати.
Може використовуватися для “камери в грі”, подальшої обробки і багатьох GFX яких тільки Ви можете собі уявити.
Рендер в текстуру
У нас є три задачі: створити текстуру, в яку будемо малювати, намалювати щось в неї та використати готову намальовану текстуру.
Створення цілі рендеру
Ми будемо малювати в те, що називається буфером фрейму. Це контейнер для текстур та необов’язкового буферу глибини. Він створюється точнісінько так само, як і будь-який інший об’єкт OpenGL:
// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.
GLuint FramebufferName = 0;
glGenFramebuffers(1, &FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
Тепер нам потрібно створити текстуру, яка буде містити RGB результат нашого шейдера. Цей код виглядає достатньо класично:
// Текстура, куди будемо малювати
GLuint renderedTexture;
glGenTextures(1, &renderedTexture);
// Прив'яжемо новостворену текстуру - все подальші функції, що працюють з текстурами, будуть її використовувати
glBindTexture(GL_TEXTURE_2D, renderedTexture);
// Дамо OpenGL пусте зображення ( останній "0" )
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, 1024, 768, 0,GL_RGB, GL_UNSIGNED_BYTE, 0);
// Погана фільтрація. Потрібно!
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
Також нам потрібен буфер глибини. Це не є обов’язковим, все залежить від того, що насправді потрібно намалювати в текстуру. Але так як ми хочемо намалювати Сюзанну, нам потрібен буфер глибини.
// Буфер глибини
GLuint depthrenderbuffer;
glGenRenderbuffers(1, &depthrenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1024, 768);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);
І в кінці налаштуємо буфер фрейму:
// Налаштуємо "renderedTexture" як кольоровий <<"attachement">> №0
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0);
// Встановимо буфер для малювання
GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, DrawBuffers); // "1" - це розмір DrawBuffers
Дещо може піти не так на протязі цього процесу, це залежить від можливостей GPU. Ось як можна перевірити це:
// Завжди перевіряйте, що з нашим буфером фрейму все гаразд
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return false;
Рендер в текстуру
Rendering to the texture is straightforward. Simply bind your framebuffer, and draw your scene as usual. Easy !
// Малюємо в фрейм буферу
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
glViewport(0,0,1024,768); // Намалюємо весь буфер фрейму, заповнивши від нижнього лівого края до верхнього правого
Наш фрагментний шейдер потребує маленької адаптації:
layout(location = 0) out vec3 color;
Це значить, що коли відбувається запис в змінну color
, ми насправді записуємо в ціль для малювання №0, яка є нашою текстурою, тому що DrawBuffers[0]
це GL_COLOR_ATTACHMENTi, що в нашому випадку renderedTexture.
Підсумуємо :
- color буде записано в перший буфер так як
layout(location=0)
. - Перший буфер є
GL_COLOR_ATTACHMENT0
тому щоDrawBuffers[1] = {GL_COLOR_ATTACHMENT0}
- GL_COLOR_ATTACHMENT0 має приєднане renderedTexture - сюди буде записуватись колір.
- Іншими словами, Ви можете замінити
GL_COLOR_ATTACHMENT0
наGL_COLOR_ATTACHMENT2
і це все ще буде працювати.
Зауважте: в OpenGL < 3.3 немає layout(location=i)
, тому використовуйте glFragData[i] = mvvalue
.
Використання текстури, в яку відбувся малювання
Ми збираємося намалювати простий чотирикутник, який заповнить екран. Нам потрібні звичайні буфери, шейдери, ідентифікатори…
// FBO повноекранного чотирикутника
GLuint quad_VertexArrayID;
glGenVertexArrays(1, &quad_VertexArrayID);
glBindVertexArray(quad_VertexArrayID);
static const GLfloat g_quad_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
};
GLuint quad_vertexbuffer;
glGenBuffers(1, &quad_vertexbuffer);
glBindBuffer(GL_ARRAY_BUFFER, quad_vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_quad_vertex_buffer_data), g_quad_vertex_buffer_data, GL_STATIC_DRAW);
// Створимо та скомпілюємо GLSL програму з наших шейдерів
GLuint quad_programID = LoadShaders( "Passthrough.vertexshader", "SimpleTexture.fragmentshader" );
GLuint texID = glGetUniformLocation(quad_programID, "renderedTexture");
GLuint timeID = glGetUniformLocation(quad_programID, "time");
Тепер Ви хочете намалювати це на екран. Це можна зробити, використовуючи 0 як другий параметр glBindFramebuffer
.
// Малюємо на екран
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0,0,1024,768); // Намалюємо весь буфер фрейму, з лівого нижнього кута в верхній правий
Ми можемо намалювати наш повноекранний чотирикутник таким шейдером:
#version 330 core
in vec2 UV;
out vec3 color;
uniform sampler2D renderedTexture;
uniform float time;
void main(){
color = texture( renderedTexture, UV + 0.005*vec2( sin(time+1024.0*UV.x),cos(time+768.0*UV.y)) ).xyz;
}
Цей код просто показує текстуру, але додає невеликий зсув, який залежить від часу.
Результат
Йдемо далі
Використання глибини
В деяких випадках Вам може знадобиться глибина, коли Ви малюєте текстуру. В цьому випадку, просто малюйте в текстуру, яку створіть наступним чином:
glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT24, 1024, 768, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0);
(24 - це точність в бітах. Ви можете обирати поміж 16, 24 та 32 в залежності від Ваших потреб. 24 зазвичай хороший вибір)
Цього повинно буди достатньо для Вас, що б почати використовувати, але ось Вам код, який це реалізує.
Зауважте, даний код може бути трішки повільний, тому що драйвер може не могти використовувати певні оптимізації, такі як Hi-Z.
На цьому знімку екрана, рівень глибини штучно “покращено”. Зазвичай, це буває дуже складно побачити щоь в текстурі глибин. «Близька координата Z біля нуля - чорна, дальня координата Z поблизу 1 - біла.»
Multisampling
Ви можете записувати в «мультисемпл» текстури а не в “базові” - просто потрібно замінити glTexImage2D
на glTexImage2DMultisample в C++ коді та sampler2D
/texture
на sampler2DMS
/texelFetch
в фрагментному шейдері.
Та тут є одне застереження - texelFetch потребує інший аргумент, який я кількістью «семплів |
зразків» для отримання. Іншими словами, нема автоматичної “фільтрації” (коректний термін, коли ми говоримо про «multisampling | мальтисеспл» є ““роздільна здатність”). |
Тому, можливо Вам доведеться вирішити це для мультисемпл текстури самостійно в іншу, не мультисемпл текстуру, використовуючи інший шейдер.
Нічого складного, просто трішки громіздко.
Кілька цілей рендеру
Ви можете записувати декілька текстур одночасно.
Просто створіть декілька текстур (всі з правильними та однаковими розмірами !), викличте glFramebufferTexture
з різними «кольоровими вкладеннями», викличте glDrawBuffers
з оновленими параметрами (щось виду (2,{GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1}})
) і додайте ще одну змінну в Ваш фрагментний шейдер:
layout(location = 1) out vec3 normal_tangentspace; // або що завгодно
Підказка 1: якщо Вам потрібно виводити вектор в текстуру, існують текстури з дійсними числами, з 16 чи 32 бітами точності в замін 8… Дивіться за посиланням glTexImage2D (шукайте GL_FLOAT
).
Підказка 2: для попередньої версії OpenGL використовуйте glFragData[1] = myvalue
.
Вправи
- Спробуйте використати
glViewport(0,0,512,768);
замістьglViewport(0,0,1024,768);
(«спробуйте обидва буфера фрейма та екрана») - Поекспериментуйте з іншими UB координатами в останньому фрагментному шейдері.
- Перетворіть квад використовуючи дійсну матрицю трансформації. Спочатку “захадркодьте”, а потом спробуйте використати функції з controls.hpp. Що Ви помітили?