下载 App
开发日志 03|不用贴图,怎样画出一个会变化的水墨世界
《一念万物》的水墨画面由程序绘制。树、石头、池塘、鸟和鱼的画法集中在 Paint.gd,四季使用不同配色,再叠加水墨与宣纸着色器。这样做让物件外观能够随着世界状态变化,同时也带来了一个很实际的问题:每帧重新画大量笔触,浏览器能否承受?
从形状到笔触
画面主要使用笔触 stroke、brush 和墨块 wash。它们让边缘、宽度与浓淡有变化,避免所有物件都由等宽线和正圆构成。四季调色板分别控制天空、远山、地面、水、墨色与强调色,夜间另有一套配色。
笔触负责物件的形状,水墨着色器负责画面整体的墨感,宣纸叠层提供底材质感。几层分别处理,调整某个物件的轮廓时,不必把纸纹也重做一遍。
程序绘制还要让状态可读。鱼安全与否、水位高低、花是否开放,不能只在数据里变化,画面必须跟着表达。鸟的起飞、鱼的转向和蛙的跳跃也需要自己的运动节奏。

图 1|当前第一关收束画面。物件笔触、远山、宣纸与水墨后处理共同组成画面;这张截图展示视觉结果,不展示帧间运动。
判定位置与表现运动分开
鸟的逻辑位置用于落脚与状态判断,表现位置则加入起飞加速、落点前减速和向上拱起的飞行弧线,身体随升降改变姿态。蛙跳上荷叶时抬起一道弧,后腿蹬直,出入水时渐显渐隐。
鱼群判定使用领头位置,画面里每条鱼保留各自的位置、朝向和摆尾相位。它们追随落点、逐渐转头,转弯时身体弯曲,游得快时摆尾也更急。这样,判定可以保持清楚,运动又不会显得像一个刚性图标在平移。
这些变化仍然由世界的 step(dt) 推进,不交给独立的 Tween。自动验证推进模拟时间时,也能推进相关状态,避免游戏中能演、自检中却停住。

图 2|2026-10-12 第 25 关收束实机截图。秋季的地面、树叶与强调色使用对应调色板,仍沿用公共绘制接口。

图 3|同日第 37 关收束实机截图。配图用于说明季节画法,不作为性能优化前后对比或帧率证据。
性能瓶颈出现在绘制调用
2026 年 10 月 5 日的优化记录中,第 1 关每帧曾新建并销毁 106 个顶点数组,执行 213 次 bufferData,产生 137 次绘制调用。大量墨块各自提交给引擎,开销不只来自 GDScript 计算,也来自渲染提交。
修改后的 Paint 先把图元转换为三角形,累积顶点与颜色,最后通过 flush() 统一提交。单位椭圆与墨块形状按段数或种子缓存,再通过仿射变换改变位置、大小和朝向,减少逐点重复计算。
这也形成了一条必须遵守的约定:使用 Paint 的绘制回调最后调用 Paint.flush(),图元都通过同一套接口提交。混入直接绘制调用会破坏原有顺序;遗漏提交则可能让画面缺失。
当时的桌面浏览器测量里,第 1 关顶点数组新建从 106 降到 1,绘制调用从 137 降到约 8,主线程耗时从 4.9 毫秒降到 3.1 毫秒。这些是那次测量的记录,不代表所有设备的表现。
缓慢变化的背景不用每帧重画
天空、远山与地面拆到 Backdrop.gd。这一层每次生成两三百个图元,但变化主要是草的轻摆与云痕,当前每秒重画 10 次,其余帧沿用已有指令。画面范围或配色改变时立即重画,减少动态的设置下则只在必要变化时重画。
Web 导出还把渲染像素比封顶为 2,控制高像素密度屏幕上的填充量。不过,真实 3 倍屏上的文字观感与触控仍未在性能记录中验收,不能仅凭像素数量下降就宣称体验更好。
优化之后,画面有没有变?
那次绘制优化前后各生成了一遍逐关截图,共比较 152 张图。差异超过阈值的像素比例中位数为 0.006%,最大为 0.24%,主要来自按真实时钟变化的鱼尾与蝶翅。比对期间还发现并修复了屏风空心矩形横边变细的问题。
这个小问题说明,减少调用次数只是优化的一半。绘制实现改变后,线宽、透明叠加和遮挡顺序都可能变化,需要把“更快”与“仍然画对”分别验证。当前记录仍把手机帧率、发热和声音听感列为待验证内容,桌面结果只能说明这次优化在已测环境里有效。





