简介:这是一个OpenGL绘制旋转六面体并纹理贴图的完整VS2019工程,面向计算机图形学初学者与对3D渲染感兴趣的C++开发者。项目通过六面体模型的构建、旋转矩阵变换和纹理映射,直观演示了OpenGL从顶点坐标设置、颜色指定、纹理坐标绑定到纹理对象创建与贴图的完整流程;六面体的每个面都可独立贴上不同图像,这一过程涵盖了位图读取、像素格式解析、纹理参数设定等内容,适合用来理解glRotatef、glTexImage2D等API的实际调用方式,也能从中体会GPU渲染管线和坐标变换的基本作用。压缩包约34.79MB,共36个文件,包含.cpp源码、.sln及.vcxproj工程文件、可直接运行的.exe、12张bmp纹理图片,以及pdb调试符号和obj、tlog等编译中间文件,目录结构清晰,便于区分代码、资源和构建产物,在Visual Studio 2019中打开即可编译运行。目前已有761人学习下载。除了基础绘制,工程中还涉及VS2019下的OpenGL库衔接配置、Glew/GLFW等辅助库的引入思路,对希望搭建自有OpenGL环境、解决链接报错或实现三维模型动态展示的开发者,这份可直接运行的工程提供了清晰的代码参考和配置模板,尤其适合课程设计、期末项目或自学练手使用。项目代码量适中,结构紧凑,适合作为入门图形学的第一个完整实例。
1. 拿到这个OpenGL旋转六面体纹理贴图的VS2019工程,先别急着关掉
很多人第一次看到"OpenGL绘制旋转六面体并纹理贴图.zip"这个文件,第一反应是网上又一份抄来抄去的作业。我自己以前也这么想,直到在VS2019里打开它,才发现这套工程把最容易被新手卡住的三件事——环境配置、索引绘制、纹理翻转——全给整理成了可运行的状态。如果你正在学图形学,或者课程设计需要交一个"有贴图、会旋转"的三维程序,这个资源能让你少踩整整两天的坑。
它解决的是这样的问题:你不用自己拼GLAD和GLFW的依赖,不用对着黑屏发愁,也不用在"为什么纹理是上下颠倒的"里浪费时间。适合刚配好环境还没写出第一个三角形的人,也适合想在作业前快速确认一套标准写法的从业者。下面我直接按拆包后的目录顺序,把每个文件干什么、每段代码怎么改,以及验证不通过时看哪里,一次讲清楚。
2. VS2019下的OpenGL环境配置:GLFW、GLAD和这个工程的文件布局
2.1 打开zip先做三件事:确认工程文件、检查依赖路径、看shader目录
解压之后,里面是一个标准的VS2019解决方案文件夹。第一件事是找到.sln后缀的解决方案文件,直接双击打开。第二件事是打开"解决方案资源管理器",确认里面有一个项目,项目下有src(或source)和shaders两个目录。第三件事是检查includes或external目录里是否已经有GLFW、glad、glm这三个头文件目录,以及lib目录里有没有glfw3.lib之类的静态库文件。
如果是课程作业里用了这个工程,你多半不需要重新下载依赖库,因为原作者已经编译好了。但如果你是自己新建项目,那就要小心:GLAD和GLFW的版本不匹配会导致链接错误。常见的现象是无法解析的外部符号 glfwInit,这种问题95%是因为你只加了include路径,没在"链接器->输入->附加依赖项"里填glfw3.lib和opengl32.lib。我一般会在所有代码运行前,先把这个附加依赖项拍个图存下来,免得后面配置链接库时翻车。
2.2 在VS2019里配置GLFW和GLAD的详细步骤
假设你要从零开始配一个同样能跑这个六面体工程的空项目,步骤如下:
- 创建空项目:选择 C++ 空项目,项目名称随意,比如
CubeDemo。 - 在项目目录下新建
vendors文件夹,把解压出来的glfw和glad两个文件夹拷贝进去。 - 打开项目属性 -> C/C++ -> 常规 -> 附加包含目录,填入:
vendors/glfw/includevendors/glad/include- 如果用到
glm,再加vendors/glm
- 在链接器 -> 常规 -> 附加库目录,填入
vendors/glfw/lib(具体看lib文件在哪)。 - 在链接器 -> 输入 -> 附加依赖项里写:
opengl32.lib glfw3.lib如果使用的是64位编译,且GLFW也编译成64位,那就不用加glfw3dll.lib。反过来,如果你把平台改成x86,而库是x64的,就会出现LNK2019无法解析外部符号,记住平台和库位数必须一致。
2.3 第一个测试窗口:用一段最少代码验证环境
配置好之后,不要急着写六面体,先写一个最小测试窗口代码。新建main.cpp,把下面这段粘进去:
#include <GLFW/glfw3.h> int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Test", NULL, NULL); if (window == NULL) { glfwTerminate(); return -1; } glfwMakeContextCurrent(window); while (!glfwWindowShouldClose(window)) { glfwSwapBuffers(window); glfwPollEvents(); } glfwDestroyWindow(window); glfwTerminate(); return 0; }这段代码的逻辑很直观:glfwInit初始化GLFW,glfwWindowHint告诉GLFW我们要创建OpenGL 3.3核心上下文,glfwCreateWindow创建窗口,glfwMakeContextCurrent把绘制上下文绑定到当前线程。进入循环后,glfwSwapBuffers把后缓冲区内容换到屏幕上,glfwPollEvents处理事件。
能弹出一个干净的空白窗口,说明GLFW环境正常。如果这一步就报错,先检查你的显卡驱动是否支持OpenGL 3.3以上版本,再检查GLAD是不是也初始化了。六面体工程里的gladLoadGL()调用一般放在glfwMakeContextCurrent之后,没有它会直接进入死循环黑屏。
3. 绘制旋转六面体的几何网格:顶点数据、VAO/VBO与索引绘制
3.1 六面体的顶点数据:为什么是36个顶点,而不是8个
一个立方体有8个顶点,但在OpenGL里绘制的六面体通常要 36 个顶点,因为每个面都需要自己的法线和纹理坐标。这个工程的顶点数组按位置(X,Y,Z) + 纹理坐标(U,V)五个float为一组,六个面的数据全部展开。
比如前面这个普通平面:
float vertices[] = { // positions // tex coords -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, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, };这里每个三角形都是逆时针排列,保证正面朝外。前三个float是顶点位置,后两个float是纹理坐标。注意立方体有六个面,所以这样的数据会有六段,每段两个三角形,共36个顶点。如果这个工程里索引数组(EBO)只有64个unsigned int数据,那你就要小心了——它可能用了24个顶点加36个索引的紧凑方式,两者绘制流程完全不同。
从可读性的角度,我更喜欢把36个顶点直接写在源文件里,这样作业答辩时能直接指着数据说"这是哪个面",不用绕一圈去推导索引。工程里如果给了EBO(索引缓冲对象),它的作用是减少重复顶点占用显存,但代价是理解和调试时心智负担重一点。
3.2 VAO与VBO:把顶点数据送进GPU的关键代码
无论是用36顶点还是24顶点+索引,核心代码都是差不多的GL调用。下面这段是标准的创建VAO和VBO的流程:
unsigned int VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 位置属性:从偏移量0开始,每5个float前3个是位置 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 纹理坐标属性:偏移3个float,取2个float glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);代码里glVertexAttribPointer的第5个参数5 * sizeof(float)是步长,告诉OpenGL每两个顶点之间隔了多少字节;第6个参数是偏移量,位置从0开始,纹理坐标从第3个float开始。这里最容易写错的是最后一个(void*)的强制转换,新手写0而不转成指针,编译会直接报错。
如果工程里用了索引绘制,后面还要加一段:
unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);注意EBO必须要在VAO绑定之后、解绑之前进行绑定,否则OpenGL会找不到索引数据。这个绑定顺序很多教程不强调,但实际运行时会表现为只画出一个三角形或直接黑屏。
3.3 着色器:把位置和纹理坐标变成颜色
六面体工程里肯定有两个shader文件:vertex.glsl和fragment.glsl。顶点着色器的核心逻辑是:
#version 330 core layout(location = 0) in vec3 aPos; layout(location = 1) in vec2 aTexCoord; out vec2 TexCoord; void main() { gl_Position = vec4(aPos, 1.0); TexCoord = aTexCoord; }片段着色器则采样纹理:
#version 330 core out vec4 FragColor; in vec2 TexCoord; uniform sampler2D tex; void main() { FragColor = texture(tex, TexCoord); }顶点着色器里layout(location = 0)对应glVertexAttribPointer的第一个参数0,这是硬编码的关联,不能写错。如果你在glVertexAttribPointer里把位置属性放在1,而shader里location=0,结果就是屏幕上一个纯色块闪烁。
4. 纹理贴图:stb_image加载与UV坐标的几个关键坑
4.1 为什么我的图片贴上去是上下颠倒的
六面体纹理贴图最常见的坑,就是图片上下颠倒。原因在于OpenGL的纹理坐标系原点在左下角,而常见的图片加载库(比如stb_image)默认数据第一行是最上面一行,即原点在左上角。直接传给glTexImage2D的话,纹理的V坐标就反了。
解决办法有三种:
- 在加载图片后调用
stbi_set_flip_vertically_on_load(true)。 - 在顶点数据里把V坐标改成
1.0 - uv_v。 - 在片段着色器里用
texture(tex, vec2(TexCoord.x, 1.0 - TexCoord.y))。
工程里通常用的是第1种,因为最省事。我建议你先用第1种跑通,再改成第2种看看效果,这样能真正理解坐标系的变换。
4.2 使用stb_image加载图片生成纹理
这段是这个工程的纹理生成标准代码:
#include "stb_image.h" unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); int width, height, nrChannels; stbi_set_flip_vertically_on_load(true); unsigned char* data = stbi_load("textures/container.jpg", &width, &height, &nrChannels, 0); if (data) { GLenum format; if (nrChannels == 3) format = GL_RGB; else if (nrChannels == 4) format = GL_RGBA; glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { // 打印错误信息,不要继续执行 } stbi_image_free(data);第四步之前,里第三步的GL_LINEAR是线性过滤,让放大后的纹理不那么生硬。GL_REPEAT是超出 [0,1] 范围时重复纹理,如果你的UV坐标稍微超出边界(比如模型分享导致),这个设置能避免黑边。
这里有个容易被忽略的format判断:如果图片是JPG(3通道),你用GL_RGBA传给glTexImage2D,纹理会花屏,或者出现透明但颜色发紫的情况。判断nrChannels是必须的,不要写死。
4.3 纹理单元:为什么片段着色器里要glUniform1i
当程序里只有一张纹理时,即使你不绑定纹理单元也能画出来。但到了六面体工程,如果你后续想加第二张贴图,或者想做多纹理混合,就必须理解纹理单元。在绘制循环里,这段代码:
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glUseProgram(shaderProgram); glUniform1i(glGetUniformLocation(shaderProgram, "tex"), 0);意思是把texture绑定到纹理单元0,然后把shader里的tex采样器设置为0。如果你省略glUniform1i,很多显卡会因为采样器默认值为0而碰巧工作,但一旦你绑定到GL_TEXTURE1就立刻画成黑色。我习惯在初始化阶段就显式设一次,避免玄学问题。
5. 避坑排查:黑屏、拉伸、花屏、纹理反转的4个真实案例
5.1 黑屏:最后的方块没画出来或者画了但看不到
现象:窗口正常打开,但没有多边形出现,背景还是默认的黑色。
原因有两种,一种是VAO/VBO中的数据没有正确绑定,另一种是着色器编译失败。最直接的排查方式是调用glGetError(),在绘制循环前加一行:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (glGetError() != GL_NO_ERROR) { // 打印错误码和说明 }解决:先在绘制的glDrawArrays前加glBindVertexArray(VAO),如果还黑屏就把shader源码打印出来,检查片段着色器是不是写了FragColor = texture(tex, TexCoord)但没声明uniform sampler2D tex。典型的翻车是你在片段着色器写死了一个颜色,比如FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);然后忘了改回来。
5.2 模型被压扁了:投影矩阵没写
现象:你能看到立方体的轮廓,但它是扁的,像一块木板,而不是一个有立体感的正方体。
原因:你把顶点坐标直接传给了gl_Position,没有乘以透视投影矩阵。在透视投影下,离相机远的物体应该变小。如果仅仅用了模型矩阵做旋转,没有投影矩阵,OpenGL的裁剪坐标范围是 [-1,1],所有顶点都按相同比例直接被压扁。
解决:用glm库构造透视矩阵。六面体工程里一般会这么写:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);然后在顶点着色器里加一个uniform mat4 uProj,把位置乘上去。
5.3 纹理花屏:像素偏移和行对齐导致的错乱
现象:贴图以后,画面像是被撕碎,每一行有横向错位,颜色也不对。
原因:这是经典的glPixelStorei(GL_UNPACK_ALIGNMENT, 1)没设置。OpenGL默认假设每行像素是4字节对齐的,而很多图片宽度不是4的倍数(比如100像素宽,3通道,每行300字节)。如果不设置,CPU读取图片数据时跨行会错位,传到GPU后就花屏。
解决:在glTexImage2D之前,加一行:
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);习惯上,我每次用stb_image加载完数据后都会强制调用这一句,不上花屏。注意,调用之后如果又加载了RGBA格式,记得把对齐改回4,否则有些显卡会忽略你的设置直接崩。
5.4 纹理反转:正面看起来像镜子反射
现象:六面体各个面的纹理图案是左右颠倒的,或者上下颠倒。
原因:我在第4章提到的V坐标方向问题,也可能是你在顶点数组里把U,V顺序写反了,导致纹理被镜像。
解决:先用最简单的单面测试,画一个三角形贴一张有文字的图片,比如左边一半红、右边一半蓝。如果颜色位置反了,就说明是顶点UV顺序问题;如果上下颠倒,就检查stbi_set_flip_vertically_on_load是否开启。这个判断只需一分钟,能节省大量时间。
6. 进阶:用glm让六面体持续旋转,并验证深度缓冲
6.1 旋转矩阵与绘制循环
六面体工程的核心卖点是"旋转"。这块代码通常在渲染循环里修改模型矩阵:
glm::mat4 model = glm::mat4(1.0f); model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));这里glfwGetTime()返回程序启动后的秒数,乘以radians(50)相当于每秒转50度。旋转轴(0.5, 1.0, 0.0)是一个斜向量,让立方体看起来转得更自然。如果你改成(0.0, 1.0, 0.0),就是严格的Y轴旋转,效果反而单调一些。
然后在顶点着色器里把模型矩阵加上:
uniform mat4 uModel; void main() { gl_Position = uProj * uModel * vec4(aPos, 1.0); }注意顺序是uProj * uModel * vertex,不能反过来。在GLSL里矩阵乘法是右乘,先应用模型变换到世界空间,再投影到裁剪空间。如果你写成uModel * uProj * vertex,物体不会围绕自己的中心旋转,而是绕着屏幕中心做奇怪的平移旋转。
6.2 深度缓冲:不开启会看到透明的六面体后面
旋转过程中,如果不开启深度测试,立方体背面的三角形会直接覆盖前面。因为在默认情况下,OpenGL按绘制顺序覆盖颜色。六面体的六个面是同一批次画出来的,顺序固定,当旋转到某个角度时,后面的面反而画在前面的面上,画面会闪烁。
解决方法是初始化时启用深度测试:
glEnable(GL_DEPTH_TEST);并且在每次绘制前清空深度缓冲:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);如果你看到的六面体表面像玻璃一样透视,不是你的纹理问题,九成就是忘了这两行。这个坑我当年在交作业前也踩过一次,后来每次写新工程,第一句就先写上glEnable(GL_DEPTH_TEST),成了我的肌肉记忆。
6.3 验证旋转和贴图是否正确的几条路径
跑起来之后,怎么确认是"正确的旋转"而不是"随机抖动"?我的习惯是三看:
一看六个面是否都有纹理,且纹理图案完整。二看旋转是否连续均匀,每秒大约转50度,可以用手机秒表粗略计时。三看转动过程中边缘是否抖动,如果抖,多半是没开垂直同步,在循环里加glfwSwapInterval(1)来限制帧率。
从拿到这个VS2019工程到跑通,我最快一次用了20分钟,最慢一次折腾了三个小时,原因是图片路径写错。工程里加载纹理的路径一般是textures/xxx.jpg,而工作目录默认是项目文件目录,不是解决方案目录。如果你发现纹理加载失败(控制台没报错但贴图黑),检查一下是否把图片放在exe所在目录了。从那以后我每次写纹理加载,都先打印一下当前工作目录,再拼图片路径,避免这种低级问题浪费人生。
如果你也想快速复现一个没有环境坑的旋转六面体,这份工程里已经把GLFW、GLAD、glm和所有shader都整理好了,下载后按我这个流程走一遍,应该不会卡太久。希望帮到你。
本文还有配套的精品资源,点击获取