ARTICLE DETAIL

资讯详情

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

wxWidgets+DialogBlocks+OpenGL:跨平台体数据渲染实战指南

wxWidgets+DialogBlocks+OpenGL:跨平台体数据渲染实战指南 前阵子接了个跨平台桌面工具需求很简单主窗口里嵌一个OpenGL渲染区用来实时显示三维数据。选型的时候在Qt和wxWidgets之间来回纠结最后拍了板用wxWidgets DialogBlocks这套组合把OpenGL画面嵌进wxFrame。整个过程做完之后回头看网上零散资料不少但把“可视化搭界面、生成代码、GL上下文、渲染循环”这条线完整串起来的教程几乎没有于是决定把自己实际跑通的流程写出来。这篇文章适合这么几类人刚接触wxWidgets、想找一套能落地的OpenGL嵌入方案的C开发者被DialogBlocks生成代码绕晕、想搞明白生成物和自定义类怎么配合的人还有手里有体素数据比如NIfTI格式的医学影像或数值模拟结果想快速搭个3D浏览工具的工程师。我会从设计思路讲到具体实现最后把踩过的坑整理成清单尽量让你少走弯路。1. 项目概述与选型思考1.1 为什么选 wxWidgets DialogBlocks 而不是 Qt先说结论如果你追求极致的跨平台UI开发效率Qt自然是第一选择。但我在这个项目里选wxWidgets有几个非常现实的原因。第一是原生控件。wxWidgets在Windows上就是包着Win32原生的皮在Linux上是GTKmacOS上是Cocoa。这意味着应用启动更快、体积更小、系统观感更统一不需要像Qt那样自带一套QPA窗口系统和风格引擎。对于工具类软件用户其实很在意“这软件看起来是不是够本机化”wxWidgets在这点上天然占优。第二是依赖足够轻。Qt发布时要带一堆DLL动辄上百MBwxWidgets这边核心库加起来也就几MB配合vc_redist就能跑这对内部工具分发特别友好。我甚至可以静态链接一个exe直接拷走。第三是许可证友好。wxWidgets是LGPL商业闭源项目可以静态链接只要你把对wxWidgets库本身的修改开源即可Qt现在虽然也用LGPL但商用授权政策变来变去公司法务那边沟通成本高。在“不是非用QML不可”的场景下wxWidgets没有额外负担。第四其实是最关键的——我手里要渲染的数据是医学影像体素渲染管线和数据加载完全在我自己这边GUI框架只需要提供一块画布、一个菜单栏、几个状态栏栏目就够。这种情况下wxWidgets的轻量反而是优势我不需要框架给我塞一整套模型视图架构。很多人会问那为什么不用纯Win32或者直接SDL因为项目还要在Linux和macOS上编译纯Win32写死就完蛋SDL做窗口没问题但做菜单、对话框、剪贴板、文件关联这些桌面功能就太裸了。wxWidgets正好卡在“不要重、但要全”的位置上。1.2 DialogBlocks 在这套组合里的真实角色DialogBlocks是CodeBlocks作者写的另一个工具本质是个可视化wxWidgets界面设计器。它和你熟悉的Qt Designer类似但它直接产出C代码或XRC资源文件并且自带完整的wxWidgets工程管理能力。我为什么不用手写sizer因为wxWidgets的布局代码手写多了确实啰嗦一个简单的垂直布局你要new若干个sizer、不断Add和SetSizer改一个间距都要来回找参数。用DialogBlocks拖拽布局关系一目了然生成之后还能反复调整。但这工具在网上教程极少很多人装了不知道从哪里下手。它有两点很反直觉第一它看起来像个IDE但你不一定要用它写代码把它当“代码生成器”用更舒服第二它内置了wxWidgets的类库说明能自动列出某个类有哪些构造参数这个功能对不熟悉wxWidgets的人帮助巨大。在我的工作流里DialogBlocks只负责两件事生成Frame类的骨架代码、维护菜单和sizer布局。至于OpenGL渲染逻辑、数据解析、体绘制算法全部写在另一个子类里DialogBlocks只放一个占位控件运行时替换成真正的GLCanvas。1.3 整体架构怎么搭这个项目的核心架构可以拆成四层窗口层wxFrame负责容纳菜单栏、工具栏、状态栏以及中央的渲染区域。渲染宿主层一个继承自wxGLCanvas的类负责承载OpenGL上下文、处理Paint/Size/Mouse事件。渲染器层使用OpenGL 3.3 Core Profile编写的GLSL着色器管线负责把体素纹理渲染到屏幕。数据层读取NIfTI或其他格式的体素数据转成GPU纹理需要的布局。层与层之间尽量解耦。比如渲染宿主只管“收到事件后调用渲染器的Draw”渲染器内部是体绘制还是切片绘制、是四维时序还是彩色映射都不需要宿主关心。这样做的好处后面会体现当你想把渲染内核换到另一个GUI框架时宿主层重写一次就行渲染器和数据层原封不动。2. DialogBlocks 建界面实操2.1 环境搭建版本和工具链先说版本这直接影响你后面能不能跑起来。wxWidgets建议用3.2.x系列这代API已经非常稳定而且对OpenGL的支持比旧版清晰得多。3.0也能用但在某些平台上有Retina坐标与DPI的坑3.2修正了很多。DialogBlocks我用的是Windows上的版本它本身跨平台但Windows下做得最顺手。它带一个依赖包下载功能可以自动拉对应平台的wxWidgets库但我不太推荐用它内置的库因为那个库不一定和你本地编译器匹配。我建议你自己编译一份wxWidgets然后让DialogBlocks指向你的头文件和lib目录。编译wxWidgets的典型流程是这样打开wxWidgets源码目录下的build/msw用你对应的Visual Studio版本打开wx_vc17.sln按Studio版本选择把Debug和Release都编出来。编的时候注意runtime library配置最好统一用/MD动态运行时不然后面接管工程的时候容易撞坑。如果你用MinGW环境则在build/msw下执行mingw32-make -f makefile.gcc。工程配置里三处最容易错C包含目录指向wxWidgets\include和wxWidgets\lib\vc_dll库目录指向lib\vc_dll预处理定义里加wxUSE_GUI1和WXUSINGDLL。还要记得在链接器输入里加上opengl32.lib和glu32.libOpenGL不是wxWidgets库的一部分要自己加。2.2 从空白工程到主窗口布局打开DialogBlocks新建工程时选“wxWidgets GUI App”模板。它会生成一个带MyApp和MyFrame的骨架。默认主窗口只是空Frame我们要改造成带菜单和中央渲染区的布局。左侧设计树里右键点Frame节点选择“Add Sizer”——这里先加一个垂直的BoxSizer作为整个客户区的根布局。然后往这个根sizer里加一个水平sizer用来放工具栏下方的顶部区域再加一个空白占位控件作为将来的GLCanvas区域。DialogBlocks的编辑区右侧是属性面板里面有一个很有用的下拉叫“Style”。默认Frame带wxRESIZE_BORDER和wxCAPTION但要注意取消wxCLOSE_BOX之外不常用的风格否则在某些Linux窗口管理器下会出现奇怪行为。中央占位控件我习惯用wxPanel初始背景色设成黑色这样程序启动还没创建GL上下文时客户区不会显示一片刺眼的白。菜单栏的创建很简单右键Frame选“Add Menu Bar”然后逐项添加File、View、Help。File菜单加Open和ExitView菜单加Toggle Render Mode和Reset Camera。记住每个菜单项的ID后面生成代码后要在事件表里用。2.3 把 wxGLCanvas 塞进界面DialogBlocks的控件列表里没有直接的wxGLCanvas这很正常因为它包含GL上下文必须有额外属性设置。实际操作有两种方式。第一种最省事在布局里先放一个wxPanel占位等DialogBlocks生成代码后手动把那个Panel替换成自定义的GLCanvas子类。缺点是你每次在DialogBlocks里重新生成布局改动会被覆盖。第二种是给DialogBlocks注册自定义类。在设计器的“Preferences - Custom Controls”里加一个类名字叫MyGLCanvas基类写成wxGLCanvas构造参数按wxGLCanvas的定义填再把你需要的头文件路径加进去。这样你在布局里就能直接“Add Custom Control”生成代码时它会自动生成MyGLCanvas的实例化代码。我用的是第二种因为后面迭代布局时不用重新改代码。这里有个关键细节wxGLCanvas的构造参数在wxWidgets 3.2里第二个参数是wxGLAttributes不是老的int[]数组。DialogBlocks的自定义控件参数列表里填构造签名时要把wxGLAttributes的构造方式对应上否则生成代码编译不过。2.4 生成代码后的第一步改造DialogBlocks默认生成的是“事件表头文件实现文件”的经典结构。生成代码后你要做的第一件事是把Frame构造函数里GL相关的东西加进去。骨架代码里Frame构造函数大概是这样创建菜单栏、创建根sizer、往sizer里Add占位panel、SetSizerAndFit。你要改成创建MyGLCanvas的实例替换占位panel。同时把渲染器的初始化调用放在GL上下文创建之后。事件处理上需要注意DialogBlocks生成的事件表和函数名。默认情况下Open和Exit菜单会生成OnOpenClick、OnExitClick这样的函数但函数体是空的。把文件对话框和退出逻辑写进去以后再手动在事件表加上EVT_PAINT、EVT_SIZE、EVT_MOTION。还有一个我习惯性的改造把Frame的析构函数里对GLCanvas和GLContext的释放顺序写清楚。先释放GL资源再删GLCanvas最后删sizer。顺序反了的话有时候Debug版会崩Release版又没事非常折磨人。3. OpenGL 与 wxWidgets 集成的核心细节3.1 像素格式黑屏的第一道门槛wxWidgets里创建OpenGL窗口的核心是wxGLCanvas但真正决定你能不能看到东西的是像素格式属性。这在3.2版本里必须通过wxGLAttributes结构体配置。我推荐的起始配置是wxGLAttributes attrs; attrs.PlatformDefaults() .RGBA() .DoubleBuffer() .Depth(16) .SampleBuffers(1) .Samplers(4) .EndList(); canvas new MyGLCanvas(this, attrs);有几点要解释。DoubleBuffer一定要开不开的话画面会疯狂闪烁因为单缓冲直接画到屏幕缓冲区重绘时全是撕裂和残影。Depth至少16如果你要渲染体数据的3D纹理或者三角网格没有深度缓冲会一团糟。SampleBuffers和Samplers是抗锯齿4倍MSAA对体绘制这种平滑渐变画面的提升非常明显。wxWidgets 3.0时代很多教程用int attrs[] {WX_GL_RGBA, WX_GL_DOUBLEBUFFER, ...}这种老写法3.2虽然还兼容但新代码应该直接用wxGLAttributes。如果一个平台不支持你请求的像素格式GLCanvas的构造函数会抛异常或返回空指针这时候要降级重试而不是直接崩掉。另外注意像素格式是“请求”不是“保证”。比如虚拟机里可能没有硬件MSAA你请求8倍采样它只给你软件模拟甚至直接拒绝。所以构造后要检查IsOk()失败就换成降低采样数的配置重试。3.2 wxGLContext 生命周期创建了wxGLCanvas只代表“这块画布可以用来显示GL画面”真正的渲染状态要由wxGLContext管理。它封装了OpenGL上下文也就是存放当前状态机、纹理绑定、着色器程序的那一套东西。生命周期上有一个黄金法则wxGLContext的创建必须发生在wxGLCanvas创建之后并且同一个canvas实例上最好只创建一个context。glContext new wxGLContext(canvas); glContext-SetCurrent(*canvas);SetCurrent是关键。OpenGL是个状态机但不同平台上的当前上下文是线程局部且全局唯一的。你在调用任何一个GL函数之前都必须确保“当前上下文”是canvas对应的那个context。最典型的报错是在Windows上glewInit没有被当前上下文激活导致所有函数指针都是空一调用就崩。在实际项目里我喜欢在第一次Paint之前做一个懒初始化创建canvas时只保存指针等收到第一个wxEVT_PAINT时才创建context并初始化GL。void MyGLCanvas::OnPaint(wxPaintEvent event) { if (!glContext) { glContext new wxGLContext(this); glContext-SetCurrent(*this); InitGL(); // 在这里初始化glew、编译着色器、上传纹理 } Render(); SwapBuffers(); }这种写法有个好处GL上下文从创建到使用都在同一个线程、同一个调用栈里避免了跨线程上下文切换的坑。实测在Windows和Linux上都没问题。3.3 渲染循环三种姿势有了canvas和context怎么驱动渲染我试过三种方式各有适用场景。第一种是纯被动渲染只在收到wxEVT_PAINT时绘制。适合界面不动的应用。wxWidgets绘制事件由系统触发窗口被遮挡再恢复、最小化再还原、拖动调整大小都会自动触发。但如果你想做交互式的旋转缩放体数据鼠标拖一下、Refresh一下pain事件自然来。优点是省电、不占CPU缺点是动画不连续——如果动画每帧来一次它每帧只画一次没问题但要是有外部数据源要主动刷新你得手动发Refresh。第二种是wxTimer定时渲染。我设置16ms或33ms的Timer间隔每触发一次刷新一帧。这个方案适合需要连续动画的场景比如旋转模型、播放时序数据。Timer事件和Paint事件不在同一时刻到达但Timer回调里调Refresh()最终还是会走Paint做真实绘制。第三种是wxEVT_IDLE持续刷新。Idle事件在wxWidgets事件队列空的时候频繁触发只要你在OnIdle里调用Refresh(true)就能实现一个“立即模式”的渲染循环。好处是帧率自适应状态没有变化时可以主动停止刷新有变化时立刻重画。代价是如果处理不好会让CPU在高分辨率屏幕上满载。我用的是“Timer 脏标记”的组合默认不启动Timer只有鼠标操作、数据更新时启动一次Timer操作结束后立即停止。这样既保证交互流畅又不会吃掉后台CPU。这里有个注意事项SwapBuffers必须跟Paint事件成对出现吗不是。wxWidgets里SwapBuffers任何时刻调用都合法只要当前上下文已经SetCurrent。但在Windows上如果窗口客户区无效系统有可能会在重绘后盖掉你的GL缓冲区内容。所以最稳妥的做法还是严格遵守“OnPaint里画、OnPaint里Swap”。3.4 交互旋转缩放里的坐标坑交互部分通常是GL程序最容易和GUI框架打架的地方。wxWidgets的鼠标事件给的坐标分两种物理像素坐标和逻辑坐标。在Retina屏上逻辑坐标和物理像素坐标倍数不为1。在wxWidgets 3.2里主要在重载的OnMouseMotion里拿到的wxPoint是逻辑坐标如果你想用它来驱动模型旋转必须乘以GetContentScaleFactor()转换到物理像素否则在高DPI屏幕上鼠标灵敏度会变奇怪。我的做法是记录鼠标按下时的位置和当前状态void MyGLCanvas::OnMouseDown(wxMouseEvent e) { if (e.Button(wxMOUSE_BTN_LEFT)) { isDragging true; lastMouseX e.GetX(); lastMouseY e.GetY(); } } void MyGLCanvas::OnMouseMotion(wxMouseEvent e) { if (isDragging) { float dx (e.GetX() - lastMouseX) * rotateSpeed; float dy (e.GetY() - lastMouseY) * rotateSpeed; renderer-Yaw(dx); renderer-Pitch(dy); lastMouseX e.GetX(); lastMouseY e.GetY(); Refresh(false); } }滚轮缩放同样要注意坐标wxWidgets的滚轮事件e.GetWheelRotation()可能是25、120或其他值直接当作delta会有问题。建议用GetLinesPerNotch()归一化或者简单除以120作为标准步长。另一个容易踩的坑是窗口尺寸变化。wxEVT_SIZE里获取客户区尺寸后要在设置视口时考虑DPI。void MyGLCanvas::OnSize(wxSizeEvent event) { wxSize size GetClientSize(); int w size.GetWidth() * GetContentScaleFactor(); int h size.GetHeight() * GetContentScaleFactor(); renderer-Resize(w, h); Refresh(false); event.Skip(); }4. 实战渲染 NIfTI 体素数据生成 3D 视图界面和GL管道都跑通后我们来做一个真正有代表性的案例读取NIfTI格式的体素数据把它渲染成可交互的3D视图。这个案例对做医学影像辅助工具、数值模拟结果可视化的人很有参考价值。4.1 数据读取与内存布局NIfTI.nii是一种医学影像通用格式一个文件里除了头部元信息就是体素数据。对于渲染而言最关心的是三个字段dim[1..3]体数据的三维尺寸、datatype数据类型、以及从某个偏移开始的原始体素字节流。最简单的读取方式是把.nii当普通二进制文件处理文件头一般是348字节包括扩展字段第40到第42字节是short类型的dim字段第70字节是datatype。当然严谨做法是找一个已有的解析库但如果你只是做可视化研究直接按规范手动读也完全可行。读取后内存里是一个一维数组按x、y、z顺序排列。常见的体素类型是uint8的灰度值或int16的CT值。注意不要用vector的size当体素数量要用dim[1]*dim[2]*dim[3]来算因为NIfTI头的dim数组第一位是维数标记。这一步最容易出错的是字节序。NIfTI文件可能是小端也可能大端头部字段里有指示。假如忘掉判断字节序大概率读出来全是乱码或者形状反转。4.2 3D纹理上传与着色器体素数据非常适合塞进GL_TEXTURE_3D。把数据上传成3D纹理后GPU就可以在采样时拿到“空间任意一点的值”这对体绘制和切面渲染都关键。glGenTextures(1, texVoxel); glBindTexture(GL_TEXTURE_3D, texVoxel); glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, nx, ny, nz, 0, GL_RED, GL_UNSIGNED_BYTE, voxelData); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);GL_R8是8位灰度纹理一个通道。如果你的数据是CT值映射过的uint16可以改成GL_R16如果原始数据是RGBA就相应调整内部格式和通道。上传纹理这块有个性能注意纹理数据几百MB的时候glTexImage3D一次传输会卡顿好几秒。解决办法是先glTexStorage3D分配空间再glTexSubImage3D分块上传不过做工具软件时如果数据在512³左右一次传也能接受。着色器方面最简单展示体素的方式是三平面正交切片。片段着色器里根据三个平面各自的方向选取一个固定的体素坐标轴将另外两个坐标作为纹理ST坐标采样纹理用GL_NEAREST还是GL_LINEAR取决于你要不要看到插值效果。如果你想做真正的体绘制最常写的是ray casting在片段着色器里根据视线方向计算射线穿过体数据的起点和终点按固定步长采样3D纹理累计颜色和透明度。代码不复杂但循环步长和透明度映射需要调参比较容易出噪点。4.3 体绘制与正交切片两种实现思路正交切片适合快速预览显示三个互相垂直的切面拖动鼠标切换切片位置能看到数据的轴位、冠状位、矢状位三个视角。实现时不需要特殊技术片元着色器里按三个平面的法向量分别取纹理坐标即可。体绘制适合观察整体结构比如CT数据里看骨组织、软组织、血管的空间分布。我用的是最常见的“发射-吸收”模型采样点颜色通过用户定义的传递函数传输函数从灰度值映射到RGBA然后从前到后累加。传递函数的设计决定体绘制效果好坏。一个简单但有效的做法是给用户提供灰度窗口和透明度阈值低于阈值的体素完全透明高于阈值按窗口内灰度映射到颜色梯度。我在界面上加了一个滑块调整阈值每次变化后重新生成传输函数纹理不需要重新上传体素数据。这两个方案切换时要特别注意着色器程序的切换时机。切换前必须确保OpenGL上下文已MakeCurrent否则程序中调用的glUseProgram会作用在别的上下文中产生难以追踪的“黑屏”或“渲染结果不一致”。现在很多项目中出现“我明明改了代码却一脸懵”的惨案基本都出在这。5. 常见问题与排查记录5.1 一列黑屏原因排查清单黑屏是GL嵌入GUI最经常遇到的问题我整理了一个排查顺序表每次黑屏都按这个顺序检查能省一大半时间。序号检查项原因修复方法1有没有调用SetCurrent没有当前上下文GL命令全部无效绘制前必须canvas关联的context调用SetCurrent2像素格式有没有开双缓冲单缓冲导致画面闪烁或黑屏wxGLAttributes里加.DoubleBuffer()3glewInit在哪初始化glewInit必须在有当前context后调用在第一次SetCurrent后、任何gl调用前初始化4glViewport是否更新窗口尺寸变化后视口没变在Resize回调里重新设置视口5纹理是否上传成功数据读取失败或纹理维度不对检查glGetError和glewGetErrorString6SwapBuffers有没有调用画了但不交换缓冲区Paint事件末尾调用SwapBuffers如果前几项都正常还有可能是颜色格式不匹配。比如你用GL_RED上传灰度纹理却在着色器里用texture().rgb读取结果就是全黑或全白。排查时在像素着色器里直接return vec4(1.0,0.0,0.0,1.0)如果画面出现一片红色说明GL管线本身没问题问题出在纹理上传和数据解析。5.2 DialogBlocks 使用的几个小坑DialogBlocks虽然好用但它生成的代码不能随意修改。每次重新生成时它会把Frame构造函数整体重写。如果你在构造函数里手动添加了GL初始化的代码下次生成就被冲掉了。解决办法有几种。第一把业务逻辑全放到自定义子类Frame构造函数只创建控件第二熟悉生成代码的结构把需要保留的代码放进事件函数而不是构造函数第三如果只是偶尔调整布局可以把生成代码后的Frame实现文件备份Diff工具手动合并。另一个坑是自定义控件注册后类名和头文件路径不能有拼写错误。DialogBlocks生成代码时不会为你创建那个类它只是“假设”你已经有这个类。如果你在另一个目录写MyGLCanvas头文件路径没配好编译报错会很长且看不出是因为include路径缺失。第三个坑在事件表上。DialogBlocks生成的Message Map是写到实现文件开头的如果你新加的事件没有放在事件表里而只是写了处理函数你会发现按钮点击没有任何反应。这是wxWidgets经典事件表机制和Qt的connect信号槽不同新手很容易忽略。5.3 OpenGL 崩溃与性能问题在Windows上做GL开发最容易遇到的崩溃是glewInit后调用glGenVertexArrays时机不对。有些人把窗口创建后立即调用初始化但此时context还没MakeCurrent导致所有函数指针为空调用任何glXxx都会访问空指针。解决思路很简单把GL初始化全部放到OnPaint的懒加载块里。性能问题方面最常见的坑是每帧都做冗余工作。比如每帧调用glTexImage3D重新上传纹理每帧new/delete临时对象。体素渲染对CPU内存带宽非常敏感尤其是体绘制那种动不动就采样几百次的操作。我的经验是纹理上传只在数据变化时执行其他时候只绑定和绘制旋转、缩放这类操作只更新Uniform矩阵不重新上传数据。垂直同步也很影响体验。在wxWidgets里可以通过wxGLCanvas::SetVSync(true)开启垂直同步避免画面撕裂。但如果机器跑不满帧率开启VSync会让帧率锁死在显示器刷新率的整数分之一体绘制旋转时会明显卡一下。做工具类应用我建议开发期关掉VSync后期再考虑开。5.4 我的排错流程遇到GL相关的诡异Bug我建议不要凭感觉改代码。先打开你的OpenGL错误输出在初始化后调用glewGetErrorString在每帧渲染后循环glGetError。我总结了一个实用的流程先用一个纯色清屏的着色器替换复杂的体绘制着色器确定管线通不通再加一个四边形确定顶点和纹理坐标传递是否正确最后再加体绘制逻辑。这样一层一层试探很快就能定位问题出在哪个环节。对于线程问题wxWidgets的GL上下文只能在创建它的线程里绑定。如果界面和数据加载在不同线程最好通过wxQueueEvent把数据事件发到主线程处理而不是直接在子线程调用Refresh。这个规则在Windows和Linux上几乎是铁律。我在最初做的时候图省事在多线程里直接刷新窗口结果症状是十天崩一次、每次崩溃栈完全不一样排查了三天才发现是线程调用违规。这一套组合用顺手之后我觉得最值得的就是它把“GUI布局设计”和“渲染逻辑”彻底分开了DialogBlocks让我专注在界面结构上而OpenGL集成层又保持得足够干净。对新上手的人我的建议是先别急着接体数据先跑一个旋转立方体把整条链路走通再逐步加入纹理、交互、数据解析。很多朋友一上来就全栈集成出了问题连是数据错还是渲染错都分不清。等这套基础跑顺了你会发现wxWidgets DialogBlocks OpenGL这套组合在轻量级跨平台工具开发里真的非常能打。
返回列表