写这篇博客的时候我还在我老家的床上,给自己放了两三天的假,决定写点东西。
在经历了visual studio的链接失败和着色器编译失败之后
深感自己力量渺小,无法棒打vs设计师(胡言乱语)
再加上之前学的有些快而不扎实,复制源码跑动了看一遍就粗略带过(有时甚至跑不动)
所以写这篇救命博客来挽救一下自己岌岌可危的大脑。
首先先祝贺自己拿到了魔方的暑期实习,已经踏上正轨了但还需努力,毕竟能不能转正,还得看今后发展的如何。但我其实更想从事引擎方面的工作。
OpenGL的教程是鼎鼎大名的learnOpenGL,中文站做了很棒的翻译工作,强烈安利。
代码中涉及到封装的函数,类,因为并不重要所以就不予展示。
更多的是自己温习一个完整OGL渲染程序的不同模块。
首先让我打开vs找找我的源码,哦对这样可能废话有点多
但我喜欢这样诙谐的语气记录自己想法,毕竟最后看到这个博客的可能只有我一个人
总而言之,让我们先看看如何创建一个窗口吧。
创建窗口部分
先丢源码,使用的是我们的寄了废物库和glad库,glad库能让我们更方便的在运行时确定函数地址
这些代码可以让我们创建一个窗口
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
int main()
{
glfwInit();//初始化
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//副版本号
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心渲染模式
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Render Window", NULL, NULL);//new一个窗口对象
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
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))
//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
....
//渲染循环
while(!glfwWindowShouldClose(window))
{
....
processInput(window);//处理输入逻辑
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);//设置清除缓冲的颜色
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);用这个颜色来清除缓冲,顺便也清除深处缓冲
....
glfwSwapBuffers(window);//交换帧缓冲
glfwPollEvents();//检查并处理各个事件
}
glfwTerminate();
return 0;
}
通过这些代码,应该可以直接创建出一个渲染窗口。
突然间有点不知道怎么组织复述各个模块了哈哈
顶点数据
这里涉及到VAO,VBO,EBO(这里没用到)的概念
详细了解这三个的概念可以看详解Opengl中VBO和VAO_代码乐的博客-CSDN博客_opengl vao
一言蔽之,vbo存数据,vao存配置信息,一般先绑vao,然后再把其他东西往上绑
vao也会把存入的vbo记上,用的时候用vao就行,东西都设置完了最后解绑vao。
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, cubeVAO;//生成这俩ID,其中的这个vao是用来管理我们渲染的箱子的
glGenVertexArrays(1, &cubeVAO);//用这个ID生成顶点数组,反正vao就用的这玩意
glGenBuffers(1, &VBO);//用这个ID生成缓冲,是缓冲哦,vbo用这种缓冲,待会其他缓冲也得用这个
glBindBuffer(GL_ARRAY_BUFFER, VBO);//把这份缓冲绑定到显存的那部分真正的缓冲上
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//往显存上开始写入顶点数据
glBindVertexArray(cubeVAO);//先把vao绑上,之后配置的各种数据vao都会记录下来
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 lightCubeVAO;//这是另一个vao哦,这个vao用来管理我们的光源
glGenVertexArrays(1, &lightCubeVAO);//一样的用ID生成
glBindVertexArray(lightCubeVAO);//一样的绑定vao
glBindBuffer(GL_ARRAY_BUFFER, VBO);//一样的绑定缓存,之前把顶点数据写入过一次这次就不写了
// note that we update the lamp's position attribute's stride to reflect the updated buffer data
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//指定着色器怎么解析顶点数据
....
while (!glfwWindowShouldClose(window))
{
...
lightingShader.use();
//使用被渲染的箱子的着色器程序
...
//渲染箱子
glBindVertexArray(cubeVAO);//使用这套vao来渲染,vao里面包括了设置好的vbo和配置信息
glDrawArrays(GL_TRIANGLES, 0, 36);//画36个顶点,以画三角形的方式
...
//使用光源的着色器程序
lightCubeShader.use();
...
//渲染光源
glBindVertexArray(lightCubeVAO);//同上,不过这个是光源
glDrawArrays(GL_TRIANGLES, 0, 36);
...
}
....
//渲染完成后删除这些数据释放资源
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightCubeVAO);
glDeleteBuffers(1, &VBO);
glGenBuffers和glGenVertexArrays的作用有点像我们new一份空间,然后就用我们指定的指针来管理这份空间一样。
然后我们再把这份生成的缓存绑定到显存的缓存上,往这份缓存写数据就是往显存写数据。
VertexArrays就直接绑着就完事了,不用管他,他会自动记录我们做过的事情。
你应该记得我们的顶点数据一行有八个数据,他们有的是顶点,有的是法线,有的是纹理坐标。
接着我们是怎么指定着色器来解析数据的呢,还记得这些代码吧
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);
接着让我们看看我们的顶点着色器是怎么导入数据的吧。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
我们指定了三个不同的位置来导入我们的顶点数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
所做的就是指定顶点着色器的0号位置,3个值,浮点数,一份顶点数据有8个值,我们从偏移量0的开始算
glEnableVertexAttribArray(0);
启用0号顶点数据
然后我们两个vao分别是所渲染箱子和光源的数据,他们都用的同一套顶点数据(都是正方体)
但第二个vao只要顶点数据就够了。我们对于两个vao使用不同的shader程序来渲染。
导入纹理
导入纹理这块我们用了一个函数来封装,包括怎么从文件读取,设置什么样的环绕方式和过滤方式。
stb_image.h是一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到工程之中。
#define STB_IMAGE_IMPLEMENTATION//没加这个宏直接把我害死,编译就一个劲链接错误
#include "stb_image.h" //这个头文件
glEnable(GL_DEPTH_TEST);//既然提到纹理就记得开启深度测试,要不然会很哈人
unsigned int diffuseMap = loadTexture("container2.png");
unsigned int loadTexture(char const* path)
{
unsigned int textureID; //定义一个纹理的ID
glGenTextures(1, &textureID);//生成一个纹理数据,就像上边顶点数据一样
int width, height, nrComponents;//长,宽,颜色通道个数
unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;//根据颜色通道个数选择纹理格式
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);//在显存上绑定纹理
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
//第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
//第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
//第三个参数告诉OpenGL我们希望把纹理储存为何种格式。
//第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
//下个参数应该总是被设为0(历史遗留的问题)。
//第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。最后一个参数是真正的图像数据。
glGenerateMipmap(GL_TEXTURE_2D);//自动生成mipmap
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//对于横轴和纵轴采取GL_REPEAT的环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//设置过滤方式,这里我们都设为线性滤波
//一个常见的错误是,将放大过滤的选项设置为mipmap选项之一。这样没有任何效果,因为mipmap主要是使用在纹理被缩小的情况下的
stbi_image_free(data);//设置完这块纹理后我们就释放这部分数据
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;//最后我们返回一个纹理ID供我们使用
}
while (!glfwWindowShouldClose(window))
{
...
lightingShader.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
//记得在调用glDrawElements之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器:
...
}
前边部分注释写在代码里没什么好说的,让我们看看片元着色器的导入
#version 330 core
out vec4 FragColor;
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
uniform Material material;
...
int main
{
...
}
你可能会奇怪为什么sampler2D
变量是个uniform,我们却不用glUniform给它赋值。
使用glUniform,我们可以给纹理采样器分配一个位置值,一个纹理的位置值通常称为一个纹理单元(Texture Unit)。
一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
只要我们首先激活对应的纹理单元,不用glUniform我们也能用glBindTexture(GL_TEXTURE_2D, texture);
将纹理传入着色器
如果有两个纹理的话,就是这样:
(这时我们就得手动设置着色器的哪个texture对应源程序代码段的哪个texture)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
...
ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置
while(...)
{
[...]
}
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
因为OpenGL要求y轴0.0
坐标是在图片的底部的,但是图片的y轴0.0
坐标通常在顶部。
所以我们还能使用stbi_set_flip_vertically_on_load(true);
翻转图片。
MVP变换
还记得顶点着色器的职责吧,我们把三维的顶点数据转化成他们在屏幕上的坐标。
所以MVP变换一般由顶点着色器做运算。
我们的思路是用glm生成矩阵,然后用uniform传入着色器
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
...
int main
{
// 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 = glm::mat4(1.0f);//模型变换矩阵
lightingShader.setMat4("model", model);
}
我们这里只展示了被渲染箱子的着色部分,不过这样就够了。
实际上我们渲染的另一个物体是个亮方块,他虽然是扮演了光源但实际上并没有起到太阳的作用
光线的数据依然是我们手动导入的,其他很多值都是我们规定的,
但把他以白色渲染出来可以更加直观的观察到光照是如何影响物体的
让我们复习一下mvp变换做的事情吧(肯定会有人说哎呀烦死了)
模型变换(model)把顶点的坐标从局部坐标系转换到世界坐标。
视点变换(view)世界坐标 >>> 观察空间
投影变换(projection)观察空间>>>剪裁空间
观察空间经常被人们称之OpenGL的摄像机(Camera),所以我们这里用camera类生成view矩阵,但不深究
来模拟摄像机的效果,利用wasd和鼠标可以随意遨游在我们构建的世界。
做完投影变换就直接到剪裁空间了,OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标
在顶点着色器则里边是这样的
#version 330 core
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
...
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
...
gl_Position = projection * view * vec4(FragPos, 1.0);
}
如果不讨论怎么法线和纹理的计算,顶点着色器所做的内容就是将传入的顶点数据进行mvp变换
ps:我们这里还需要输出世界坐标系的顶点坐标进行后续运算,所以将计算分开了。
phong模型计算(着色器详解)
重点介绍一下渲染循环中phong模型中的几个分量。
lightingShader是我们箱子的着色器程序,或许他叫box shader会更合适些
while (!glfwWindowShouldClose(window))
{
...
lightingShader.use();
lightingShader.setVec3("light.position", lightPos);
lightingShader.setVec3("viewPos", camera.Position);
// light properties
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);
// material properties
lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);
lightingShader.setFloat("material.shininess", 64.0f);
}
如果对冯模型有了解的话(下次我自己看的时候估计会觉得是废话23333)
会知道在shader里面的计算是把环境光(ambient)漫反射(diffuse)高光(specular)的贡献相加
三个部分的计算便是由由我们传入的uniform值来参与的。
还记得片元着色器的职责是决定一个片元的颜色吧,这次我们展示一个完整的片元着色器:
#version 330 core
out vec4 FragColor;
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform Material material;
uniform Light light;
void main()
{
// ambient
vec3 ambient = light.ambient * texture(material.diffuse, TexCoords).rgb;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, TexCoords).rgb;
// specular
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 * material.specular);
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
环境光部分,我们这里就暂且认为环境光是我们的物体颜色,毕竟我们现在也做不出什么全局光照。
所以这里我们直接拿纹理和微弱的光照分量相乘就行
漫反射光照能对物体产生显著的视觉影响,或者说物体呈现出什么样子就是漫反射决定的。
计算漫反射需要法线和入射光线,入射光线是我们由上阶段的顶点着色器输出的顶点坐标(世界坐标系)和光照计算而来的
这里的法线则是很有意思的地方,本身我们渲染的并不是一个箱子的模型,而是一个个手填的顶点
不存在什么表面更不存在什么法线,所以法线我们也是手填的(233333)
最后乘上漫反射系数,这决定了这部分的贡献是如何的。
所以尽管计算部分是参照了兰伯特光照模型,但我们在背光处也能看到物体的纹理,这便是由不真实的法线造成的。
高光部分会复杂些,需要观察向量,反射方向,光泽度。
观察方向由我们填入的camera.position和顶点计算得出。
反射方向的计算是一个耗时的计算,若改成计算半程向量(half vector)便能加速此部分的计算,也能得到不错的效果(布林冯)
光泽度则影响高光亮点的大小。在公式里面扮演指数的部分,光泽度越高亮点就越小。
最后我们会乘上高光反射系数,这里填的(1.0,1.0,1.0),因为我们理所当然觉得高光很明显。
如此一来便完成了phong模型的着色部分的计算,我们便能渲出我们可爱的小箱箱了。
完成这篇水博客之后感觉精神好了很多,不像之前那样一看到vs就烦躁了,学习的欲望也重新被激起了
今天没什么干劲了,明天再努力吧.jpg