LearnOpenGL学习笔记—高级光照 04:法线贴图

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

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

1 引入

我们场景中已经充满了多边形物体,其中每个都可能由成百上千平坦的三角形组成。

我们以向三角形上附加纹理的方式来增加额外细节,提升真实感,隐藏多边形几何体是由无数三角形组成的事实。

纹理确有助益,然而当近看它们时,这个事实便隐藏不住了。

现实中的物体表面并非是平坦的,而是表现出无数(凹凸不平的)细节。

例如,砖块的表面。砖块的表面非常粗糙,显然不是完全平坦的:它包含着接缝处水泥凹痕,以及非常多的细小的空洞。

如果我们在一个有光的场景中看这样一个砖块的表面,问题就出来了。

下图中我们可以看到砖块纹理应用到了平坦的表面,并被一个点光源照亮。

在这里插入图片描述
光照并没有呈现出任何裂痕和孔洞,完全忽略了砖块之间凹进去的线条;表面看起来完全就是平的。

我们需要的是某种可以告知光照系统给所有有关物体表面类似深度这样的细节的方式。

如果我们以光的视角来看这个问题:是什么使表面被视为完全平坦的表面来照亮?答案会是表面的法线向量。

以光照算法的视角考虑的话,只有一件事决定物体的形状,这就是垂直于它的法线向量。

砖块表面只有一个法线向量,表面完全根据这个法线向量被以一致的方式照亮。

如果每个fragment都是用自己的不同的法线会怎样?

这样我们就可以根据表面细微的细节对法线向量进行改变;这样就会获得一种表面看起来要复杂得多的幻觉:
在这里插入图片描述
每个fragment使用了自己的法线,我们就可以让光照相信一个表面由很多微小的(垂直于法线向量的)平面所组成,物体表面的细节将会得到极大提升。
这种每个fragment使用各自的法线,替代一个面上所有fragment使用同一个法线的技术叫做法线贴图(normal mapping)或凹凸贴图(bump mapping)。
可以再之前Games101这讲的第十节找到相关的信息

应用到砖墙上,效果会像这样:
在这里插入图片描述
可以看到细节获得了极大提升,开销却不大。因为我们只改变每个fragment的法线向量,而没有去移动他们的位置,并不需要改变所有光照公式。
现在我们是为每个fragment传递一个法线,不再使用插值表面法线。这样光照使表面拥有了自己的细节。

2 法线贴图

为使法线贴图工作,我们需要为每个fragment提供一个法线。

像diffuse贴图和specular贴图一样,我们可以使用一个2D纹理来储存法线数据。

2D纹理不仅可以储存颜色和光照数据,还可以储存法线向量。

这样我们可以从2D纹理中采样得到特定纹理的法线向量。

由于法线向量是个几何工具,而纹理通常只用于储存颜色信息,用纹理储存法线向量不是非常直接。

但是可以这样,用纹理中的颜色向量r、g、b元素代表一个3D向量。

类似的我们也可以将法线向量的x、y、z元素储存到纹理中,代替颜色的r、g、b元素。

法线向量的范围在-1到1之间,所以我们先要将其映射到0到1的范围:

vec3 rgb_normal = normal * 0.5 + 0.5; // 从 [-1,1] 转换至 [0,1]

将法线向量变换为像这样的RGB颜色元素,我们就能把根据表面的形状的fragment的法线保存在2D纹理中。
开头展示的那个砖块的例子的法线贴图如下所示:
在这里插入图片描述
这会是一种偏蓝色调的纹理(在网上找到的几乎所有法线贴图都是这样的)。
这是因为所有法线的指向都偏向z轴(0, 0, 1)这是一种偏蓝的颜色。

法线向量从z轴方向也向其他方向轻微偏移,颜色也就发生了轻微变化,这样看起来便有了一种深度。

例如,你可以看到在每个砖块的顶部,颜色倾向于偏绿,这是因为砖块的顶部的法线偏向于指向正y轴方向(0, 1, 0),这样它就是绿色的了。

在一个简单的朝向正z轴的平面上,我们可以用以下的diffuse纹理和这个法线贴图来渲染。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201030201358975.png在这里插入图片描述

在这里插入图片描述

加载纹理,把它们绑定到合适的纹理单元,然后使用下面的思路的片段着色器来渲染一个平面:

uniform sampler2D normalMap;  

void main()
{
    
               
    // 从法线贴图范围[0,1]获取法线
    normal = texture(normalMap, fs_in.TexCoords).rgb;
    // 将法线向量转换为范围[-1,1]
    normal = normalize(normal * 2.0 - 1.0);   

    [...]
    // 像往常那样处理光照
}

这里我们将被采样的法线颜色从0到1重新映射回-1到1,便能将RGB颜色重新处理成法线,然后使用采样出的法线向量应用于光照的计算。
在这里插入图片描述

2.1 代码

顶点着色器:

#version 330 core									  
layout(location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0
layout(location = 1) in vec2 aTexCoord; // 纹理坐标的属性位置值为 1

out vec2 TexCoord;


//着色点
out vec3 FragPos;

uniform mat4 modelMat;

uniform mat4 viewMat;
uniform mat4 projMat;

void main(){
    
    										   
   gl_Position =  projMat * viewMat * modelMat * vec4(aPos.xyz,1.0);   
   FragPos=vec3(modelMat * vec4(aPos.xyz,1.0));
   TexCoord=aTexCoord;
}

片段着色器:

#version 330 core	

 
in vec2 TexCoord;
in vec3 FragPos;
uniform sampler2D normalMap; 
uniform sampler2D DiffuseTexture; 


struct LightPoint{
    
    
    vec3 pos;
    vec3 color;
	float constant;
	float linear;
	float quadratic;
};
uniform LightPoint lightP0;


out vec4 FragColor;			


uniform vec3 ambientColor;
uniform vec3 cameraPos;
vec3 CalcLightPoint(LightPoint light, vec3 uNormal, vec3 dirToCamera){
    
    
	vec3 dirToLight=normalize(light.pos-FragPos);	
	//diffuse
	float diffIntensity = max(dot(dirToLight,uNormal), 0.0);
	vec3 diffuseColor =   diffIntensity * texture(DiffuseTexture,TexCoord).rgb * light.color;
	//Blinn-Phong specular
	vec3 halfwarDir = normalize(dirToLight + dirToCamera);
	float specularAmount = pow(max(dot(uNormal, halfwarDir), 0.0),150.0f);
	vec3 specularColor = vec3(0.2) * specularAmount * light.color;
	vec3 result = (diffuseColor+specularColor);
	return result;
}



void main(){
    
    
	vec3 finalResult = vec3(0,0,0);
	// 从法线贴图范围[0,1]获取法线
    // 将法线向量转换为范围[-1,1]
	vec3 uNormal =texture(normalMap, TexCoord).rgb;

    uNormal =normalize( uNormal * 2.0f - 1.0f);   


	vec3 dirToCamera = normalize(cameraPos - FragPos);
	//ambient
	vec3 ambient=  ambientColor * texture(DiffuseTexture,TexCoord).rgb;
	
	finalResult += CalcLightPoint(lightP0, uNormal, dirToCamera);

	finalResult += ambient;
	
	FragColor=vec4(finalResult,1.0);


	//FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

main.cpp

#include <iostream>

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

#include "Material.h"
#include "Shader.h"
#include "Camera.h"

#include "LightPoint.h"
#include "LightDirectional.h"

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





#pragma region Camera Declare
//建立camera
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 5.0f);
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, -1.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();
}
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

// positions
glm::vec3 pos1(-1.0, 1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
// texture coordinates
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);


GLfloat planeVertices[] = {
    
    
	// Positions             // TexCoords 
	pos1.x, pos1.y, pos1.z,  uv1.x, uv1.y,
	pos2.x, pos2.y, pos2.z,  uv2.x, uv2.y,
	pos3.x, pos3.y, pos3.z,  uv3.x, uv3.y,

	pos1.x, pos1.y, pos1.z,  uv1.x, uv1.y,
	pos3.x, pos3.y, pos3.z,  uv3.x, uv3.y,
	pos4.x, pos4.y, pos4.z,  uv4.x, uv4.y
};


LightPoint lightP0(glm::vec3(1.0f, 0.0f, 1.0f),
glm::vec3(1.0f, 1.0f, 1.0f));
//LightDirectional lightD(
//	glm::vec3(45.0f, 0, 0),
//	glm::vec3(0.9f, 0.9f, 0.9f));
//glm::vec3 lightPos(0.0f, 10.0f, 10.0f);


//加载一般的图片
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;
}

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
#pragma region Init Shader Program
	Shader* Shader_NormalTest = new Shader("vertex_NormalTest.vert", "fragment_NormalTest.frag");
#pragma endregion

	GLuint DiffuseTexture = LoadImageToGPU("brickwall.jpg", GL_RGB, GL_RGB);
	GLuint NormalTexture = LoadImageToGPU("brickwall_normal.jpg", GL_RGB, GL_RGB);
	
	Shader_NormalTest->use();
	glUniform1i(glGetUniformLocation(Shader_NormalTest->ID, "DiffuseTexture"), 0);
	glUniform1i(glGetUniformLocation(Shader_NormalTest->ID, "normalMap"), 1);
	
	
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), planeVertices, GL_STATIC_DRAW);


	
	// 顶点位置
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 顶点纹理坐标	
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);
	


	//model
	glm::mat4 modelMat;
	//view
	glm::mat4 viewMat;
	//projection
	glm::mat4 projMat;
	//modelMat = glm::rotate(modelMat, glm::radians(-90.0f), glm::vec3(1.0, 0.0, 0.0));
	while (!glfwWindowShouldClose(window))
	{
    
    
		

		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);
		
		// 显示相关的数据
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		// 检测输入

		processInput(window);
		
		//设置好uniform缓冲中的观察矩阵
		viewMat = camera.GetViewMatrix();


		//设置好透视矩阵
		projMat = glm::perspective(glm::radians(fov), 800.0f/600.0f, 0.1f, 100.0f);
		

		Shader_NormalTest->use();

		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "cameraPos"), camera.Position.x, camera.Position.y, camera.Position.z);
		
		//lightP0.position = glm::vec3(2.0f, sin((GLfloat)glfwGetTime()), 1.0f);
	
		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "lightP0.pos"), lightP0.position.x, lightP0.position.y, lightP0.position.z);
		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "lightP0.color"), lightP0.color.x, lightP0.color.y, lightP0.color.z);
		glUniform1f(glGetUniformLocation(Shader_NormalTest->ID, "lightP0.constant"), lightP0.constant);
		glUniform1f(glGetUniformLocation(Shader_NormalTest->ID, "lightP0.linear"), lightP0.linear);
		glUniform1f(glGetUniformLocation(Shader_NormalTest->ID, "lightP0.quadratic"), lightP0.quadratic);


		

		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "ambientColor"), 0.3f, 0.3f, 0.3f);
		//modelMat = glm::rotate(modelMat, glm::radians(1.0f), glm::vec3(1.0, 0.0, 0.0));
		glUniformMatrix4fv(glGetUniformLocation(Shader_NormalTest->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
		glUniformMatrix4fv(glGetUniformLocation(Shader_NormalTest->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
		glUniformMatrix4fv(glGetUniformLocation(Shader_NormalTest->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, DiffuseTexture);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, NormalTexture);

		// 绘制网格
		glBindVertexArray(VAO);

		glDrawArrays(GL_TRIANGLES,0, 6);


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

	glfwTerminate();
	return 0;

}

3 问题

我们发现有个问题限制了刚才讲的那种法线贴图的使用
我们使用的那个法线贴图里面的所有法线向量都是指向正z方向的,上面的例子能用,是因为那个平面的表面法线也是指向正z方向的。
可是,如果我们在表面法线指向正y方向的平面上使用同一个法线贴图会发生什么?
在这里插入图片描述
光照看起来完全不对,发生这种情况是平面的表面法线现在指向了y,而采样得到的法线仍然指向的是z。
结果就是光照仍然认为表面法线和之前朝向正z方向时一样;这样光照就不对了。
下面的图片类似展示了这个表面上采样的法线的近似情况:
在这里插入图片描述
可以看到所有法线都指向z方向,但是它们本该朝着表面法线指向y方向的。

一个可行方案是为每个表面制作一个单独的法线贴图。如果是一个立方体的话我们就需要6个法线贴图,但是如果模型上有无数的朝向不同方向的表面,就很难了。

另一个解决方案是,在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照有关的向量都需要相对到这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。

这个坐标空间叫做切线空间(tangent space)。

4 切线空间

  • 如果法线处于世界坐标中的(world space),那称为world space normal。如果是处于物体本身局部坐标中的,那称为object space normal。
    很容易想象,world space normal一旦从贴图里解压出来后,就可以直接用了,效率很高。
    但是有个缺点,这个world space normal 是固定了,如果物体没有保持原来的方向和位置,那原来生成的normal map就作废了。

  • 因此又有人保存了object space normal。它从贴图里解压,还需要乘以model-view矩阵转换到世界坐标,或者转换到其他坐标取决于计算过程及需求。
    object space normal生成的贴图,物体可以被旋转和位移.基本让人满意。
    但仍有一个缺点。就是一张贴图只能对应特定的一个模型,模型不能有变形(deform)。

  • 变形时,顶点关系改变了,即面的形状,方向改变了。
    如果面上存在一个固定的坐标系,那当物体变形、移动、旋转时,这个坐标系必定跟着面一起运动。
    当整个面发生变化时,我们只需要计算面上的坐标系到世界坐标系的转换矩阵,那么定义在这个面上的点或坐标(固定的),乘以这个矩阵即可得到在世界中的坐标。
    这个坐标系术语里称为tangent space。

  • 按照新方法每个面都有一个局部坐标系,当低模变形时,即三角面变化时,它的tangent space也会跟着变化,保存在贴图里的法线乘以低模这个面的tangent space到外部坐标系的转换矩阵,即可得到外部坐标。

这种矩阵叫做TBN矩阵。

这三个字母分别代表tangent、bitangent和normal向量。这是建构这个矩阵所需的向量。

已知上向量是表面的法线向量。右和前向量是切线(Tagent)和副切线(Bitangent)向量。下面的图片展示了表面的三个向量:
在这里插入图片描述

Tangent space是一个三维空间。
对3D空间中的一个顶点来说,切空间的三条座标轴分别对应该点的法线N,切线T,和副法线B,显然,对不同的顶点来说,切空间是不同的。

那么在已知三角形三个顶点及其纹理坐标的时候,如何计算出T,B呢?

下图显示了一个三角形及其所在的切线空间。
在这里插入图片描述
目前已知的数据有三角形的三个顶点在世界坐标中的位置: P 0 P_0 P0, P 1 P_1 P1, P 2 P_2 P2, 以及相应的纹理坐标在纹理空间中的位置 ( U 0 , V 0 ) , ( U 1 , V 1 ) , ( U 2 , V 2 ) (U_0,V_0), (U_1,V_1), (U_2,V_2) (U0,V0)(U1,V1)(U2,V2)

我们可以得到边 E 1 E_1 E1, E 2 E_2 E2(顶点相减得到的边是绝对的,不论是在什么坐标系下都是一样的,所以能一边用世界坐标的位置,一边用纹理坐标,然后划等号)

并且法线贴图的切线和副切线与纹理坐标的两个方向对齐
所以用这个特性计算每个表面的切线和副切线可以以u,v算T,B
在这里插入图片描述
在这里插入图片描述
有了最后这个等式,我们可以用一个三角形的顶点和纹理坐标(因为纹理坐标和切线向量在同一空间中)计算出切线和副切线。

  • 上面的方法求到的切线空间是基于单个三角形的,而在3D管线中,我们的处理是基于顶点进行的。
    因此我们需要获得顶点对应的切线空间。

  • 不过有了每个三角形的切换空间,每个顶点的切线空间就很容易处理了,即对于任一顶点,我们使用其所在的所有三角形所对应的切线空间向量的平均值,作为该顶点的切线空间。

  • 熟悉法线求法的可能会发现,这种方法与通过三角形法线求取顶点法线的方法思路是完全一样的。

4.1 手工计算切线和副切线

我们的场景中有一个简单的2D平面,它朝向正z方向。

这次我们使用切线空间来实现法线贴图,所以我们可以使平面朝向任意方向,法线贴图仍然能够工作。

使用前面讨论的数学方法,我们来手工计算出表面的切线和副切线向量。

假设平面使用下面的向量建立起来(1、2、3和1、3、4,它们是两个三角形):

// positions
glm::vec3 pos1(-1.0,  1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
// texture coordinates
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);
// normal vector
glm::vec3 nm(0.0, 0.0, 1.0);

我们先计算第一个三角形的边和deltaUV坐标:

glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;

有了计算切线和副切线的必备数据,我们就可以开始写出来自于前面部分中的下列等式:

GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
tangent1 = glm::normalize(tangent1);

bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
bitangent1 = glm::normalize(bitangent1);  

[...] // 对平面的第二个三角形采用类似步骤计算切线和副切线

我们预先计算出等式的分数部分f,然后把它和每个向量的元素进行相应矩阵乘法。
最后我们还要进行标准化,来确保切线/副切线向量最后是单位向量。

  • 因为一个三角形永远是平坦的形状,我们只需为每个三角形计算一个切线/副切线,它们对于每个三角形上的顶点都是一样的。
  • 像之前提到的,大多数三角形和三角形之间都会共享顶点。
    这种情况下开发者通常将每个顶点的法线和切线/副切线等顶点属性平均化,以获得更加柔和的效果。
    但是我们的平面的三角形之间尽管分享了一些顶点,但是因为两个三角形相互并行,因此并不需要将结果平均化。

最后的切线和副切线向量的值应该是(1, 0, 0)和(0, 1, 0),它们和法线(0, 0, 1)组成相互垂直的TBN矩阵。
每个顶点定义了切线和副切线向量,我们就可以开始实现正确的法线贴图了。

4.2 切线空间法线贴图

为让法线贴图工作,我们先得在着色器中创建一个TBN矩阵。我们先将前面计算出来的切线和副切线向量传给顶点着色器,作为它的属性:
(记住我们这时候的normal是用来算TBN矩阵的,不是算光照的normal)

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;

在顶点着色器的main函数中我们创建TBN矩阵:

void main()
{
    
    
   [...]
   vec3 T = normalize(vec3(model * vec4(tangent,   0.0)));
   vec3 B = normalize(vec3(model * vec4(bitangent, 0.0)));
   vec3 N = normalize(vec3(model * vec4(normal,    0.0)));
   mat3 TBN = mat3(T, B, N)
}

我们先将所有TBN向量变换到我们所操作的坐标系中(乘以model矩阵),现在是世界空间。

然后我们创建实际的TBN矩阵,直接把相应的向量应用到mat3构造器就行。

  • 注意,如果我们希望更精确的话就不要将TBN向量乘以model矩阵,而是使用法线矩阵,但我们只关心向量的方向,不会平移也和缩放这个变换。

  • 从技术上讲,顶点着色器中无需副切线。所有的这三个TBN向量都是相互垂直的所以我们可以在顶点着色器中用T和N向量的叉乘,自己计算出副切线:vec3 B = cross(T, N)可以节省每个顶点的数据量。

现在我们有了TBN矩阵,如果来使用它呢?通常来说有两种方式使用它,我们会把这两种方式都说明一下:

  • 我们直接使用TBN矩阵,这个矩阵可以把切线坐标空间的向量转换到世界坐标空间。因此我们把它传给片段着色器中,把通过采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。
  • 我们也可以使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间,这样法线和其他光照变量再一次在一个坐标系中了。

4.2.1 TBN矩阵使用一

我们来看看第一种情况。
我们从法线贴图采样得来的法线向量,是在切线空间表示的,但是其他光照向量都是在世界空间表示的。
把TBN传给片段着色器,我们就能将采样得来的切线空间的法线乘以这个TBN矩阵,将法线向量变换到和其他光照向量一样的参考空间中。这种方式随后所有光照计算都可以简单的理解。

把TBN矩阵发给片段着色器思路很简单:

out VS_OUT {
    
    
    vec3 FragPos;
    vec2 TexCoords;
    mat3 TBN;
} vs_out;  

void main()
{
    
    
    [...]
    vs_out.TBN = mat3(T, B, N);
}

在片段着色器中我们用mat3作为输入变量:

in VS_OUT {
    
    
    vec3 FragPos;
    vec2 TexCoords;
    mat3 TBN;
} fs_in

有了TBN矩阵我们现在就可以更新法线贴图代码,引入切线到世界空间变换:

normal = texture(normalMap, fs_in.TexCoords).rgb;
normal = normalize(normal * 2.0 - 1.0);   
normal = normalize(fs_in.TBN * normal);

因为最后的normal现在在世界空间中了,就不用改变其他像素着色器的代码了,因为光照代码就是假设法线向量在世界空间中。

4.2.2 TBN矩阵使用二

我们同样看看第二种情况。
我们用TBN矩阵的逆矩阵将所有相关的世界空间向量转变到,采样所得法线向量的空间:切线空间。TBN的建构还是一样,但我们在将其发送给片段着色器之前先要求逆矩阵:

vs_out.TBN = transpose(mat3(T, B, N));

注意,这里我们使用transpose函数,而不是inverse函数。
正交矩阵(每个轴既是单位向量同时相互垂直)的一大属性是一个正交矩阵的置换矩阵与它的逆矩阵相等。

这个属性重要是因为逆矩阵的求得比求置换开销大。

在片段着色器中我们不用对法线向量变换,但我们要把其他相关向量转换到切线空间,它们是lightDir和viewDir。这样每个向量还是在同一个空间(切线空间)中了。

void main()
{
    
               
    vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
    normal = normalize(normal * 2.0 - 1.0);   

    vec3 lightDir = fs_in.TBN * normalize(lightPos - fs_in.FragPos);
    vec3 viewDir  = fs_in.TBN * normalize(viewPos - fs_in.FragPos);    
    [...]
}

第二种方法看似要做的更多,它还需要在像素着色器中进行更多的乘法操作,所以为何还用第二种方法呢?

将向量从世界空间转换到切线空间有个额外好处。

我们可以把所有相关向量在顶点着色器中转换到切线空间,不用在片段着色器中做这件事。

这是可行的,因为lightPos和viewPos不是每个fragment运行都要改变,对于fs_in.FragPos,我们也可以在顶点着色器计算它的切线空间位置。

基本上,不需要把任何向量在片段着色器中进行变换,而第一种方法中就是必须的,因为第一种方法采样出来的法线向量对于每个像素着色器都不一样。

所以现在不是把TBN矩阵的逆矩阵发送给像素着色器,而是将切线空间的光源位置,观察位置以及顶点位置发送给片段着色器。

这样我们就不用在片段着色器里进行矩阵乘法了。这是一个极佳的优化,因为顶点着色器通常比像素着色器运行的少。

这也是为什么这种方法是一种更好的实现方式的原因。

out VS_OUT {
    
    
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} vs_out;

uniform vec3 lightPos;
uniform vec3 viewPos;

[...]

void main()
{
    
        
    [...]
    mat3 TBN = transpose(mat3(T, B, N));
    vs_out.TangentLightPos = TBN * lightPos;
    vs_out.TangentViewPos  = TBN * viewPos;
    vs_out.TangentFragPos  = TBN * vec3(model * vec4(position, 0.0));
}

在像素着色器中我们使用这些新的输入变量来计算切线空间的光照。因为法线向量已经在切线空间中了,光照就有意义了。

将法线贴图应用到切线空间上,这次我们可以将平面朝向各个方向,光照一直都会是正确的:
在这里插入图片描述
在这里插入图片描述

5 复杂物体

关于如何使用Assimp的那个加载器实现导入法线贴图这些具体的细节就看官网的了。
简单来说,使用法线贴图也是一种提升场景表现的重要方式。
在使用法线贴图之前我们不得不使用相当多的顶点才能表现出一个更精细的网格,但使用了法线贴图我们可以使用更少的顶点表现出同样丰富的细节。
在这里插入图片描述
在这里插入图片描述
高精度网格和使用法线贴图的低精度网格几乎区分不出来。
所以法线贴图不仅看起来漂亮,它也是一个将高精度多边形转换为低精度多边形而不失细节的重要工具。

关于法线贴图还有最后一个技巧要讨论,它可以在不必花费太多性能开销的情况下稍稍提升画质表现。

  • 之前我们提过,当在更大的网格上计算切线向量的时候,它们往往有很大数量的共享顶点,当法向贴图应用到这些表面时将切线向量平均化通常能获得更好更平滑的结果。

这样做有个问题,就是TBN向量可能会不能互相垂直,这意味着TBN矩阵不再是正交矩阵了。法线贴图可能会稍稍偏移,但这仍然可以改进。

使用施密特正交化,我们可以对TBN向量进行重正交化,这样每个向量就又会重新垂直了。在顶点着色器中我们这样做:
(T是减去T在N上的投影)

vec3 T = normalize(vec3(model * vec4(tangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(normal, 0.0)));
// re-orthogonalize T with respect to N
T = normalize(T - dot(T, N) * N);
// then retrieve perpendicular vector B with the cross product of T and N
vec3 B = cross(T, N);
mat3 TBN = mat3(T, B, N)

6 代码

使用TBN矩阵后有一些修改
顶点着色器如下:

#version 330 core									  

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;

out VS_OUT {
    
    
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} vs_out;



uniform mat4 modelMat;

uniform mat4 viewMat;
uniform mat4 projMat;


uniform vec3 lightPos;
uniform vec3 viewPos;

void main(){
    
    										   
   gl_Position =  projMat * viewMat * modelMat * vec4(position.xyz,1.0);   
   vs_out.FragPos=vec3(modelMat * vec4(position.xyz,1.0));
   vs_out.TexCoords=texCoords;

   mat3 normalMatrix = transpose(inverse(mat3(modelMat)));
   vec3 T = normalize(normalMatrix * tangent);
   vec3 B = normalize(normalMatrix * bitangent);
   vec3 N = normalize(normalMatrix * normal);    
      
   mat3 TBN = transpose(mat3(T, B, N));  
   vs_out.TangentLightPos = TBN * lightPos;
   vs_out.TangentViewPos  = TBN * viewPos;
   vs_out.TangentFragPos  = TBN * vs_out.FragPos;
}

片段着色器如下:

#version 330 core	

 
in VS_OUT {
    
    
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} fs_in;


uniform sampler2D normalMap; 
uniform sampler2D DiffuseTexture; 

out vec4 FragColor;			

uniform vec3 lightColor;
uniform vec3 ambientColor;




void main(){
    
    
	vec3 finalResult = vec3(0,0,0);
	// 从法线贴图范围[0,1]获取法线
    // 将法线向量转换为范围[-1,1]
	vec3 uNormal =texture(normalMap, fs_in.TexCoords).rgb;

    uNormal =normalize( uNormal * 2.0f - 1.0f);   


	
	//ambient
	vec3 ambient=  ambientColor * texture(DiffuseTexture,fs_in.TexCoords).rgb;

	vec3 dirToCamera = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
	vec3 dirToLight=normalize(fs_in.TangentLightPos-fs_in.TangentFragPos);	
	//diffuse
	float diffIntensity = max(dot(dirToLight,uNormal), 0.0);
	vec3 diffuseColor =   diffIntensity * texture(DiffuseTexture,fs_in.TexCoords).rgb * lightColor;
	//Blinn-Phong specular
	vec3 halfwarDir = normalize(dirToLight + dirToCamera);
	float specularAmount = pow(max(dot(uNormal, halfwarDir), 0.0),32.0f);
	vec3 specularColor = vec3(0.2) * specularAmount * lightColor;
	vec3 result = (diffuseColor+specularColor);

	finalResult += result;
	finalResult += ambient;
	
	FragColor=vec4(finalResult,1.0);
}

main.cpp如下:

#include <iostream>

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

#include "Material.h"
#include "Shader.h"
#include "Camera.h"



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





#pragma region Camera Declare
//建立camera
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 5.0f);
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, -1.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();
}
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



#pragma region Light Declare
glm::vec3 LightPos(1.0f, 0.0f, 1.0f);
glm::vec3 LightColor(1.0f, 1.0f, 1.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;
}

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

#pragma region Plane Declare

	// positions
	glm::vec3 pos1(-1.0, 1.0, 0.0);
	glm::vec3 pos2(-1.0, -1.0, 0.0);
	glm::vec3 pos3(1.0, -1.0, 0.0);
	glm::vec3 pos4(1.0, 1.0, 0.0);
	// texture coordinates
	glm::vec2 uv1(0.0, 1.0);
	glm::vec2 uv2(0.0, 0.0);
	glm::vec2 uv3(1.0, 0.0);
	glm::vec2 uv4(1.0, 1.0);
	// normal vector
	glm::vec3 nm(0.0, 0.0, 1.0);

	// calculate tangent/bitangent vectors of both triangles
	glm::vec3 tangent1, bitangent1;
	glm::vec3 tangent2, bitangent2;
	// - triangle 1
	glm::vec3 edge1 = pos2 - pos1;
	glm::vec3 edge2 = pos3 - pos1;
	glm::vec2 deltaUV1 = uv2 - uv1;
	glm::vec2 deltaUV2 = uv3 - uv1;

	GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

	tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
	tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
	tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
	tangent1 = glm::normalize(tangent1);

	bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
	bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
	bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
	bitangent1 = glm::normalize(bitangent1);

	// - triangle 2
	edge1 = pos3 - pos1;
	edge2 = pos4 - pos1;
	deltaUV1 = uv3 - uv1;
	deltaUV2 = uv4 - uv1;

	f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

	tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
	tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
	tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
	tangent2 = glm::normalize(tangent2);


	bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
	bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
	bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
	bitangent2 = glm::normalize(bitangent2);



	GLfloat planeVertices[] = {
    
    
		// Positions            // normal         // TexCoords  // Tangent                          // Bitangent
		pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
		pos2.x, pos2.y, pos2.z, nm.x, nm.y, nm.z, uv2.x, uv2.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
		pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,

		pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
		pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
		pos4.x, pos4.y, pos4.z, nm.x, nm.y, nm.z, uv4.x, uv4.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z
	};


#pragma endregion


#pragma region Init Shader Program
	Shader* Shader_NormalTest = new Shader("vertex_NormalTest.vert", "fragment_NormalTest.frag");
#pragma endregion

	GLuint DiffuseTexture = LoadImageToGPU("brickwall.jpg", GL_RGB, GL_RGB);
	GLuint NormalTexture = LoadImageToGPU("brickwall_normal.jpg", GL_RGB, GL_RGB);
	
	Shader_NormalTest->use();
	glUniform1i(glGetUniformLocation(Shader_NormalTest->ID, "DiffuseTexture"), 0);
	glUniform1i(glGetUniformLocation(Shader_NormalTest->ID, "normalMap"), 1);
	
	
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), planeVertices, GL_STATIC_DRAW);


	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
	glEnableVertexAttribArray(3);
	glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(8 * sizeof(GLfloat)));
	glEnableVertexAttribArray(4);
	glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(11 * sizeof(GLfloat)));
	


	//model
	glm::mat4 modelMat;
	//view
	glm::mat4 viewMat;
	//projection
	glm::mat4 projMat;
	//modelMat = glm::rotate(modelMat, glm::radians(-90.0f), glm::vec3(1.0, 0.0, 0.0));
	while (!glfwWindowShouldClose(window))
	{
    
    
		

		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);
		
		// 显示相关的数据
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		// 检测输入

		processInput(window);
		
		//设置好uniform缓冲中的观察矩阵
		viewMat = camera.GetViewMatrix();


		//设置好透视矩阵
		projMat = glm::perspective(glm::radians(fov), 800.0f/600.0f, 0.1f, 100.0f);
		

		Shader_NormalTest->use();
		modelMat = glm::rotate(modelMat, glm::radians(0.5f), glm::vec3(1.0, 0.0, 0.0));
		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "viewPos"), camera.Position.x, camera.Position.y, camera.Position.z);
		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "lightPos"), LightPos.x, LightPos.y, LightPos.z);

		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "ambientColor"), 0.3f, 0.3f, 0.3f);
		glUniform3f(glGetUniformLocation(Shader_NormalTest->ID, "lightColor"),LightColor.x, LightColor.y, LightColor.z);
		

		//modelMat = glm::rotate(modelMat, glm::radians(1.0f), glm::vec3(1.0, 0.0, 0.0));
		glUniformMatrix4fv(glGetUniformLocation(Shader_NormalTest->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
		glUniformMatrix4fv(glGetUniformLocation(Shader_NormalTest->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
		glUniformMatrix4fv(glGetUniformLocation(Shader_NormalTest->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, DiffuseTexture);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, NormalTexture);

		// 绘制网格
		glBindVertexArray(VAO);

		glDrawArrays(GL_TRIANGLES,0, 6);


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

	glfwTerminate();
	return 0;

}

猜你喜欢

转载自blog.csdn.net/weixin_43803133/article/details/109392166