图形学实验框架 Dandelion 始末(六):运动和碰撞

《始末》系列是关于 Dandelion 1.0 版本的个人回忆,而这是系列里关于实现过程和细节的最后一篇。完成前面的诸多工作之后,利用场景、物体数据和渲染机制,再配合独立性很强的求解器,物体就可以运动起来了。

图形学中的动画

我把实时的动画渲染粗略分成三种,实现的工作量(未必等于难度)依次增加:

  • 运动动画:把物体视为刚体,根据刚体运动学和动力学改变物体的位置和姿态。
  • 骨骼动画:把 Mesh 作为蒙皮,绑定到骨骼(端点相互连接的一组刚性杆)上,让 Mesh 随骨骼的运动发生形变和运动。
  • 物理动画:物体是流体或软体,使用划分微元的方法实时模拟物体各局部的运动,从而产生形变或运动。

这种划分不怎么严谨,因为上述三种动画是可以相互交叉的。比如物理模拟也包括刚体物理模拟,而刚体模拟的反馈也可以影响骨骼的运动,并没有严格的界限。对 Dandelion 来说,第一种机制最容易实现,后两者则都需要大幅度地完善甚至重构框架。所以,Dandelion 最终只实现了刚体运动动画,甚至还不包括刚体的转动,只完成了平动。这也是我认为 Dandelion 在动画方面最为薄弱的原因。

不过既然作了这样的取舍,就是想要快速高效地实现刚体的运动和碰撞,而后续的发展也确实如我所愿。

让画面动起来:解算和更新

发布实时渲染实现这篇文章到现在几乎过去了一年之久,所以我想先简单回顾一遍 Dandelion 逐帧渲染的流程:

  1. Controller::render 函数中,根据 main_camera 的参数计算出 View / Projection 矩阵,并设置 shader 中的 uniform 变量。
  2. 进入 Scene::render 函数,遍历所有的 Object 并调用 Object::render 函数渲染物体。
  3. 进入 Object::render 函数,设置 shader uniform 变量传入 Model 矩阵和材质参数,遍历所有的 GL::Mesh 对象,调用 GL::Mesh::render 渲染 mesh 。
  4. 调用 glDrawArrays / glDrawElements 绘制图元。

在第三步里,Object 的 Model 矩阵是每帧都会发送到 GPU 的,所以我们只要稍加改动,让 Model 矩阵随时间变化,就可以制造平动和转动的效果。而刚体的平动方程非常简单:

{dxdt=v(t)dvdt=a(t)\begin{cases} \frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t}=\mathbf{v}(t) \\ \frac{\mathrm{d}\mathbf{v}}{\mathrm{d}t}=\mathbf{a}(t) \end{cases}

只要给一个边界条件 x(0)=x0\mathbf{x}(0)=\mathbf{x}_0 就能定解,再用这个解更新物体的位置 x(t)\mathbf{x}(t) 就是平动了。求解微分方程的工具就是求解器,在 Dandelion 中,解算器也和渲染器一样是与场景数据解耦的,有统一的接口:输入 tt 时刻的状态 KineticState 和步长 Δt\Delta t,计算 t+Δtt+\Delta t 时刻的新状态。而这个过程会发生在渲染下一帧之前,也就是在上述第二步和第三步之间插入更新运动状态的操作。

实际上 Δt\Delta t 一般是定值,用于控制微分方程的数值求解精度。而渲染两帧的时间间隔并不是定值,这就要求我们将累加的时间以预设的 Δt\Delta t 为单位重新划分。由于实验手册里已经讲了很多,这里就不再赘述。

到这里,运动就算是完成了——是的,就是这么短,加入的修改也很少:

  • Scene 中增加了一个状态,区分动画是正在播放还是被暂停。
  • Object 增加了一个运动状态 KineticState,还增加了一个调用求解器的 Object::update 方法。
  • Scene::render 中额外做了一些检查,并负责调用 Object::update

求解器本身就是一个函数,Object 类有一个静态成员负责保存现在要用的求解器:

static std::function<KineticState(const KineticState&, const KineticState&)> step;

碰撞与响应

其实加入碰撞响应的动机非常朴素:在 siyuanluo 学弟写好求解器、我加入状态更新机制以后,我们感觉就这么点儿动画功能是不是太乏味了?这时我恰好想到为渲染器而实现的 BVH 也能用来加速碰撞求解,那就加个碰撞吧!

我选择的碰撞判定机制是 Mesh 对 Mesh 的离散检测 (Discrete Collision Detection),要在每一帧两两检查物体是否存在穿插,存在则视为碰撞,并回退碰撞物体的位置、修改它们的速度。只要稍微修改 Object::update 轮询其他物体,然后分别试试 naive_intersectBVH::intersect 能否正确检测到碰撞,这部分代码就算完成了。

回顾

相较于之前的每一段工作,动画部分的编程实在是太快太省,以至于为完善文档多留了一些时间出来。尽管如此,2023 年完成 Dandelion 1.0 版本也消耗了我几乎所有的课余时间——以及一部分工作时间。到研一的期末考试完全结束后,我甚至一度连续工作了 42 天(从 GitHub 提交记录上数出来的),那真是一段忘我投入的日子。1.0 版本从 4 月开发到 9 月,当我敲下 git tag v1.0.0 并在 GitHub 上创建第一个公开 Release 时,与小伙伴们和老师一起工作讨论的经历确实如走马灯一般在眼前闪过。

相较于许许多多杰出的开源项目,Dandelion 的开发者既不算多也不算强,所以从第一篇到第六篇,我在总是在谈“取舍”,更甚于谈“优化”。这些取舍最终让 1.0 版本得以同正好 100 页的实验手册(以及说不清多少的开发者文档)一起及时发布,但也确实给我留下了许多遐想。与我而言,Dandelion 既是作品也是产品。从作品的角度看待它,我总有新的想法、新的期待和追求;从产品的角度看待它,能够为本科的同学打开一扇初窥图形学的小小窗户,并且保证这窗户不因意外的风雨而破损,这是最重要的。所以,《始末》系列的下一篇也是最后一篇文章中,我想谈谈自己为什么要成为助教、对一门计算机系专业选修课的认识和期望是什么——换言之,在第一个 commit 的起始之前以及 1.0 版本结束之后,我作为 Dandelion 背后“第一作者”的所思所想。


图形学实验框架 Dandelion 始末(六):运动和碰撞
https://greyishsong.ink/图形学实验框架-Dandelion-始末(六):运动和碰撞/
作者
greyishsong
发布于
2025年1月5日
许可协议