本篇文章是对源代码的逐行解读,如果是初学者,本博文将带您一步一步理解OpenGL制作3D渲染的步骤。
创作不易,希望大家多多点赞,支持。
[!IMPORTANT]
源代码地址:一个实现3D旋转的财神爷贴图盒子的源码资源-优快云文库
内容包括:
主文件:rotatingFortuneGod.cpp
头文件:shader_m.h,stb_image.h
资源文件:
6.2.coordinate_systems.fs(片段着色器)
6.2.coordinate_systems.vs(顶点着色器)
container.jpg以及flyingmoney.png
代码实现的目标
如下:
一个旋转的盒子上面贴着财神的纹理。
源代码的解析
主文件
先说一下rotatingFortuneGod.cpp文件中的内容
首先是头文件部分:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "shader_m.h"
#include <iostream>
这里有几个重要的点
-
glad头文件要放在glfw3头文件的前面。
-
使用stb_image.h头文件之前要先定义参数STB_IMAGE_IMPLEMENTATION。stb_image.h头文件中含有大量处理图像的内容,几乎所有的图像格式,它都可以处理。但是在使用之前,必须加上语句“#define STB_IMAGE_IMPLEMENTATION”,否则编译会报错。这个库的详细介绍可以参看我的博文:从零开始写3D游戏引擎(开发环境VS2022+OpenGL)之七 如何让自己的图形翻转平移旋转,包含代码与解释的保姆包教会系列-优快云博客
-
glm库文件是适合于OpenGL的数学库,在3D渲染中,需要调用到这些数学库文件。
-
shader_m.h头文件里面含有处理各种着色器的内容,具体的内容将在本文后半部分予以解释。
-
iostream头文件必须放在最后,否则编译报错。
接下来是两组函数声明:
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
这两个都类似于回调函数,前者是处理窗口变化的函数,后者则是处理键盘和鼠标消息的函数。
然后则是一些屏幕参数的设置:
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
这些都比较简单,下面就进入到了main函数:
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库,主要告诉GPU我们使用的是GLFW_OPENGL_CORE_PROFILE。
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
这部份是用于MAC上编译的,如果没有这段代码,编译会报错。
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, u8"!!!优快云金沙阳的博客欢迎你的光临,所有源码与解释都可以在这里找到https://blog.youkuaiyun.com/u012648507!!!", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
下面就是初始化窗口了。
熟悉windows编程的同学肯定知道,窗口在渲染出来之前,首先要创建,同时还要有变量将这个窗口的句柄保存下来。
这几段代码的作用就是这样。
需要注意的是glfwMakeContextCurrent函数,这个作用就是将窗口句柄激活,这样就可以让glfw库函数对这个句柄进行操作了。
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
接下来做的事情,就是要载入glad所有的函数指针了。只有这样,编译才能识别出所有的OpenGL函数。
// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);
上面的这段代码相当重要。起到的作用就是告诉OpenGL对整个图像做3D透视处理。
// build and compile our shader zprogram
// ------------------------------------
Shader ourShader("6.2.coordinate_systems.vs", "6.2.coordinate_systems.fs");
上面做的事情,就是要编译顶点着色器和片段着色器。这两个着色器文件都以单独文件的形式存在。Shader是我们实现编写好的类。这样做的目的就是能够让我们方便的处理着色器。
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float 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
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
这一部分,首先是给出36个顶点的参数,然后将他们的属性统统绑定。
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// texture coord attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
然后是给出位置和纹理的位移,这一部分将有助于数据传递到GPU。
// load and create a texture
// -------------------------
unsigned int texture1, texture2;
// texture 1
// ---------
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.
unsigned char* data = stbi_load("caisheng.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
首先是要有寄存纹理文件的地址变量texture1。
然后指定这个纹理变量的属性,此处用了GL_TEXTURE_2D这个参数。然后就是指定处理纹理的方式,纹理环绕的参数,纹理过滤的参数。
接下来,这部分就是载入纹理文件了。使用的依旧是stb_image.h头文件中的stbi_load函数。
然后就是按照指定方法,将2D图片上的数据映射成纹理,并产生Mipmaps 这些将用于图形的纹理。
ourShader.use();
ourShader.setInt("texture1", 0);
接下来就是做着色器的编译。
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.1f, 0.0f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // also clear the depth buffer now!
// bind textures on corresponding texture units
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
//glActiveTexture(GL_TEXTURE1);
//glBindTexture(GL_TEXTURE_2D, texture2);
// activate shader
ourShader.use();
// create transformations
glm::mat4 model = glm::mat4(1.0f); // make sure to initialize matrix to identity matrix first
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 projection = glm::mat4(1.0f);
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.5f, 1.0f, 0.0f));
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
// retrieve the matrix uniform locations
unsigned int modelLoc = glGetUniformLocation(ourShader.ID, "model");
unsigned int viewLoc = glGetUniformLocation(ourShader.ID, "view");
// pass them to the shaders (3 different ways)
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, &view[0][0]);
// note: currently we set the projection matrix each frame, but since the projection matrix rarely changes it's often best practice to set it outside the main loop only once.
ourShader.setMat4("projection", projection);