ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenGL旋转六面体纹理贴图实战:从GLFW到MVP矩阵完全指南

OpenGL旋转六面体纹理贴图实战:从GLFW到MVP矩阵完全指南

简介:一份基于VS2019开发的OpenGL三维图形渲染练习工程,围绕旋转六面体与纹理贴图展开,适合计算机图形学初学者、OpenGL入门者及需要完成课程设计的高校学生。工程以C++源码调用OpenGL接口,完整演示六面体模型构建、glRotatef矩阵旋转变换、glTexImage2D纹理加载与映射流程,可帮助理解三维物体从建模到动态渲染的关键环节。压缩包共36个文件,以bmp纹理图片、cpp源码、vcxproj/sln工程配置、exe可执行程序和pdb调试信息为主要组成,整体约34.79MB,解压后可直接用VS2019打开运行。数据目录中存放12张位图,每个面可独立贴图,源码结构简洁,便于修改旋转速度或替换纹理后即时观察效果。已有761人学习下载,对希望掌握OpenGL纹理映射、坐标变换及VS工程配置的开发者具有直接参考价值。

1. 旋转六面体加纹理贴图:一个过了这关才算入门OpenGL的Demo

先想清楚:为什么选“绘制旋转六面体并纹理贴图”而不是“画个三角形”?画三角形只覆盖顶点缓冲和着色器,旋转六面体把矩阵变换、深度测试、索引缓冲、纹理坐标全拉进来了。这类项目是图形学入门标准验收项:单位立方体、绕轴旋转、六个面贴图,同时也是很多人用OpenGL写的第一个“看起来像游戏”的程序。适合刚学完shader和缓冲、准备做小demo的开发者,也适合想快速唤起OpenGL记忆的从业者。照着骨架、几何、旋转、纹理、排错五条线走完,你会在本地复现出可旋转、可贴图的六面体,并且知道你调的每个参数在改什么。

2. 先搭OpenGL工程骨架:GLFW+GLAD、shader编译与主循环

2.1 为什么选GLFW+GLAD而不是FreeGLUT:环境配置与交叉编译

老教程里常见FreeGLUT和GLUT,配合VS2010的OpenGL环境配置,代码里全是glBegin/glEnd,固定管线一把梭。这类代码短,但和现代OpenGL完全是两个世界。现在常见做法是GLFW负责窗口和输入,GLAD负责加载OpenGL函数指针。理由很直接:GLFW跨平台、回调机制简单;GLAD能让你在运行时拿到glCreateShader、glDrawElements这些函数地址,不用在不同驱动上手工声明一堆函数指针。

如果你在嵌入式板子或Linux服务器上做OpenGL交叉编译,这个组合依然成立,只是链接时要加X11相关库,具体是-lX11 -lpthread。环境配置阶段最忌一上来贴一堆预处理指令。我的习惯是,先写一个最小窗口程序,窗口能开出来、背景色能改,再往里面加shader和缓冲。这样OpenGL环境配置问题会最先暴露,后面绘图代码才不会被环境干扰。

最小初始化骨架长这样:

#include <glad/glad.h> #include <GLFW/glfw3.h> #include <cstdio> int main() { glfwSetErrorCallback([](int code, const char* desc) { fprintf(stderr, "glfw error %d: %s\n", code, desc); }); if (!glfwInit()) return -1; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_SAMPLES, 4); // 4倍MSAA GLFWwindow* win = glfwCreateWindow(800, 600, "opengl rotating cube", NULL, NULL); if (!win) { glfwTerminate(); return -1; } glfwMakeContextCurrent(win); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { fprintf(stderr, "glad load failed\n"); return -1; } glfwSwapInterval(1); // 开启垂直同步 while (!glfwWindowShouldClose(win)) { glClearColor(0.12f, 0.12f, 0.16f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glfwSwapBuffers(win); glfwPollEvents(); } glfwTerminate(); return 0; }

代码先把OpenGL 3.3 Core Profile固定下来,GLFW_SAMPLES设为4,开4倍MSAA,后面旋转六面体的边缘会平滑不少。glfwSwapInterval(1)是垂直同步,把帧率锁到显示器刷新率,动画才不撕裂。glfwSetErrorCallback从开局就挂上,窗口创建失败的真实原因经常藏在回调里,比只看返回值具体得多。Core Profile下不调用gladLoadGLLoader直接glClear,等于解引用空函数指针,基本崩在启动。交叉编译时还要确认GLFW和GLAD的库路径传给了链接器,编译通过但链接失败是常事。能开出一个窗口并看到深色清屏,环境这关才算过。

2.2 shader编译与Program链接,错误日志要一次打全

OpenGL 3.3之后没有glBegin,绘制必须走shader。顶点着色器处理位置和纹理坐标,片段着色器负责采样纹理、输出颜色。编译shader是固定三步:glCreateShader创建对象、glShaderSource喂源码、glCompileShader执行编译,然后查状态。很多人图省事不查错误,结果屏幕黑了靠猜,时间全耗在这。我习惯把编译和链接封装成一个函数,出错时把日志整个打到控制台:

GLuint buildShader(const char* vsSrc, const char* fsSrc) { GLuint vs = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vs, 1, &vsSrc, NULL); glCompileShader(vs); GLint ok = 0; glGetShaderiv(vs, GL_COMPILE_STATUS, &ok); if (!ok) { char log[1024]; glGetShaderInfoLog(vs, 1024, NULL, log); fprintf(stderr, "vertex shader error:\n%s\n", log); } GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fs, 1, &fsSrc, NULL); glCompileShader(fs); glGetShaderiv(fs, GL_COMPILE_STATUS, &ok); if (!ok) { char log[1024]; glGetShaderInfoLog(fs, 1024, NULL, log); fprintf(stderr, "fragment shader error:\n%s\n", log); } GLuint prog = glCreateProgram(); glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog); glGetProgramiv(prog, GL_LINK_STATUS, &ok); if (!ok) { char log[1024]; glGetProgramInfoLog(prog, 1024, NULL, log); fprintf(stderr, "program link error:\n%s\n", log); } glDeleteShader(vs); glDeleteShader(fs); return prog; }

参数说明:日志缓冲1024字节对这个小demo足够,如果shader被宏或长注释撑大,日志会被截断,行号看不清。建议直接改成std::vector 动态长度。GLSL编译错误会输出行列,比如ERROR: 0:5: 'gl_Vertex' : undeclared identifier,看到先检查#version声明和attribute拼写。两个高频错误:一是忘了写#version 330 core,驱动按旧方言解析,in/out全报错;二是顶点着色器写了out vec2 vTexCoord,片段着色器的in vec2 vTexCoord类型、名字必须完全一致,否则链接期匹配不上。纹理贴图项目里,vTexCoord丢失会让采样坐标未初始化,贴图变成噪点。

2.3 主循环、双缓冲与垂直同步

主循环本质是三个动作:处理事件、更新状态、绘制。旋转六面体里,更新状态就是累加旋转角度;绘制是绑定VAO、更新uniform、glDrawElements。glfwSwapBuffers交换前后缓冲,glfwPollEvents处理窗口和键盘事件。OpenGL窗口默认双缓冲,只画不换,画面不会上屏,所以主循环结构不能乱砍。垂直同步在初始化设一次,glfwSwapInterval(1)开启,0关闭。关闭vsync后帧率可能冲到几百,旋转速度如果按帧累加,六面体转速跟着帧率飘。因此动画要用时间增量算,每帧用glfwGetTime做差得到deltaTime,刷新率怎么变,转速不变。上一帧的时间和这一帧的时间一减,就是当前帧耗时,矩阵和键盘逻辑都拿它当时间基准。旋转六面体的顺滑感,一半来自vsync,一半来自deltaTime。

3. 六面体几何与旋转:24个顶点、索引缓冲与MVP矩阵

3.1 为什么六面体需要24个顶点而不是8个

这是旋转六面体第一个反直觉的地方。立方体几何上只有8个角点,但OpenGL的顶点不是空间点,而是“位置+纹理坐标+法线”的属性集合。一个角点同时属于三个面:正面用这个角点时纹理坐标是(1,1),顶面用同一个位置时纹理坐标可能是(1,0),侧面又不同。如果只存8个顶点,shader拿到相同的纹理坐标,六个面贴图全部错乱,光照法线也算不对。正确做法是每个面单独存一组顶点:六个面,每个面4个顶点,一共24个。这个“重复顶点”不是浪费,是图形学基本规则。纹理贴图项目里,uv按面独立是最先要建立的意识。

顶点布局我习惯用position+texcoord的结构体:

struct Vertex { float x, y, z; float u, v; };

3.2 顶点数据与纹理坐标布局:一个面的完整写法

以边长2、中心在原点的单位立方体为例,前后面给一组完整数据:

// 前面 z = +1,从立方体外面看是逆时针 Vertex v[24]; v[0] = {-1.0f, -1.0f, 1.0f, 0.0f, 0.0f}; v[1] = { 1.0f, -1.0f, 1.0f, 1.0f, 0.0f}; v[2] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; v[3] = {-1.0f, 1.0f, 1.0f, 0.0f, 1.0f}; // 后面 z = -1,从外面看也是逆时针 v[4] = { 1.0f, -1.0f, -1.0f, 0.0f, 0.0f}; v[5] = {-1.0f, -1.0f, -1.0f, 1.0f, 0.0f}; v[6] = {-1.0f, 1.0f, -1.0f, 1.0f, 1.0f}; v[7] = { 1.0f, 1.0f, -1.0f, 0.0f, 1.0f}; // 其余四面按同样规则补全

这段数据有两个关键点。第一,位置坐标三个分量决定面朝向,OpenGL默认+z朝屏幕外,所以正面(对着相机)是z=+1那组。第二,纹理坐标u、v决定贴图方向和比例,每个面的uv原点我都放在该面从外面看的左下角。顶面和底面的uv特别容易写反,因为从上方俯视时“左边”和从正面看时的“左边”不是同一个方向。写完后先别急着贴真图,用带箭头标记的测试图验证方向,能省大量排错时间。

3.3 索引缓冲:6个面、12个三角形、36个索引

24个顶点存进VBO后,GPU不会自动知道这些顶点怎么组成图形。GPU原生只画三角形,六面体每个面是四边形,要拆成两个三角形。六个面,每个面两个三角形,一共12个三角形,索引数组36个元素。索引的职责是告诉GPU哪三个顶点组成一个三角形。

GLuint indices[36] = { 0, 1, 2, 2, 3, 0, // 前面 4, 5, 6, 6, 7, 4, // 后面 8, 9, 10, 10, 11, 8, // 顶面 12, 13, 14, 14, 15, 12, // 底面 16, 17, 18, 18, 19, 16, // 右面 20, 21, 22, 22, 23, 20, // 左面 };

注意一个细节:三角形的环绕方向决定正面朝外还是朝内。默认逆时针是正面。如果把某个面的索引写成顺时针,开启背面剔除后这个面直接消失。出现“六面体少了一个面”的怪现象,先关掉GL_CULL_FACE,再逐面核对索引。绘制调用是:

glBindVertexArray(vao); glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

glDrawElements的第三个参数是索引类型,GL_UNSIGNED_INT对应4字节,和索引数组类型一致;第四个参数是索引缓冲偏移,0表示从头开始。36是整个索引数组的长度,写成24这种顶点数就会漏画。VAO/VBO的绑定顺序也有讲究:先glBindVertexArray创建记录,再绑定VBO、glEnableVertexAttribArray、glVertexAttribPointer,最后解绑。Core Profile下漏了VAO,glVertexAttribPointer会报GL_INVALID_OPERATION。

3.4 旋转矩阵与MVP:glRotatef退休之后怎么办

旧版OpenGL有glRotatef、glTranslatef这类内置矩阵函数,内部维护矩阵栈,代码短但属于立即模式,在Core Profile已不可用。现代做法是把模型、视图、投影三个矩阵算好,作为uniform传给顶点着色器。计算矩阵用GLM库最常见,头文件风格,包含glm/glm.hpp、glm/gtc/matrix_transform.hpp即可。旋转六面体的核心就一行:

float deltaTime = currentTime - lastTime; angle += speed * deltaTime; glm::mat4 model = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(1.0f, 1.0f, 0.0f)); // 绕(1,1,0)轴转 glm::mat4 view = glm::lookAt( glm::vec3(3.0f, 3.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)); glm::mat4 proj = glm::perspective( glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

glm::rotate第一个参数是初始矩阵,glm::mat4(1.0f)是单位矩阵;第二个是旋转角,弧度制;第三个是旋转轴。绕(1,1,0)轴转是斜着转,视觉上比单纯绕Y轴丰富。lookAt的三个向量是相机位置、观察目标、向上方向,旋转六面体相机拉远到(3,3,3),六面体在画面里大小合适,不会顶满窗口。perspective的四个参数是fov、宽高比、近裁剪面、远裁剪面。fov太大(超过90度)立方体边缘拉伸明显,太小像望远镜。near平面0.1,far平面100,对这个demo足够。

矩阵传给shader用uniform:

GLuint mvpLoc = glGetUniformLocation(prog, "uMVP"); glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(proj * view * model));

顶点着色器里声明uniform mat4 uMVP,用它乘顶点位置。矩阵乘法顺序必须proj x view x model,模型矩阵在最右边,先作用在顶点上。顺序一旦反掉,图形要么消失,要么旋转方向完全错误。glUniformMatrix4fv的第三个参数GL_FALSE表示不转置,GLM按列主序存储,正好匹配OpenGL。这一步做完,旋转六面体的几何和动画骨架就齐全了,剩下让表面显示颜色还是图片,取决于第4章的纹理贴图。

4. 纹理贴图到旋转六面体:stb_image加载、参数设置与采样绘制

4.1 stb_image加载图片:通道数与翻转顺序

纹理贴图第一步是把图片文件变成像素数组上传到显存。常见做法是集成stb_image.h,单头文件库,接口简洁。加载的核心是stbi_load,参数分别是文件路径、宽、高、通道数指针、期望通道数。期望通道数传4强制RGBA,免去后面分通道处理的分支;传0则按图片实际通道加载。

int w, h, ch; stbi_set_flip_vertically_on_load(1); // 解决图片上下颠倒 unsigned char* data = stbi_load("face.jpg", &w, &h, &ch, 4); if (data) { glGenTextures(1, &texId); glBindTexture(GL_TEXTURE_2D, texId); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap链 stbi_image_free(data); } else { fprintf(stderr, "stbi_load failed: %s\n", "face.jpg"); }

stbi_set_flip_vertically_on_load(1)这一行解决图片上下颠倒:图片文件第一行是图像顶部,OpenGL纹理坐标(0,0)在左下,不翻转时贴图倒着。这个开关是全局的,加载完记得传0复位。glTexImage2D的第二个参数0是基础mipmap层级;第三个参数GL_RGBA是GPU内部格式;第五个参数也是GL_RGBA,这两个要一致,带alpha通道的png如果用GL_RGB上传,颜色会错位。stbi_image_free在数据上传完就能释放,显存里已经有拷贝,文件像素数组不再需要。

4.2 纹理过滤、环绕与mipmap参数:一张表解决

纹理显示成什么样,由glTexParameteri决定。旋转六面体转起来,某些面会朝向远处缩小,某些面会转过来放大,过滤参数直接决定视觉质量。我起步都用下边这套:

参数取值对应场景
GL_TEXTURE_WRAP_SGL_REPEAT水平方向超出[0,1]平铺
GL_TEXTURE_WRAP_TGL_REPEAT垂直方向超出[0,1]平铺
GL_TEXTURE_MIN_FILTERGL_LINEAR_MIPMAP_LINEAR纹理缩小时三线性采样
GL_TEXTURE_MAG_FILTERGL_LINEAR纹理放大时双线性采样

GL_REPEAT适合可平铺的贴图。六面体六张面如果各贴一张完整图,uv在0到1之间,REPEAT和CLAMP_TO_EDGE看不出差别。一旦某个面uv超出1,REPEAT会把贴图重复好几遍,图案立刻乱掉。MIN_FILTER用GL_LINEAR_MIPMAP_LINEAR的前提是先调用glGenerateMipmap;如果没生成mipmap链却设置了mipmap过滤器,驱动可能直接显示黑图,还可能报GL_INVALID_OPERATION。忘了glGenerateMipmap是纹理贴图高频翻车点,报错信息又不会在控制台出现,排查全靠glGetError。

设置这些参数的调用长这样:

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_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

顺序上先设置参数再上传数据也可以,但glGenerateMipmap必须在上传数据之后调用,因为它要从基础层生成各级金字塔。六面体旋转起来后,近处画面放大看的是MAG_FILTER的效果,远处缩小看的是MIN_FILTER配合mipmap的效果。只设放大不设缩小,或者只设缩小不生成mipmap,都会让远处画面高频闪烁,这是“贴图看着一直在抖”的一大来源。

4.3 把纹理绑进shader,完成glDrawElements收尾

纹理要生效,要打通三条线:像素数组进了纹理对象,顶点纹理坐标进了VBO,shader里有sampler2D。代码上先建好纹理对象,再在绘制时绑定到纹理单元。OpenGL的纹理单元可以理解为“插槽”,sampler声明决定从哪个插槽采样。

glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texId); glUseProgram(program); glUniform1i(glGetUniformLocation(program, "uTexture"), 0);

glActiveTexture激活纹理单元0,glBindTexture把纹理对象放进当前单元。glUniform1i把shader里的sampler和单元号绑死,0表示GL_TEXTURE0。顺序上先glUseProgram再设置uniform,否则uniform设置会被忽略,这个错误不报,只会一直显示黑图。片段着色器采样:

outColor = texture(uTexture, vTexCoord);

vTexCoord必须是顶点着色器传过来的插值结果。如果顶点着色器忘了把aTexCoord赋给out vec2 vTexCoord,采样坐标是未定义的,画面出现彩色噪点。多纹理时,第二个纹理用GL_TEXTURE1,uniform传1即可。旋转六面体的六个面共用同一张纹理时,只需要一个纹理对象,重点检查每个面的uv是否正确,而不是重复建纹理。到这里,一个转起来的、六个面可见的纹理贴图六面体已经能完整绘制,剩下是排错和进阶交互。

5. 纹理贴图常见问题排查:五个翻车场景,现象、原因、解决

做纹理贴图不像写shader那样有即时报错,很多问题画面里能看到,但日志里什么都没有。下面的五条都是旋转六面体这类项目里最常遇到的,按“现象、原因、解决”三条线写,方便照着自己排查。

5.1 贴图全黑,或者只有部分面全黑

现象:六面体转起来了,但表面是全黑的,模型轮廓在背景上能看到。原因有三个高频来源:着色器里采样纹理但不生效;纹理坐标超出范围被钳到无效区域;上传格式错位导致纹理内容全零。第一个最隐蔽,glGetUniformLocation拼错名字返回-1,没人检查。解决:绘制前打印uniform位置,-1就是名字不匹配;上glGetError,看到GL_INVALID_OPERATION优先怀疑绑定或格式。再准备一张纯白色小图替换测试,能快速排除图片本身问题。

5.2 贴图上下颠倒

现象:六面体表面文字、图案是倒的,转到一个角度特别明显。原因:图片文件的像素第一行是图像顶部,OpenGL纹理坐标原点默认在图像左下,二者方向不一致。解决:在stbi_load之前调用stbi_set_flip_vertically_on_load(1);或者在顶点数据里把v坐标改成1.0减去原值。两个做法等效,但不要双管齐下——既翻转图片又在顶点里翻转uv,等于翻两次,贴图再次倒过来。这个坑几乎每个做纹理贴图的人都会踩,第一次遇到直接在翻转开关上排查。

5.3 某一面纹理被拉伸、错位或镜像

现象:五个面正常,顶面或底面的图案斜了、拉伸了,甚至左右反向。原因:该面4个顶点的uv赋值顺序没按该面的视角重排。立方体六个面朝向不同,从外面看每个面的“左边”方向都不同,照抄一个面的uv到另一个面必然翻转。解决:逐面核对uv分配。最笨但最稳的办法是给每个面画一张带方向标记的测试图,比如顶面左上角画红点,运行后确认红点方向。uv坐标没有捷径,一个面一个面验证,验证过一轮后面怎么写都不会错。

5.4 旋转时背面透视,远处贴图闪烁

现象:六面体转起来能看到本应被挡住的面,像透视一样穿透;贴图在远处高频抖动。原因:深度测试没开启,绘制顺序决定遮挡关系;mipmap缺失导致缩小采样噪声。解决:初始化加glEnable(GL_DEPTH_TEST),清屏时glClear加上GL_DEPTH_BUFFER_BIT,两个缺一不可。闪烁问题看GL_TEXTURE_MIN_FILTER是否用了mipmap系列,没生成mipmap链就按远景图分辨率采样,每帧像素抖动。还有个低频原因:glDepthFunc默认GL_LESS,如果以前改过要记得复位。

5.5 图片加载成功,颜色却偏紫或偏暗

现象:纹理能看见,但整体偏紫,或者比原图暗不少。原因:偏紫几乎总是格式不一致,比如上传使用GL_RGB但图片带alpha通道,或者反过来;偏暗多半是色彩空间问题,图片是sRGB但纹理内部格式用了GL_RGBA,着色器也没做gamma处理。解决:先统一用GL_RGBA加载和上传,确认不再偏色后,再尝试把内部格式改成GL_SRGB8_ALPHA8,让GPU自动做sRGB解码;同时检查shader里采样结果是否又乘了顶点色,乘了会指数级变暗。颜色问题看着像玄学,但把格式、色彩空间、顶点色三个变量固定住,一定能查出来。

6. 进阶:给旋转六面体加键盘控制和双纹理切换

6.1 键盘改变旋转速度

旋转六面体跑通后,第一个值得加的是交互。用glfwGetKey在循环里读取方向键,改绕X轴和Y轴的角速度。这样不用重新编译就能观察不同旋转轴下纹理面的变化,也方便验证背面剔除设置。主循环里先算deltaTime,再累加角度:

float t = (float)glfwGetTime(); float deltaTime = t - lastTime; lastTime = t; if (glfwGetKey(win, GLFW_KEY_UP) == GLFW_PRESS) rotSpeedY += 0.4f * deltaTime; if (glfwGetKey(win, GLFW_KEY_DOWN) == GLFW_PRESS) rotSpeedY -= 0.4f * deltaTime; angle += rotSpeedY * deltaTime;

glfwGetKey返回GLFW_PRESS表示按住,松开自动停止。0.4f是角加速度,乘以deltaTime保证不同帧率下手感一致。不要直接按帧数累加角度,否则60帧和200帧下转速差三倍。

6.2 空格切换两张纹理

第二个技巧:加载第二张纹理对象texId2,按空格在两张图之间切换。实现上是给uniform传不同的sampler索引,程序里用一个整型变量记录当前纹理对象,空格翻转它。这类小交互的价值在于把纹理参数对比变成实时实验:同一张图,按住空格在GL_LINEAR和GL_LINEAR_MIPMAP_LINEAR之间切换,远处闪烁的差异一眼可见。我自己的习惯是任何图形学demo都留一套快捷键,跑起来边看边调参,而不是改一行代码重新编译一次。排查图形问题同理:先固定变量,再输出错误日志,最后定位到具体调用。这个demo做完,如果你能说清“顶点为什么是24、索引为什么是36、贴图倒置在哪一步处理、mipmap为什么影响远近画面的稳定性”,OpenGL绘制的基本功就到位了。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进