这是对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);
}