demo 设计
设计目标
终端演示表明,库中的各个包只需极少的粘合代码就能组合成一个可用的渲染器:一个场景、一个相机、build_draw_list 和一个后端。它还检验了库中测试无法凭肉眼判断的部分(透视、光照、阴影、滑动变焦、曝光效果),并生成可以转换为视频的录像。所有涉及操作系统的东西都放在这里:参数解析、环境变量、时钟、终端和文件。
数学背景
滑动变焦
使用科学相机时,沿观察轴距离为 、高度为 的物体看上去有 行高(在终端压缩之前),其中 (见 view 设计)。要在相机移动时保持主体大小不变,需要
各场景使用以下数值:
| 场景 | 距离 | 基准 | 焦距范围 |
|---|---|---|---|
--hitchcock | 对第 帧为 ,位于 | 13.6–32 mm | |
--dolly | ,位于 | 23–500 mm |
推拉场景的最远距离 是为了让焦距的峰值恰好为 500 mm。位于主体之后、距相机 的物体,其缩放比例为
(相对于基准视图)。当 时,该系数趋近于 :位于立方体后方约 个单位的圆盘墙会膨胀到其基准大小的约 倍,而立方体保持不变。长焦端的相机趋近于正交投影,这就是背景变扁平的原因。测试检查了主体投影大小的比例在各帧之间保持不变。
曝光
曝光模式用 个采样近似图像在快门区间内的时间积分(见 frontend 设计):
其中 是一个动画步长(推拉场景为一帧,网格场景为一次旋转增量)。使用 --flow-exposure 时,每个采样 先通过搜索半径 4、块半径 1 的块匹配光流 扭曲到当前帧上。程序会调用 ExposureSettings::auto,但它总是返回一个采样,因此采样数由演示程序自己的常量 12 决定。
设计决策
所有系统访问集中在一个包中
读取 COLUMNS 和 LINES、帧计时、用 ANSI 转义序列清屏以及通过 moonbitlang/x/fs 进行文件 I/O,是本仓库中仅有的不可移植操作,它们全部位于 src/demo/main.mbt 中。库中的各个包保持纯粹且与目标无关。演示程序留出一行给 shell 提示符,使打印整帧时不会滚屏。
小而宽容的命令行
选项都是按完全匹配识别的普通标志,具有固定的优先级和静默的回退。这使解析器只有寥寥几行,也让演示程序很难因命令行而崩溃,代价是不会报告拼写错误。出于同样的原因,数值选项只保留其中的数字。
忙等待的帧节奏控制
实时循环把 @env.now() 与上一帧的时间比较,并空转直到过去 33 ms。这不需要可移植标准库所缺少的休眠原语,并能提供稳定的节奏。代价是演示运行期间会占满一个 CPU 核心。
流式录制
--record 在内存中构建一个 TuiSequence 并在最后编码,这很简单,也检验了库中的编码器。--record-stdout 逐帧打印同样的格式,因此录制成千上万个大帧也只需一帧的内存;由于帧数可以从时间轴得知,格式中的 frames= 头可以先写出。
正确性与不变量
- 推拉场景的主体保持其投影大小: 按构造为常数(已测试)。
- 每一帧都恰好具有配置的宽度和高度,因此录像能按其文件头中的尺寸解码。
- 录像的帧数为
Timeline::frame_count()= 。 - 曝光权重之和为 1,因此曝光帧保持在其各采样的亮度范围之内。
被否决的方案
- 参数解析库 会为十几个标志引入一个依赖。
- 原始终端模式和增量重绘 能减少闪烁,但需要特定于平台的终端控制。
- 在帧之间休眠 需要在每个目标上使用 FFI。
边界
演示程序不会:
- 导出任何 MoonBit API,或在
native以外的目标上运行; - 运行时读取输入、随终端调整尺寸或提供配置文件;
- 校验选项或报告未知选项;
- 以彩色或在浏览器中渲染(见 demo_canvas 和 demo_gsap)。