backend/gsap 设计
设计目标
GSAP 后端把 DrawList 显示为与分辨率无关的矢量图形,并让成熟的动画库 GSAP 负责播放:播放、暂停、倒放、跳转、调速和循环。场景数学留在 MoonBit 中;JavaScript 只保存多边形并运行时钟。由于 SVG 没有深度缓冲,后端也没有,因此必须通过绘制顺序决定可见性。本页说明这种顺序何时是精确的。
数学背景
画家算法
SVG 按文档顺序绘制其子节点,每个都覆盖之前绘制的内容。如果三角形按由远到近的顺序输出,较近的表面就会覆盖较远的表面。后端用每个三角形在相机空间中的平均深度作为排序键,
按降序排序,并列时按其在绘制列表中的位置决定,因此结果是确定的。
何时顺序是精确的。 屏幕投影不重叠的两个三角形可以按任意顺序绘制。对于重叠的三角形,只要较近的那个后画,顺序就是正确的。一个充分条件是它们的深度范围互不相交:若 ,则 ,于是 先画,而处处更近的 覆盖它。
单个凸物体总是精确的。 设网格围成一个凸体,并且前端已去掉背面。从眼睛穿过某个像素的射线至多与凸体的边界交于两点:从一个正面进入,从一个背面离开。因此每条射线上至多有一个正面,不同正面的投影至多沿公共边重叠。于是任何绘制顺序都是正确的,排序后的顺序当然也是。立方体、球、圆柱、圆锥和棱锥都是凸的。
何时会失败。 对于非凸网格(圆环)和由多个物体组成的场景,重叠的三角形可能有重叠的深度范围。此时平均深度只是一种启发式方法,而且有两种配置会让任何以整个三角形为单位的排序失败:
- 循环重叠: 覆盖 的一部分, 覆盖 的一部分, 又覆盖 的一部分;
- 相互穿插:两个三角形相交,因此在相交线的两侧各自位于对方前面。
要得到正确结果,需要拆分三角形(Newell 算法)或使用深度缓冲。Canvas 后端有深度缓冲,在这些情况下是精确的。11 M. E. Newell, R. G. Newell and T. L. Sancha, “A solution to the hidden surface problem”, Proc. ACM National Conference, 1972.
着色
填充色使用与 Canvas 后端相同的量化:,颜色为 ,亮度误差至多为 。
时间是唯一的输入
GsapPlayer 以线性缓动把一个时钟对象从 补间到 秒。对于时间轴位置 (以 GSAP 自己的时间计,已乘以速度因子),回调在 时收到 。MoonBit 一侧把场景渲染为 的纯函数:。因此跳转、倒放、循环和调速都不需要额外状态:无论 GSAP 报告哪个 ,画出的都是该 对应的画面。更新以浏览器的刷新率到达,而不是固定帧率,因此后端从不假设帧间隔。
设计决策
采用画家排序的矢量输出
问题:由三角形生成清晰、可缩放、浏览器可以保留并重新设定样式的输出。SVG 多边形正好如此,但 SVG 没有深度缓冲。可选方案有:光栅化成图像(失去矢量的优点)、拆分三角形以获得精确顺序(一般情况下复杂且慢),或对整个三角形排序。最终选择了排序:它对本库的凸基本形状是精确的,对典型场景接近正确,而且只需 的开销。
以平均深度为键,平局保持稳定
平均深度计算便宜,且对顶点对称。按原始下标打破平局使输出确定,因此测试可以断言精确的多边形顺序,静态场景的各帧也不会闪烁。
复用 DOM 节点
每次调用都复用已有的 <polygon> 元素,只增加或删除数量上的差额。每帧创建数千个元素会主导开销并给垃圾回收器带来压力;而每个多边形更新两个属性则不会。标记 data-gsap-svg-background 和 data-gsap-svg-triangles 让后端能找到自己的节点;对于尚无这些节点的 <svg>,后端会替换其子节点。
由 GSAP 掌管时钟
播放控制在 GSAP 中已是成熟的问题。后端只暴露一个最小且经过截断的子集(seek 限制在时长内,set_progress 限制在 ,正的速度因子,重复次数 ),使 MoonBit 调用者无法把时间轴驱动到演示界面未预料的状态。GSAP 在构造时从 globalThis.gsap 查找,而不是导入,因此由页面决定如何加载它(CDN、打包器或本地副本)。
正确性与不变量
render_draw_list之后,三角形分组中多边形的数量恰好等于绘制列表中三角形的数量,并按平均深度降序、平局稳定地排列。- 只要重叠的三角形深度范围互不相交,画面就是精确的,特别是对背面剔除后的任何单个凸网格。
- 填充色是有效的
rgb(r, g, b)字符串,通道位于 。 - 对于通过其方法传入的值,
GsapPlayer保证 、、速度因子 、重复次数 。
每帧开销:前端管线、对 个三角形的 排序,以及 次属性更新。
被否决的方案
- 在 SVG 中逐像素保存深度(例如每个像素段一个多边形)会放弃 SVG 的所有长处。
- BSP 树或 Newell 算法。 两者都能给出精确顺序,但会拆分三角形,并使多边形数量逐帧变化;对演示场景而言,平均深度排序的可见错误既少见又短暂。
- 从 MoonBit 驱动
requestAnimationFrame。 这会重新实现 GSAP 已经提供的播放、暂停、倒放和跳转。
边界
GSAP 后端不会:
- 在
js以外的目标上运行,或在没有 DOM 和全局gsap的情况下工作; - 解决循环重叠或相交的三角形;
- 添加描边来掩盖相邻多边形之间可能出现的细微抗锯齿接缝;
- 构建播放器界面、加载 GSAP,或决定在某个时刻渲染什么(这些由
demo_gsap包负责); - 允许调用者从包外更改颜色或明暗级数(配置字段在包外是只读的)。
Footnotes
-
M. E. Newell, R. G. Newell and T. L. Sancha, “A solution to the hidden surface problem”, Proc. ACM National Conference, 1972. ↩