尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VS2019 OpenGL旋转六面体纹理贴图工程详解与避坑指南

VS2019 OpenGL旋转六面体纹理贴图工程详解与避坑指南 简介这是一个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初始化GLFWglfwWindowHint告诉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里location0结果就是屏幕上一个纯色块闪烁。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判断如果图片是JPG3通道你用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都整理好了下载后按我这个流程走一遍应该不会卡太久。希望帮到你。本文还有配套的精品资源点击获取
返回列表