什么是颜色??
我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。
下图显示的是一个珊瑚红的玩具,它以不同强度反射了多个颜色。
你可以看到,白色的阳光实际上是所有可见颜色的集合,物体吸收了其中的大部分颜色。它仅反射了代表物体颜色的部分,被反射颜色的组合就是我们所感知到的颜色(此例中为珊瑚红)。
在OpenGL中具体表示:
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);//灯光颜色向量
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);//物体颜色向量
glm::vec3 result = lightColor * toyColor;
颜色向量(Color Vector): 一个通过红绿蓝(RGB)分量的组合描绘大部分真实颜色的向量。一个物体的颜色实际上是该物体所不能吸收的反射颜色分量。
要创建一个光照场景必不可少光照(光源)对象,简单起见,我们使用一个立方体来代表光。
创建光源着色器:
顶点着色器 light_cube.vs
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
片元着色器 light_cube.fs
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // set all 4 vector values to 1.0
}
完整源码
#include
#include
#include
#include
#include
#define STB_IMAGE_IMPLEMENTATION
#include
// https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader_m.h
#include
// https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h
#include
void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void mouse_callback(GLFWwindow *window, double xposIn, double yposIn);
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// 相机
Camera camera(glm::vec3(0.0f, 0.0f, 6.0f));
float lastX = static_cast<float>(SCR_WIDTH) / 2.0;
float lastY = static_cast<float>(SCR_HEIGHT) / 2.0;
bool firstMouse = true;
// timing
float deltaTime = 0.0f; // time between current frame and last frame
float lastFrame = 0.0f;
// lighting
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
GLFWwindow *window;
int main()
{
InitGLFW(); // 初始化GLFW
bool isCreated = CreateWindow(); // 创建一个窗口对象
if (!isCreated)
return -1;
bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!isGLAD)
return -1;
// 启用深度测试
glEnable(GL_DEPTH_TEST);
// 构建和编译着色程序
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/1.colors/colors.cpp
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/1.colors/1.colors.fs
Shader lightingShader("shader/P1_Basic/08_LightModel/colors.vs", "shader/P1_Basic/08_LightModel/colors.fs");
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/1.colors/1.colors.fs
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/1.colors/1.colors.fs
Shader lightCubeShader("shader/P1_Basic/08_LightModel/light_cube.vs", "shader/P1_Basic/08_LightModel/light_cube.fs");
// 设置顶点数据(和缓冲区)并配置顶点属性
// 1.设置立方体顶点输入 一共需要36个顶点
float vertices[] = {
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
};
// 2.设置索引缓冲对象
unsigned int VBO, cubeVAO;
glGenVertexArrays(1, &cubeVAO); // 生成一个VAO对象
glGenBuffers(1, &VBO); // 生成一个VBO对象
// 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中
glBindVertexArray(cubeVAO);
// 4.配置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
unsigned int lightCubeVAO;
glGenVertexArrays(1, &lightCubeVAO);
glBindVertexArray(lightCubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅需要顶点位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 循环渲染
while (!glfwWindowShouldClose(window))
{
// 计算帧间隔时间
float currentFrame = static_cast<float>(glfwGetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 输入
processInput(window);
// 渲染
// 清除颜色缓冲
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 清除深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
// 投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), static_cast<float>(SCR_WIDTH) / static_cast<float>(SCR_HEIGHT), 0.1f, 100.0f);
lightingShader.setMat4("projection", projection);
// 观察矩阵
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("view", view);
// 世界坐标变换
glm::mat4 model = glm::mat4(1.0f);
lightingShader.setMat4("model", model);
// 渲染立方体
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 同时 绘制灯光对象
lightCubeShader.use();
lightCubeShader.setMat4("projection", projection);
lightCubeShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lightCubeShader.setMat4("model", model);
// 渲染光照立方体
glBindVertexArray(lightCubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
// 可选:一旦资源超出其用途,就取消分配所有资源:
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightCubeVAO);
glDeleteBuffers(1, &VBO);
// 释放/删除之前的分配的所有资源
glfwTerminate();
return 0;
}
void InitGLFW()
{
// 初始化GLFW
glfwInit();
// 配置GLFW 第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
// 第二个参数接受一个整型,用来设置这个选项的值。
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
}
bool CreateWindow()
{
// 创建一个窗口对象
window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
// 创建失败,终止程序
glfwTerminate();
return false;
}
// 将我们窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
// 设置窗口大小改变时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 设置鼠标移动回调函数
glfwSetCursorPosCallback(window, mouse_callback);
// 设置滚轮滚动回调函数
glfwSetScrollCallback(window, scroll_callback);
// 隐藏并捕捉鼠标
// glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
return true;
}
bool InitGLAD()
{
// 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
// 初始化失败,终止程序
return false;
}
return true;
}
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
// 设置窗口的维度
glViewport(0, 0, width, height);
}
// 输入
void processInput(GLFWwindow *window)
{
// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
// 关闭应用程序
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// 鼠标移动回调函数
void mouse_callback(GLFWwindow *window, double xposIn, double yposIn)
{
// 长按T键,鼠标才能控制相机
if (glfwGetKey(window, GLFW_KEY_T) != GLFW_PRESS)
return;
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
// 鼠标滚轮滚动回调函数
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(static_cast<float>(yoffset));
}
标准光照模型在这里也叫冯氏光照模型(Phong Lighting Model)。
著名学者裴祥风(Bui Tuong Phong)提出了的,其背后的基本理念:
标准光照模型只关心直接光照(direct light), 也就是那些直接从光源发射出来照射到物体表面后,
经过物体表面的一 次反射直接进入摄像机的光线。
冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。
环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都使用这个环境光。
我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。
在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。
漫反射光照符合兰伯特定律 (Lambert's law)
: 反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。漫反射部分的计算如下:
C d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) m a x ( 0 , n ⃗ ⋅ I ) C_{diffuse}=(c_{light}\cdot m_{diffuse})max(0,\vec{n} \cdot I) Cdiffuse=(clight⋅mdiffuse)max(0,n⋅I)
法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。
我们能够使用一个小技巧,使用叉乘
对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中。
计算漫反射光照我们可以参考漫反射公式,具体实现主要片元着色器:
我们可以先定义材质颜色和灯光颜色:
uniform vec3 lightColor;
uniform vec3 objectColor;
计算表面法线 n ⃗ \vec{n} n
vec3 norm = normalize(Normal);
计算光源方向 I I I
vec3 lightDir = normalize(lightPos - FragPos);
下一步,我们对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫反射影响。结果值再乘以光的颜色,得到漫反射分量。
//利用上面漫反射公式计算结果
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor* objectColor;
当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。
在片元着色器完整实现:
最后,加上环境灯光需要调整一下。
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 FragPos;
uniform vec3 lightPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
}
#include
#include
#include
#include
#include
#define STB_IMAGE_IMPLEMENTATION
#include
// https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader_m.h
#include
// https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h
#include
void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void mouse_callback(GLFWwindow *window, double xposIn, double yposIn);
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// 相机
Camera camera(glm::vec3(0.0f, 0.0f, 6.0f));
float lastX = static_cast<float>(SCR_WIDTH) / 2.0;
float lastY = static_cast<float>(SCR_HEIGHT) / 2.0;
bool firstMouse = true;
// timing
float deltaTime = 0.0f; // time between current frame and last frame
float lastFrame = 0.0f;
// lighting
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
GLFWwindow *window;
int main()
{
InitGLFW(); // 初始化GLFW
bool isCreated = CreateWindow(); // 创建一个窗口对象
if (!isCreated)
return -1;
bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!isGLAD)
return -1;
// 启用深度测试
glEnable(GL_DEPTH_TEST);
// 构建和编译着色程序
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/2.1.basic_lighting.vs
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/2.1.basic_lighting.fs
Shader lightingShader("shader/P1_Basic/08_LightModel/basic_lighting.vs", "shader/P1_Basic/08_LightModel/basic_lighting.fs");
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/2.1.light_cube.vs
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/2.1.light_cube.fs
Shader lightCubeShader("shader/P1_Basic/08_LightModel/light_cube.vs", "shader/P1_Basic/08_LightModel/light_cube.fs");
// 设置顶点数据(和缓冲区)并配置顶点属性
// 1.设置立方体顶点输入 一共需要36个顶点
float 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};
// 2.设置索引缓冲对象
unsigned int VBO, cubeVAO;
glGenVertexArrays(1, &cubeVAO); // 生成一个VAO对象
glGenBuffers(1, &VBO); // 生成一个VBO对象
// 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中
glBindVertexArray(cubeVAO);
// 4.配置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// normal attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
unsigned int lightCubeVAO;
glGenVertexArrays(1, &lightCubeVAO);
glBindVertexArray(lightCubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅需要顶点位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 循环渲染
while (!glfwWindowShouldClose(window))
{
// 计算帧间隔时间
float currentFrame = static_cast<float>(glfwGetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 输入
processInput(window);
// 渲染
// 清除颜色缓冲
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 清除深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
lightingShader.setVec3("lightPos", lightPos);
// 投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), static_cast<float>(SCR_WIDTH) / static_cast<float>(SCR_HEIGHT), 0.1f, 100.0f);
lightingShader.setMat4("projection", projection);
// 观察矩阵
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("view", view);
// 世界坐标变换
glm::mat4 model = glm::mat4(1.0f);
lightingShader.setMat4("model", model);
// 渲染立方体
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 同时 绘制灯光对象
lightCubeShader.use();
lightCubeShader.setMat4("projection", projection);
lightCubeShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lightCubeShader.setMat4("model", model);
// 渲染光照立方体
glBindVertexArray(lightCubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
// 可选:一旦资源超出其用途,就取消分配所有资源:
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightCubeVAO);
glDeleteBuffers(1, &VBO);
// 释放/删除之前的分配的所有资源
glfwTerminate();
return 0;
}
void InitGLFW()
{
// 初始化GLFW
glfwInit();
// 配置GLFW 第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
// 第二个参数接受一个整型,用来设置这个选项的值。
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
}
bool CreateWindow()
{
// 创建一个窗口对象
window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
// 创建失败,终止程序
glfwTerminate();
return false;
}
// 将我们窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
// 设置窗口大小改变时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 设置鼠标移动回调函数
glfwSetCursorPosCallback(window, mouse_callback);
// 设置滚轮滚动回调函数
glfwSetScrollCallback(window, scroll_callback);
// 隐藏并捕捉鼠标
// glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
return true;
}
bool InitGLAD()
{
// 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
// 初始化失败,终止程序
return false;
}
return true;
}
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
// 设置窗口的维度
glViewport(0, 0, width, height);
}
// 输入
void processInput(GLFWwindow *window)
{
// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
// 关闭应用程序
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// 鼠标移动回调函数
void mouse_callback(GLFWwindow *window, double xposIn, double yposIn)
{
// 长按T键,鼠标才能控制相机
if (glfwGetKey(window, GLFW_KEY_T) != GLFW_PRESS)
return;
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
// 鼠标滚轮滚动回调函数
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(static_cast<float>(yoffset));
}
高光反射也叫镜面高光(Specular Highlight)。和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,但是它也决定于观察方向。
我们通过根据法向量翻折入射光的方向来计算反射向量。
然后我们计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大。
由此产生的效果就是,我们看向在入射光在表面的反射方向时,会看到一点高光。
基本光照模型中高光反射部分的计算公式:
C s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , v ⃗ ⋅ r ) m g l o s s C_{specular}=(c_{light}\cdot m_{specular})max(0,\vec{v}\cdot r)^{m_{gloss}} Cspecular=(clight⋅mspecular)max(0,v⋅r)mgloss
首先,我们定义一个镜面强度(Specular Intensity)变量 m g l o s s m_{gloss} mgloss ,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
float specularStrength = 0.5;
下一步,我们计算视线方向向量 v ⃗ \vec{v} v,和对应的沿着法线轴的反射向量 r r r :
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
这里,OpenGL提供了reflect函数来计算反射向量 r r r。
reflect函数要求第一个向量是从光源指向片段位置的向量,但是lightDir当前正好相反,是从片段指向光源(由先前我们计算lightDir向量时,减法的顺序决定)。为了保证我们得到正确的reflect向量,我们通过对lightDir向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。
剩下要做的是计算镜面分量代入计算公式。下面的代码完成了这件事:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
其中,这个32是高光的反光度(Shininess) m g l o s s m_{gloss} mgloss。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。
在下面的图片里,你会看到不同反光度的视觉效果影响:
剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
创建高光的顶点着色器和片元着色器
basic_specular_lighting.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
//在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
basic_specular_lighting.fs
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 FragPos;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
}
在C++中实现
#include
#include
#include
#include
#include
#define STB_IMAGE_IMPLEMENTATION
#include
// https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader_m.h
#include
// https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h
#include
void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void mouse_callback(GLFWwindow *window, double xposIn, double yposIn);
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// 相机
Camera camera(glm::vec3(0.0f, 0.0f, 6.0f));
float lastX = static_cast<float>(SCR_WIDTH) / 2.0;
float lastY = static_cast<float>(SCR_HEIGHT) / 2.0;
bool firstMouse = true;
// timing
float deltaTime = 0.0f; // time between current frame and last frame
float lastFrame = 0.0f;
// lighting
glm::vec3 lightPos(0.5f, 0.5f, 1.0f);
GLFWwindow *window;
int main()
{
InitGLFW(); // 初始化GLFW
bool isCreated = CreateWindow(); // 创建一个窗口对象
if (!isCreated)
return -1;
bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!isGLAD)
return -1;
// 启用深度测试
glEnable(GL_DEPTH_TEST);
// 构建和编译着色程序
Shader lightingShader("shader/P1_Basic/08_LightModel/basic_specular_lighting.vs", "shader/P1_Basic/08_LightModel/basic_specular_lighting.fs");
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/2.1.light_cube.vs
// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/2.1.light_cube.fs
Shader lightCubeShader("shader/P1_Basic/08_LightModel/light_cube.vs", "shader/P1_Basic/08_LightModel/light_cube.fs");
// 设置顶点数据(和缓冲区)并配置顶点属性
// 1.设置立方体顶点输入 一共需要36个顶点
float 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};
// 2.设置索引缓冲对象
unsigned int VBO, cubeVAO;
glGenVertexArrays(1, &cubeVAO); // 生成一个VAO对象
glGenBuffers(1, &VBO); // 生成一个VBO对象
// 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中
glBindVertexArray(cubeVAO);
// 4.配置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// normal attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
unsigned int lightCubeVAO;
glGenVertexArrays(1, &lightCubeVAO);
glBindVertexArray(lightCubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅需要顶点位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 循环渲染
while (!glfwWindowShouldClose(window))
{
// 计算帧间隔时间
float currentFrame = static_cast<float>(glfwGetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 输入
processInput(window);
// 渲染
// 清除颜色缓冲
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 清除深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
lightingShader.setVec3("lightPos", lightPos);
lightingShader.setVec3("viewPos", camera.Position);
// 投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), static_cast<float>(SCR_WIDTH) / static_cast<float>(SCR_HEIGHT), 0.1f, 100.0f);
lightingShader.setMat4("projection", projection);
// 观察矩阵
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("view", view);
// 世界坐标变换
glm::mat4 model = glm::mat4(1.0f);
lightingShader.setMat4("model", model);
// 渲染立方体
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 同时 绘制灯光对象
lightCubeShader.use();
lightCubeShader.setMat4("projection", projection);
lightCubeShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lightCubeShader.setMat4("model", model);
// 渲染光照立方体
glBindVertexArray(lightCubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
// 可选:一旦资源超出其用途,就取消分配所有资源:
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightCubeVAO);
glDeleteBuffers(1, &VBO);
// 释放/删除之前的分配的所有资源
glfwTerminate();
return 0;
}
void InitGLFW()
{
// 初始化GLFW
glfwInit();
// 配置GLFW 第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
// 第二个参数接受一个整型,用来设置这个选项的值。
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
}
bool CreateWindow()
{
// 创建一个窗口对象
window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
// 创建失败,终止程序
glfwTerminate();
return false;
}
// 将我们窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
// 设置窗口大小改变时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 设置鼠标移动回调函数
glfwSetCursorPosCallback(window, mouse_callback);
// 设置滚轮滚动回调函数
glfwSetScrollCallback(window, scroll_callback);
// 隐藏并捕捉鼠标
// glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
return true;
}
bool InitGLAD()
{
// 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
// 初始化失败,终止程序
return false;
}
return true;
}
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
// 设置窗口的维度
glViewport(0, 0, width, height);
}
// 输入
void processInput(GLFWwindow *window)
{
// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
// 关闭应用程序
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// 鼠标移动回调函数
void mouse_callback(GLFWwindow *window, double xposIn, double yposIn)
{
// 长按T键,鼠标才能控制相机
if (glfwGetKey(window, GLFW_KEY_T) != GLFW_PRESS)
return;
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
// 鼠标滚轮滚动回调函数
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(static_cast<float>(yoffset));
}
在光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型。在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。
然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。
在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。记住,由于插值,这种光照看起来有点逊色。冯氏着色能产生更平滑的光照效果。