
如果你是第一次接触 OpenGL或者刚从某本旧教材开始入门大概率会在目录里看到一大片以gl开头的函数glBegin、glVertex3f、glColor3f、glMatrixMode、glLoadIdentity、glLightfv……这些接口曾经是 OpenGL 学习的标准起点但在现在以核心模式Core Profile为主的开发环境里基本已经不被支持甚至很多新版本驱动里一调用就直接报错。这篇笔记就是想把这批“过时”的基本用法讲清楚。重点不是让你继续照着写老代码而是帮你看懂它们到底在做什么为什么被淘汰以及现在应该用什么思路去替代。我会先还原一个典型的旧版 OpenGL“立即模式”开发流程再从实际编码角度拆解每一步的底层逻辑最后给出迁移到现代 OpenGL 的路线。这篇文章适合两类人一类是刚接触图形编程、正在看老教程的新手另一类是维护过老项目、经常被兼容模式坑到的开发者。如果你已经能熟练写 shader 和 VBO那可以直接跳到第 4 节看对比总结。1. 为什么还要聊这些“过时”的用法很多年以前我入图形学这行时老师给的第一段代码就是glBegin(GL_TRIANGLES)加三个glVertex3f。当时的体感就是觉得 OpenGL 挺好学画个三角形几行代码搞定不用理解太多底层细节。后来换了环境才知道这套写法早已不是主流的“正确姿势”。但如果你因此就把旧接口全部丢掉也会失去一部分理解图形管线的好素材。1.1 固定管线与可编程管线的区别所谓固定管线指的是 GPU 内部有一套设计好的处理流程包括顶点变换、光照计算、纹理混合、雾化、裁剪等。早期 OpenGL2.1 及以前把每个阶段都做成了“固定功能单元”。开发者要做的事情就是通过一堆状态函数往这些单元里配置参数。就像一台傻瓜相机你只需要按快门曝光、对焦、白平衡都由相机自动完成。可编程管线则不一样。OpenGL 3.2 开始引入核心模式把固定管线的很多功能从 API 里删掉了取而代之的是让你自己写顶点着色器Vertex Shader和片元着色器Fragment Shader。着色器实际上就是跑在 GPU 上的小段程序你完全掌控每个顶点的位置怎么算、每片元的颜色怎么来。这像从傻瓜相机换成了单反手动模式上限更高但入门门槛也上去了。为了说清楚差异我整理了一个对比表对比项早期固定管线现代核心模式顶点传入方式glBegin/glVertex逐个传CPU 到 GPU 拷贝频繁顶点数据放到 VBO顶点缓冲对象里批量上传一次坐标变换glMatrixMode glTranslate/glRotate维护矩阵栈自己创建MVP矩阵在 shader 中用 uniform 传参光照glLightfv/glMaterialfv配置光源和材质属性在 shader 中实现光照模型数据任意定义纹理glEnable(GL_TEXTURE_2D) glTexCoord使用纹理单元和 sampler 采样器状态管理全局状态机大量枚举参数管线状态显式管理多用对象VAO、VBO等从这个表就能看出来固定管线的设计思路是“开箱即用”它把很多数学运算藏在 API 里而现代 OpenGL 把“怎么算”交给了开发者。从 3.2 开始凡是和固定功能相关的 API 在核心模式下都被标记为废弃甚至直接不可用。所以你在新工程里看不到glTranslatef这类函数。1.2 学习旧接口的三个价值既然废弃了为什么还要花时间了解第一个价值是读老代码。很多开源项目、老版本引擎、竞赛用模板、大学实验代码用的还是兼容模式。你要是完全不懂glBegin/glEnd拿到这些代码时连入口都找不到。第二个价值是理解概念原型。现代 shader 里经常提到的模型矩阵、视图矩阵、投影矩阵、法线变换、纹理坐标其实都是从固定管线那套逻辑继承过来的。你知道了glMatrixMode(GL_PROJECTION)在做什么再看现代代码里用glm::perspective生成矩阵时思路完全能对得上。旧 API 只不过把矩阵细节藏起来了而现代写法是让你自己算数学内核没变。第三个价值是面试和课程应对。某些图形学课程仍然保留着固定管线教学阶段面试也偶尔会问“OpenGL 的固定管线和可编程管线有什么区别”。你不需要多精通旧接口但至少要知道它们存在的年代和解决的问题。2. OpenGL 窗口与上下文初始化的基本套路任何 OpenGL 程序的第一步不是画图而是创建一个 OpenGL 上下文。旧教程通常用 GLUT 来做这件事因为 GLUT 把创建窗口和处理事件封装得很简单。现在更多人用 GLFW功能更完整也持续在维护。理解旧套路有助于你明白上下文初始化的本质。2.1 用 GLUT 还是 GLFW历史包袱与新选择GLUTOpenGL Utility Toolkit是很老的工具库提供了glutInit、glutCreateWindow、glutDisplayFunc这些跨平台函数让我们能快速搭起一个窗口。但它已经停止维护多年很多新系统上编译会遇到问题而且它对高 DPI、键盘输入、窗口管理的支持都很弱。出于学习目的了解 GLUT 的初始化顺序仍然有意义如果写新代码我更推荐 GLFW它同时支持 OpenGL、OpenGL ES、Vulkanapi 设计也比较现代。用 GLUT 创建一个窗口经典代码大概是这样的#include GL/glut.h void display() { glClear(GL_COLOR_BUFFER_BIT); glFlush(); } int main(int argc, char** argv) { glutInit(argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(800, 600); glutCreateWindow(Legacy OpenGL Demo); glutDisplayFunc(display); // 注册绘制回调 glutMainLoop(); // 进入事件循环 return 0; }glutInitDisplayMode的作用是告诉系统你希望使用哪种帧缓冲模式比如单缓冲还是双缓冲RGB 颜色模式还是带索引的以及要不要深度缓冲。这里用GLUT_SINGLE意味着绘制完成后直接显示但现代程序基本都用GLUT_DOUBLE避免闪烁。glutMainLoop则是让程序进入一个无限事件循环一直等待窗口刷新或输入事件。很多老的教程还会在创建窗口后调用glewInit()。GLEW 是 OpenGL 扩展加载库用来获取新函数的指针。在早期很多 OpenGL 新功能不是标准 API而是由驱动提供的扩展GLEW 帮你把这些扩展函数地址加载进当前上下文。即使你不写现代代码只要想调用某些核心模式函数也需要先初始化 GLEW。如果忘了glewInit经常会出现莫名其妙的空指针调用窗口画不出东西。2.2 初始化步骤与最简单的绘制循环假设我们用兼容模式并且想画一个最简单的彩色三角形完整流程包括创建窗口、设置清屏颜色、注册回调、在主循环里清屏和绘制。旧式写法如下#include GL/glut.h void display() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_TRIANGLES); glColor3f(1.0f, 0.0f, 0.0f); glVertex2f(-0.5f, -0.5f); glColor3f(0.0f, 1.0f, 0.0f); glVertex2f( 0.5f, -0.5f); glColor3f(0.0f, 0.0f, 1.0f); glVertex2f( 0.0f, 0.5f); glEnd(); glFlush(); } int main(int argc, char** argv) { glutInit(argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(640, 480); glutCreateWindow(tri); glutDisplayFunc(display); glutMainLoop(); return 0; }这里有几个关键点。glClearColor设置的颜色只在调用glClear(GL_COLOR_BUFFER_BIT)时生效它设置的是“当前清屏颜色”。glBegin(GL_TRIANGLES)到glEnd()之间的代码描述了一个图元。每调用一次glVertex2f就传一个顶点坐标在顶点之前调用glColor3f会给这个顶点单独设置颜色。三个顶点分别被赋予红、绿、蓝于是 GPU 在片元阶段会自动做颜色插值最后屏幕上看到一个渐变平滑的三角形。这就是早期 OpenGL 最典型的“立即模式”每次绘制时CPU 把顶点数据一个一个送进管线GPU 再按照当时的固定功能状态去处理。这个教学模式很适合理解图元装配和颜色插值但它把很多运算法则硬编码了比如它默认没有深度测试、没有光照顶点位置直接就是裁剪坐标。你现在看到的是最原始的样子为了让它更接近“真实三维渲染”还需要补充矩阵变换、光照、纹理等步骤。3. 经典固定管线绘制流程拆解固定管线的核心理念是“状态机”。它维护一大堆状态比如当前颜色、当前矩阵模式、当前纹理绑定对象、当前光照开关。你需要按照特定顺序设置这些状态然后再提交顶点数据。下面把这些状态逐一分组看。3.1 从 glBegin 到 glEnd顶点数据的传递方式glBegin和glEnd是固定管线中最经典的一对函数。glBegin(GLenum mode)接受一个枚举值表示接下来要绘制什么图元GL_POINTS、GL_LINES、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_QUADS、GL_POLYGON等。在glBegin和glEnd之间你可以调用一组允许的顶点属性函数glVertex3f(x, y, z)设置顶点坐标。glColor3f(r, g, b)设置当前颜色。glNormal3f(nx, ny, nz)设置当前法线。glTexCoord2f(u, v)设置当前纹理坐标。glMultiTexCoord2f(GL_TEXTUREi, u, v)设置多纹理坐标一般用不到。这些“当前属性”会像粘贴在顶点上一样被记录到图元里。比如先设置颜色为红色然后传两个顶点这两个顶点都会被记录为红色再改成蓝色传两个顶点那后面两个就是蓝色。这种“状态值随顶点提交”的行为在当时是一个非常直观的 API 设计。但问题也很明显。每个顶点都通过函数调用从 CPU 传到 GPU数据是流式的GPU 收到一个顶点就得处理一个顶点。如果场景有几万三角形就需要几十万次函数调用CPU 会累死带宽也扛不住。后来的现代做法是先把所有顶点数据放进一个数组上传到 GPU 显存里的 VBO然后在绘制指令里直接用偏移量引用。这就是为什么glBegin/glEnd被无情淘汰。3.2 坐标变换与 glMatrix 家族固定管线里的矩阵操作是一个“栈”结构。你可以想象有一摞盘子最上面的盘子表示当前使用的矩阵。glMatrixMode用来选择操作哪个栈GL_MODELVIEW模型视图矩阵栈、GL_PROJECTION投影矩阵栈、GL_TEXTURE纹理矩阵栈。这其实是把多个变换矩阵分开管理避免乱套。典型的三维绘制前你会做两件事设置投影矩阵和设置模型视图矩阵。投影矩阵决定“视景体”长什么样常见的有正交投影和透视投影。老代码里通常这样写glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f, (float)width / height, 0.1f, 100.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);glLoadIdentity()把当前矩阵设为单位矩阵意思是“先重置”。gluPerspective生成一个透视投影矩阵参数分别是视场角、宽高比、近裁剪面、远裁剪面。gluLookAt则是构造一个观察矩阵用“眼睛位置、目标点位置、上向量”三个参数确定相机姿态。设置完这两个矩阵后你在绘制物体时可以通过glTranslatef、glRotatef、glScalef来进一步修改模型视图矩阵。比如画一个绕原点旋转的立方体glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0f, 0.0f, -3.0f); glRotatef(angle, 0.0f, 1.0f, 0.0f); // 然后绘制立方体的 8 个顶点这里最容易踩的坑是矩阵相乘的顺序。OpenGL 用的是列向量约定调用glTranslatef之后调glRotatef实际效果是先旋转再平移。很多新手把代码顺序理解成“先平移后旋转”结果物体绕着世界原点而不是自身中心转。现在用 GLM 库时你还会遇到这个坑model glm::translate * glm::rotate * scale顺序恰好和视觉直觉相反。学习旧接口的矩阵栈其实能帮你养成对“变换顺序”的敏感度。3.3 光照材质与 glLight/glMaterial 的魔法固定管线提供了内置光照模型一般称为逐顶点光照per-vertex lighting。它会在顶点着色阶段根据给定的光源信息和顶点法线计算出每个顶点的颜色再在片元阶段插值。开启光照的代码很典型glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); GLfloat lightPos[] { 1.0f, 1.0f, 2.0f, 1.0f }; GLfloat lightColor[] { 1.0f, 1.0f, 1.0f, 1.0f }; glLightfv(GL_LIGHT0, GL_POSITION, lightPos); glLightfv(GL_LIGHT0, GL_DIFFUSE, lightColor); GLfloat matDiffuse[] { 0.8f, 0.6f, 0.4f, 1.0f }; glMaterialfv(GL_FRONT, GL_DIFFUSE, matDiffuse);glLightfv中的fv表示参数是浮点数向量。GL_POSITION设置光源位置GL_DIFFUSE设置漫反射光的颜色。这里有个关键细节光源位置是四维向量第四位是 1.0 时表示点光源是 0.0 时表示方向光。这个技巧现在看有些别扭但在当时OpenGL 用一个第四分量区分两种光源很节省状态设置。材质颜色通过glMaterialfv设置它会与光源颜色做分量乘加运算最后生成顶点颜色。要让光照结果正确必须在每个顶点前调用glNormal3f设置法线。如果法线缺失或错误光的计算结果就会混乱。我记得当时画一个立方体法线给错了结果每个面都出现奇怪的明暗过渡像是几何错位。这也说明一个本质问题固定管线把光照模型固定了你只能往里塞参数而现代 shader 里你可以自己控制“漫反射高光环境光”的计算细节甚至实现 PBR 光照。3.4 纹理映射的基本用法纹理映射在固定管线里同样很有“咒语味”。经典步骤包括生成纹理对象glGenTextures(1, texId)。绑定纹理对象glBindTexture(GL_TEXTURE_2D, texId)。设置纹理采样参数glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)。上传图像数据glTexImage2D(...)。启用纹理glEnable(GL_TEXTURE_2D)。绘制时指定纹理坐标glTexCoord2f。一个最简单的平面贴纹理代码glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texId); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2f(-1, -1); glTexCoord2f(1, 0); glVertex2f( 1, -1); glTexCoord2f(1, 1); glVertex2f( 1, 1); glTexCoord2f(0, 1); glVertex2f(-1, 1); glEnd();glTexCoord2f的第一个参数是横向 u第二个是纵向 v范围通常是 0 到 1。纹理坐标和顶点坐标绑定在一起。最容易被忽略的是glTexParameteri中GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER的设置。不设置或者用默认值放大或缩小时可能产生严重的锯齿或方块甚至会因为 mipmap 不完整导致纹理呈黑色。另一个坑是图像数据在内存里的排列顺序。OpenGL 默认认为图像的第一行是纹理坐标的底部v0而很多图片格式的第一行是顶部直接上传后纹理是上下颠倒的。现代图像加载库通常会帮你翻转 y 轴但老代码里经常要手动把图片翻转一次。固定管线还有一个全局开关GL_TEXTURE_2D你必须在绘制前glEnable否则glBindTexture绑定得再好也不会出现在画面里。这个全局开关的设计也是“状态机”哲学的一个鲜明例证。4. 这些方法为什么被弃用现代 OpenGL 替代方案是什么如果你已经试着跑过上面的代码可能会说这也没那么难啊但是一旦场景复杂度上来固定管线的弊端就会暴露得淋漓尽致。这也是为什么 GPU 厂商和标准制定者最终决定掀桌子引入可编程管线。4.1 弃用的原因状态机陷阱与性能瓶颈先说状态机陷阱。固定管线里大量状态是全局的它们相互影响当前矩阵、当前材质、当前纹理、当前是否开启深度测试、当前 blend 函数。你很难判断某一段代码在另一个环境下是否还能正常工作因为状态可以被其他模块修改。比如你在绘制阴影时改了深度测试方式但忘了恢复后面所有物体都表现异常。这种“全局变量”式的 API 设计越到后来越难维护。性能瓶颈更致命。glBegin/glEnd的逐顶点调用模式会让 CPU 和 GPU 之间产生极高频率的往返通信。每传一个顶点就做一次函数调用和数据拷贝GPU 很难做批量优化。同时固定管线把很多计算固定在了远古硬件设计上现代 GPU 的并行架构更适合做大量顶点/片元的统一处理而不是处理“一串随机的即时模式命令”。还有一个隐藏问题是驱动无法优化。由于状态切换非常灵活驱动只能为每种状态组合生成对应的代码路径一旦多个状态交叠优化极难。与其这样不如把“状态机”的职责交给应用层提供更小的功能原语。这就是现代 OpenGL 核心模式的设计立场能用对象VBO、VAO、Framebuffer表示就绝不藏状态能让你写 shader 就绝不硬编码光照。4.2 现代 OpenGL 核心模式下的做法现代 OpenGL 的绘制流程不再是“设置状态 逐个传顶点”而是“准备数据 写 shader 一次提交”。核心概念有三个第一顶点数据存储。你把顶点坐标、颜色、法线、纹理坐标统统写进一个数组上传到 VBO 中。再用一个 VAO顶点数组对象记录“每个属性的格式”比如第 0 个属性是坐标偏移多少步长多少。之后绘制时绑定这个 VAOGPU 就能从显存里批量读取数据。第二着色器阶段。你至少需要一个顶点着色器和一个片元着色器。顶点着色器接收layout(location 0) in vec3 position;这样的输入并用 uniform 接收 CPU 端传进来的 MVP 矩阵计算gl_Position。片元着色器则接收从顶点着色器插值过来的变量比如颜色、纹理坐标再输出一个vec4的颜色。自由度非常高。第三批次提交。绘制时不再glBegin/glEnd而是glDrawArrays(GL_TRIANGLES, 0, vertexCount)。GPU 直接根据 VAO 描述去显存中读取数据一次调用绘制成百上千个三角形效率高很多。举例来说同样画一个三角形现代 OpenGL 的伪代码流程是// 1. 创建 VBO 并上传顶点数据 glGenBuffers(1, vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 2. 创建 VAO 并描述属性 glGenVertexArrays(1, vao); glBindVertexArray(vao); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 3. 编译 shader 并链接 GLuint program createShaderProgram(); // 4. 绘制 glUseProgram(program); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES, 0, 3);仔细看现代 API 没有glTranslate没有glRotate没有glLight。矩阵变换完全靠你在 shader 里写mat4 model和gl_Position projection * view * model * vec4(position, 1.0)完成。光照更是在片元着色器里写循环、分支、BRDF 公式。这种自由度意味着高手能做非常复杂的渲染算法但也意味着“从零开始画一个三角形”的入门门槛比旧 API 高不少。4.3 从旧代码迁移的思路如果你手里有一份老代码想迁移到现代 OpenGL建议按下面几步走先搭一个现代窗口推荐 GLFW加上 GLAD 或 GLEW 来加载函数。创建上下文时请求核心模式并设置 OpenGL 版本为 3.3 以上。把顶点属性整理成结构体数组确定有坐标、颜色、法线、纹理坐标然后生成 VBO 和 VAO把数据一次性上传。写好基础 shader顶点着色器处理矩阵变换片元着色器处理颜色输出。一开始不需要太复杂只把旧代码中的固定颜色输出即可验证流程通顺。用 GLM 写矩阵数学用glm::perspective、glm::lookAt、glm::translate把旧代码里的矩阵操作替换成 CPU 端矩阵再通过 uniform 传到 shader。把光照要求转换成 shader 代码旧代码的glLight相当于给出了光源位置和颜色你在 shader 里用 uniform 接过来手动计算漫反射和高光。迁移过程中最容易出错的是“变换顺序”和“坐标空间”。旧 API 会自动处理裁剪坐标输出新 API 中你必须自己在顶点着色器里把 object 坐标变换到 clip 坐标。另外旧代码里很多隐含状态比如“当前纹理是否启用”都没有了你必须显式地在 shader 里判断是否采样纹理。这是一个痛苦但必要的重构过程。5. 实操中的常见问题与排查记录在这套旧用法上我踩过不少坑也见过很多人被同样的问题卡住。如果你在跑老代码下面这些排查经验大概率用得上。5.1 黑屏、空白窗口的常见原因费了半天劲窗口是出来了但里面黑乎乎一片什么都看不到。黑屏通常不是“画错了”而是“根本没画上去”。先检查这几点是否在创建上下文时启用了兼容模式现在很多系统默认给你一个兼容性最好的上下文但有些环境只支持核心模式你调glBegin时直接报GL_INVALID_OPERATION。是否忘了调用glutDisplayFunc如果你没有注册绘制回调窗口只会执行一次初始化和事件循环display 内容永远不会被绘制。这是最常见的“黑屏”原因。是否设置了glClearColor但没调用glClear那只是设置颜色不会清除帧缓冲屏幕上显示的还是之前的东西。是否调用了glFlush或glutSwapBuffers如果你用了双缓冲绘制后必须交换缓冲区才能显示否则画面一直停留在“未显示”的背面缓冲上。一个高效的黑屏排查思路是先只画一个纯色三角形去掉所有矩阵、光照、纹理操作。如果连纯色都显示不出来问题一定在初始化和绘制循环如果纯色可以那再逐层加光照、纹理。5.2 矩阵变换“诡异”的坑矩阵变换的坑大多来自“顺序”和“矩阵栈”的理解偏差。老代码里glLoadIdentity是重置glTranslatef是累加乘到当前矩阵上。举个例子glTranslatef(1, 0, 0); glRotatef(90, 0, 0, 1);实际效果是先绕原点旋转 90 度再平移到 (1,0)。如果你以为先平移再旋转物体位置会完全不对。排查变换问题时最直观的办法是临时把旋转角度设成 0看看平移是否按预期工作再把平移设成 0看旋转是否围绕原点工作。这样能把“变换本身写错”和“顺序理解错”区分开。另一个常见问题是投影矩阵设置不正确。如果你忘了设置glMatrixMode(GL_PROJECTION)和glLoadIdentity默认投影矩阵有可能是单位矩阵视锥体的近远范围非常窄[-1,1]。物体在 z0.5 时能看到在 z-5 时就被裁掉了。这时候可以试着把物体沿着 z 轴慢慢移近相机或者用glOrtho设置大的视景体先让画面出现再说。5.3 纹理显示不正确的排查思路纹理相关的老代码问题集中在四个地方图片上下颠倒。解决办法是翻转图片的 y 坐标或者翻转纹理坐标。在glTexImage2D之前将图像内存按行反转是最常见的处理方式。没有启用纹理。老代码中忘记glEnable(GL_TEXTURE_2D)会导致物体颜色变成纯色纹理想显示也显示不出来。GL_UNPACK_ALIGNMENT不对。如果图片宽度不是 4 的倍数默认按 4 字节对齐读取时会错位导致纹理出现斜条纹或花屏。调用glPixelStorei(GL_UNPACK_ALIGNMENT, 1)可以解决。过滤参数没有设置完整。特别是GL_TEXTURE_MIN_FILTER如果你设置了GL_LINEAR_MIPMAP_LINEAR却没有生成 mipmap纹理就会是黑色或空白。要么改成GL_LINEAR要么调用glGenerateMipmap新版本支持。排查纹理问题我建议画一个很小的纹理比如 2x2 像素填充四个分明颜色然后看输出。这样很容易判断是纹理坐标错位、对齐错位还是过滤参数的问题。5.4 兼容模式在 macOS 上的限制如果你在 macOS 上练习旧代码会遇到一个比较特殊的问题。macOS 对 OpenGL 的支持最高只到 4.1并且系统默认提供的是核心模式的上下文并不提供真正的固定管线和立即模式。你需要通过NSOpenGLProfileVersionLegacy或 GLFW 的GLFW_OPENGL_COMPAT_PROFILE来请求兼容模式但即便如此很多旧函数在驱动层也仅部分支持glBegin仍然会报错或者使用急救包。这里我不展开具体配置因为你只要记住结论在 macOS 上与其折腾兼容模式不如直接用现代 OpenGL。所有经典固定管线代码最好在 Windows 或 Linux 虚拟机里跑。如果你只是“想了解”这些旧用法完全可以在 macOS 上跑现代 OpenGL配合阅读老代码脑内把概念映射过去就行。6. 整理旧接口时的一些实际体会真正动手整理这些旧接口时我有一个很明显的感受它们并不难懂难点反而在“能不能忘掉它们”。就像学开车时先学手动挡虽然手动挡在城里开起来很累但它会让你搞明白离合器、换挡和发动机转速之间的关系。学完再开自动挡你会知道自己脚下的操作本质上是在控制什么。旧版 OpenGL 的glMatrixMode、glLightfv、glTexEnv就是那根离合器现代 shader 里的矩阵乘法和光照计算就是自动挡的电子控制单元。我的建议是新手可以从这段历史学起但不要在上面花费超过两周时间。换到现代 OpenGL 之后你会发现很多 API 名称都变了但底层的渲染流水线概念没变。你过去调glVertex3f时写的每个顶点现在变成了vertices数组里的一个个浮点数以前调glLightfv时给的光源位置现在变成了 shader 里的uniform vec3 lightPos。知识并没有消失只是换了一种更暴露本质的形态。最后分享一个小技巧如果你想快速验证一段老式 OpenGL 代码的逻辑又不想折腾兼容模式可以先用一个现代的 OpenGL 窗口把顶点数据处理成 VBO然后在顶点着色器里用简单的gl_Position projection * view * vec4(position, 1.0)来复现。那些旧接口里的“魔法矩阵”你就把它当成一组预先算好的 uniform 来理解。这样一来你看老教程时会觉得豁然开朗写新代码时也不会重蹈状态机的覆辙。旧接口是一段值得了解的历史但绝不是现代项目的最佳起点。看懂它们然后放下它们才是这类“基本用法介绍”真正想传达的东西。