本文基于C++语言,描述OpenGL的投光物
前置知识可参考:
笔者这里不过多描述每个名词、函数和细节,更详细的文档可以参考:
投光物,即光源,主要有平行光源、点光源和聚光源
平行光源可以使用一个方向向量来模拟
点光源可以使用一个点来模拟,另外,点光源应该有衰减模拟,衰减公式为
\[\begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation} \]\(K_l\)与\(K_q\)的取值可以参考实验值:-Point Light Attenuation | Ogre Wiki (ogre3d.org)
聚光源类似于手电筒、聚光灯,只照亮灯光方向的一部分,如下图所示
图中,参数含义如下:
LightDir
:从片段指向光源的向量SpotDir
:聚光所指向的方向Phi
\(\phi\):指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮Theta
\(\theta\):LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小计算LightDir向量和SpotDir向量之间的点积得到两个单位向量夹角的余弦值,并将它与切光角ϕ值对比,即可判断是否被照亮
使用一个光线方向向量来模拟平行光
在片段着色器中定义光线的方向向量:
struct Light { // vec3 position; // 使用定向光就不再需要了 vec3 direction; vec3 ambient; vec3 diffuse; vec3 specular; }; ... void main() { vec3 lightDir = normalize(-light.direction); ... }
输入方向向量:
lightingShader.setVec3("light.direction", -1.0f, 0.0f, 0.0f);
结果如下:
给定一个点位置来模拟点光源,并且设置衰减的参数
这里\(K_l\)与\(K_q\)的取值使用的是50米光源的实验值,分别0.09、0.032
在片段着色器中定义参数:
struct Light { vec3 position; vec3 ambient; vec3 diffuse; vec3 specular; float constant; float linear; float quadratic; };
计算衰减:
float distance = length(light.position - FragPos); float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); ambient *= attenuation; diffuse *= attenuation; specular *= attenuation;
想GPU输入数据:
lightingShader.setFloat("light.constant", 1.0f); lightingShader.setFloat("light.linear", 0.09f); lightingShader.setFloat("light.quadratic", 0.032f);
实现效果如下:
在片段着色器中定义聚光源的参数:
struct Light { vec3 position; vec3 direction; float cutOff; ... };
计算是否照亮:
float theta = dot(lightDir, normalize(-light.direction)); if(theta > light.cutOff) { // 执行光照计算 } else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗 color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
向GPU传输数据:
lightingShader.setVec3("light.position", cameraPos); lightingShader.setVec3("light.direction", cameraFront); lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(35.0f)));
结果如下:
目前看起来边缘过渡,使用一个外半径进行边缘过渡是必要的
计算公式为:
\[\begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation} \]这里\(\epsilon\)(Epsilon)是内(\(\theta\))和外圆锥(\(\gamma\))之间的余弦值差(\(\epsilon = \phi - \gamma\)),最终的\(I\)值就是在当前片段聚光的强度
在片段着色器中定义参数:
struct Light { float outerCutOff; ... }; ... void main() { .... float theta = dot(lightDir, normalize(-light.direction)); float epsilon = light.cutOff - light.outerCutOff; float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); ... // 将不对环境光做出影响,让它总是能有一点光 diffuse *= intensity; specular *= intensity; ... }
输入数据:
lightingShader.setFloat("light.outerCutOff", glm::cos(glm::radians(40.0f)));
实现效果如下:
主要文件caster.cpp
:
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include <math.h> #include "Shader.hpp" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include <glm/glm.hpp> #include <glm/ext/matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale #include <glm/ext/matrix_clip_space.hpp> // glm::perspective #include <glm/gtc/type_ptr.hpp> //全局变量 glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 10.0f); glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 lightPos(0.8f, 1.0f, 2.0f); // 函数声明 void framebuffer_size_callback(GLFWwindow *window, int width, int height); void process_input(GLFWwindow *window); int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); GLFWwindow *window = glfwCreateWindow(800, 600, "caster", nullptr, nullptr); if (window == nullptr) { std::cout << "Faild to create window" << std::endl; glfwTerminate(); } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Faild to initialize glad" << std::endl; return -1; } glad_glViewport(0, 0, 800, 600); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //配置项 glEnable(GL_DEPTH_TEST); Shader lightCubeShader("../light_cube.vs.glsl", "../light_cube.fs.glsl"); Shader lightingShader("../cube.vs.glsl", "../cube.fs.glsl"); unsigned int cubeVAO; glGenVertexArrays(1, &cubeVAO); glBindVertexArray(cubeVAO); float vertices[] = { // positions // normals // texture coords -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }; unsigned int VBO; glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3*sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6*sizeof(float))); glEnableVertexAttribArray(2); // 纹理 unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); // 为当前绑定的纹理对象设置环绕、过滤方式 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); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 加载并生成纹理 int width, height, nrChannels; unsigned char *data = stbi_load("../container2.png", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); lightingShader.setInt("material.diffuse", 0); // 镜面反射纹理 unsigned int texture1; glGenTextures(1, &texture1); glBindTexture(GL_TEXTURE_2D, texture1); // 为当前绑定的纹理对象设置环绕、过滤方式 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); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 加载并生成纹理 data = stbi_load("../container2_specular.png", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); lightingShader.setInt("material.diffuse", 1); unsigned int lightCubeVAO; glGenVertexArrays(1, &lightCubeVAO); glBindVertexArray(lightCubeVAO); // 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据 glBindBuffer(GL_ARRAY_BUFFER, VBO); // 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // positions all containers glm::vec3 cubePositions[] = { glm::vec3( 0.0f, 0.0f, 0.0f), glm::vec3( 2.0f, 5.0f, -15.0f), glm::vec3(-1.5f, -2.2f, -2.5f), glm::vec3(-3.8f, -2.0f, -12.3f), glm::vec3( 2.4f, -0.4f, -3.5f), glm::vec3(-1.7f, 3.0f, -7.5f), glm::vec3( 1.3f, -2.0f, -2.5f), glm::vec3( 1.5f, 2.0f, -2.5f), glm::vec3( 1.5f, 0.2f, -1.5f), glm::vec3(-1.3f, 1.0f, -1.5f) }; while (!glfwWindowShouldClose(window)) { process_input(window); glClearColor(0.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture1); 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", cameraPos); lightingShader.setFloat("material.shininess", 32.0f); lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f); lightingShader.setVec3("light.diffuse", 0.5f, 0.5f, 0.5f); // 将光照调暗了一些以搭配场景 lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f); // lightingShader.setVec3("light.direction", -1.0f, 0.0f, 0.0f); // lightingShader.setFloat("light.constant", 1.0f); // lightingShader.setFloat("light.linear", 0.09f); // lightingShader.setFloat("light.quadratic", 0.032f); lightingShader.setVec3("light.position", cameraPos); lightingShader.setVec3("light.direction", cameraFront); lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(35.0f))); lightingShader.setFloat("light.outerCutOff", glm::cos(glm::radians(40.0f))); glm::mat4 model = glm::mat4(1.0f); model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f)); glm::mat4 view = glm::mat4(1.0f); // view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp); glm::mat4 projection = glm::mat4(1.0f); projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f); // 模型矩阵 int modelLoc = glGetUniformLocation(lightingShader.ID, "model"); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); // 观察矩阵和投影矩阵与之类似 int viewLoc = glGetUniformLocation(lightingShader.ID, "view"); glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); int projectionLoc = glGetUniformLocation(lightingShader.ID, "projection"); glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection)); // render the cube glBindVertexArray(cubeVAO); // glDrawArrays(GL_TRIANGLES, 0, 36); for (unsigned int i = 0; i < 10; i++) { // calculate the model matrix for each object and pass it to shader before drawing glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, cubePositions[i]); float angle = 20.0f * i; model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f)); lightingShader.setMat4("model", model); glDrawArrays(GL_TRIANGLES, 0, 36); } // also draw the lamp object // 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(); } glfwTerminate(); return 0; } void framebuffer_size_callback(GLFWwindow *window, int width, int height) { glViewport(0, 0, width, height); } void process_input(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(window, true); } float cameraSpeed = 0.05f; // adjust accordingly if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) cameraPos += cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) cameraPos -= cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; }
立方体顶点着色器GLSLcube.vs.glsl
:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoords; out vec3 Normal; out vec3 FragPos; out vec2 TexCoords; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); FragPos = vec3(model * vec4(aPos, 1.0)); Normal = aNormal; TexCoords = aTexCoords; }
立方体片段着色器GLSLcube.fs.glsl
:
#version 330 core struct Material { sampler2D diffuse; sampler2D specular; float shininess; }; struct Light { vec3 position; vec3 direction; float cutOff; float outerCutOff; vec3 ambient; vec3 diffuse; vec3 specular; // float constant; // float linear; // float quadratic; }; in vec3 Normal; in vec3 FragPos; in vec2 TexCoords; out vec4 FragColor; uniform vec3 objectColor; uniform vec3 lightColor; uniform vec3 lightPos; uniform vec3 viewPos; uniform Material material; uniform Light light; void main() { // 环境光 // 将环境光下的材质颜色设置为漫反射材质颜色同样的值 vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)); // 漫反射 vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); // vec3 lightDir = normalize(-light.direction); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); // 镜面光 vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords)); // 计算衰减 // float distance = length(light.position - FragPos); // float attenuation = 1.0 / (light.constant + light.linear * distance + // light.quadratic * (distance * distance)); // ambient *= attenuation; // diffuse *= attenuation; // specular *= attenuation; float theta = dot(lightDir, normalize(-light.direction)); if(theta > light.cutOff) { // 执行光照计算 vec3 result = ambient + diffuse + specular; FragColor = vec4(result, 1.0); } else{ // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗 float epsilon = light.cutOff - light.outerCutOff; float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); // 将不对环境光做出影响,让它总是能有一点光 diffuse *= intensity; specular *= intensity; vec3 result = ambient + diffuse + specular; FragColor = vec4(result, 1.0); } }
着色器Shader.hpp
、光源顶点着色器GLSLlight_cube.vs.glsl
、光源片段着色器GLSLlight_cube.fs.glsl
见:
[1]投光物 - LearnOpenGL CN (learnopengl-cn.github.io)