LearnOpenGL学习笔记—高级光照 06:HDR

【项目地址:点击这里这里这里

本节对应官网学习内容:HDR
结合英文原站,中文站,以及个人实践进行描述。

1 引入

一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。
这个看起来无辜的语句,使我们一直将亮度与颜色的值设置在这个范围内,尝试着与场景契合。
这样是能够运行的,也能给出还不错的效果。

但是如果我们遇上了一个特定的区域,其中有多个亮光源使这些数值总和超过了1.0,又会发生什么呢?

答案是这些片段中超过1.0的亮度或者颜色值会被约束在1.0,从而导致场景混成一片,难以分辨:
在这里插入图片描述
这是由于大量片段的颜色值都非常接近1.0,在很大一个区域内每一个亮的片段都有相同的白色。这损失了很多的细节,使场景看起来非常假。

解决这个问题的一个方案是减小光源的强度从而保证场景内没有一个片段亮于1.0。

然而这并不是一个好的方案,因为这需要使用不切实际的光照参数。

一个更好的方案是让颜色暂时超过1.0,然后将其转换至0.0到1.0的区间内,从而防止损失细节。

虽然显示器被限制为只能显示值为0.0到1.0间的颜色,但是在光照方程中却没有这个限制。

通过使片段的颜色超过1.0,我们有了一个更大的颜色范围,这也被称作HDR(High Dynamic Range, 高动态范围)

有了HDR,亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节。

HDR原本只是被运用在摄影上,摄影师对同一个场景采取不同曝光拍多张照片,捕捉大范围的色彩值。
这些图片被合成为HDR图片,从而综合不同的曝光等级使得大范围的细节可见。

看下面这个例子
在低曝光(窗口)的明亮区域中的许多细节,但这些细节在高曝光下却消失了
右边这张图的高曝光却可以让之前看不出来的黑暗区域显现出来。
在这里插入图片描述
这与我们眼睛工作的原理非常相似,也是HDR渲染的基础。当光线很弱的啥时候,人眼会自动调整从而使过暗和过亮的部分变得更清晰,就像人眼有一个能自动根据场景亮度调整的自动曝光滑块。

HDR渲染和其很相似,我们允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。

转换HDR值到LDR值得过程叫做色调映射(Tone Mapping),现在现存有很多的色调映射算法,这些算法致力于在转换过程中保留尽可能多的HDR细节。

这些色调映射算法经常会包含一个选择性倾向黑暗或者明亮区域的参数。

在实时渲染中,HDR不仅允许我们超过LDR的范围[0.0, 1.0]与保留更多的细节,同时还让我们能够根据光源的真实强度指定它的强度。

比如太阳有比闪光灯之类的东西更高的强度,那么我们为什么不这样子设置呢?(比如说设置一个10.0的亮度) 。

这允许我们用更现实的光照参数恰当地配置一个场景的光照,而这在LDR渲染中是不能实现的,因为他们会被上限约束在1.0。

因为显示器只能显示在0.0到1.0范围之内的颜色,我们肯定要做一些转换从而使得当前的HDR颜色值符合显示器的范围。

简单地取平均值重新转换这些颜色值并不能很好的解决这个问题,因为明亮的地方会显得更加显著。我们能做的是用一个不同的方程与/或曲线来转换这些HDR值到LDR值,从而给我们对于场景的亮度完全掌控,这就是之前说的色调变换,也是HDR渲染的最终步骤。

2 浮点帧缓冲

在实现HDR渲染之前,我们首先需要一些,防止颜色值在每一个片段着色器运行后被限制约束的方法。

当帧缓冲使用了一个标准化的定点格式(像GL_RGB)为其颜色缓冲的内部格式,OpenGL会在将这些值存入帧缓冲前自动将其约束到0.0到1.0之间。

这一操作对大部分帧缓冲格式都是成立的,除了专门用来存放被拓展范围值的浮点格式。

当一个帧缓冲的颜色缓冲的内部格式被设定成了GL_RGB16F, GL_RGBA16F, GL_RGB32F 或者GL_RGBA32F时,这些帧缓冲被叫做浮点帧缓冲(Floating Point Framebuffer),浮点帧缓冲可以存储超过0.0到1.0范围的浮点值,所以非常适合HDR渲染。

想要创建一个浮点帧缓冲,我们只需要改变颜色缓冲的内部格式参数就行了(注意GL_FLOAT参数):

glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);  

默认的帧缓冲默认一个颜色分量只占用8位(bits)。
当使用一个使用32位每颜色分量的浮点帧缓冲时(使用GL_RGB32F 或者GL_RGBA32F),我们需要四倍的内存来存储这些颜色。
所以除非需要一个非常高的精确度,32位不是必须的,使用GL_RGB16F就足够了。

有了一个带有浮点颜色缓冲的帧缓冲,我们可以放心渲染场景到这个帧缓冲中。
在这个教程的例子当中,我们先渲染一个光照的场景到浮点帧缓冲中,之后再在一个铺屏四边形(Screen-filling Quad)上应用这个帧缓冲的颜色缓冲,代码会是这样子:

glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
    // [...] 渲染(光照的)场景
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 现在使用一个不同的着色器将HDR颜色缓冲渲染至2D铺屏四边形上
hdrShader.Use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
RenderQuad();

这里场景的颜色值存在一个可以包含任意颜色值的浮点颜色缓冲中,值可能是超过1.0的。
这个简单的演示中,场景被创建为一个被拉伸的立方体通道和四个点光源,其中一个非常亮的在隧道的尽头:

std::vector<glm::vec3> lightColors;
lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f));
lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f));
lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f));
lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f));  

渲染至浮点帧缓冲和渲染至一个普通的帧缓冲是一样的。
新的东西就是这个的hdrShader的片段着色器,用来渲染最终拥有浮点颜色缓冲纹理的2D四边形。
我们来定义一个简单的直通片段着色器(Pass-through Fragment Shader):

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D hdrBuffer;
uniform bool hdr;
uniform float exposure;

void main()
{
    
                 
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
    FragColor = vec4(hdrColor, 1.0);
}  

这里我们直接采样了浮点颜色缓冲并将其作为片段着色器的输出。
尽管我们已经有了一些存在浮点颜色纹理的值超过了1.0,然而,这个2D四边形的输出是被直接渲染到默认的帧缓冲中,导致所有片段着色器的输出值被约束在0.0到1.0间。
在这里插入图片描述

很明显,在隧道尽头的强光的值被约束在1.0,因为一大块区域都是白色的,过程中超过1.0的地方损失了所有细节。
因为我们直接转换HDR值到LDR值,这就像我们根本就没有应用HDR一样。
为了修复这个问题我们需要做的是无损转化所有浮点颜色值回0.0-1.0范围中。我们需要应用到色调映射。

2.1 基础代码

我们会在上面的场景尝试HDR
我们会用到一组光照的着色器和一组HDR的着色器
我们主要会修改HDR的片段着色器,其他基本不变
同时以下是用到的材质
在这里插入图片描述

目前的hdr.frag如下

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D hdrBuffer;
uniform bool hdr;
uniform float exposure;

void main()
{
    
                 
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
    FragColor = vec4(hdrColor, 1.0);
}  

其他的
hdr.vert:

#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);
}

light.vert:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

out VS_OUT {
    
    
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

uniform bool inverse_normals;

void main()
{
    
    
    vs_out.FragPos = vec3(model * vec4(aPos, 1.0));   
    vs_out.TexCoords = aTexCoords;
    
    vec3 n = inverse_normals ? -aNormal : aNormal;
    
    mat3 normalMatrix = transpose(inverse(mat3(model)));
    vs_out.Normal = normalize(normalMatrix * n);
    
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

light.frag:

#version 330 core
out vec4 FragColor;

in VS_OUT {
    
    
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} fs_in;

struct Light {
    
    
    vec3 Position;
    vec3 Color;
};

uniform Light lights[16];
uniform sampler2D diffuseTexture;
uniform vec3 viewPos;

void main()
{
    
               
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);
    // ambient
    vec3 ambient = 0.0 * color;
    // lighting
    vec3 lighting = vec3(0.0);
    for(int i = 0; i < 16; i++)
    {
    
    
        // diffuse
        vec3 lightDir = normalize(lights[i].Position - fs_in.FragPos);
        float diff = max(dot(lightDir, normal), 0.0);
        vec3 diffuse = lights[i].Color * diff * color;      
        vec3 result = diffuse;        
        // attenuation (use quadratic as we have gamma correction)
        float distance = length(fs_in.FragPos - lights[i].Position);
        result *= 1.0 / (distance * distance);
        lighting += result;
                
    }
    FragColor = vec4(ambient + lighting, 1.0);
}

main.cpp:

#include <iostream>

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include "Shader.h"
#include "Camera.h"
#include <vector>


#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

//鼠标移动镜头
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
//滚轮缩放
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
//检测输入
void processInput(GLFWwindow *window);
//导入图
unsigned int LoadImageToGPU(const char* filename, GLint internalFormat, GLenum format);
// Options
GLboolean hdr = true; // Change with 'Space'
GLfloat exposure = 1.0f; // Change with j and k

#pragma region Camera Declare
//建立camera
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 5.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
Camera camera(cameraPos, cameraTarget, cameraUp);
#pragma endregion

#pragma region Input Declare
//移动用
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间

void processInput(GLFWwindow* window) {
    
    
	//看是不是按下esc键 然后退出
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
    
    
		glfwSetWindowShouldClose(window, true);
	}
	//更流畅点的摄像机系统
	if (deltaTime != 0) {
    
    
		camera.cameraPosSpeed = 5 * deltaTime;
	}
	//camera前后左右根据镜头方向移动
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		camera.PosUpdateForward();
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		camera.PosUpdateBackward();
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		camera.PosUpdateLeft();
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		camera.PosUpdateRight();
	if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
		camera.PosUpdateUp();
	if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
		camera.PosUpdateDown();


	if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS)
	{
    
    
		hdr = !hdr;
	}

	// Change parallax height scale
	if (glfwGetKey(window, GLFW_KEY_J) == GLFW_PRESS)
		exposure -= 0.5 * deltaTime;
	if (glfwGetKey(window, GLFW_KEY_K) == GLFW_PRESS)
		exposure += 0.5 * deltaTime;
		
}
float lastX;
float lastY;
bool firstMouse = true;



//鼠标控制镜头方向
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    
    
	if (firstMouse == true)
	{
    
    
		lastX = xpos;
		lastY = ypos;
		firstMouse = false;
	}
	float deltaX, deltaY;
	float sensitivity = 0.05f;

	deltaX = (xpos - lastX)*sensitivity;
	deltaY = (ypos - lastY)*sensitivity;

	lastX = xpos;
	lastY = ypos;

	camera.ProcessMouseMovement(deltaX, deltaY);

};
//缩放
float fov = 45.0f;

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    
    
	if (fov >= 1.0f && fov <= 45.0f)
		fov -= yoffset;
	if (fov <= 1.0f)
		fov = 1.0f;
	if (fov >= 45.0f)
		fov = 45.0f;
}

#pragma endregion

unsigned int LoadImageToGPU(const char* filename, GLint internalFormat, GLenum format) {
    
    
	unsigned int TexBuffer;
	glGenTextures(1, &TexBuffer);
	glBindTexture(GL_TEXTURE_2D, TexBuffer);

	// 为当前绑定的纹理对象设置环绕、过滤方式
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	// 加载并生成纹理
	int width, height, nrChannel;
	unsigned char *data = stbi_load(filename, &width, &height, &nrChannel, 0);
	if (data) {
    
    
		glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
    
    
		printf("Failed to load texture");
	}
	glBindTexture(GL_TEXTURE_2D, 0);
	stbi_image_free(data);
	return TexBuffer;
}

// RenderQuad() Renders a 1x1 quad in NDC, best used for framebuffer color targets
// and post-processing effects.
GLuint quadVAO = 0;
GLuint quadVBO;
void RenderQuad()
{
    
    
	if (quadVAO == 0)
	{
    
    
		GLfloat 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(GLfloat), (GLvoid*)0);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
	}
	glBindVertexArray(quadVAO);
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
	glBindVertexArray(0);
}

// RenderCube() Renders a 1x1 3D cube in NDC.
GLuint cubeVAO = 0;
GLuint cubeVBO = 0;
void RenderCube()
{
    
    
	// Initialize (if necessary)
	if (cubeVAO == 0)
	{
    
    
		GLfloat vertices[] = {
    
    
			// Back face
			-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // Bottom-left
			0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right
			0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, // bottom-right         
			0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,  // top-right
			-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,  // bottom-left
			-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,// top-left
			// Front face
			-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left
			0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,  // bottom-right
			0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,  // top-right
			0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right
			-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,  // top-left
			-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // bottom-left
			// Left face
			-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right
			-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-left
			-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,  // bottom-left
			-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left
			-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,  // bottom-right
			-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right
			// Right face
			0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left
			0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right
			0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-right         
			0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,  // bottom-right
			0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,  // top-left
			0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-left     
			// Bottom face
			-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right
			0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, // top-left
			0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,// bottom-left
			0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, // bottom-left
			-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, // bottom-right
			-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right
			// Top face
			-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left
			0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
			0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, // top-right     
			0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
			-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left
			-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f // bottom-left        
		};
		glGenVertexArrays(1, &cubeVAO);
		glGenBuffers(1, &cubeVBO);
		// Fill buffer
		glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
		// Link vertex attributes
		glBindVertexArray(cubeVAO);
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindVertexArray(0);
	}
	// Render Cube
	glBindVertexArray(cubeVAO);
	glDrawArrays(GL_TRIANGLES, 0, 36);
	glBindVertexArray(0);
}



int main() {
    
    

#pragma region Open a Window
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	// Open GLFW Window
	GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
	if (window == NULL)
	{
    
    
		printf("Open window failed.");
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	// 关闭鼠标显示
	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
	// 回调函数监听鼠标
	glfwSetCursorPosCallback(window, mouse_callback);

	// 回调函数监听滚轮
	glfwSetScrollCallback(window, scroll_callback);


	// Init GLEW
	glewExperimental = true;
	if (glewInit() != GLEW_OK)
	{
    
    
		printf("Init GLEW failed.");
		glfwTerminate();
		return -1;
	}

	glEnable(GL_DEPTH_TEST);
	glViewport(0, 0, 800, 600);
#pragma endregion

	// Light sources
	// - Positions
	std::vector<glm::vec3> lightPositions;
	lightPositions.push_back(glm::vec3(0.0f, 0.0f, 49.5f)); // back light
	lightPositions.push_back(glm::vec3(-1.4f, -1.9f, 9.0f));
	lightPositions.push_back(glm::vec3(0.0f, -1.8f, 4.0f));
	lightPositions.push_back(glm::vec3(0.8f, -1.7f, 6.0f));
	// - Colors
	std::vector<glm::vec3> lightColors;
	lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f));
	lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f));
	lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f));
	lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f));

	Shader* Shader_Light = new Shader("lighting.vert", "lighting.frag");
	Shader* Shader_HDR = new Shader("hdr.vert", "hdr.frag");

	GLuint woodTexture = LoadImageToGPU("wood.jpg", GL_RGB, GL_RGB);


	// Set up floating point framebuffer to render scene to
	GLuint hdrFBO;
	glGenFramebuffers(1, &hdrFBO);
	// - Create floating point color buffer
	GLuint colorBuffer;
	glGenTextures(1, &colorBuffer);
	glBindTexture(GL_TEXTURE_2D, colorBuffer);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 800, 600, 0, GL_RGBA, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	// - Create depth buffer (renderbuffer)
	GLuint rboDepth;
	glGenRenderbuffers(1, &rboDepth);
	glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 800, 600);
	// - Attach buffers
	glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBuffer, 0);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
		std::cout << "Framebuffer not complete!" << std::endl;
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

	while (!glfwWindowShouldClose(window))
	{
    
    
		// Set frame time
		GLfloat currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		// Check and call events
		processInput(window);

		// 1. Render scene into floating point framebuffer
		glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glm::mat4 projection = glm::perspective(glm::radians(fov), (float)800 / (float)600, 0.1f, 100.0f);
		glm::mat4 view = camera.GetViewMatrix();
		glm::mat4 model;
		Shader_Light->use();
		glUniformMatrix4fv(glGetUniformLocation(Shader_Light->ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
		glUniformMatrix4fv(glGetUniformLocation(Shader_Light->ID, "view"), 1, GL_FALSE, glm::value_ptr(view));
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, woodTexture);
		// - set lighting uniforms
		for (GLuint i = 0; i < lightPositions.size(); i++)
		{
    
    
			glUniform3fv(glGetUniformLocation(Shader_Light->ID, ("lights[" + std::to_string(i) + "].Position").c_str()), 1, &lightPositions[i][0]);
			glUniform3fv(glGetUniformLocation(Shader_Light->ID, ("lights[" + std::to_string(i) + "].Color").c_str()), 1, &lightColors[i][0]);
		}
		glUniform3f(glGetUniformLocation(Shader_Light->ID, "viewPos"), camera.Position.x, camera.Position.y, camera.Position.z);

		// - render tunnel
		model = glm::mat4();
		model = glm::translate(model, glm::vec3(0.0f, 0.0f, 25.0));
		model = glm::scale(model, glm::vec3(5.0f, 5.0f, 55.0f));
		glUniformMatrix4fv(glGetUniformLocation(Shader_Light->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
		glUniform1i(glGetUniformLocation(Shader_Light->ID, "inverse_normals"), GL_TRUE);
		RenderCube();
		glBindFramebuffer(GL_FRAMEBUFFER, 0);

		// 2. Now render floating point color buffer to 2D quad and tonemap HDR colors to default framebuffer's (clamped) color range
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		Shader_HDR->use();
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, colorBuffer);
		glUniform1i(glGetUniformLocation(Shader_HDR->ID, "hdr"), hdr);
		glUniform1f(glGetUniformLocation(Shader_HDR->ID, "exposure"), exposure);
		RenderQuad();

		std::cout << "exposure: " << exposure << std::endl;

		// Swap the buffers
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwTerminate();
	return 0;
}

3 色调映射

色调映射(Tone Mapping)的过程是一个损失较小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程,通常会伴有特定的风格的色平衡(Stylistic Color Balance)。

最简单的色调映射算法是Reinhard色调映射,它涉及到分散整个HDR颜色值到LDR颜色值上,所有的值都有对应。
Reinhard色调映射算法平均地将所有亮度值分散到LDR上。
我们将Reinhard色调映射应用到之前的片段着色器上,并且为了更好的测量加上一个Gamma校正过滤(包括SRGB纹理的使用):
main.cpp导入材质的时候记得改

GLuint woodTexture = LoadImageToGPU("wood.jpg", GL_SRGB, GL_RGB);

void main()
{
    
                 
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    // Reinhard色调映射
    vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
    // Gamma校正
    mapped = pow(mapped, vec3(1.0 / gamma));

    FragColor = vec4(mapped, 1.0);
}    

有了Reinhard色调映射的应用,我们不再会在场景明亮的地方损失细节。当然,这个算法是倾向明亮的区域的,暗的区域会不那么精细也不那么有区分度。
在这里插入图片描述

现在可以看到在隧道的尽头木头纹理变得可见了。
用了这个非常简单地色调映射算法,我们可以合适的看到存在浮点帧缓冲中整个范围的HDR值,使我们能在不丢失细节的前提下,对场景光照有精确的控制。

另一个有趣的色调映射应用是曝光(Exposure)参数的使用。
可能还记得之前我们在介绍里讲到的,HDR图片包含在不同曝光等级的细节。

如果我们有一个场景要展现日夜交替,我们当然会在白天使用低曝光,在夜间使用高曝光,就像人眼调节方式一样。

有了这个曝光参数,我们可以去设置可以同时在白天和夜晚不同光照条件工作的光照参数,我们只需要调整曝光参数就行了。

一个简单的使用指数形式的曝光色调映射算法会像这样,我们修改hdr的片段着色器:

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D hdrBuffer;
uniform bool hdr;
uniform float exposure;


void main()
{
    
                 
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

	if(hdr)
    {
    
    
        // reinhard
        // vec3 result = hdrColor / (hdrColor + vec3(1.0));
        // exposure
        vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
        // also gamma correct while we're at it       
        result = pow(result, vec3(1.0 / gamma));
        FragColor = vec4(result, 1.0);
    }
    else
    {
    
    
        vec3 result = pow(hdrColor, vec3(1.0 / gamma));
        FragColor = vec4(result, 1.0);
    }
}

在这里我们将exposure定义为默认为1.0的uniform,从而允许我们更加精确设定我们是要注重黑暗还是明亮的区域的HDR颜色值。
举例来说,高曝光值会使隧道的黑暗部分显示更多的细节
然而低曝光值会显著减少黑暗区域的细节,但允许我们看到更多明亮区域的细节。
下面这组图片展示了在不同曝光值下的通道:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个图片清晰地展示了HDR渲染的优点。
通过改变曝光等级,我们可以看见场景的很多细节,而这些细节可能在LDR渲染中都被丢失了。
比如说隧道尽头,在正常曝光下木头结构隐约可见,但用低曝光木头的花纹就可以清晰看见了。对于近处的木头花纹来说,在高曝光下会能更好的看见。

4 HDR拓展

在这里展示的两个色调映射算法仅仅是大量(更先进)的色调映射算法中的一小部分,这些算法各有长短。
一些色调映射算法倾向于特定的某种颜色/强度,也有一些算法同时显示低高曝光颜色从而能够显示更加多彩和精细的图像。
也有一些技巧被称作自动曝光调整(Automatic Exposure Adjustment)或者叫人眼适应(Eye Adaptation)技术,它能够检测前一帧场景的亮度并且缓慢调整曝光参数模仿人眼使得场景在黑暗区域逐渐变亮或者在明亮区域逐渐变暗。

比如这篇使命召唤手游的自动曝光方法

HDR渲染的真正优点在庞大和复杂的场景中应用复杂光照算法会被显示出来,但是出于教学目的创建这样复杂的演示场景是很困难的,这个教程用的场景是很小的,而且缺乏细节。
但是如此简单的演示也是能够显示出HDR渲染的一些优点:在明亮和黑暗区域无细节损失,因为它们可以通过色调映射重新获得;多个光照的叠加不会导致亮度被截断的区域的出现,光照可以被设定为它们原来的亮度值而不是被LDR值限制。
而且,HDR渲染也使一些有趣的效果更加可行和真实; 其中一个效果叫做泛光(Bloom),我们将在下一节讨论。

5 其他色调映射方法

我们有很多色调映射的方法,比如这些
全局方法:Gamma校正,对数校正,直方图规定化,分段灰度变换
以及局部方法
相关的有一些图像处理的知识,这学期上课也有讲到,也做了精确直方图均衡以及滤波之类的作业,到时候会整理一下

猜你喜欢

转载自blog.csdn.net/weixin_43803133/article/details/109408288
HDR