转载自我的博客,原文链接:图形学实验框架 Dandelion 始末(一):需求与设计
图形学实验框架 Dandelion 始末(一):需求与设计
和同学聊天时开玩笑说,建设一门课就好像养了一个孩子,时时处处需要关心。今年我为本科的图形学课程编写了全新的实验框架,也算是慢慢走出了完全模仿 CMU 15/462 的阶段,试图更好地在有限的教学条件下展现图形学的趣味。其实在此之前我并未写过如此规模的程序,完成框架的过程也因此成了锻炼自己的过程。如今框架初成,用这几篇文章回顾和总结一些设计与开发的经验,既是一段给后来开发者的说明,也是记一些自己的小故事。
开发前的思考
为什么不继续沿用 CMU 的方案?
在 2022 至 2023 学年,我们的图形学实验方案是直接来自 CMU 15/462 Computer Graphics 这门课程的。但我们只有 32 课时可用,而同学的工程能力是不如 CMU 的,因此去掉了许多实验内容,只留下了其中的六个实验,重组成难度较低的三个实验(必做)与相对较难的三个实验(三选一)。
然而这种策略与其说是删减,还不如说是挑选——因为我们放弃的实验比留用的还要多很多。这导致各实验之间几乎没有关联性可言,很难说是“一套”实验。更麻烦的是 CMU 在光栅图形学上分配的内容较少(只有一个二维光栅化器),所以我们不想采用 15/462 的光栅化实验。为了补齐这一部分,我们又引入了深圳大学的图形学实验,最终带来了更强的割裂感和很多随之而来的麻烦。于是,2022 至 2023 学年的图形学课程就留下了既可喜又遗憾的尾声:可喜的是实验和文档的质量迎来了大幅改善;遗憾的是一路磕磕绊绊,且实验和课堂并不太合拍。
虽然不完全赞同 CMU 的实验题目设计,但我确实钦佩这些教授与 TA 们为了开发 Scotty3D 实验框架所作的努力。他们留下的是一个集场景布局、模型编辑、离线渲染、骨骼动画与粒子系统于一身的全功能框架,并且代码可读性相当不错。
那么我们能否修改 Scotty3D 来适应自己的教学需求呢?我认为是可能的。但 Scotty3D 足足有一万六千行代码,却只有一千多行注释,且没有任何架构文档用于介绍模块组织方式和数据交换。我们的核心开发者仅有两个人,技术实力也偏弱,仅仅有代码可读性还是稍显不够,需要花费很多时间才能完全掌握这个具体而微的框架。这个时间是多久呢?也许一两个月,也许四五个月,我没有把握。
既然如此,不如从头重写一个自己的实验框架吧。这个想法一出现在我的脑海里,就扎根扎得越来越深,不到半个月就驱使我作出了决定。虽然造轮子不是商业美德,但它是学习美德嘛。除了“想做点什么”的动力以外,也有一些实际的考虑:
- 自己的框架是真正“可控”的。这种可控不仅是指增减功能的自由,更是指架构和源代码的理解。只要开发过程能坚持及时记录足够的文档和注释,我们完全可以准确地理解每一个类、每一个接口的功能和设计思路,这是后续开发改进的最重要保障。
- 相较于效率,Scotty3D 更强调编写简单,这在实际教学过程中产生了一些性能问题。对于常见的建模工具,加载、编辑或渲染几万个三角形面的模型是很容易的。但 Scotty3D 的某些设计导致它在进入建模模式后效率很低,部分同学在轻薄笔记本上甚至要顶着个位数的帧率做实验,造成了不小的麻烦。
- Scotty3D 非常强调光线追踪,很多用于光线追踪的代码与程序主体耦合得很紧,想要再编写其他的软渲染器就要大改框架。但我们认为光线追踪并不是渲染的全部,更想要一个像 Blender 那样可以切换渲染器的框架。
- 我们在动画方面的教学力量比较弱,所以其实不需要 Scotty3D 那么强的动画功能。
我们需要多少功能?
一个像 Blender 一样全能的成品是很诱人的,如果我一开始没有想好要舍弃哪些功能,那么之后颇有可能陷入增添功能的热情不可自拔,进而失去了维护文档的精力,也就背离了“可控”的初衷。正如所有软件需求分析的第一步,我需要确定“做什么、不做什么”。
首先我同样想要做一个功能全面的框架,所有的实验都在同一个框架下完成。凭借自己有限的经验和知识,我大致将框架开发工作划分成五个部分:
- 图形 API 和平台 API 抽象:在不同平台上创建程序的运行环境,负责处理退出时释放资源的操作。
- 场景布局和 UI:赋予使用者摆放、操作模型的能力。
- 渲染系统和渲染器:设置渲染参数、实现渲染管线。
- 建模和编辑:修改模型的几何形状,实现一些几何处理算法。
- 动画:基于物理的简单动画。
作为一个带有中文图形界面的软件,这个框架大概会需要加载字体;而要展示三维数据,至少要使用一种图形 API,也就需要创建类似上下文的环境并加载可用的函数地址。如果整个框架只能加载指定的几个模型,那实在谈不上有趣,因此它至少应该能任意加载一两种格式的模型文件,也能将载入的模型放在任何位置、摆出任意姿态。到此为止,可以看作是一个带有 GUI 的模型加载和预览工具。
接下来是对应于图形学三个核心问题(渲染、建模、动画)的功能性要求:
- 由于我们希望这个框架能实现各种渲染流程,显然它应该像 Blender 一样可以切换“渲染器”,每个渲染器都对应一种渲染管线。为避免某个实验过于艰深,可以牺牲管线的灵活程度,只保留较少的可编程部分。要加载纹理贴图并保持通用性,势必为“如何识别种类繁多的图像格式”所扰,大幅增加测试成本。因此,尽管纹理贴图是渲染与几何交叉的重要领域,我还是决定暂且放弃它。
- 这个框架应该具备完成任意形变的能力,同时也能通过几何处理算法做一些半自动的建模。但真正成熟的建模工具需要为提高建模效率而额外开发很多功能(例如基本的对称、区域选择、区域编辑,高级的雕刻等等),这些都可以不考虑。
- 要实现一个骨骼动画系统,至少要处理好关节运动解算、蒙皮变形、关键帧编辑等问题,我们这方面的技术不足,因此直接放弃基于关键帧和骨骼的动画。基于物理仿真的动画则可以比较容易地实现,因为只要舍弃形变和复杂的转动判定,并且只做离散的运动仿真,就不必大量增加功能模块。
功能以外
对于一份作业而言,通常功能即一切;然而对于一个软件而言,功能只是起步。我们的框架是用于教学的,那么如何让同学快速上手规模较大的代码,尤其是如何让同学顺畅地理解不属于实验任务的代码,是非常重要的问题。另外,一个功能全面的框架要包含许多调整和设置用的 UI 元素,那么如何减少同学学习软件操作的成本也值得考虑。
第一个问题通常是由文档解决的。在以往的教学过程中,所有的接口细节都被放入实验手册中,每当我们认为同学需要使用某个接口时就在相应的章节介绍之。要把所有资料都写进实验文档,就要在实验章节末尾附加很长的调用说明和代码片段,排版到分页的 PDF 文档上不便阅读;接口说明与实验讲解绑定的做法也不利于同学之后重新查找。一个成熟的库或框架通常会有树形结构的文档,按照模块组织所有类和接口的含义说明。我准备为所有的接口编写一份类似的开发者文档,既便于同学查找,也有助于后续师弟师妹接手框架继续开发。开发者文档用网页而不是分页文档的形式发布,版式更适合代码,也更方便作小范围更新。
第二个问题则要求软件操作方法足够简单。现代的三维建模工具往往设置大量快捷键和组合键,以提高创作效率。然而我们的目标是让同学在十五分钟以内了解 Dandelion 的所有操作,点击的方式虽然低效但不必专门学习,比快捷键更合适。而各处修改属性数值的 UI 元素也应该尽可能保持相同的风格,这样尝试一次就能会用。从另一个角度来说,一套好的快捷键系统也需要经过反复的讨论、设计和修改,我们并没有完成它的专业素养和时间。
自顶向下的设计:从数据展示开始
软件设计通常有两种思路:一是自顶向下,先设计模块再考虑细节;二是自底向上,先实现功能再尝试组合。
Dandelion 是一个与用户频繁交互的三维设计工具,当我开始设计它时,首先想到的是一些粗略的交互方式、操作界面,以及它应该如何展示各种数据,而不是某些功能具体如何实现。因此,我是采用自顶向下的思路设计 Dandelion 的。
界面与模式
当我只有一个尝试的念头时,Scotty3D 和 Blender 给了我 Dandelion 最初的设计目标。
Scotty3D 和 Blender 共有的界面设计思路是“模式切换”:通过顶栏上模式选项卡可以改变当前的操作模式,不同的操作模式对应不同的功能,但共享同一份三维数据(场景数据)。对于一个全功能的设计软件来说,这样有利于将不同功能的 UI 元素聚集在一起,至少我是想不出更好的解决方案了。
回顾一下之前提到的五个部分:图形 API 和平台 API 抽象、场景布局和 UI、渲染系统和渲染器、建模和编辑、动画
确定了采用这种 UI 方案,就确认了所有模式下共用的基础代码:API 抽象和场景预览。
技术选型
我希望搭建一个跨平台的框架,那么就需要处理各平台上的渲染、UI、文件交互等问题。另外,图形学实验中涉及一些线性代数计算,也需要一个数学库。
首先要选择的就是图形 API 和 GUI 框架。Dandelion 对硬件渲染的需求仅限于预览场景,复杂的渲染管线都属于离线渲染(软渲染)部分。因此,我认为没有必要用 Vulkan 等现代 API 来提高性能,选择跨平台更容易、开发也更简单的 OpenGL 更合适。配合 OpenGL 使用的 loader 是 GLFW 和 GLAD。
在 GUI 方面,完全跨平台的 C++ GUI 方案主要是 Qt 和各类 IMGUI(立即模式 GUI)。从课程实验框架的角度看,
- Qt 能力更全面,但依赖更重、构建更复杂、学习成本更高
- IMGUI 依赖更少,但稳定性可能不如 Qt
由于我更看重轻量化和降低学习成本,一个 IMGUI 库更合适些,最后选择了经典的 Dear ImGui。
文件交互方面,
- Scotty3D 的做法是自己封装跨平台的文件对话框 API,在 Windows / Linux / macOS 上分别使用 Win32 / GTK3 / Apple 原生 API 创建文件对话框
- Blender 的做法是直接实现一套跨平台自绘 GUI,直接封装操作系统的文件操作 API
二者成本都相当高,并且在 Linux 上需要引入额外的依赖 (pkg-config) 来编译 GTK3。后来我找到了 portable-file-dialog 这个项目,它是对各平台原生文件对话框的简单封装,在 Windows 上使用 Win32 API、GNOME / KDE 上调用命令、macOS 上使用 AppleScript,整个项目只有一个文件,依赖的原生 API 或命令(脚本语言)也都很稳定,完全可以满足需求。
数学计算方面有 GLM 和 Eigen 两个候选项。GLM 更轻并且与 OpenGL API 结合得更好,但缺乏求解方程组的能力,而几何处理和物理仿真很可能需要它,所以我还是选择了 Eigen。
GUI 软件的架构模式:MVC
模型 - 视图 - 控制器 (Model-View-Controller, MVC) 也许是最经典的现代 GUI 应用架构模式了。这种架构方法将整个软件分为三部分:
- 进行逻辑处理并提供数据的模型
- 接收数据进行可视化的视图
- 处理输入和交互逻辑的控制器
之后还有 MVVM 等改进的架构,不过对 Dandelion 这个不大的软件来说经典的 MVC 应该够用了。将开发任务按照 MVC 架构进行充足,就形成了一个初步的架构图:
上图中低层 API 抽象、GUI 组件两部分所对应的功能都受到了 Dear ImGui 这个 GUI 库的影响:使用立即模式 GUI 时,开发者在每一帧调用函数来请求绘制 UI 组件,而 UI 组件内部的状态是不可见的,对外表现为无状态 UI。因此,与用户交互相关的全局状态需要在控制器部分维护,以此决定要请求绘制哪些 UI 组件。场景预览的部分直接调用 OpenGL API 渲染而不经过 Dear ImGui,对场景的交互操作(例如旋转、平移视角)也需要通过直接封装 GLFW API 来检测和响应。
考虑场景预览、渲染、动画、建模等过程,用箭头标出模型层与视图层间的数据流向,可以看到场景存储是很多视图模块的数据源,这正是多个模式共用场景数据的结果。
最初原型
根据之前所作的的需求分析、架构设计,第一阶段的原型需要实现的部分是:
- 低层 API 抽象
- 场景存储与访问
- 场景预览
- 物体参数
- 鼠标和按键输入处理
- 全局状态维护
这个原型完成后的效果大致是一个可以打开模型文件并显示在场景中的预览工具,除此以外没有其他功能。最初的设计至此完成,下一步的工作就是开始设计类和接口,开始写代码了。