这是对learnopengl的简单笔记。原教程网址:learnopengl。原教程同时涉及图形学的基本理论与opengl API,本文更多关注API,而简化甚至省略了背后的图形学原理性内容。
一个相机渲染一张纹理
在unity中看到,相机的渲染结果不一定要放到屏幕上,还可以放到一张纹理里面。这在openGL里面很容易做到。
总体而言,就是
- 申请一块帧缓冲
- 申请一些纹理,将这个帧缓冲和这个纹理绑定在一起
- 调用 glFrameTexture2D 将纹理绑定成帧缓冲的输出“对象”,就可以把相机渲染的帧放到一张纹理图像里*。
// 申请帧缓冲与纹理 GLuint pingpongFBO, pingpongColorbuffers; glGenFramebuffers(1, &pingpongFBO); glGenTextures(1, &pingpongColorbuffers); // 激活绑定帧缓冲,并将帧缓冲和它的目标绑定在一起. glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]); glBindTexture(GL_TEXTURE_2D, pingpongColorbuffers[i]); // 像设置普通纹理贴图那样设置帧缓冲输出到的纹理对象. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, frameWidth, frameHeight, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // we clamp to the edge as the blur filter would otherwise sample repeated texture values! glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // 指定将帧缓冲输出到这个纹理. glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongColorbuffers[i], 0); // also check if framebuffers are complete (no need for depth buffer) // 一些检查 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "Framebuffer not complete!" << std::endl;
此后,任何 glBindFramebuffer(pingpongFBO);
后渲染的场景,都会输入到对应纹理 pingpongColorBuffers
中,而不会输入到屏幕。
PS:glBindFrameBuffer(0) 的渲染结果才会直接输出的到屏幕
一个相机渲染到多张纹理里
相机可以通过在 片段着色器中 设置 layout
的out变量,控制结果输出面元颜色到不同的纹理对象上,具体方法是 glFramebufferTexture2D
中的 GL_COLOR_ATTACHMENT0 参数,它和 layout 布局相对应。
比如:
/* Fragment GLSL */ #version 330 core layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; in VS_OUT { vec3 FragPos; vec3 Normal; vec2 TexCoords; } fs_in; uniform vec3 lightColor; void main() { FragColor = vec4(lightColor, 1.0); //calculate BrightColor //to do.. //就是输出到BrightColor的大于阈值的颜色. float brightness = dot(lightColor, vec3(0.7126, 0.7152, 0.722)); if(brightness > 1.0) BrightColor = vec4(lightColor, 1.0); else BrightColor = vec4(0.0,0.0,0.0,1.0); }
这里规定了两个输出“通道” FragColor, BrightColor,分别对应两张要输出的纹理。上面是一个HDR+泛光的渲染的其中一步,最后的得到的两张纹理分别是 16位浮点数的HDR颜色缓冲、光亮超过某一阈值的位置的颜色的缓冲(低于这一阈值的位置直接置0,高于的不变)。这两张纹理分别对应 FragColor, BrightColor.
而绑定帧缓冲与颜色缓冲的过程:
GLuint hdrFBO, colorBuffers[]; glGenFramebuffers(1, &hdrFBO); glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); // create 2 floating point color buffers (1 for normal endering, other for brightness treshold values) glGenTextures(2, colorBuffers); for (unsigned int i = 0; i < 2; i++) { glBindTexture(GL_TEXTURE_2D, colorBuffers[i]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, frameWidth, frameHeight, 0, GL_RGB, GL_FLOAT, NULL); // 注意申请的是GL_RGB16F纹理,不会自动归一化. // 像普通纹理那样设置属性. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // we clamp to the edge as the blur filter would otherwise sample repeated texture values! glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // attach texture to framebuffer glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0); 注意 GL_COLOR_ATTACHMENT0+i }
关键在于最后的 glFramebufferTexture2D,里面的 GL_COLOR_ATTACHMENTx 和片段着色器中 (layout = x) out
的输出通道对应。
GL_COLOR_ATTACHMENTx 的定义时连续的,所以 GL_COLOR_ATTACHMENT0 + i = GL_COLOR_ATTACHMENTi
将相机渲染的纹理又展示回屏幕
相机渲染出来的纹理一般是平铺满整个屏幕的,所以只需要将纹理“采样”贴到屏幕上就可,所以方法是:画一个铺满窗口的平面矩形,直接在这个矩形上用相机渲染的纹理采样表示颜色,即可。
着色器:
顶点着色器直接传递平面坐标,平面坐标就是纹理坐标,也直接当成世界坐标. #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoords; out vec2 TexCoords; void main() { TexCoords = aTexCoords; gl_Position = vec4(aPos, 1.0); } ------------------------------------------- ------------------------------------------- 片段着色器直接在纹理上采样,并处理一些Gamma矫正、曝光等。 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D scene; //colorBuffer[0](hdr颜色值) uniform sampler2D bloomBlur; //pingpongColorBuffer(模糊后的高亮.) uniform bool bloom; uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(scene, TexCoords).rgb; vec3 bloomColor = texture(bloomBlur, TexCoords).rgb; //calculate the final FragColor by blending hdrColor and bloomColor, then exposure tone mapping. // to do if(bloom) hdrColor += bloomColor; //直接相加.. //曝光度:1-e^(-hdrColor * exposure) hdrColor = vec3(1.0) - exp(-hdrColor * exposure); //Gamma矫正. hdrColor = pow(hdrColor, vec3(1.0/gamma)); FragColor = vec4(hdrColor, 1.0); } 这是Gamma+HDR+泛光的片段着色器,注意连个sampler2D就是要融合展示的、由相机渲染的纹理。
在渲染的时候,设置视口,绑定帧缓冲为0指定输出屏幕,然后激活、绑定纹理为将要展示的、由相机渲染的纹理,设置着色器后,画一个大矩形renderQuad 铺满屏幕即可。
lViewport(0, 0, windowWidth, windowHeight); glBindFramebuffer(0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, HDRcolor); //这个纹理对应第一个sampler2D glActivateTexture(1); glBindTexture(GL_TEXTURE_2D, BLOOMcolor); //对应第二个sampler2D // ... 设置着色器 ... renderQuad(); glViewport(0, 0, frameWidth, frameHeight);
这样就把纹理处理后整个贴到屏幕上了。
下面是画那个大矩形,只用定义四角。
void renderQuad() { if (quadVAO == 0) { float quadVertices[] = { // positions // texture Coords -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, }; // setup plane VAO glGenVertexArrays(1, &quadVAO); glGenBuffers(1, &quadVBO); glBindVertexArray(quadVAO); glBindBuffer(GL_ARRAY_BUFFER, quadVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); } glBindVertexArray(quadVAO); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glBindVertexArray(0); }