王宇没有从“大而全的渲染引擎”讲起。他考虑的是一个更具体的问题:客户端的大部分界面由常规组件组成,只有某个局部需要高频绘制、曲线交互或特别的动画,怎样满足它,又不让整个应用为此改变架构?
这段分享来自上海“移动端技术实践”录播,稿件发布于 2022 年 3 月 13 日,实际活动日期未确认。视频以当时的 iOS 与 OpenGL ES 实现为例,这些接口选择不等于今天的新项目选型建议。
什么情况下,自己做一个引擎才有意义
王宇先承认 UIKit 等常规组件可以覆盖大部分交互。遇到特殊界面,也已有其他图形框架和现成引擎可用。自研的理由不能仅仅是“能够画出来”,而要对应现有方案难以合适满足的局部需求。原片 01:04—03:59
他给出的判断包含四点:自定义内容只是原生界面中的一部分;基础组件难以直接表达;交互对绘制速度有较高要求;承载的业务逻辑相对简单。以手势驱动曲线的场景为例,目标是让曲线随着输入变化,又能轻便地嵌入普通页面。引入完整 UI 引擎可能带来不成比例的代码量和接入工作。原片 04:00—06:19
这仍然需要实际测量。分享中的性能动机来自特定实现,不能由此推导出“系统绘图全部依赖 CPU”或“自写 GPU 代码必然更快”。自研是否值得,取决于具体瓶颈、维护成本和集成代价。
第一层基础:物体怎样出现在画面里
为了让客户端开发者理解后面的抽象,王宇花了较多时间回顾图形知识。他先解释 Model、View、Projection:模型变换确定物体在世界中的位置,视图变换表达观察位置和方向,投影变换则决定怎样投到可显示的范围。
透视投影体现近大远小,正交投影适合另一类空间表达。王宇用球体和立方体的相对位置说明,同一组物体随着观察方式变化,可以得到不同画面。这些变换是完整成像流程的一部分,不能把矩阵相乘简单等同于已经完成所有屏幕映射。原片 07:15—11:25
接下来是光栅化管线:输入顶点、颜色和纹理等数据,处理几何,再形成片段,计算片段输出并经过相应测试与混合,最后写入目标缓冲区。分享重点介绍顶点着色器和片段着色器两个可编程环节,帮助理解应用代码究竟能在哪些位置影响结果。原片 11:26—14:09
第二层基础:把数据、程序和输出目标连接起来
以 OpenGL 风格的接口为例,一次绘制先需要上下文来承载相关状态,再准备顶点、纹理坐标、纹理等输入数据。着色器程序也有独立的创建、编译和链接过程;最后把合适的数据与程序关联起来,提交绘制。
这一过程输出的目标可以是供显示的缓冲区,也可以是后续处理需要的图像。把“准备数据”“准备程序”“执行绘制”“呈现结果”拆开,后续设计引擎接口时就有了对应的职责边界。原片 14:09—17:42
API 的抽象:场景里有什么,观察方式是什么
王宇借一个常见的 3D 绘制示例解释高层 API:先创建场景、相机和渲染器,再用几何数据与材质构成网格,把网格和光源放入场景。需要动画时,在连续更新中改变物体的位置或姿态,再把当前场景与相机交给渲染器。原片 17:47—23:53
这段示例的作用,是说明如何把底层图形操作整理成开发者能使用的对象。它不意味着每一个小型引擎都必须复制完整的 3D 类体系;后面他恰恰会根据自己的需求删减这些抽象。
自研目标由此收敛为三个:性能与体量符合局部需求,能够像普通视图一样接进现有项目,并为不同绘制场景留下扩展位置。没有明确目标,就无法判断新增代码究竟换来了什么。原片 23:53—26:13
每一帧,从更新到显示
从概念上看,一轮渲染需要更新观察方式、场景元素和相关状态,再绘制并呈现结果。实际实现中,输入和绘制未必适合放在同一处执行,因此王宇把 UI 侧生成的帧请求封装后交给队列,再由负责渲染的执行环节消费。原片 26:14—30:30
这里的队列解决的是两端速度不同、请求交接和同步的问题。它不是一项“放进去便不会丢帧”的保证;具体系统仍要决定队列长度、积压时怎样处理,以及显示节奏与任务完成时间怎样配合。视频中的“GPU 线程”也是应用侧负责提交渲染工作的线程角色,不能和 GPU 的硬件执行单元混为一谈。
消费请求后,引擎准备绘制数据与命令,组织所需程序,执行渲染,再把结果呈现到视图中。这条链路将输入组织、命令准备和实际绘制分开,使业务输入不必直接操纵每一个底层图形调用。原片 30:08—31:34
Thread、Event Loop 和 Task Runner 分别负责什么
王宇专门解释了三个容易混在一起的概念:线程提供执行载体;事件循环持续接收和处理工作;Task Runner 则提供提交任务的接口。通过不同的任务执行入口,可以把逻辑和绘制组织到合适的执行环境中。原片 31:35—34:23
但轻量组件并不一定值得永久占有两条专用线程。以 iOS 为例,他尝试利用系统已有的主队列和串行队列组织工作,并提醒:队列、线程与 Run Loop 不是同一回事,不能假定换了执行方式后,原先依赖的界面提交时机仍会自动成立。
分享提到,他在这一过程中检查过渲染事务的提交,并处理了没有预期 Run Loop 行为时的更新问题。这是具体接入时需要验证的环节,而不是给所有异步绘制统一加一个调用就能解决。原片 34:25—37:05
用视图承载输出,用 Command 承载扩展
在 OpenGL ES 示例中,王宇继续解释 framebuffer、颜色与深度等 renderbuffer,以及如何把输出与 CAEAGLLayer 连接。需要准备和绑定相应存储,再在绘制后呈现结果。对业务层,他希望这些细节最终收在一个普通视图形态的组件里。原片 37:06—39:41
由于目标主要是局部二维内容,不需要完整的光照和可移动相机体系,他把入口进一步缩减到渲染视图与命令队列。Surface 负责协调视图、底层显示层、上下文和输出目标,Command 则描述每次需要执行的绘制。原片 39:42—42:57
命令可以只执行一次绘制,也可以按时间采样形成动画;还可以把前一个命令的输出作为后一个命令的输入,组成滤镜处理链。每种命令再准备自己的 shader、顶点、纹理与其他数据。扩展点因此落在“新增一种绘制操作”,而不是每次都改动整个引擎循环。原片 42:59—44:43
应用案例:从手势和配置走到绘制
最后的曲线案例面向儿童描画交互:用户操作小刷子沿路径画线,完成后呈现动态的绘制效果。引擎在这里服务于一个具体的产品体验,而不只是展示能够画出图形。原片 44:45—45:14
王宇把处理拆成手势采样、路径描述、客户端逻辑和渲染几个环节。输入路径先形成描述或配置,再由绘制实现消费这些数据。这样,产品交互和图形算法能够通过明确的数据形式连接。原片 45:15—47:21
王宇也提到接入骨骼动画运行时,并开始说明图集等概念。现有录播在这一段结束,没有包含其完整后续讲解或问答,因此这里不补写实现细节。原片 47:21—47:44
这场分享最值得保留的是从局部需求逐步推导抽象的过程。具体 API 则有历史边界:Apple 已从 iOS 12 起弃用 OpenGL ES,并建议使用 Metal。阅读旧实现可以帮助理解渲染系统的组织,但新项目仍应依据目标平台选择受支持的图形方案。Apple 官方说明
