引言
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是以目前我们所拥有的处理能力无法模拟的。因此OpenGL的光照仅仅使用了简化的模型并基于对现实的估计来进行模拟,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
环境光照:即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远都给物体一些颜色。
漫反射光照:模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。
镜面光照:模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。
下图中立方体依次为对物体施加环境光照、漫反射光照、镜面反射所呈现出的渲染效果。最后一个立方体代表同时施加环境光照、漫反射光照、镜面反射所呈现出的渲染效果。
环境光照
光通常都不是来自于同一光源,而是来自散落于我们周围的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散和反弹,所以光最后到达的地点可能并不是它所临近的直射方向;光能够像这样反射(Reflect)到其他表面,一个物体的光照可能受到来自一个非直射的光源影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。
因为我们不是复杂和昂贵算法的死忠粉丝,所以我们将会使用一种简化的全局照明模型,叫做环境光照(Ambient Lighting)。如你在前面章节所见,我们使用一个(数值)很小的常量(光)颜色添加进物体片段(Fragment,指当前讨论的光线在物体上的照射点)的最终颜色里,这看起来就像即使没有直射光源也始终存在着一些发散的光。
把环境光照添加到场景里非常简单。我们用光的颜色乘以一个(数值)很小常量环境因子,再乘以物体的颜色,然后使用它作为片段的颜色。
顶点着色器代码如下:
#version 330 core
// 物体反射出的颜色
out vec4 color;
// 物体本身的颜色
uniform vec3 objectColor;
// 光源的颜色
uniform vec3 lightColor;
void main()
{
// 定义环境光照的强度
float ambientStrength = 0.1f;
// 根据环境光照强度计算 环境光的RGB值
// 环境光肯定是来自光源的,因此环境光颜色值 = 环境光强度 * 光源颜色值
vec3 ambient = ambientStrength * lightColor;
// 在此例中我们只想研究环境光的效果,所以忽略光源直射
// 因忽略直射光,环境光为唯一光源,所以:物体反射光 = 环境光 * 物体颜色
vec3 result = ambient * objectColor;
color = vec4(result, 1.0f);
}
如果你还需要其他代码,可以看上一期《OpenGL光照:颜色》。
运行结果:
如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但不是完全的黑暗,因为我们应用了环境光照(注意发光立方体没被环境光照影响是因为我们对它使用了另一个着色器)。
漫反射光照
环境光本身不提供最明显的光照效果,但是漫反射光照(Diffuse Lighting)会对物体产生显著的视觉影响。漫反射光使物体上与光线排布越近的片段越能从光源处获得更多的亮度。为了更好的理解漫反射光照,请看下图:
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线与它所接触片段之间的角度。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一种向量(这里以黄色箭头表示),我们在后面再讲这个东西。两个向量之间的角度就能够根据点乘计算出来。
你可能记得在变换那一节教程里,我们知道两个单位向量的角度越小,它们点乘的结果越倾向于1。当两个向量的角度是90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响越小。
注意,我们使用的是单位向量(Unit Vector,长度是1的向量)取得两个向量夹角的余弦值,所以我们需要确保所有的向量都被标准化,否则点乘返回的值就不仅仅是余弦值了(如果你不明白,可以复习变换那一节的点乘部分)。注意,我们需要的不是法向量和入射光方向之间的夹角,我们需要的夹角是法向量和入射光的反方向之间的夹角。
点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,基于不同片段所朝向光源的方向的不同,这些片段被照亮的情况也不同。
所以,我们需要些什么来计算漫反射光照?
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光的位置和片段的位置之间的向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
法向量
法向量(Normal Vector)是垂直于顶点表面的(单位)向量。由于顶点自身并没有表面(它只是空间中一个独立的点),我们利用顶点周围的顶点计算出这个顶点的表面。我们能够使用叉乘这个技巧为立方体所有的顶点计算出法线,但是由于3D立方体不是一个复杂的形状,所以我们可以简单的把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在这里找到。试着去想象一下,这些法向量真的是垂直于立方体的各个面的表面的(一个立方体由6个面组成)。
顶点数据:
GLfloat vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
发光物着色器顶点数据的不完全使用看起来有点低效,但是这些顶点数据已经从立方体对象载入到GPU的内存里了,所以GPU内存不是必须再储存新数据。相对于重新给发光物分配VBO,实际上却是更高效了。
教程原文中顶点数组类型为float,而我们配置VAO用的是GLfloat,所以按照原文将出现错误信息。
计算漫反射光照
计算等知识内容都包含于代码注释中。
顶点着色器的代码如下:
#version 330 core
// 顶点的位置
layout( location = 0 ) in vec3 position;
// 顶点的法向量
layout (location = 1) in vec3 normal;
// 变换矩阵
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
// 顶点的位置(在世界坐标系中)
out vec3 FragPos;
// 顶点的法向量
out vec3 Normal;
void main()
{
// 计算顶点在屏幕上的位置,以便于渲染
gl_Position = projection * view * model * vec4(position, 1.0f);
// 计算顶点在世界坐标系中的位置
// 原始顶点位置(即局部坐标系)* 模型矩阵 = 世界坐标系位置(注意坐标要使用齐次坐标)
FragPos = vec3(model * vec4(position, 1.0f));
// 将法向量传给片段着色器(不能直接out normal, 所以声明一个输出 Normal)
Normal = normal;
}
片段着色器
#version 330 core
// 输入顶点位置和法向量
in vec3 FragPos;
in vec3 Normal;
// 输出顶点的颜色
out vec4 color;
// 顶点本身的颜色
uniform vec3 objectColor;
// 光源的颜色和位置
uniform vec3 lightColor;
uniform vec3 lightPos;
void main()
{
// 向量的标准化
vec3 norm = normalize(Normal);
// 注意,要计算夹角,应该使用法向量和 入射光向量的反向量(因为它们两个向量间夹角才是我们需要的)
vec3 lightDir = normalize(lightPos - FragPos);
// 计算漫反射光照的散射因子
float diff = max(dot(norm, lightDir), 0.0);
// 漫反射光的入射光的颜色值 = 漫反射光照的散射因子 * 漫反射光光源的颜色值
vec3 diffuse = diff * lightColor;
// 定义环境光照的强度
float ambientStrength = 0.1f;
// 环境光肯定是来自光源的,因此环境光颜色值 = 环境光强度 * 光源颜色值
vec3 ambient = ambientStrength * lightColor;
// 最终顶点接收到的光颜色值 = 所有入射光颜色值之和 = 环境光颜色值 + 漫反射入射光颜色值
// 最终顶点的颜色 = 顶点本身的颜色值 * 顶点接收到的光颜色值
vec3 result = (ambient + diffuse) * objectColor;
color = vec4(result, 1.0f);
}
运行结果为:
主程序:
// 标准输出
#include <string>
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// 着色器类和摄像机类
#include "Shader.h"
#include "Camera.h"
// GLM 数学库
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// 图片数据读取库
#include <SOIL.h>
// 定义窗口大小
GLuint screenWidth = 800, screenHeight = 600;
// GLFW窗口需要注册的函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
// 处理摄像机位置移动的函数
void Do_Movement();
// 定义摄像机
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
// 定义数组存储按键信息
bool keys[1024];
//
GLfloat lastX = 400, lastY = 300;
bool firstMouse = true;
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
glm::vec3 lightPos(1.2f, 1.0f, -2.0f);
// The MAIN function, from here we start our application and run our Game loop
int main()
{
// 初始化GLFW
glfwInit();
// 设置glfw使用的OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 设置使用OpenGL的核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 设置窗口大小可改变性 为不可变
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
glfwWindowHint(GLFW_SAMPLES, 4);
// 创建GLFW的窗口
GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "LearnOpenGL", nullptr, nullptr);
// 设置window窗口线程为主线程
glfwMakeContextCurrent(window);
// 注册窗口的事件
glfwSetKeyCallback(window, key_callback); // 按键检测
glfwSetCursorPosCallback(window, mouse_callback); // 鼠标移动检测
glfwSetScrollCallback(window, scroll_callback); // 滚轮滑动检测
// 设置隐藏光标模式
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 设置GLEW为更现代的模式
glewExperimental = GL_TRUE;
// 初始化GLEW
glewInit();
// 设置视口的位置和大小(位置是相对于窗口左下角的)
glViewport(0, 0, screenWidth, screenHeight);
// 开启深度测试功能
glEnable(GL_DEPTH_TEST);
// 根据顶点着色器和片段着色器位置创建着色器程序
Shader lightingShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt", "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt");
Shader lampShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt", "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\lightFragmentShader.txt");
// 顶点数据(位置+纹理坐标)
GLfloat vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
// 设置VBO和VAO
GLuint VBO, VAO;
// 先申请VAO和VBO的显存
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定VAO
glBindVertexArray(VAO);
// 绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将顶点数据传输到显存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 位置数据解析
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(0); // 解绑VAO
GLuint lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅设置灯的顶点数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// 游戏循环
while (!glfwWindowShouldClose(window))
{
// 计算帧相差时间
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 窗口的事件检测
glfwPollEvents();
// 根据事件移动摄像机
Do_Movement();
// 设置清空屏幕颜色缓存所使用颜色
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 清空 屏幕颜色缓存、深度缓存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 被光源照亮的普通物体的着色器
lightingShader.Use();
GLint objectColorLoc = glGetUniformLocation(lightingShader.Program, "objectColor");
GLint lightColorLoc = glGetUniformLocation(lightingShader.Program, "lightColor");
glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色
// 创建观察矩阵( 只要创建矩阵记得初始化= glm::mat4(1.0f) )
glm::mat4 view = glm::mat4(1.0f);
// 获取观察矩阵(根据摄像机的状态)
view = camera.GetViewMatrix();
// 创建投影矩阵
glm::mat4 projection = glm::mat4(1.0f);
// 计算投影矩阵(fov视野为摄像机的属性camera.Zoom)
projection = glm::perspective(camera.Zoom, (float)screenWidth / (float)screenHeight, 0.1f, 1000.0f);
// 计算顶点着色器中矩阵的位置值
GLint modelLoc = glGetUniformLocation(lightingShader.Program, "model");
GLint viewLoc = glGetUniformLocation(lightingShader.Program, "view");
GLint projLoc = glGetUniformLocation(lightingShader.Program, "projection");
// 将观察矩阵和投影矩阵传入对应的位置(记得转换)
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
// 定义模型矩阵
glBindVertexArray(VAO);
glm::mat4 model = glm::mat4(1.0f);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// 使用光源着色器
lampShader.Use();
modelLoc = glGetUniformLocation(lampShader.Program, "model");
viewLoc = glGetUniformLocation(lampShader.Program, "view");
projLoc = glGetUniformLocation(lampShader.Program, "projection");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(1.0f)); // Make it a smaller cube
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// 交换前后缓冲区
glfwSwapBuffers(window);
}
// 释放VAO和VBO的显存
glDeleteVertexArrays(1, &VAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
// 释放glfw的内存
glfwTerminate();
return 0;
}
// 根据按键信息移动摄像机
void Do_Movement()
{
// 如果某个键按下,就执行摄像机对应的方法(更新摄像机的位置)
if (keys[GLFW_KEY_W])
camera.ProcessKeyboard(FORWARD, deltaTime);
if (keys[GLFW_KEY_S])
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (keys[GLFW_KEY_A])
camera.ProcessKeyboard(LEFT, deltaTime);
if (keys[GLFW_KEY_D])
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// 按键回调函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
// 如果按下ESE则设置窗口应该关闭
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (key >= 0 && key < 1024)
{
// 如果按下某一个键,则设置其keys为true,如果松开则设置回false
if (action == GLFW_PRESS)
keys[key] = true;
else if (action == GLFW_RELEASE)
keys[key] = false;
}
}
// 鼠标移动的回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
// 第一次进窗口鼠标坐标很大,所以第一次调用函数我们需要把它设置为一个正常的值
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
// 计算鼠标在竖直方向和水平方向的偏移(屏幕坐标系往右x大,但往下是y大,所以运算顺序不同)
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos; // Reversed since y-coordinates go from bottom to left
// 更新鼠标的坐标
lastX = xpos;
lastY = ypos;
// 调用摄像机的鼠标移动函数(根据位置偏移更新摄像机方式)
camera.ProcessMouseMovement(xoffset, yoffset);
}
// 鼠标滚动的回调函数
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
// 调用摄像机的鼠标滚动函数(根据位置偏移更新摄像机fov视野大小)
camera.ProcessMouseScroll(yoffset);
}
如果你想直接运行以上程序,还需要为你的工程添加一些自定义的头文件,这些文件都可以在《OpenGL光照:颜色》中找到。
正规矩阵
现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里,我们都是在世界空间坐标中进行计算的,所以,我们不是应该把法向量转换为世界空间坐标吗?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。
首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,平移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要把模型矩阵左上角的3×3矩阵从模型矩阵中移除(译注:所谓移除就是设置为0),它是模型矩阵的平移部分(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;同样可以移除平移)。对于法向量,我们只能对它应用缩放(Scale)和旋转(Rotation)变换。
其次,如果模型矩阵执行了不等比缩放,法向量就不再垂直于表面了,顶点就会以这种方式被改变了。因此,我们不能用这样的模型矩阵去乘以法向量。下面的图展示了应用了不等比缩放的矩阵对法向量的影响:
无论何时当我们提交一个不等比缩放(注意:等比缩放不会破坏法线,因为法线的方向没被改变,而法线的长度很容易通过标准化进行修复),法向量就不会再垂直于它们的表面了,这样光照会被扭曲。
修复这个行为的诀窍是使用另一个为法向量专门定制的模型矩阵。这个矩阵称之为正规矩阵(Normal Matrix),它是进行了一点线性代数操作移除了对法向量的错误缩放效果。如果你想知道这个矩阵是如何计算出来的,我建议看这个文章。
正规矩阵被定义为“模型矩阵左上角的逆矩阵的转置矩阵”。真拗口,如果你不明白这是什么意思,别担心;我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。
在顶点着色器中,我们可以使用inverse和transpose函数自己生成正规矩阵,inverse和transpose函数对所有类型矩阵都有效。注意,我们也要把这个被处理过的矩阵强制转换为3×3矩阵,这是为了保证它失去了平移属性,之后它才能乘以法向量。
Normal = mat3(transpose(inverse(model))) * normal;
修改后的顶点着色器:
#version 330 core
layout( location = 0 ) in vec3 position;
layout (location = 1) in vec3 normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
FragPos = vec3(model * vec4(position, 1.0f));
Normal = mat3(transpose(inverse(model))) * normal;
}
在环境光照部分,光照表现没问题,这是因为我们没有对物体本身执行任何缩放操作,因而不是非得使用正规矩阵不可,用模型矩阵乘以法线也没错。可是,如果你进行了不等比缩放,使用正规矩阵去乘以法向量就是必不可少的了。
对于着色器来说,逆矩阵也是一种开销比较大的操作,因此,无论何时,在着色器中只要可能就应该尽量避免逆操作,因为它们必须为你场景中的每个顶点进行这样的处理。以学习的目的这样做很好,但是对于一个对于效率有要求的应用来说,在绘制之前,你最好用CPU计算出正规矩阵,然后通过uniform把值传递给着色器(和模型矩阵一样)。
现在就让我们来实现使用CPU计算正规矩阵,并且通过uniform把值传给着色器。
顶点着色器:
#version 330 core
layout( location = 0 ) in vec3 position;
// 每个顶点在局部坐标下的法向量
layout( location = 1 ) in vec3 normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
// 由CPU计算出的正规矩阵
uniform mat3 normatr;
out vec3 FragPos;
// 输出给片段着色器的 世界坐标系下的法向量
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
FragPos = vec3(model * vec4(position, 1.0f));
// 世界坐标系法向量 = 正规矩阵 * 局部坐标系法向量
Normal = normatr * normal;
}
主程序中的核心代码:
// 绑定VAO
glBindVertexArray(VAO);
GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
// 计算模型矩阵
glm::mat4 model = glm::mat4(1.0f);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
// 根据模型矩阵model计算正规矩阵normatr。inverse取逆和transpose转置 函数使用glm对应函数
GLuint normalLoc = glGetUniformLocation(lightingShader.Program, "normatr");
glm::mat3 normatr = (glm::mat3)(glm::transpose(glm::inverse(model)));
// 向物体的顶点着色器传入正规矩阵normatr
glUniformMatrix3fv(normalLoc, 1, GL_FALSE, glm::value_ptr(normatr));
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
运行结果:
小结
我们可以看出,光源不仅片段着色器和普通物体不一样,光源的顶点着色器也和普通物体不一样。因此我们在此做一个小结,将展示出所有的着色器和主程序代码。
普通物体的顶点着色器:
#version 330 core
layout( location = 0 ) in vec3 position;
layout( location = 1 ) in vec3 normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat3 normatr;
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
FragPos = vec3(model * vec4(position, 1.0f));
Normal = normatr * normal;
}
普通物体的片段着色器:
#version 330 core
// 输入顶点位置和法向量
in vec3 FragPos;
in vec3 Normal;
// 输出顶点的颜色
out vec4 color;
// 顶点本身的颜色
uniform vec3 objectColor;
// 光源的颜色和位置
uniform vec3 lightColor;
uniform vec3 lightPos;
void main()
{
// 向量的标准化
vec3 norm = normalize(Normal);
// 注意,要计算夹角,应该使用法向量和 入射光向量的反向量(因为它们两个向量间夹角才是我们需要的)
vec3 lightDir = normalize(lightPos - FragPos);
// 计算漫反射光照的散射因子
float diff = max(dot(norm, lightDir), 0.0);
// 漫反射光的入射光的颜色值 = 漫反射光照的散射因子 * 漫反射光光源的颜色值
vec3 diffuse = diff * lightColor;
// 定义环境光照的强度
float ambientStrength = 0.1f;
// 环境光肯定是来自光源的,因此环境光颜色值 = 环境光强度 * 光源颜色值
vec3 ambient = ambientStrength * lightColor;
// 最终顶点接收到的光颜色值 = 所有入射光颜色值之和 = 环境光颜色值 + 漫反射入射光颜色值
// 最终顶点的颜色 = 顶点本身的颜色值 * 顶点接收到的光颜色值
vec3 result = (ambient + diffuse) * objectColor;
color = vec4(result, 1.0f);
}
光源的的顶点着色器:
#version 330 core
layout( location = 0 ) in vec3 position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
}
光源的的片段着色器:
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f); //设置四维向量的所有元素为 1.0f
}
主程序:
// 标准输出
#include <string>
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// 着色器类和摄像机类
#include "Shader.h"
#include "Camera.h"
// GLM 数学库
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// 图片数据读取库
#include <SOIL.h>
// 定义窗口大小
GLuint screenWidth = 800, screenHeight = 600;
// GLFW窗口需要注册的函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
// 处理摄像机位置移动的函数
void Do_Movement();
// 定义摄像机
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
// 定义数组存储按键信息
bool keys[1024];
//
GLfloat lastX = 400, lastY = 300;
bool firstMouse = true;
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
glm::vec3 lightPos(1.2f, 1.0f, -2.0f);
// The MAIN function, from here we start our application and run our Game loop
int main()
{
// 初始化GLFW
glfwInit();
// 设置glfw使用的OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 设置使用OpenGL的核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 设置窗口大小可改变性 为不可变
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
glfwWindowHint(GLFW_SAMPLES, 4);
// 创建GLFW的窗口
GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "LearnOpenGL", nullptr, nullptr);
// 设置window窗口线程为主线程
glfwMakeContextCurrent(window);
// 注册窗口的事件
glfwSetKeyCallback(window, key_callback); // 按键检测
glfwSetCursorPosCallback(window, mouse_callback); // 鼠标移动检测
glfwSetScrollCallback(window, scroll_callback); // 滚轮滑动检测
// 设置隐藏光标模式
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 设置GLEW为更现代的模式
glewExperimental = GL_TRUE;
// 初始化GLEW
glewInit();
// 设置视口的位置和大小(位置是相对于窗口左下角的)
glViewport(0, 0, screenWidth, screenHeight);
// 开启深度测试功能
glEnable(GL_DEPTH_TEST);
// 根据顶点着色器和片段着色器位置创建着色器程序
Shader lightingShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt", "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt");
Shader lampShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\lightVertexShader.txt", "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\lightFragmentShader.txt");
// 顶点数据(位置+纹理坐标)
GLfloat vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
// 设置VBO和VAO
GLuint VBO, VAO;
// 先申请VAO和VBO的显存
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定VAO
glBindVertexArray(VAO);
// 绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将顶点数据传输到显存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 位置数据解析
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(0); // 解绑VAO
GLuint lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅设置灯的顶点数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// 游戏循环
while (!glfwWindowShouldClose(window))
{
// 计算帧相差时间
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 窗口的事件检测
glfwPollEvents();
// 根据事件移动摄像机
Do_Movement();
// 设置清空屏幕颜色缓存所使用颜色
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 清空 屏幕颜色缓存、深度缓存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 被光源照亮的普通物体的着色器
lightingShader.Use();
GLint objectColorLoc = glGetUniformLocation(lightingShader.Program, "objectColor");
GLint lightColorLoc = glGetUniformLocation(lightingShader.Program, "lightColor");
glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色
// 创建观察矩阵( 只要创建矩阵记得初始化= glm::mat4(1.0f) )
glm::mat4 view = glm::mat4(1.0f);
// 获取观察矩阵(根据摄像机的状态)
view = camera.GetViewMatrix();
// 创建投影矩阵
glm::mat4 projection = glm::mat4(1.0f);
// 计算投影矩阵(fov视野为摄像机的属性camera.Zoom)
projection = glm::perspective(camera.Zoom, (float)screenWidth / (float)screenHeight, 0.1f, 1000.0f);
// 计算顶点着色器中矩阵的位置值
GLint modelLoc = glGetUniformLocation(lightingShader.Program, "model");
GLint viewLoc = glGetUniformLocation(lightingShader.Program, "view");
GLint projLoc = glGetUniformLocation(lightingShader.Program, "projection");
// 将观察矩阵和投影矩阵传入对应的位置(记得转换)
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 绑定VAO
glBindVertexArray(VAO);
GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
// 计算模型矩阵
glm::mat4 model = glm::mat4(1.0f);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
// 根据模型矩阵model计算正规矩阵normatr。inverse取逆和transpose转置 函数使用glm对应函数
GLuint normalLoc = glGetUniformLocation(lightingShader.Program, "normatr");
glm::mat3 normatr = (glm::mat3)(glm::transpose(glm::inverse(model)));
// 向物体的顶点着色器传入正规矩阵normatr
glUniformMatrix3fv(normalLoc, 1, GL_FALSE, glm::value_ptr(normatr));
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// 使用光源着色器
lampShader.Use();
modelLoc = glGetUniformLocation(lampShader.Program, "model");
viewLoc = glGetUniformLocation(lampShader.Program, "view");
projLoc = glGetUniformLocation(lampShader.Program, "projection");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.1f)); // Make it a smaller cube
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// 交换前后缓冲区
glfwSwapBuffers(window);
}
// 释放VAO和VBO的显存
glDeleteVertexArrays(1, &VAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
// 释放glfw的内存
glfwTerminate();
return 0;
}
// 根据按键信息移动摄像机
void Do_Movement()
{
// 如果某个键按下,就执行摄像机对应的方法(更新摄像机的位置)
if (keys[GLFW_KEY_W])
camera.ProcessKeyboard(FORWARD, deltaTime);
if (keys[GLFW_KEY_S])
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (keys[GLFW_KEY_A])
camera.ProcessKeyboard(LEFT, deltaTime);
if (keys[GLFW_KEY_D])
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// 按键回调函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
// 如果按下ESE则设置窗口应该关闭
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (key >= 0 && key < 1024)
{
// 如果按下某一个键,则设置其keys为true,如果松开则设置回false
if (action == GLFW_PRESS)
keys[key] = true;
else if (action == GLFW_RELEASE)
keys[key] = false;
}
}
// 鼠标移动的回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
// 第一次进窗口鼠标坐标很大,所以第一次调用函数我们需要把它设置为一个正常的值
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
// 计算鼠标在竖直方向和水平方向的偏移(屏幕坐标系往右x大,但往下是y大,所以运算顺序不同)
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos; // Reversed since y-coordinates go from bottom to left
// 更新鼠标的坐标
lastX = xpos;
lastY = ypos;
// 调用摄像机的鼠标移动函数(根据位置偏移更新摄像机方式)
camera.ProcessMouseMovement(xoffset, yoffset);
}
// 鼠标滚动的回调函数
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
// 调用摄像机的鼠标滚动函数(根据位置偏移更新摄像机fov视野大小)
camera.ProcessMouseScroll(yoffset);
}
着色器类:
#ifndef SHADER_H
#define SHADER_H
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <GL/glew.h>
class Shader
{
public:
GLuint Program;
// Constructor generates the shader on the fly
Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
{
// 1. Retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// ensures ifstream objects can throw exceptions:
vShaderFile.exceptions(std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::badbit);
try
{
// Open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// Read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// Convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const GLchar* vShaderCode = vertexCode.c_str();
const GLchar* fShaderCode = fragmentCode.c_str();
// 2. Compile shaders
GLuint vertex, fragment;
GLint success;
GLchar infoLog[512];
// Vertex Shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// Print compile errors if any
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// Fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
// Print compile errors if any
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// Shader Program
this->Program = glCreateProgram();
glAttachShader(this->Program, vertex);
glAttachShader(this->Program, fragment);
glLinkProgram(this->Program);
// Print linking errors if any
glGetProgramiv(this->Program, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(this->Program, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// Delete the shaders as they're linked into our program now and no longer necessery
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// Uses the current shader
void Use()
{
glUseProgram(this->Program);
}
};
#endif
摄像机类:
#pragma once
// Std. Includes
#include <vector>
// GL Includes
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
// 常量指定摄像机的移动按键
enum Camera_Movement {
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
// 这里定义摄像机一些属性的初始值
// 摄像机旋转的两个值
const GLfloat YAW = -90.0f;
const GLfloat PITCH = 0.0f;
// 摄像机移动速度
const GLfloat SPEED = 3.0f;
// 摄像机旋转速度
const GLfloat SENSITIVTY = 0.25f;
// 摄像机角度(关乎远近)
const GLfloat ZOOM = 45.0f;
// An abstract camera class that processes input and calculates the corresponding Eular Angles, Vectors and Matrices for use in OpenGL
class Camera
{
public:
// 摄像机的位置、方向、上向量、右向量、世界上向量
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// 旋转角度
GLfloat Yaw;
GLfloat Pitch;
// 移动速度和旋转速度
GLfloat MovementSpeed;
GLfloat MouseSensitivity;
// 摄像机的视野(fov)
GLfloat Zoom;
// 无参构造函数将使用定义的全局变量值对摄像机对象的所有属性进行初始化
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), GLfloat yaw = YAW, GLfloat pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM)
{
this->Position = position;
this->WorldUp = up;
this->Yaw = yaw;
this->Pitch = pitch;
this->updateCameraVectors();
}
// 有参构造函数接收的参数:摄像机位置、摄像机的上向量、摄像机的旋转角度
Camera(GLfloat posX, GLfloat posY, GLfloat posZ, GLfloat upX, GLfloat upY, GLfloat upZ, GLfloat yaw, GLfloat pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM)
{
this->Position = glm::vec3(posX, posY, posZ);
this->WorldUp = glm::vec3(upX, upY, upZ);
this->Yaw = yaw;
this->Pitch = pitch;
this->updateCameraVectors();
}
// 根据摄像机位置、朝向、上向量生成其对应的观察矩阵
glm::mat4 GetViewMatrix()
{
return glm::lookAt(this->Position, this->Position + this->Front, this->Up);
}
// 当按下某一个键时,根据按键和帧时间进行移动(移动摄像机的位置)
void ProcessKeyboard(Camera_Movement direction, GLfloat deltaTime)
{
GLfloat velocity = this->MovementSpeed * deltaTime;
if (direction == FORWARD)
this->Position += this->Front * velocity;
if (direction == BACKWARD)
this->Position -= this->Front * velocity;
if (direction == LEFT)
this->Position -= this->Right * velocity;
if (direction == RIGHT)
this->Position += this->Right * velocity;
}
// 当鼠标移动时,根据横纵坐标差值移动摄像机的朝向,可以设置是否限制角度
void ProcessMouseMovement(GLfloat xoffset, GLfloat yoffset, GLboolean constrainPitch = true)
{
xoffset *= this->MouseSensitivity;
yoffset *= this->MouseSensitivity;
this->Yaw += xoffset;
this->Pitch += yoffset;
// Make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch)
{
if (this->Pitch > 89.0f)
this->Pitch = 89.0f;
if (this->Pitch < -89.0f)
this->Pitch = -89.0f;
}
// 更新摄像机状态
this->updateCameraVectors();
}
// 根据鼠标滚轮的缩放信息调整视野fov的大小
void ProcessMouseScroll(GLfloat yoffset)
{
// 鼠标滚动数值太大因此缩小一些
yoffset = yoffset * 0.1f;
if (this->Zoom >= 1.0f && this->Zoom <= 45.0f)
this->Zoom -= yoffset;
if (this->Zoom <= 1.0f)
this->Zoom = 1.0f;
if (this->Zoom >= 45.0f)
this->Zoom = 45.0f;
}
private:
// 更新摄像机状态(使用Yaw、Pitch、WorldUp,得到Front、Right、Up)
void updateCameraVectors()
{
// 根据旋转信息Yaw和Pitch计算摄像机的朝向
glm::vec3 front;
front.x = cos(glm::radians(this->Yaw)) * cos(glm::radians(this->Pitch));
front.y = sin(glm::radians(this->Pitch));
front.z = sin(glm::radians(this->Yaw)) * cos(glm::radians(this->Pitch));
this->Front = glm::normalize(front);
// 使用摄像机朝向和世界上向量计算摄像机右朝向
this->Right = glm::normalize(glm::cross(this->Front, this->WorldUp));
// 使用摄像机朝向和右向量计算摄像机上向量
this->Up = glm::normalize(glm::cross(this->Right, this->Front));
}
};
镜面光照
如果你还没被这些光照计算搞得精疲力尽,我们就再把镜面高光(Specular Highlight)加进来,这样冯氏光照才算完整。
和环境光照一样,镜面光照(Specular Lighting)同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:
我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
观察向量是镜面光照的一个附加变量,我们可以使用观察者世界空间位置(Viewer’s World Space Position)和片段的位置来计算。之后,我们计算镜面光亮度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色。
我们选择在世界空间(World Space)进行光照计算,但是大多数人趋向于在观察空间(View Space)进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是,我发现出于学习的目的,在世界空间计算光照更符合直觉。如果你仍然希望在视野空间计算光照的话,那就使用观察矩阵应用到所有相关的需要变换的向量(不要忘记,也要改变正规矩阵)。
为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器。
现在我们已经获得所有需要的变量,可以计算高光亮度了。首先,我们定义一个镜面强度(Specular Intensity)变量specularStrength,给镜面高光一个中等亮度颜色,这样就不会产生过度的影响了。
如果我们把它设置为1.0f,我们会得到一个对于珊瑚色立方体来说过度明亮的镜面亮度因子。下一节教程,我们会讨论所有这些光照亮度的合理设置,以及它们是如何影响物体的。下一步,我们计算视线方向坐标,和沿法线轴的对应的反射坐标。
需要注意的是我们使用了lightDir向量的相反数。reflect函数要求的第一个是从光源指向片段位置的向量,但是lightDir当前是从片段指向光源的向量(由先前我们计算lightDir向量时,(减数和被减数)减法的顺序决定)。为了保证我们得到正确的reflect坐标,我们通过lightDir向量的相反数获得它的方向的反向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。
我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。在下面的图片里,你会看到不同发光值对视觉(效果)的影响:
我们不希望镜面成分过于显眼,所以我们把指数设置为32。剩下的最后一件事情是把它添加到环境光颜色和散射光颜色里,然后再乘以物体颜色。
运行结果:
普通物体的片段着色器:
#version 330 core
// 输入顶点位置和法向量
in vec3 FragPos;
in vec3 Normal;
// 输出顶点的颜色
out vec4 color;
// 顶点本身的颜色
uniform vec3 objectColor;
// 光源的颜色和位置
uniform vec3 lightColor;
uniform vec3 lightPos;
// 观察者的位置(世界坐标)
uniform vec3 viewPos;
void main()
{
// 向量的标准化
vec3 norm = normalize(Normal);
// 注意,要计算夹角,应该使用法向量和 入射光向量的反向量(因为它们两个向量间夹角才是我们需要的)
vec3 lightDir = normalize(lightPos - FragPos);
// 计算漫反射光照的散射因子
float diff = max(dot(norm, lightDir), 0.0);
// 漫反射光的入射光的颜色值 = 漫反射光照的散射因子 * 漫反射光光源的颜色值
vec3 diffuse = diff * lightColor;
// 定义环境光照的强度
float ambientStrength = 0.1f;
// 环境光肯定是来自光源的,因此环境光颜色值 = 环境光强度 * 光源颜色值
vec3 ambient = ambientStrength * lightColor;
// 定义镜面强度
float specularStrength = 0.5f;
// 计算视线方向向量(指向观察者)
vec3 viewDir = normalize(viewPos - FragPos);
// 根据入射光和法向量得到反射光线向量
vec3 reflectDir = reflect(-lightDir, norm);
// 计算视线方向和反射方向的点乘(确保大于0)
// 32为高光的发光值
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
// 镜面光照颜色值 = 镜面强度 * 发光值 * 光源的颜色值
vec3 specular = specularStrength * spec * lightColor;
// 最终顶点接收到的光颜色值 = 所有入射光颜色值之和 = 环境光颜色值 + 漫反射入射光颜色值 + 镜面反射光颜色值
// 最终顶点的颜色 = 顶点本身的颜色值 * 顶点接收到的光颜色值
vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);
}
主程序:
// 标准输出
#include <string>
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// 着色器类和摄像机类
#include "Shader.h"
#include "Camera.h"
// GLM 数学库
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// 图片数据读取库
#include <SOIL.h>
// 定义窗口大小
GLuint screenWidth = 800, screenHeight = 600;
// GLFW窗口需要注册的函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
// 处理摄像机位置移动的函数
void Do_Movement();
// 定义摄像机
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
// 定义数组存储按键信息
bool keys[1024];
//
GLfloat lastX = 400, lastY = 300;
bool firstMouse = true;
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
glm::vec3 lightPos(1.2f, 1.0f, -0.5f);
// The MAIN function, from here we start our application and run our Game loop
int main()
{
// 初始化GLFW
glfwInit();
// 设置glfw使用的OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 设置使用OpenGL的核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 设置窗口大小可改变性 为不可变
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
glfwWindowHint(GLFW_SAMPLES, 4);
// 创建GLFW的窗口
GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "LearnOpenGL", nullptr, nullptr);
// 设置window窗口线程为主线程
glfwMakeContextCurrent(window);
// 注册窗口的事件
glfwSetKeyCallback(window, key_callback); // 按键检测
glfwSetCursorPosCallback(window, mouse_callback); // 鼠标移动检测
glfwSetScrollCallback(window, scroll_callback); // 滚轮滑动检测
// 设置隐藏光标模式
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 设置GLEW为更现代的模式
glewExperimental = GL_TRUE;
// 初始化GLEW
glewInit();
// 设置视口的位置和大小(位置是相对于窗口左下角的)
glViewport(0, 0, screenWidth, screenHeight);
// 开启深度测试功能
glEnable(GL_DEPTH_TEST);
// 根据顶点着色器和片段着色器位置创建着色器程序
Shader lightingShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt", "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt");
Shader lampShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\lightVertexShader.txt", "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\lightFragmentShader.txt");
// 顶点数据(位置+纹理坐标)
GLfloat vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
// 设置VBO和VAO
GLuint VBO, VAO;
// 先申请VAO和VBO的显存
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定VAO
glBindVertexArray(VAO);
// 绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将顶点数据传输到显存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 位置数据解析
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(0); // 解绑VAO
GLuint lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅设置灯的顶点数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// 游戏循环
while (!glfwWindowShouldClose(window))
{
// 计算帧相差时间
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 窗口的事件检测
glfwPollEvents();
// 根据事件移动摄像机
Do_Movement();
// 设置清空屏幕颜色缓存所使用颜色
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 清空 屏幕颜色缓存、深度缓存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 被光源照亮的普通物体的着色器
lightingShader.Use();
GLint objectColorLoc = glGetUniformLocation(lightingShader.Program, "objectColor");
GLint lightColorLoc = glGetUniformLocation(lightingShader.Program, "lightColor");
GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos");
glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);
// 创建观察矩阵( 只要创建矩阵记得初始化= glm::mat4(1.0f) )
glm::mat4 view = glm::mat4(1.0f);
// 获取观察矩阵(根据摄像机的状态)
view = camera.GetViewMatrix();
// 创建投影矩阵
glm::mat4 projection = glm::mat4(1.0f);
// 计算投影矩阵(fov视野为摄像机的属性camera.Zoom)
projection = glm::perspective(camera.Zoom, (float)screenWidth / (float)screenHeight, 0.1f, 1000.0f);
// 计算顶点着色器中矩阵的位置值
GLint modelLoc = glGetUniformLocation(lightingShader.Program, "model");
GLint viewLoc = glGetUniformLocation(lightingShader.Program, "view");
GLint projLoc = glGetUniformLocation(lightingShader.Program, "projection");
// 将观察矩阵和投影矩阵传入对应的位置(记得转换)
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 绑定VAO
glBindVertexArray(VAO);
// 计算模型矩阵
glm::mat4 model = glm::mat4(1.0f);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
// 根据模型矩阵model计算正规矩阵normatr。inverse取逆和transpose转置 函数使用glm对应函数
GLuint normalLoc = glGetUniformLocation(lightingShader.Program, "normatr");
glm::mat3 normatr = (glm::mat3)(glm::transpose(glm::inverse(model)));
// 向物体的顶点着色器传入正规矩阵normatr
glUniformMatrix3fv(normalLoc, 1, GL_FALSE, glm::value_ptr(normatr));
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// 使用光源着色器
lampShader.Use();
modelLoc = glGetUniformLocation(lampShader.Program, "model");
viewLoc = glGetUniformLocation(lampShader.Program, "view");
projLoc = glGetUniformLocation(lampShader.Program, "projection");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.1f)); // Make it a smaller cube
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// 交换前后缓冲区
glfwSwapBuffers(window);
}
// 释放VAO和VBO的显存
glDeleteVertexArrays(1, &VAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
// 释放glfw的内存
glfwTerminate();
return 0;
}
// 根据按键信息移动摄像机
void Do_Movement()
{
// 如果某个键按下,就执行摄像机对应的方法(更新摄像机的位置)
if (keys[GLFW_KEY_W])
camera.ProcessKeyboard(FORWARD, deltaTime);
if (keys[GLFW_KEY_S])
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (keys[GLFW_KEY_A])
camera.ProcessKeyboard(LEFT, deltaTime);
if (keys[GLFW_KEY_D])
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// 按键回调函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
// 如果按下ESE则设置窗口应该关闭
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (key >= 0 && key < 1024)
{
// 如果按下某一个键,则设置其keys为true,如果松开则设置回false
if (action == GLFW_PRESS)
keys[key] = true;
else if (action == GLFW_RELEASE)
keys[key] = false;
}
}
// 鼠标移动的回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
// 第一次进窗口鼠标坐标很大,所以第一次调用函数我们需要把它设置为一个正常的值
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
// 计算鼠标在竖直方向和水平方向的偏移(屏幕坐标系往右x大,但往下是y大,所以运算顺序不同)
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos; // Reversed since y-coordinates go from bottom to left
// 更新鼠标的坐标
lastX = xpos;
lastY = ypos;
// 调用摄像机的鼠标移动函数(根据位置偏移更新摄像机方式)
camera.ProcessMouseMovement(xoffset, yoffset);
}
// 鼠标滚动的回调函数
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
// 调用摄像机的鼠标滚动函数(根据位置偏移更新摄像机fov视野大小)
camera.ProcessMouseScroll(yoffset);
}
现在你可以看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照,影响到为我们所有物体的片段颜色。下一节中,我们会更深入的研究光照模型,看看我们还能做些什么。
练习
练习一
目前,我们的光源时静止的,尝试使用sin和cos函数让光源在场景中来回移动。
运行效果:
核心代码:
model = glm::mat4(1.0f);
GLfloat lightX = sin(glfwGetTime());
GLfloat lightZ = cos(glfwGetTime());
GLfloat lightY = 1;
lightPos = glm::vec3(lightX, lightY, lightZ);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.1f)); // Make it a smaller cube
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
练习二
尝试使用不同的环境光、散射镜面强度,观察光照效果。改变镜面光照的shininess因子试试。
使用蓝色的环境光,其RGB值为(0, 255, 255),要注意在OpenGL中颜色值处于0-1之间,所以应该转换为(0,1,1)。其颜色值如下图所示:
运行结果:
注意,如果环境光照强度为0.1则上图中两个阴暗面会显示出黑色(在我们看来),所以我们将其调整为0.3。如上图所示,改变环境光后整个物体的颜色大变样,这是由于环境光的强度被我们调大了,环境光将更大程度的影响物体的颜色,所以物体被光直射的地方看起来没有最初那么红了。由于光源围绕物体旋转的,所以会导致物体很多面没有被光直射,会导致很多面呈现一种暗蓝色。
我们将镜面强度调整为0.1,会导致高光变得非常模糊,运行结果:
我们将镜面强度调整为0.9,会导致高光变得非常清楚,运行结果:
我们将发光值(Shininess)调整为12,会导致高光点变大,运行结果:
我们将发光值(Shininess)调整为128,会导致高光点变得变小,运行结果:
如果在此时,我们再将镜面强度调整为0.3,会导致高光变得暗淡模糊,运行结果:
片段着色器:
#version 330 core
// 输入顶点位置和法向量
in vec3 FragPos;
in vec3 Normal;
// 输出顶点的颜色
out vec4 color;
// 顶点本身的颜色
uniform vec3 objectColor;
// 光源的颜色和位置
uniform vec3 lightColor;
uniform vec3 lightPos;
// 观察者的位置(世界坐标)
uniform vec3 viewPos;
void main()
{
// 向量的标准化
vec3 norm = normalize(Normal);
// 注意,要计算夹角,应该使用法向量和 入射光向量的反向量(因为它们两个向量间夹角才是我们需要的)
vec3 lightDir = normalize(lightPos - FragPos);
// 计算漫反射光照的散射因子
float diff = max(dot(norm, lightDir), 0.0);
// 漫反射光的入射光的颜色值 = 漫反射光照的散射因子 * 漫反射光光源的颜色值
vec3 diffuse = diff * lightColor;
// 定义环境光照的强度
float ambientStrength = 0.3f;
// 环境光肯定是来自光源的,因此环境光颜色值 = 环境光强度 * 光源颜色值
// vec3 ambient = ambientStrength * lightColor;
vec3 ambient = ambientStrength * vec3(0, 1.0, 1.0);
// 最终顶点接收到的光颜色值 = 所有入射光颜色值之和 = 环境光颜色值 + 漫反射入射光颜色值
// 最终顶点的颜色 = 顶点本身的颜色值 * 顶点接收到的光颜色值
float specularStrength = 0.2f;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0000), 128);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);
}
练习三
在观察空间(而不是世界空间)中计算冯氏光照。
练习四
尝试实现一个Gouraud光照来模拟冯氏光照。