ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VC6 OpenGL三维图形工程解析:MFC框架与渲染循环实战

VC6 OpenGL三维图形工程解析:MFC框架与渲染循环实战 简介这份资源面向希望在 Visual C 6.0 环境下入门 OpenGL 三维图形绘制的开发者与在校学生解决从零搭建图形渲染程序、理解坐标变换与光照模型等核心概念的问题。压缩包共 36 个文件约 1.84MB包含 6 个 h 头文件、5 个 cpp 源文件以及 obj、pdb、exe、ico、rc 等编译中间件与工程配置资源另有 dsw、dsp 工程文件与 ReadMe 说明构成一套可直接编译运行的完整工程。资源围绕窗口创建、OpenGL 上下文初始化、重绘回调、矩阵变换、光照与材质、纹理映射等知识点展开代码结构清晰便于对照学习 Win32 API 与 GLUT 的配合方式。目前已有 675 人学习下载适合作为图形学课程实验或自学练手的参考案例帮助读者快速理解三维图形渲染的基本流程与调试思路。1. 从一份 VC6 时代的 OpenGL 工程包说起它到底能跑出什么如果你手里正好有一份使用OpenGL实现绘制三维图形.rar解压后看到OpenGLBase.dsw、OpenGLBaseView.cpp、MainFrm.cpp这一串文件第一反应大概率是这是 MFC 单文档框架套了个 OpenGL 视图。没错它就是一个典型的 Visual C 6.0 工程用 MFC 的文档/视图结构承载 OpenGL 渲染上下文最终在窗口里画出一个可旋转、可缩放的三维图形。这类资源在今天依然有它的价值——不是因为它多先进而是因为它把「Win32 窗口 OpenGL 上下文 渲染循环」这条链路完整地摊开给你看没有现代引擎帮你藏掉任何一步。它适合谁一是正在学计算机图形学、需要亲手摸一遍 OpenGL 管线而不是只背公式的学生二是维护老项目、突然被要求改一个 VC6 工程里三维显示模块的工程师三是想理解「窗口消息循环和渲染循环怎么配合」的底层爱好者。你不需要先装一个几 GB 的 Visual StudioVC6 加上系统自带的opengl32.lib就能把这份工程跑起来。但前提是你得知道它为什么能跑以及跑不起来的时候该看哪里。2. 拆开工程看骨架MFC 文档视图怎么托住 OpenGL 上下文2.1 文件清单里藏着的调用链先把压缩包里几个关键文件的作用理清楚不然后面改代码就是盲人摸象。OpenGLBase.dsw和OpenGLBase.dsp是 VC6 的工作区和工程文件双击.dsw就能打开整个项目。OpenGLBase.cpp里是CWinApp派生类负责应用启动MainFrm.cpp是主框架窗口OpenGLBaseDoc.cpp是文档类通常用来存场景数据OpenGLBaseView.cpp才是重头戏它继承自CViewOpenGL 的初始化、渲染、窗口大小变化响应基本都落在这里。Resource.h和OpenGLBase.rc管资源StdAfx.cpp是预编译头。这里有个容易忽略的点MFC 的CView本身只是一个空白客户区它不会自动给你一个 OpenGL 上下文。所以这份工程一定在OpenGLBaseView里做了三件事——重写PreCreateWindow去掉窗口的CS_OWNDC之外的多余样式、在OnCreate里做像素格式设置和wglCreateContext、在OnDraw或OnPaint里发渲染命令。你打开OpenGLBaseView.cpp应该能对上号。2.2 像素格式与渲染上下文两行代码背后的硬约束OpenGL 在 Windows 上不是直接画到窗口的它需要一个「像素格式」来描述帧缓冲的布局再用wglCreateContext拿到渲染上下文最后wglMakeCurrent把上下文绑到当前线程的设备上下文上。这三步缺一不可而且顺序不能乱。常见做法是在OnCreate里先填充PIXELFORMATDESCRIPTOR然后ChoosePixelFormat、SetPixelFormat、wglCreateContext、wglMakeCurrent一路走下来。// OpenGLBaseView.cpp 中 OnCreate 的典型片段 int COpenGLBaseView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) -1) return -1; // 获取当前窗口的设备上下文 HDC hDC ::GetDC(m_hWnd); // 描述像素格式双缓冲、RGBA、深度缓冲 24 位 PIXELFORMATDESCRIPTOR pfd { sizeof(PIXELFORMATDESCRIPTOR), 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, PFD_TYPE_RGBA, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 8, 0, PFD_MAIN_PLANE, 0, 0, 0, 0 }; int nPixelFormat ChoosePixelFormat(hDC, pfd); if (nPixelFormat 0) { ::ReleaseDC(m_hWnd, hDC); return -1; // 没有匹配的像素格式直接失败 } SetPixelFormat(hDC, nPixelFormat, pfd); // 创建并绑定 OpenGL 渲染上下文 m_hRC wglCreateContext(hDC); wglMakeCurrent(hDC, m_hRC); ::ReleaseDC(m_hWnd, hDC); return 0; }这段代码里PFD_DOUBLEBUFFER决定了你是否需要SwapBufferscDepthBits设为 24 是三维场景的常规选择少了它深度测试会失效物体前后关系就乱了。ChoosePixelFormat返回 0 是最常见的翻车点通常是因为显卡驱动没装好或者远程桌面会话不支持硬件加速。如果你在远程桌面里跑像素格式可能只能拿到软件实现帧率会低得感人。2.3 窗口尺寸变化时的视口与投影矩阵MFC 窗口被拖动改变大小时会触发OnSize如果你不在这里同步更新glViewport和投影矩阵画面就会被拉伸变形。这份工程里应该有一个OnSize重写里面先调用glViewport(0, 0, cx, cy)再切换投影矩阵重新计算透视参数。常见做法是用gluPerspective或者手动构造投影矩阵视口宽高比要和投影的宽高比一致否则圆会变成椭圆。void COpenGLBaseView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); if (cy 0) return; // 防止除零 glViewport(0, 0, cx, cy); glMatrixMode(GL_PROJECTION); glLoadIdentity(); // 45 度视角近裁剪面 0.1远裁剪面 100 gluPerspective(45.0, (double)cx / (double)cy, 0.1, 100.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); }gluPerspective的第二个参数是宽高比必须用当前客户区的cx/cy不能用固定值。近裁剪面设得太小会导致深度精度不足出现 Z-fighting 闪烁远裁剪面设得太大同样会稀释深度缓冲精度。一般场景里近裁剪面 0.1、远裁剪面 100 到 1000 之间比较稳妥。3. 把三维图形画出来从清屏到交换缓冲的完整渲染循环3.1 OnDraw 里的渲染顺序不能乱MFC 的OnDraw会在窗口需要重绘时被调用但 OpenGL 的渲染不应该只依赖OnDraw因为窗口遮挡、移动都会触发重绘。更稳妥的做法是在OnDraw里发渲染命令同时用Invalidate(FALSE)配合定时器或空闲处理来驱动连续动画。渲染顺序上先glClear清颜色和深度缓冲再设置模型视图矩阵然后画几何体最后SwapBuffers。void COpenGLBaseView::OnDraw(CDC* pDC) { COpenGLBaseDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); // 清除颜色和深度缓冲 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // 把相机往后拉 5 个单位否则物体在裁剪面内看不见 gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0); // 画一个彩色立方体 glBegin(GL_QUADS); // 前面 glColor3f(1.0f, 0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); glVertex3f( 1.0f, -1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // 后面 glColor3f(0.0f, 1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // 其余四个面省略原理相同 glEnd(); // 交换前后缓冲把画好的内容显示出来 SwapBuffers(wglGetCurrentDC()); }glBegin/glEnd是立即模式现代 OpenGL 早就废弃了但在这份 VC6 工程里它是能跑通的因为 VC6 自带的opengl32.lib只暴露 OpenGL 1.1 接口。gluLookAt的九个参数分别是相机位置、观察目标、上方向上方向不能和视线方向平行否则视图矩阵会退化。SwapBuffers必须传当前设备上下文用wglGetCurrentDC()拿最稳。3.2 深度测试和光照让立方体有前后和明暗如果你只画一个立方体但不开启深度测试后画的画会盖住先画的看起来就像纸片。开启深度测试只需要两行glEnable(GL_DEPTH_TEST)和glDepthFunc(GL_LEQUAL)。光照则更复杂一些需要设置光源位置、光源颜色、材质参数并且开启GL_LIGHTING和至少一个光源GL_LIGHT0。// 在 OnCreate 或初始化函数里调用一次 glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); // 光源位置最后一个 1.0 表示点光源0.0 表示方向光 GLfloat lightPos[] { 5.0f, 5.0f, 5.0f, 1.0f }; GLfloat lightAmbient[] { 0.2f, 0.2f, 0.2f, 1.0f }; GLfloat lightDiffuse[] { 0.8f, 0.8f, 0.8f, 1.0f }; glLightfv(GL_LIGHT0, GL_POSITION, lightPos); glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient); glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuse); // 材质物体对光的反应 GLfloat matSpecular[] { 1.0f, 1.0f, 1.0f, 1.0f }; GLfloat matShininess[] { 50.0f }; glMaterialfv(GL_FRONT, GL_SPECULAR, matSpecular); glMaterialfv(GL_FRONT, GL_SHININESS, matShininess);开启光照后glColor3f就不再直接决定颜色了颜色由材质和光照共同决定。如果你发现物体全黑先检查glEnable(GL_LIGHTING)之后有没有设置法线——没有法线光照计算无法进行。glNormal3f要在每个顶点之前指定且必须是单位向量否则光照强度会算错。3.3 用定时器驱动旋转动画静态立方体看不出三维效果让它转起来才直观。MFC 里最省事的做法是SetTimer加OnTimer在OnTimer里改变旋转角度然后Invalidate(FALSE)触发重绘。注意Invalidate的参数传FALSE表示不擦除背景避免闪烁。// 在 OnCreate 末尾 SetTimer(1, 30, NULL); // 约 33 帧每秒 void COpenGLBaseView::OnTimer(UINT nIDEvent) { if (nIDEvent 1) { m_fAngle 1.0f; // 每帧转 1 度 if (m_fAngle 360.0f) m_fAngle - 360.0f; Invalidate(FALSE); } CView::OnTimer(nIDEvent); } // 在 OnDraw 里应用旋转 glRotatef(m_fAngle, 0.0f, 1.0f, 0.0f); // 绕 Y 轴旋转SetTimer的间隔单位是毫秒30 毫秒对应约 33 帧对 VC6 时代的机器足够流畅。glRotatef要放在gluLookAt之后、画几何体之前否则旋转会作用到相机上而不是物体上。如果你发现旋转方向反了把角度取负即可。4. 避坑与排查VC6 下 OpenGL 工程最容易翻车的五个地方4.1 编译报错cannot open file gl/gl.h现象是编译时提示找不到gl/gl.h或glu.h。原因是 VC6 默认的 include 路径里没有 OpenGL 头文件目录或者你写成了#include GL/gl.h但系统里根本没有 GL 文件夹。解决方法是确认 VC6 安装目录下的VC98\Include\GL是否存在如果不存在从 Windows SDK 里拷贝一份或者把包含路径改成#include gl/gl.h并确保VC98\Include在搜索路径里。更省事的做法是直接在工程设置里把C:\Program Files\Microsoft Visual Studio\VC98\Include加进附加包含目录。4.2 链接报错unresolved external symbol __imp__wglCreateContext现象是编译通过但链接失败提示wglCreateContext、glClear之类的符号找不到。原因是工程没有链接opengl32.lib、glu32.lib和glaux.lib。解决方法是打开「工程设置 → 链接 → 对象/库模块」把这三个库加进去或者在代码里用#pragma comment(lib, opengl32.lib)显式指定。注意 VC6 的glaux.lib可能不在默认库路径里如果找不到就删掉对它的依赖改用glu的函数替代。4.3 窗口出来了但一片漆黑现象是程序能运行、窗口能弹出但客户区全黑看不到任何图形。原因通常有三个一是没有调用glClear或者清屏颜色就是黑色二是相机位置和物体位置重叠物体在近裁剪面之内被裁掉了三是没有开启深度测试导致后画的背景盖住了物体。排查顺序是先确认glClearColor设了非黑值再检查gluLookAt的相机位置和glTranslatef的物体位置是否合理最后确认glEnable(GL_DEPTH_TEST)有没有漏掉。如果用的是双缓冲但忘了SwapBuffers画面也会一直停在初始状态。4.4 拖动窗口大小时画面撕裂或变形现象是改变窗口尺寸后图形被拉伸或者出现闪烁撕裂。原因是OnSize里没有同步更新glViewport和投影矩阵的宽高比或者Invalidate传了TRUE导致背景擦除和重绘不同步。解决方法是确保OnSize里先glViewport再重设投影矩阵并且Invalidate(FALSE)配合双缓冲使用。如果撕裂严重检查SwapBuffers是否在OnDraw的最后一行不要在它之后再做任何绘制。4.5 换到新机器上提示缺少microsoft visual c 2015-2022 redistributable现象是把编译好的 exe 拷到另一台机器上运行弹窗提示缺少运行库。原因是 VC6 编译的程序依赖的是老版msvcr60.dll但如果你后来用新版 Visual Studio 重编译过就会依赖vcruntime140.dll之类的运行库。解决方法是安装对应的visual c redistributable或者用静态链接把运行库打进 exe。对于这份 VC6 工程最稳的做法是保持用 VC6 编译然后把msvcr60.dll和mfc42.dll一起拷过去或者干脆在新机器上装一个 VC6 运行库合集。5. 进阶技巧用顶点数组和显示列表把绘制效率提上去5.1 立即模式的瓶颈在哪里前面用的glBegin/glEnd每画一个顶点就要走一次函数调用一个立方体 24 个顶点还能忍一旦你要画几千个三角形CPU 和 GPU 之间的总线就会被大量小命令堵死。VC6 环境下虽然用不了 VBO但可以用顶点数组和显示列表来减少函数调用次数。顶点数组把顶点数据放在连续内存里一次glDrawArrays或glDrawElements提交显示列表把一组绘制命令编译成 GPU 端的指令序列后续调用只需要一个glCallList。5.2 顶点数组的写法与参数含义// 定义顶点和颜色数组 static GLfloat vertices[] { -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f }; static GLfloat colors[] { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; // 启用顶点数组 glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vertices); glColorPointer(3, GL_FLOAT, 0, colors); // 一次提交 8 个顶点画四边形 glDrawArrays(GL_QUADS, 0, 8); glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_VERTEX_ARRAY);glVertexPointer的第一个参数是每个顶点几个分量三维坐标就是 3第二个参数是数据类型GL_FLOAT对应float第三个参数是步长0 表示紧密排列第四个参数是数组指针。glDrawArrays的第一个参数是图元类型GL_QUADS表示每四个顶点组成一个四边形第二个参数是起始索引第三个参数是顶点数量。注意顶点数组里的顶点顺序要符合图元的绕序要求否则背面剔除会把它剔掉。5.3 显示列表的编译与调用GLuint listID glGenLists(1); glNewList(listID, GL_COMPILE); // 这里放所有绘制命令和普通绘制写法一样 glBegin(GL_QUADS); // ... 顶点数据 glEnd(); glEndList(); // 渲染时只需要一行 glCallList(listID);glGenLists(1)申请一个显示列表编号GL_COMPILE表示只编译不执行。显示列表一旦编译里面的顶点数据就固定了适合画静态场景。如果物体需要每帧改变顶点位置显示列表就不适用还是得用顶点数组或者回到立即模式。VC6 环境下显示列表的兼容性很好几乎所有显卡都支持。5.4 验证渲染是否真的走了硬件加速一个很实用的技巧是查glGetString(GL_RENDERER)和glGetString(GL_VENDOR)看看当前上下文用的是哪家驱动。如果返回的是GDI Generic或者Microsoft Corporation说明你跑在软件渲染上帧率上不去是正常的。硬件加速下应该返回显卡厂商名字比如NVIDIA或Intel。这个检查在排查「为什么我的 OpenGL 程序这么慢」时特别管用。const char* renderer (const char*)glGetString(GL_RENDERER); const char* vendor (const char*)glGetString(GL_VENDOR); // 在调试输出里打印这两个字符串 TRACE(Renderer: %s, Vendor: %s\n, renderer, vendor);从那以后我每次拿到一份老 OpenGL 工程都先跑一遍glGetString确认渲染器再决定要不要花时间优化绘制代码。如果底层就是软件渲染再怎么改顶点数组也是白搭。希望这份拆解能帮你把这份 VC6 时代的 OpenGL 工程顺利跑起来并且知道每一步为什么这么写。本文还有配套的精品资源点击获取
返回列表