• 友链

  • 首页

  • 文章归档
h u a n b l o g
h u a n b l o g

欢

HI,Friend

05月
18
Shader
C++

OpenGL笔记8-光照

发表于 2024-05-18 • 字数统计 38811 • 被 2,121 人看爆

序言

以glew、glfw库
OpenGL学习网站
glfw官网
OpenGL-API文档
glew官网

颜色

现实世界中有无数种颜色,每一个物体都有它们自己的颜色。我们要做的工作是使用(有限的)数字来模拟真实世界中(无限)的颜色,因此并不是所有的现实世界中的颜色都可以用数字来表示。然而我们依然可以用数字来代表许多种颜色,并且你甚至可能根本感觉不到他们与真实颜色之间的差异。颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为RGB。这三个不同的分量组合在一起几乎可以表示存在的任何一种颜色。例如,要获取一个珊瑚红(Coral)颜色我们可以这样定义一个颜色向量:

glm::vec3 coral(1.0f, 0.5f, 0.31f);

我们在现实生活中看到某一物体的颜色并不是这个物体的真实颜色,而是它所反射(Reflected)的颜色。换句话说,那些不能被物体吸收(Absorb)的颜色(被反射的颜色)就是我们能够感知到的物体的颜色。例如,太阳光被认为是由许多不同的颜色组合成的白色光(如下图所示)。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有颜色,不被吸收的蓝色光被反射到我们的眼中,使我们看到了一个蓝色的玩具。下图显示的是一个珊瑚红的玩具,它以不同强度的方式反射了几种不同的颜色。
珊瑚红颜色.png

正如你所见,白色的阳光是一种所有可见颜色的集合,上面的物体吸收了其中的大部分颜色,它仅反射了那些代表这个物体颜色的部分,这些被反射颜色的组合就是我们感知到的颜色(此例中为珊瑚红)。

这些颜色反射的规律被直接地运用在图形领域。我们在OpenGL中创建一个光源时都会为它定义一个颜色。在前面的段落中所提到光源的颜色都是白色的,那我们就继续来创建一个白色的光源吧。当我们把光源的颜色与物体的颜色相乘,所得到的就是这个物体所反射该光源的颜色(也就是我们感知到的颜色)。让我们再次审视我们的玩具(这一次它还是珊瑚红)并看看如何计算出他的反射颜色。我们通过检索结果颜色的每一个分量来看一下光源色和物体颜色的反射运算:

glm::vec3 lightColor(1.0f, 1.0f, 1.0f);         //光源颜色(白色)
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);          //珊瑚红颜色 
glm::vec3 result = lightColor * toyColor;       //(1.0f, 0.5f, 0.31f); 反射的颜色珊瑚红

我们可以看到玩具在进行反射时吸收了白色光源颜色中的大部分颜色,但它对红、绿、蓝三个分量都有一定的反射,反射量是由物体本身的颜色所决定的。这也代表着现实中的光线原理。由此,我们可以定义物体的颜色为这个物体从一个光源反射各个颜色分量的多少。现在,如果我们使用一束绿色的光又会发生什么呢?

glm::vec3 lightColor(0.0f, 1.0f, 0.0f);         //绿色
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);          //珊瑚红
glm::vec3 result = lightColor * toyColor;       //=(0.0f, 0.5f, 0.0f); 深绿色

可以看到,我们的玩具没有红色和蓝色的光让它来吸收或反射,这个玩具也吸收了光线中一半的绿色,当然它仍然反射了光的一半绿色。它现在看上去是深绿色(Dark-greenish)的。我们可以看到,如果我们用一束绿色的光线照来照射玩具,那么只有绿色能被反射和感知到,没有红色和蓝色能被反射和感知。这样做的结果是,一个珊瑚红的玩具突然变成了深绿色物体。现在我们来看另一个例子,使用深橄榄绿色(Dark olive-green)的光线:

glm::vec3 lightColor(0.33f, 0.42f, 0.18f);      //深橄榄绿色
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);          //珊瑚红
glm::vec3 result = lightColor * toyColor;       //=(0.33f, 0.21f, 0.06f);

如你所见,我们可以通过物体对不同颜色光的反射来的得到意想不到的不到的颜色,从此创作颜色已经变得非常简单。

创建一个光照场景

在接下来的教程中,我们将通过模拟真实世界中广泛存在的光照和颜色现象来创建有趣的视觉效果。现在我们将在场景中创建一个看得到的物体来代表光源,并且在场景中至少添加一个物体来模拟光照。

首先我们需要一个物体来投光(Cast the light),我们将使用前面教程中的立方体箱子。我们还需要一个物体来代表光源,它代表光源在这个3D空间中的确切位置。简单起见,我们依然使用一个立方体来代表光源(我们已拥有立方体的顶点数据是吧?)。

所以,我们首先需要一个顶点着色器来绘制箱子。与上一个教程的顶点着色器相比,容器的顶点位置保持不变(虽然这一次我们不需要纹理坐标),因此顶点着色器中没有新的代码。我们将会使用上一篇教程顶点着色器的精简版(OpenGL笔记7-摄像机):

顶点着色器

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

请确认更新你的顶点数据和属性对应的指针与新的顶点着色器一致(这一节主要是光照,需不需要纹理关系不大)。

因为我们还要创建一个表示灯(光源)的立方体,所以我们还要为这个灯创建一个特殊的VAO。当然我们也可以让这个灯和其他物体使用同一个VAO然后对他的model(模型)矩阵做一些变换,然而接下来的教程中我们会频繁地对顶点数据做一些改变并且需要改变属性对应指针设置,我们并不想因此影响到灯(我们只在乎灯的位置),因此我们有必要为灯创建一个新的VAO。

GLuint lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性指针(仅设置灯的顶点数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);

这段代码对你来说应该非常直观。既然我们已经创建了表示灯和被照物体的立方体,我们只需要再定义一个东西就行了了,那就是片段着色器:
片段着色器

#version 330 core
out vec4 color;

void main()
{
    color = vec4(1.0f); //设置四维向量的所有元素为 1.0f
}

当我们想要绘制我们的物体的时候,我们需要使用刚刚定义的光照着色器绘制箱子(或者可能是其它的一些物体),让我们想要绘制灯的时候,我们会使用灯的着色器。在之后的教程里我们会逐步升级这个光照着色器从而能够缓慢的实现更真实的效果。

这个片段着色器接受两个分别表示物体颜色和光源颜色的uniform变量。正如本篇教程一开始所讨论的一样,我们将光源的颜色与物体(能反射)的颜色相乘。这个着色器应该很容易理解。接下来让我们把物体的颜色设置为上一节中所提到的珊瑚红并把光源设置为白色:

// 在此之前不要忘记首先'使用'对应的着色器程序(来设定uniform)
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);      //光源设置为白色

要注意的是,当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯对象的颜色在接下来的教程中因光照计算的结果而受到影响,而希望它能够独立。希望表示灯不受其他光照的影响而一直保持明亮(这样它才更像是一个真实的光源)。

为了实现这个目的,我们需要为灯创建另外的一套着色器程序,从而能保证它能够在其他光照着色器变化的时候保持不变。顶点着色器和我们当前的顶点着色器是一样的,所以你可以直接把灯的顶点着色器复制过来。片段着色器保证了灯的颜色一直是亮的,我们通过给灯定义一个常量的白色来实现:

#version 330 core
out vec4 color;

void main()
{
    color = vec4(1.0f); //设置四维向量的所有元素为 1.0f
}

当我们想要绘制我们的物体的时候,我们需要使用刚刚定义的光照着色器绘制箱子(或者可能是其它的一些物体),让我们想要绘制灯的时候,我们会使用灯的着色器。在之后的教程里我们会逐步升级这个光照着色器从而能够缓慢的实现更真实的效果。

使用这个灯立方体的主要目的是为了让我们知道光源在场景中的具体位置。我们通常在场景中定义一个光源的位置,但这只是一个位置,它并没有视觉意义。为了显示真正的灯,我们将表示光源的灯立方体绘制在与光源同样的位置。我们将使用我们为它新建的片段着色器让它保持它一直处于白色状态,不受场景中的光照影响。

我们声明一个全局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));

绘制灯立方体的代码应该与下面的类似:

glUseProgram(lightShaderProgram);       //光源
// 设置模型、视图和投影矩阵uniform
...
// 绘制灯立方体对象
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);

光照场景效果:
光照场景效果.png

完整代码

#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>


//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 2) in vec2 texCoord;\n"

"out vec2 TexCoord;\n"

"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"   TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);\n"      //翻转图片位置 
"}\n";

//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec2 TexCoord;\n"
"out vec4 color;\n"
"uniform sampler2D ourTexture1;\n"
"uniform sampler2D ourTexture2;\n"
"uniform vec3 objectColor;\n"
"uniform vec3 lightColor;\n"

"void main()\n"
"{\n"
"     color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2) * vec4(lightColor * objectColor, 1.0f) ;\n"
"}\n";

//光源-顶点着色器
const char* lightVertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 2) in vec2 texCoord;\n"

"out vec2 TexCoord;\n"

"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"   TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);\n"      //翻转图片位置 
"}\n";

//光源-片段着色器
const char* lightFragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"uniform sampler2D ourTexture1;\n"
"uniform sampler2D ourTexture2;\n"

"void main()\n"
"{\n"
"     color = vec4(1.0f);\n"
"}\n";

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void do_movement();

const GLuint WIDTH = 800, HEIGHT = 600;

//摄像机信息
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);          //摄像机位置
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);       //摄像机朝向
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);           //上向量

GLfloat deltaTime = 0.0f;   //当前帧遇上一帧的时间差
GLfloat lastFrame = 0.0f;   //上一帧的时间

GLfloat lastX = WIDTH / 2, lastY = HEIGHT / 2;       //鼠标上一次位置,默认屏幕中心
GLfloat pitch = 0.0f;       //俯仰角
GLfloat yaw = -90.0f;       //偏航角
GLfloat aspect = 45.0f;     //视角大小
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

bool keys[1024];

int main()
{
    glfwInit();     //必须要将glfw初始化
    //告诉GLFW使用OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  //使用的是OpenGL核心模式
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);   //不允许调整窗口大小

    //创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW Window" << std::endl;

        glfwTerminate();    //销毁窗口与数据

        return -1;
    }

    glfwMakeContextCurrent(window);     //将OpenGL指向为当前窗口

    glewExperimental = GL_TRUE;     //用于告知GLEW使用现化OpenGL技术

    //glew初始化
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;

        return -1;
    }

    //视口
    int width = 800, height = 600;
    glfwGetFramebufferSize(window, &width, &height);        //设置OpenGL渲染窗口的尺寸
    glViewport(0, 0, width, height);    //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度
    glfwSetKeyCallback(window, key_callback);       //注册按键回调事件
    glfwSetCursorPosCallback(window, mouse_callback);       //注册鼠标回调事件
    //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);        //隐藏鼠标,并捕获他
    glfwSetScrollCallback(window, scroll_callback);                 //鼠标滚动回调事件

    glEnable(GL_DEPTH_TEST);            //深度缓存区

    //顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

    GLint vertexSuccess;
    GLchar vertexInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    GLint fragmentSuccess;
    GLchar fragmentInfoLog[512];

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, fragmentInfoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
    }


    //着色器链接程序
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    GLint programSuccess;
    GLchar programInfoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
    }

    //光源-顶点着色器
    GLuint lightVertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(lightVertexShader, 1, &lightVertexShaderSource, nullptr);
    glCompileShader(lightVertexShader);

    GLint lightVertexSuccess;
    GLchar lightVertexInfoLog[512];

    glGetShaderiv(lightVertexShader, GL_COMPILE_STATUS, &lightVertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(lightVertexShader, 512, nullptr, lightVertexInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::VERTEX::COMPILATION_FAILED\n" << lightVertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint lightFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(lightFragmentShader, 1, &lightFragmentShaderSource, nullptr);
    glCompileShader(lightFragmentShader);

    GLint lightFragmentSuccess;
    GLchar lightFragmentInfoLog[512];

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &lightFragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, lightFragmentInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::FRAGMENT::COMPILATION_FAILED\n" << lightFragmentInfoLog << std::endl;
    }


    //光源着色器链接程序
    GLuint lightShaderProgram = glCreateProgram();
    glAttachShader(lightShaderProgram, lightVertexShader);
    glAttachShader(lightShaderProgram, lightFragmentShader);
    glLinkProgram(lightShaderProgram);

    GLint lightProgramSuccess;
    GLchar lightProgramInfoLog[512];
    glGetProgramiv(lightShaderProgram, GL_LINK_STATUS, &lightProgramSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(lightShaderProgram, 512, nullptr, lightProgramInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::PROGRAM::LINKING_FAILED\n" << lightProgramInfoLog << std::endl;
    }


    GLfloat vertices[] = {
        //-----位置           ---纹理坐标
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };

    //顶点数据
    GLuint VBO, VAO;
    glGenBuffers(1, &VBO);      //顶点缓冲对象
    glGenVertexArrays(1, &VAO); //顶点数组对象

    glBindVertexArray(VAO);

    //顶点数据
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    //纹理坐标
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(2);



    //纹理1--------------
    //加载纹理
    int textureWidht, textureHeight;
    unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    GLuint texture1;
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);


    //加载纹理2---------------
    image = SOIL_load_image("awesomeface.png", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    GLuint texture2;
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);

    //解除绑定
    SOIL_free_image_data(image);          //释放图像资源
    glBindTexture(GL_TEXTURE_2D, 0);      //解除纹理绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    GLuint lightVAO;
    glGenVertexArrays(1, &lightVAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBindVertexArray(lightVAO);
    
    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    //纹理坐标
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(2);

    //纹理1--------------
    //加载纹理
    image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);


    //加载纹理2---------------
    image = SOIL_load_image("awesomeface.png", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);

    //解除绑定
    SOIL_free_image_data(image);          //释放图像资源
    glBindTexture(GL_TEXTURE_2D, 0);      //解除纹理绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    while (!glfwWindowShouldClose(window)) {
        //检查GLFW是否退出,即窗口是否关闭了,true代表结束了

        glfwPollEvents();       //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数  键盘事件:glfwSetKeyCallback(window, key_callback);  key_callback即设定的回调函数
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);       //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
        //glClear(GL_COLOR_BUFFER_BIT);       //清空屏幕缓冲,这里是颜色缓冲
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        do_movement();
        //渲染指令
        //glBindTexture(GL_TEXTURE_2D, texture);
        glUseProgram(shaderProgram);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture1"), 0);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);
        glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture2"), 1);


        glBindTexture(GL_TEXTURE_2D, texture1);
        glUniform1i(glGetUniformLocation(lightShaderProgram, "ourTexture1"), 0);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);
        glUniform1i(glGetUniformLocation(lightShaderProgram, "ourTexture2"), 1);

        glm::mat4 model;        //模型矩阵
        glm::mat4 view;         //观察矩阵
        glm::mat4 projection;   //投影矩阵-透视投影
        view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        projection = glm::perspective(aspect, (GLfloat)WIDTH / (GLfloat)HEIGHT, 0.1f, 100.0f);  //缩放效果

        GLint objectColorLoc = glGetUniformLocation(shaderProgram, "objectColor");
        GLint lightColorLoc = glGetUniformLocation(shaderProgram, "lightColor");
        glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
        glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色

        GLint modelLoc = glGetUniformLocation(shaderProgram, "model");
        GLint viewLoc = glGetUniformLocation(shaderProgram, "view");
        GLint projLoc = glGetUniformLocation(shaderProgram, "projection");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);

        //光源
        glUseProgram(lightShaderProgram);       //光源
        modelLoc = glGetUniformLocation(lightShaderProgram, "model");
        viewLoc = glGetUniformLocation(lightShaderProgram, "view");
        projLoc = glGetUniformLocation(lightShaderProgram, "projection");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

        glBindVertexArray(lightVAO);
        model = glm::translate(model, lightPos);
        model = glm::scale(model, glm::vec3(0.2f));
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);

        glfwSwapBuffers(window);  //交换颜色缓冲,用来绘制,输出显示在屏幕上
    }

    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);

    glfwTerminate();
    return 0;
}

/*
* 按键回调事件
* @param window 窗口
* @param key 按键
* @param scancode 扫描码
* @param action 表示这个按键是被按下还是释放
* @param mode 是否有Ctrl、Shift、Alt、Super等按钮的操作
*/
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) {

    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
        //esc键按下,关闭窗口
        glfwSetWindowShouldClose(window, GL_TRUE);
    }

    if (action == GLFW_PRESS)   //按下
        keys[key] = true;
    else if (action == GLFW_RELEASE)    //松开
        keys[key] = false;

}

void do_movement() {
    GLfloat currentFrame = glfwGetTime();
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;
    GLfloat cameraSpeed = 5.0f * deltaTime;    //速度

    if (keys[GLFW_KEY_W])
        cameraPos += cameraSpeed * cameraFront;
    if (keys[GLFW_KEY_S])
        cameraPos -= cameraSpeed * cameraFront;
    if (keys[GLFW_KEY_A])
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (keys[GLFW_KEY_D])
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (keys[GLFW_KEY_SPACE])       //向上
        cameraPos += cameraSpeed * cameraUp;
}

bool firstMouse = true;     //除了第一次鼠标移动外,其他不能移动
/*
* 鼠标回调事件
* @param window 窗口
* @param xpos 鼠标x坐标
* @param ypos 鼠标y坐标
*/
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    GLfloat xoffset = xpos - lastX;
    GLfloat yoffset = lastY - ypos;
    lastX = xpos;
    lastY = ypos;

    GLfloat sensitivity = 0.05;         //鼠标转向速度
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw += xoffset;
    pitch += yoffset;

    if (pitch > 89.0f)
        pitch = 89.0f;
    if (pitch < -89.0f)
        pitch = -89.0f;

    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

/*
* 鼠标滚轮回调事件
*/
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    if (aspect >= 1.0f && aspect <= 45.0f)
        aspect -= yoffset;
    if (aspect <= 1.0f)
        aspect = 1.0f;
    if (aspect >= 45.0f)
        aspect = 45.0f;
}

注意:两个glUseProgram不要放在一起,默认使用最新glUseProgram

glUseProgram(shaderProgram);
glUseProgram(lightShaderProgram);       //只会使用这个着色器程序,不会两个都使用

//shaderProgram相关代码
//lightShaderProgram相关代码

glUseProgram(shaderProgram);
//shaderProgram相关代码

glUseProgram(lightShaderProgram);
//lightShaderProgram相关代码

光照基础

现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是以目前我们所拥有的处理能力无法模拟的。因此OpenGL的光照仅仅使用了简化的模型并基于对现实的估计来进行模拟,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。这些光照元素看起来像下面这样:
光照元素.png

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远都给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。

环境光照

光通常都不是来自于同一光源,而是来自散落于我们周围的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散和反弹,所以光最后到达的地点可能并不是它所临近的直射方向;光能够像这样反射(Reflect)到其他表面,一个物体的光照可能受到来自一个非直射的光源影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。

因为我们不是复杂和昂贵算法的死忠粉丝,所以我们将会使用一种简化的全局照明模型,叫做环境光照(Ambient Lighting)。如你在前面上面颜色所见,我们使用一个(数值)很小的常量(光)颜色添加进物体片段(Fragment,指当前讨论的光线在物体上的照射点)的最终颜色里,这看起来就像即使没有直射光源也始终存在着一些发散的光。

把环境光照添加到场景里非常简单。我们用光的颜色乘以一个(数值)很小常量环境因子,再乘以物体的颜色,然后使用它作为片段的颜色:

物体片段着色器

void main()
{
    float ambientStrength = 0.1f;
    vec3 ambient = ambientStrength * lightColor;
    vec3 result = ambient * objectColor;
    color = vec4(result, 1.0f);
}

如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但不是完全的黑暗,因为我们应用了环境光照(注意发光立方体没被环境光照影响是因为我们对它使用了另一个着色器)。它看起来应该像这样:
物体添加环境光效果.png

漫反射光照

环境光本身不提供最明显的光照效果,但是漫反射光照(Diffuse Lighting)会对物体产生显著的视觉影响。

漫反射光使物体上与光线排布越近的片段越能从光源处获得更多的亮度。为了更好的理解漫反射光照,请看下图:
漫反射原理.png

图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线与它所接触片段之间的角度。如果光线垂直于物体表面,这束光对物体的影响会最大化(更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一种向量(这里以黄色箭头表示)。两个向量之间的角度就能够根据点乘计算出来。

记得在OpenGL笔记5-变换,我们知道两个单位向量的角度越小,它们点乘的结果越倾向于1。当两个向量的角度是90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响越小。

注意,我们使用的是单位向量(Unit Vector,长度是1的向量)取得两个向量夹角的余弦值,所以我们需要确保所有的向量都被标准化,否则点乘返回的值就不仅仅是余弦值了。

点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,基于不同片段所朝向光源的方向的不同,这些片段被照亮的情况也不同。
所以,我们需要些什么来计算漫反射光照?

  • 法向量:一个垂直于顶点表面的向量。
  • 定向的光线:作为光的位置和片段的位置之间的向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。

法向量

法向量(Normal Vector) 是垂直于顶点表面的(单位)向量。由于顶点自身并没有表面(它只是空间中一个独立的点),我们利用顶点周围的顶点计算出这个顶点的表面。我们能够使用叉乘这个技巧为立方体所有的顶点计算出法线,但是由于3D立方体不是一个复杂的形状,所以我们可以简单的把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在这里找到。试着去想象一下,这些法向量真的是垂直于立方体的各个面的表面的(一个立方体由6个面组成)。

因为我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器:

const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 normal;\n"

"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"}\n";

现在我们已经向每个顶点添加了一个法向量,已经更新了顶点着色器,我们还要更新顶点属性指针(Vertex Attibute Pointer)。注意,发光物使用同样的顶点数组作为它的顶点数据,然而发光物的着色器没有使用新添加的法向量。我们不会更新发光物的着色器或者属性配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小:

glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);

发光物着色器顶点数据的不完全使用看起来有点低效,但是这些顶点数据已经从立方体对象载入到GPU的内存里了,所以GPU内存不是必须再储存新数据。相对于重新给发光物分配VBO,实际上却是更高效了。

所有光照的计算需要在片段着色器里进行,所以我们需要把法向量由顶点着色器传递到片段着色器。我们这么做:

out vec3 Normal;
void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    Normal = normal;
}

剩下要做的事情是,在片段着色器中定义相应的输入值:

in vec3 Normal;

计算漫反射

每个顶点现在都有了法向量,但是我们仍然需要光的位置向量和片段的位置向量。由于光的位置是一个静态变量,我们可以简单的在片段着色器中把它声明为uniform:

uniform vec3 lightPos;

然后再游戏循环中(外面也可以,因为它不会变)更新uniform。我们使用在前面教程中声明的lightPos向量作为光源位置:

GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);

最后,我们还需要片段的位置(Position)(用于反射在物体上的那片光)。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(Model Matrix, 只用模型矩阵不需要用观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以让我们就声明一个输出(out)变量,然后计算它的世界空间坐标:

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 = normal;
}

最后,在片段着色器中添加相应的输入变量。

in vec3 FragPos;

现在,所有需要的变量都设置好了,我们可以在片段着色器中开始光照的计算了。

我们需要做的第一件事是计算光源和片段位置(即反射在物体上的那片光)之间的方向向量。前面提到,光的方向向量是光的位置向量与片段的位置向量之间的向量差。你可能记得,在OpenGL笔记5-变换教程中,我们简单的通过两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和方向向量这个结果都进行标准化:
片段着色器

vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);

当计算光照时我们通常不关心一个向量的“量”或它的位置,我们只关心它们的方向。所有的计算都使用单位向量完成,因为这会简化了大多数计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,这样它们才会保证自身为单位向量。忘记对向量进行标准化是一个十分常见的错误。

下一步,我们对norm和lightDir向量进行点乘,来计算光对当前片段的实际的散射影响。结果值再乘以光的颜色,得到散射因子。两个向量之间的角度越大,散射因子就会越小:

片段着色器

float diff = max(dot(norm, lightDir), 0.0);     //散射影响
vec3 diffuse = diff * lightColor;

如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致散射因子变为负数。为此,我们使用max函数返回两个参数之间较大的参数,从而保证散射因子不会变成负数。负数的颜色是没有实际定义的,所以最好避免它,除非你是那种古怪的艺术家。

既然我们有了一个环境光照颜色和一个散射光颜色,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。

片段

vec3 result = (ambient + diffuse) * objectColor;        //ambient环境光照
color = vec4(result, 1.0f);

效果
漫反射效果.png

完整代码

#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>


//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 normal;\n"

"out vec3 Normal;\n"            //法向量
"out vec3 FragPos;\n"           //片段位置(如立方体六个面,每个面两个三角形,三角形形成的那个片段)

"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"   FragPos = vec3(model * vec4(position, 1.0f));\n"               //片段位置,将片段位置转化为世界坐标
"   Normal = normal;\n"
"}\n";

//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 Normal;\n"
"in vec3 FragPos;\n"

"out vec4 color;\n"

"uniform vec3 objectColor;\n"
"uniform vec3 lightColor;\n"
"uniform vec3 lightPos;\n"

"void main()\n"
"{\n"
"     float ambientStrength = 0.1f;\n"          //环境因子
"     vec3 ambient = ambientStrength * lightColor;\n"

"     vec3 norm = normalize(Normal);\n"         //法向量单位化
"     vec3 lightDir = normalize(lightPos - FragPos);\n"        //计算光的方向,即方向向量
"     float diff = max(dot(norm, lightDir), 0.0);\n"          //计算光对当前片段的实际散射影响
"     vec3 diffuse = diff * lightColor;\n"          //得到散射因子
"     vec3 result = (ambient + diffuse) * objectColor;\n"         //环境光
"     color = vec4(result, 1.0f) ;\n"
"}\n";

//光源-顶点着色器
const char* lightVertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"


"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"}\n";

//光源-片段着色器
const char* lightFragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"

"void main()\n"
"{\n"
"     color = vec4(1.0f);\n"
"}\n";

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void do_movement();

const GLuint WIDTH = 800, HEIGHT = 600;

//摄像机信息
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);          //摄像机位置
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);       //摄像机朝向
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);           //上向量

GLfloat deltaTime = 0.0f;   //当前帧遇上一帧的时间差
GLfloat lastFrame = 0.0f;   //上一帧的时间

GLfloat lastX = WIDTH / 2, lastY = HEIGHT / 2;       //鼠标上一次位置,默认屏幕中心
GLfloat pitch = 0.0f;       //俯仰角
GLfloat yaw = -90.0f;       //偏航角
GLfloat aspect = 45.0f;     //视角大小
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

bool keys[1024];

int main()
{
    glfwInit();     //必须要将glfw初始化
    //告诉GLFW使用OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  //使用的是OpenGL核心模式
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);   //不允许调整窗口大小

    //创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW Window" << std::endl;

        glfwTerminate();    //销毁窗口与数据

        return -1;
    }

    glfwMakeContextCurrent(window);     //将OpenGL指向为当前窗口

    glewExperimental = GL_TRUE;     //用于告知GLEW使用现化OpenGL技术

    //glew初始化
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;

        return -1;
    }

    //视口
    int width = 800, height = 600;
    glfwGetFramebufferSize(window, &width, &height);        //设置OpenGL渲染窗口的尺寸
    glViewport(0, 0, width, height);    //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度
    glfwSetKeyCallback(window, key_callback);       //注册按键回调事件
    glfwSetCursorPosCallback(window, mouse_callback);       //注册鼠标回调事件
    //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);        //隐藏鼠标,并捕获他
    glfwSetScrollCallback(window, scroll_callback);                 //鼠标滚动回调事件

    glEnable(GL_DEPTH_TEST);            //深度缓存区

    //顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

    GLint vertexSuccess;
    GLchar vertexInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    GLint fragmentSuccess;
    GLchar fragmentInfoLog[512];

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, fragmentInfoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
    }


    //着色器链接程序
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    GLint programSuccess;
    GLchar programInfoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
    }

    //光源-顶点着色器
    GLuint lightVertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(lightVertexShader, 1, &lightVertexShaderSource, nullptr);
    glCompileShader(lightVertexShader);

    GLint lightVertexSuccess;
    GLchar lightVertexInfoLog[512];

    glGetShaderiv(lightVertexShader, GL_COMPILE_STATUS, &lightVertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(lightVertexShader, 512, nullptr, lightVertexInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::VERTEX::COMPILATION_FAILED\n" << lightVertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint lightFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(lightFragmentShader, 1, &lightFragmentShaderSource, nullptr);
    glCompileShader(lightFragmentShader);

    GLint lightFragmentSuccess;
    GLchar lightFragmentInfoLog[512];

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &lightFragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, lightFragmentInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::FRAGMENT::COMPILATION_FAILED\n" << lightFragmentInfoLog << std::endl;
    }


    //光源着色器链接程序
    GLuint lightShaderProgram = glCreateProgram();
    glAttachShader(lightShaderProgram, lightVertexShader);
    glAttachShader(lightShaderProgram, lightFragmentShader);
    glLinkProgram(lightShaderProgram);

    GLint lightProgramSuccess;
    GLchar lightProgramInfoLog[512];
    glGetProgramiv(lightShaderProgram, GL_LINK_STATUS, &lightProgramSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(lightShaderProgram, 512, nullptr, lightProgramInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::PROGRAM::LINKING_FAILED\n" << lightProgramInfoLog << std::endl;
    }


    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
    };

    //顶点数据
    GLuint VBO, VAO;
    glGenBuffers(1, &VBO);      //顶点缓冲对象
    glGenVertexArrays(1, &VAO); //顶点数组对象

    glBindVertexArray(VAO);

    //顶点数据
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    //法向量
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    //光源
    GLuint lightVAO;
    glGenVertexArrays(1, &lightVAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBindVertexArray(lightVAO);

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    while (!glfwWindowShouldClose(window)) {
        //检查GLFW是否退出,即窗口是否关闭了,true代表结束了

        glfwPollEvents();       //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数  键盘事件:glfwSetKeyCallback(window, key_callback);  key_callback即设定的回调函数
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);       //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
        //glClear(GL_COLOR_BUFFER_BIT);       //清空屏幕缓冲,这里是颜色缓冲
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        do_movement();
        //渲染指令
        //glBindTexture(GL_TEXTURE_2D, texture);
        glUseProgram(shaderProgram);


        glm::mat4 model;        //模型矩阵
        glm::mat4 view;         //观察矩阵
        glm::mat4 projection;   //投影矩阵-透视投影
        view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        projection = glm::perspective(aspect, (GLfloat)WIDTH / (GLfloat)HEIGHT, 0.1f, 100.0f);  //缩放效果

        GLint objectColorLoc = glGetUniformLocation(shaderProgram, "objectColor");
        GLint lightColorLoc = glGetUniformLocation(shaderProgram, "lightColor");
        glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
        glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色

        GLint modelLoc = glGetUniformLocation(shaderProgram, "model");
        GLint viewLoc = glGetUniformLocation(shaderProgram, "view");
        GLint projLoc = glGetUniformLocation(shaderProgram, "projection");
        GLint lightPosLoc = glGetUniformLocation(shaderProgram, "lightPos");

        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
        glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);

        //光源
        glUseProgram(lightShaderProgram);       //光源
        modelLoc = glGetUniformLocation(lightShaderProgram, "model");
        viewLoc = glGetUniformLocation(lightShaderProgram, "view");
        projLoc = glGetUniformLocation(lightShaderProgram, "projection");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

        glBindVertexArray(lightVAO);
        model = glm::translate(model, lightPos);
        model = glm::scale(model, glm::vec3(0.2f));
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);

        glfwSwapBuffers(window);  //交换颜色缓冲,用来绘制,输出显示在屏幕上
    }

    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);

    glfwTerminate();
    return 0;
}

/*
* 按键回调事件
* @param window 窗口
* @param key 按键
* @param scancode 扫描码
* @param action 表示这个按键是被按下还是释放
* @param mode 是否有Ctrl、Shift、Alt、Super等按钮的操作
*/
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) {

    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
        //esc键按下,关闭窗口
        glfwSetWindowShouldClose(window, GL_TRUE);
    }

    if (action == GLFW_PRESS)   //按下
        keys[key] = true;
    else if (action == GLFW_RELEASE)    //松开
        keys[key] = false;

}

void do_movement() {
    GLfloat currentFrame = glfwGetTime();
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;
    GLfloat cameraSpeed = 5.0f * deltaTime;    //速度

    if (keys[GLFW_KEY_W])
        cameraPos += cameraSpeed * cameraFront;
    if (keys[GLFW_KEY_S])
        cameraPos -= cameraSpeed * cameraFront;
    if (keys[GLFW_KEY_A])
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (keys[GLFW_KEY_D])
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (keys[GLFW_KEY_SPACE])       //向上
        cameraPos += cameraSpeed * cameraUp;
}

bool firstMouse = true;     //除了第一次鼠标移动外,其他不能移动
/*
* 鼠标回调事件
* @param window 窗口
* @param xpos 鼠标x坐标
* @param ypos 鼠标y坐标
*/
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    GLfloat xoffset = xpos - lastX;
    GLfloat yoffset = lastY - ypos;
    lastX = xpos;
    lastY = ypos;

    GLfloat sensitivity = 0.05;         //鼠标转向速度
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw += xoffset;
    pitch += yoffset;

    if (pitch > 89.0f)
        pitch = 89.0f;
    if (pitch < -89.0f)
        pitch = -89.0f;

    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

/*
* 鼠标滚轮回调事件
*/
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    if (aspect >= 1.0f && aspect <= 45.0f)
        aspect -= yoffset;
    if (aspect <= 1.0f)
        aspect = 1.0f;
    if (aspect >= 45.0f)
        aspect = 45.0f;
}

你可以看到使用了散射光照,立方体看起来就真的像个立方体了。尝试在你的脑中想象,通过移动正方体,法向量和光的方向向量之间的夹角增大,片段变得更暗。

最后一件事

现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里,我们都是在世界空间坐标中进行计算的,所以,我们不是应该把法向量转换为世界空间坐标吗?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。

首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,平移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要把模型矩阵左上角的3×3矩阵从模型矩阵中移除(所谓移除就是设置为0),它是模型矩阵的平移部分(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;同样可以移除平移)。对于法向量,我们只能对它应用缩放(Scale)和旋转(Rotation)变换。

其次,如果模型矩阵执行了不等比缩放,法向量就不再垂直于表面了,顶点就会以这种方式被改变了。因此,我们不能用这样的模型矩阵去乘以法向量。下面的图展示了应用了不等比缩放的矩阵对法向量的影响:
不等比缩放的矩阵对法向量的影响.png

无论何时当我们提交一个不等比缩放(注意:等比缩放不会破坏法线,因为法线的方向没被改变,而法线的长度很容易通过标准化进行修复),法向量就不会再垂直于它们的表面了,这样光照会被扭曲。

修复这个行为的诀窍是使用另一个为法向量专门定制的模型矩阵。这个矩阵称之为正规矩阵(Normal Matrix),它是进行了一点线性代数操作移除了对法向量的错误缩放效果。如果你想知道这个矩阵是如何计算出来的,我建议看这个文章。

正规矩阵被定义为“模型矩阵左上角的逆矩阵的转置矩阵”。真拗口,如果你不明白这是什么意思,别担心;我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。

在顶点着色器中,我们可以使用inverse和transpose函数自己生成正规矩阵,inverse和transpose函数对所有类型矩阵都有效。注意,我们也要把这个被处理过的矩阵强制转换为3×3矩阵,这是为了保证它失去了平移属性,之后它才能乘以法向量。

Normal = mat3(transpose(inverse(model))) * normal;

在环境光照部分,光照表现没问题,这是因为我们没有对物体本身执行任何缩放操作,因而不是非得使用正规矩阵不可,用模型矩阵乘以法线也没错。可是,如果你进行了不等比缩放,使用正规矩阵去乘以法向量就是必不可少的了。

对于着色器来说,逆矩阵也是一种开销比较大的操作,因此,无论何时,在着色器中只要可能就应该尽量避免逆操作,因为它们必须为你场景中的每个顶点进行这样的处理。以学习的目的这样做很好,但是对于一个对于效率有要求的应用来说,在绘制之前,你最好用CPU计算出正规矩阵,然后通过uniform把值传递给着色器(和模型矩阵一样)。

镜面反射

和环境光照一样,镜面光照(Specular Lighting) 同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:
镜面反射.png

我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。

观察向量是镜面光照的一个附加变量,我们可以使用观察者世界空间位置(Viewer’s World Space Position)和片段的位置来计算。之后,我们计算镜面光亮度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色。

我们选择在世界空间(World Space)进行光照计算,但是大多数人趋向于在观察空间(View Space)进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是,我发现出于学习的目的,在世界空间计算光照更符合直觉。如果你仍然希望在视野空间计算光照的话,那就使用观察矩阵应用到所有相关的需要变换的向量(不要忘记,也要改变正规矩阵)。

为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:

uniform vec3 viewPos;

GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos");
glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);

现在我们已经获得所有需要的变量,可以计算高光亮度了。首先,我们定义一个镜面强度(Specular Intensity)变量specularStrength,给镜面高光一个中等亮度颜色,这样就不会产生过度的影响了。

float specularStrength = 0.5f;

如果我们把它设置为1.0f,我们会得到一个对于珊瑚色立方体来说过度明亮的镜面亮度因子。下一节教程,我们会讨论所有这些光照亮度的合理设置,以及它们是如何影响物体的。下一步,我们计算视线方向坐标,和沿法线轴的对应的反射坐标:

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);

需要注意的是我们使用了lightDir向量的相反数。reflect函数要求的第一个是从光源指向片段位置的向量,但是lightDir当前是从片段指向光源的向量(由先前我们计算lightDir向量时,(减数和被减数)减法的顺序决定)。为了保证我们得到正确的reflect坐标,我们通过lightDir向量的相反数获得它的方向的反向(之前说过朝向和指向是相反的)。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。

剩下要做的是计算镜面亮度分量。下面的代码完成了这件事:

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。在下面的图片里,你会看到不同发光值对视觉(效果)的影响:
不同发光值对视觉效果的影响.png

我们不希望镜面成分过于显眼,所以我们把指数设置为32。剩下的最后一件事情是把它添加到环境光颜色和散射光颜色里,然后再乘以物体颜色:

vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);

效果
镜面反射效果.png

完整代码

#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>


//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 normal;\n"

"out vec3 Normal;\n"            //法向量
"out vec3 FragPos;\n"           //片段位置(如立方体六个面,每个面两个三角形,三角形形成的那个片段)

"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"   FragPos = vec3(model * vec4(position, 1.0f));\n"               //片段位置,将片段位置转化为世界坐标
"   Normal = normal;\n"
"}\n";

//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 Normal;\n"
"in vec3 FragPos;\n"

"out vec4 color;\n"

"uniform vec3 objectColor;\n"
"uniform vec3 lightColor;\n"
"uniform vec3 lightPos;\n"
"uniform vec3 viewPos;\n"           //摄像机位置

"void main()\n"
"{\n"
      //环境光
"     float ambientStrength = 0.1f;\n"          //环境因子
"     vec3 ambient = ambientStrength * lightColor;\n"

      //漫反射
"     vec3 norm = normalize(Normal);\n"         //法向量单位化
"     vec3 lightDir = normalize(lightPos - FragPos);\n"        //计算光的方向,即方向向量  指向光源方向lightPos, 朝向片段方向FragPos
"     float diff = max(dot(norm, lightDir), 0.0);\n"          //计算光对当前片段的实际散射影响
"     vec3 diffuse = diff * lightColor;\n"          //得到散射因子

      //镜面反射
"     float specularStrength = 0.5f;\n"          //镜面强度
"     vec3 viewDir = normalize(viewPos - FragPos);\n"     //指向摄像机viewPos,朝向FragPos
"     vec3 reflectDir = reflect(-lightDir, norm);\n"      //反射的方向
"     float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);\n"     
"     vec3 specular = specularStrength * spec * lightColor;\n"  //计算镜面分量

"     vec3 result = (ambient + diffuse + specular) * objectColor;\n"         //冯氏光照模型 = 环境光 + 漫反射 + 镜面反射 objectColor取决于需不需要反射物体得到颜色
"     color = vec4(result, 1.0f) ;\n"
"}\n";

//光源-顶点着色器
const char* lightVertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"


"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"

"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(position, 1.0f);\n"       //数学库矩阵转换
"}\n";

//光源-片段着色器
const char* lightFragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"

"void main()\n"
"{\n"
"     color = vec4(1.0f);\n"
"}\n";

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void do_movement();

const GLuint WIDTH = 800, HEIGHT = 600;

//摄像机信息
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);          //摄像机位置
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);       //摄像机朝向
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);           //上向量

GLfloat deltaTime = 0.0f;   //当前帧遇上一帧的时间差
GLfloat lastFrame = 0.0f;   //上一帧的时间

GLfloat lastX = WIDTH / 2, lastY = HEIGHT / 2;       //鼠标上一次位置,默认屏幕中心
GLfloat pitch = 0.0f;       //俯仰角
GLfloat yaw = -90.0f;       //偏航角
GLfloat aspect = 45.0f;     //视角大小
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

bool keys[1024];

int main()
{
    glfwInit();     //必须要将glfw初始化
    //告诉GLFW使用OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  //使用的是OpenGL核心模式
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);   //不允许调整窗口大小

    //创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW Window" << std::endl;

        glfwTerminate();    //销毁窗口与数据

        return -1;
    }

    glfwMakeContextCurrent(window);     //将OpenGL指向为当前窗口

    glewExperimental = GL_TRUE;     //用于告知GLEW使用现化OpenGL技术

    //glew初始化
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;

        return -1;
    }

    //视口
    int width = 800, height = 600;
    glfwGetFramebufferSize(window, &width, &height);        //设置OpenGL渲染窗口的尺寸
    glViewport(0, 0, width, height);    //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度
    glfwSetKeyCallback(window, key_callback);       //注册按键回调事件
    glfwSetCursorPosCallback(window, mouse_callback);       //注册鼠标回调事件
    //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);        //隐藏鼠标,并捕获他
    glfwSetScrollCallback(window, scroll_callback);                 //鼠标滚动回调事件

    glEnable(GL_DEPTH_TEST);            //深度缓存区

    //顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

    GLint vertexSuccess;
    GLchar vertexInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    GLint fragmentSuccess;
    GLchar fragmentInfoLog[512];

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, fragmentInfoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
    }


    //着色器链接程序
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    GLint programSuccess;
    GLchar programInfoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
    }

    //光源-顶点着色器
    GLuint lightVertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(lightVertexShader, 1, &lightVertexShaderSource, nullptr);
    glCompileShader(lightVertexShader);

    GLint lightVertexSuccess;
    GLchar lightVertexInfoLog[512];

    glGetShaderiv(lightVertexShader, GL_COMPILE_STATUS, &lightVertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(lightVertexShader, 512, nullptr, lightVertexInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::VERTEX::COMPILATION_FAILED\n" << lightVertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint lightFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(lightFragmentShader, 1, &lightFragmentShaderSource, nullptr);
    glCompileShader(lightFragmentShader);

    GLint lightFragmentSuccess;
    GLchar lightFragmentInfoLog[512];

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &lightFragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, lightFragmentInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::FRAGMENT::COMPILATION_FAILED\n" << lightFragmentInfoLog << std::endl;
    }


    //光源着色器链接程序
    GLuint lightShaderProgram = glCreateProgram();
    glAttachShader(lightShaderProgram, lightVertexShader);
    glAttachShader(lightShaderProgram, lightFragmentShader);
    glLinkProgram(lightShaderProgram);

    GLint lightProgramSuccess;
    GLchar lightProgramInfoLog[512];
    glGetProgramiv(lightShaderProgram, GL_LINK_STATUS, &lightProgramSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(lightShaderProgram, 512, nullptr, lightProgramInfoLog);
        std::cout << "ERROR::SHADER::LIGHT::PROGRAM::LINKING_FAILED\n" << lightProgramInfoLog << std::endl;
    }


    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
    };

    //顶点数据
    GLuint VBO, VAO;
    glGenBuffers(1, &VBO);      //顶点缓冲对象
    glGenVertexArrays(1, &VAO); //顶点数组对象

    glBindVertexArray(VAO);

    //顶点数据
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    //法向量
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    //光源
    GLuint lightVAO;
    glGenVertexArrays(1, &lightVAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBindVertexArray(lightVAO);

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    while (!glfwWindowShouldClose(window)) {
        //检查GLFW是否退出,即窗口是否关闭了,true代表结束了

        glfwPollEvents();       //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数  键盘事件:glfwSetKeyCallback(window, key_callback);  key_callback即设定的回调函数
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);       //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
        //glClear(GL_COLOR_BUFFER_BIT);       //清空屏幕缓冲,这里是颜色缓冲
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        do_movement();
        //渲染指令
        //glBindTexture(GL_TEXTURE_2D, texture);
        glUseProgram(shaderProgram);


        glm::mat4 model;        //模型矩阵
        glm::mat4 view;         //观察矩阵
        glm::mat4 projection;   //投影矩阵-透视投影
        view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        projection = glm::perspective(aspect, (GLfloat)WIDTH / (GLfloat)HEIGHT, 0.1f, 100.0f);  //缩放效果

        GLint objectColorLoc = glGetUniformLocation(shaderProgram, "objectColor");
        GLint lightColorLoc = glGetUniformLocation(shaderProgram, "lightColor");
        glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.31f);// 我们所熟悉的珊瑚红
        glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // 依旧把光源设置为白色

        GLint modelLoc = glGetUniformLocation(shaderProgram, "model");
        GLint viewLoc = glGetUniformLocation(shaderProgram, "view");
        GLint projLoc = glGetUniformLocation(shaderProgram, "projection");
        GLint lightPosLoc = glGetUniformLocation(shaderProgram, "lightPos");
        GLint viewPosLoc = glGetUniformLocation(shaderProgram, "viewPos");

        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
        glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
        glUniform3f(viewPosLoc, cameraPos.x, cameraPos.y, cameraPos.z);

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);

        //光源
        glUseProgram(lightShaderProgram);       //光源
        modelLoc = glGetUniformLocation(lightShaderProgram, "model");
        viewLoc = glGetUniformLocation(lightShaderProgram, "view");
        projLoc = glGetUniformLocation(lightShaderProgram, "projection");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

        glBindVertexArray(lightVAO);
        model = glm::translate(model, lightPos);
        model = glm::scale(model, glm::vec3(0.2f));
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);

        glfwSwapBuffers(window);  //交换颜色缓冲,用来绘制,输出显示在屏幕上
    }

    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);

    glfwTerminate();
    return 0;
}

/*
* 按键回调事件
* @param window 窗口
* @param key 按键
* @param scancode 扫描码
* @param action 表示这个按键是被按下还是释放
* @param mode 是否有Ctrl、Shift、Alt、Super等按钮的操作
*/
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) {

    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
        //esc键按下,关闭窗口
        glfwSetWindowShouldClose(window, GL_TRUE);
    }

    if (action == GLFW_PRESS)   //按下
        keys[key] = true;
    else if (action == GLFW_RELEASE)    //松开
        keys[key] = false;

}

void do_movement() {
    GLfloat currentFrame = glfwGetTime();
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;
    GLfloat cameraSpeed = 5.0f * deltaTime;    //速度

    if (keys[GLFW_KEY_W])
        cameraPos += cameraSpeed * cameraFront;
    if (keys[GLFW_KEY_S])
        cameraPos -= cameraSpeed * cameraFront;
    if (keys[GLFW_KEY_A])
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (keys[GLFW_KEY_D])
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (keys[GLFW_KEY_SPACE])       //向上
        cameraPos += cameraSpeed * cameraUp;
}

bool firstMouse = true;     //除了第一次鼠标移动外,其他不能移动
/*
* 鼠标回调事件
* @param window 窗口
* @param xpos 鼠标x坐标
* @param ypos 鼠标y坐标
*/
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    GLfloat xoffset = xpos - lastX;
    GLfloat yoffset = lastY - ypos;
    lastX = xpos;
    lastY = ypos;

    GLfloat sensitivity = 0.05;         //鼠标转向速度
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw += xoffset;
    pitch += yoffset;

    if (pitch > 89.0f)
        pitch = 89.0f;
    if (pitch < -89.0f)
        pitch = -89.0f;

    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

/*
* 鼠标滚轮回调事件
*/
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    if (aspect >= 1.0f && aspect <= 45.0f)
        aspect -= yoffset;
    if (aspect <= 1.0f)
        aspect = 1.0f;
    if (aspect >= 45.0f)
        aspect = 45.0f;
}

原教程

颜色原教程
光照基础原教程

该教程源码

git地址

分享到:
OpenGL笔记9-材质
OpenGL笔记7-摄像机
  • 文章目录
  • 站点概览
欢

网红 欢

你能抓到我么?

Email RSS
看爆 Top5
  • mac系统版本与Xcode版本有冲突 4,080次看爆
  • JAVA_HOME环境配置问题 3,730次看爆
  • AssetBundle使用 3,499次看爆
  • VSCode配置C++开发环境 3,257次看爆
  • Lua反射 3,133次看爆

Copyright © 2025 欢 粤ICP备2020105803号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图