Learn OpenGL (八):顏色
當我們在OpenGL中建立一個光源時,我們希望給光源一個顏色。在上一段中我們有一個白色的太陽,所以我們也將光源設定為白色。當我們把光源的顏色與物體的顏色值相乘,所得到的就是這個物體所反射的顏色(也就是我們所感知到的顏色)。
建立一個光照場景
在接下來的教程中,我們將會廣泛地使用顏色來模擬現實世界中的光照效果,創造出一些有趣的視覺效果。由於我們現在將會使用光源了,我們希望將它們顯示為可見的物體,並在場景中至少加入一個物體來測試模擬光照的效果。
首先我們需要一個物體來作為被投光(Cast the light)的物件,我們將使用前面教程中的那個著名的立方體箱子。我們還需要一個物體來代表光源在3D場景中的位置。簡單起見,我們依然使用一個立方體來代表光源(我們已擁有立方體的
填一個頂點緩衝物件(VBO),設定一下頂點屬性指標和其它一些亂七八糟的東西現在對你來說應該很容易了,所以我們就不再贅述那些步驟了。如果你仍然覺得這很困難,我建議你複習之前的教程,並且在繼續學習之前先把練習過一遍。
我們首先需要一個頂點著色器來繪製箱子。與之前的頂點著色器相比,容器的頂點位置是保持不變的(雖然這一次我們不需要紋理座標了),因此頂點著色器中沒有新的程式碼。我們將會使用之前教程頂點著色器的精簡版:
#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); }
記得更新你的頂點資料和屬性指標使其與新的頂點著色器保持一致(當然你可以繼續留著紋理資料和屬性指標。在這一節中我們將不會用到它們,但有一個全新的開始也不是什麼壞主意)。
因為我們還要建立一個表示燈(光源)的立方體,所以我們還要為這個燈建立一個專門的VAO。當然我們也可以讓這個燈和其它物體使用同一個VAO,簡單地對它的model(模型)矩陣做一些變換就好了,然而接下來的教程中我們會頻繁地對頂點資料和屬性指標做出修改,我們並不想讓這些修改影響到燈(我們只關心燈的頂點位置),因此我們有必要為燈建立一個新的VAO。
unsigned int lightVAO; glGenVertexArrays(1, &lightVAO); glBindVertexArray(lightVAO); // 只需要繫結VBO不用再次設定VBO的資料,因為箱子的VBO資料中已經包含了正確的立方體頂點資料 glBindBuffer(GL_ARRAY_BUFFER, VBO); // 設定燈立方體的頂點屬性(對我們的燈來說僅僅只有位置資料) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
這段程式碼對你來說應該非常直觀。現在我們已經建立了表示燈和被照物體箱子,我們只需要再定義一個片段著色器就行了:
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
FragColor = vec4(lightColor * objectColor, 1.0);
}
這個片段著色器從uniform變數中接受物體的顏色和光源的顏色。正如本節一開始所討論的那樣,我們將光源的顏色和物體(反射的)顏色相乘。這個著色器理解起來應該很容易。我們把物體的顏色設定為之前提到的珊瑚紅色,並把光源設定為白色。
// 在此之前不要忘記首先 use 對應的著色器程式(來設定uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
要注意的是,當我們修改頂點或者片段著色器後,燈的位置或顏色也會隨之改變,這並不是我們想要的效果。我們不希望燈的顏色在接下來的教程中因光照計算的結果而受到影響,而是希望它能夠與其它的計算分離。我們希望燈一直保持明亮,不受其它顏色變化的影響(這樣它才更像是一個真實的光源)。
為了實現這個目標,我們需要為燈的繪製建立另外的一套著色器,從而能保證它能夠在其它光照著色器發生改變的時候不受影響。頂點著色器與我們當前的頂點著色器是一樣的,所以你可以直接把現在的頂點著色器用在燈上。燈的片段著色器給燈定義了一個不變的常量白色,保證了燈的顏色一直是亮的:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // 將向量的四個分量全部設定為1.0
}
當我們想要繪製我們的物體的時候,我們需要使用剛剛定義的光照著色器來繪製箱子(或者可能是其它的物體)。當我們想要繪製燈的時候,我們會使用燈的著色器。在之後的教程裡我們會逐步更新這個光照著色器,從而能夠慢慢地實現更真實的效果。
使用這個燈立方體的主要目的是為了讓我們知道光源在場景中的具體位置。我們通常在場景中定義一個光源的位置,但這只是一個位置,它並沒有視覺意義。為了顯示真正的燈,我們將表示光源的立方體繪製在與光源相同的位置。我們將使用我們為它新建的片段著色器來繪製它,讓它一直處於白色的狀態,不受場景中的光照影響。
我們宣告一個全域性vec3
變數來表示光源在場景的世界空間座標中的位置:
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
然後我們把燈位移到這裡,然後將它縮小一點,讓它不那麼明顯:
model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));
繪製燈立方體的程式碼應該與下面的類似:
lampShader.use();
// 設定模型、檢視和投影矩陣uniform
...
// 繪製燈立方體物件
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
請把上述的所有程式碼片段放在你程式中合適的位置,這樣我們就能有一個乾淨的光照實驗場地了。如果一切順利,執行效果將會如下圖所示:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <learnopengl/filesystem.h>
#include <learnopengl/shader_m.h>
#include <learnopengl/camera.h>
#include <iostream>
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
// lighting
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
void processInput(GLFWwindow *window)
{
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);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
// glfw: whenever the mouse moves, this callback is called
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
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);
}
// glfw: whenever the mouse scroll wheel scrolls, this callback is called
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
int main()
{
// glfw: initialize and configure
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfw window creation
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
// tell GLFW to capture our mouse
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// glad: load all OpenGL function pointers
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// configure global opengl state
glEnable(GL_DEPTH_TEST);
// build and compile our shader zprogram
Shader lightingShader("1.colors.vs", "1.colors.fs");
Shader lampShader("1.lamp.vs", "1.lamp.fs");
// set up vertex data (and buffer(s)) and configure vertex attributes
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,
};
// first, configure the cube's VAO (and VBO)
unsigned int VBO, cubeVAO;
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindVertexArray(cubeVAO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// second, configure the light's VAO (VBO stays the same; the vertices are the same for the light object which is also a 3D cube)
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// we only need to bind to the VBO (to link it with glVertexAttribPointer), no need to fill it; the VBO's data already contains all we need (it's already bound, but we do it again for educational purposes)
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// render loop
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// input
processInput(window);
// render
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// be sure to activate shader when setting uniforms/drawing objects
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
//setVec3其實在shader中
/*
void setVec3(const std::string &name, float x, float y, float z) const
{
glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z);
}
*/
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
// world transformation
glm::mat4 model;
lightingShader.setMat4("model", model);
// render the cube
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// also draw the lamp object
lampShader.use();
lampShader.setMat4("projection", projection);
lampShader.setMat4("view", view);
model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lampShader.setMat4("model", model);
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
glfwTerminate();
return 0;
}