· Sharp.go Team
引擎内部:sharp.go 如何以 60fps 渲染 361 颗棋子
五年前,具备完整 3D 检视能力的 60fps 围棋棋盘是渲染器的梦想。今天它只是渲染器的午后工作。本文是我们开始时所期望的那种注释式漫游 — 架构选择、死胡同,以及产生差异的细微之处。
● 问题
01为什么这很难
塑造了当前 3D 引擎的三项约束。
一盘围棋是一棵树,而不是一条线。在任何时刻,玩家都能分支出成千上万种变化,每一种都在 19×19 棋盘上拥有自己关于棋子的幻想。我们想要同时拥有三者:可交互的 3D 相机、低于 100ms 的分析延迟、以及走过第 200 手仍不崩溃的棋谱。
每一项单独都可解。问题在于它们都触及同一条热路径 — 棋子落盘的瞬间 — 而每一条需要的时间又各不相同。
● 着法编码
0221 字节的线路格式
紧凑、确定、可跨版本重放。
sharp.go 中的每手棋都是 21 字节的载荷。我们试过 JSON、CBOR 与 protobuf,最终选择了手工编写的二进制格式。格式足够紧凑:一盘 400 手的真实对局仅占 8.4 KB — 与棋盘自身的压缩 JPEG 大致相当。
- 01.
字节 0 — 颜色
黑为 0x00,白为 0x01。一字节,不协商。
- 02.
字节 1–2 — 坐标
两个小端字节:行 (0–18) 与列 (0–18)。19×19 网格上的 361 个有效位置。
- 03.
字节 3–6 — 序号
无符号 32 位着法索引。同一对局内连续,跨重放确定。
- 04.
字节 7–10 — 挂钟时间戳
对局开始以来的毫秒数。用于重放拖动。
- 05.
字节 11–14 — 局面哈希
32 位 Zobrist 哈希。无需扫描整个棋盘即可检测重复局面与超级劫。
- 06.
字节 15–20 — 引擎信号保留位
提子、劫标记、虚着标志。为前向兼容而保留。
"
我们最初并不是要发明一种二进制格式。我们最初是要让分析感觉即时,而字节数恰好是最后最容易放松的约束。
"
● 渲染管线
03三趟渲染,一块画布
顺序比成本更重要。
渲染器是严格的三趟管线。我们不在趟之间排序、混合或反射 — 每趟写入自己的纹理,下一趟再读取。
● Tabs
● 设计决策
04为什么先做 3D
第一个产品的设计依据,而不是对 2D 的判决。
● Comparison
引擎对比
| ● Feature | sharp.go | 2D 客户端 | 手机应用 |
|---|---|---|---|
| 3D 棋盘检视 | ✓ | ✗ | ✗ |
| 低于 100ms 的 AI 反馈 | ✓ | ✗ | ✓ |
| 开放的棋谱格式 | ✓ | ✓ | ✗ |
| 协作复盘 | ✓ | ✗ | ✗ |
| 两次点击的分享链接 | ✓ | ✗ | ✓ |
| 仅浏览器模式 | ✓ | ✗ | ✗ |
- ✓
整个棋盘上的实例化渲染
361 个实例,一次绘制调用。其他方式都更慢。
- ✓
缓存的反射,而不是实时的
相机移动超过 5° 时重渲染。玩家察觉不到。
- ✓
磁盘上的二进制着法格式
紧凑、确定,2026 年的对局重放在 2027 年仍能加载。
- ✓
作为一等公民的深度链接 URL
每个局面都有稳定 URL。分享棋盘是复制粘贴,不是截图。
● 常见问题
01 引擎为什么对局面使用 Zobrist 哈希? ▾
Zobrist 哈希每手 O(1) 更新 (对变化位做异或),并允许我们通过一次 32 位比较检测超级劫与重复局面。每手都扫描整个棋盘在 19×19 上没问题,但无法扩展到我们计划支持的 25×25。
02 我可以在另一台设备上从 URL 重放对局吗? ▾
可以。每盘对局都有稳定的 URL,着法序列被编码在路径中。在任何设备、任何浏览器打开该 URL,重放都会从同一局面开始。
03 AI 在离线时也保持同一水平吗? ▾
是的。设备端 AI 完全离线运行,默认校准到初学者级别。你可以在菜单里调到更高的强度,无需任何网络往返。
04 为什么二进制着法格式未在公开规范中记录? ▾
稳定性。该格式是我们重放兼容性承诺的一部分。我们将在 1.0 引擎发布时公布稳定的 v1 规范。在此之前,小版本之间可能发生变化。
● 下一步
05路线图,不是承诺
我们正在做的工作,按预期成本排序。
接下来的六个月围绕重放的可移植性。你下的每盘棋都应该比下棋的客户端更长寿。
"
最好的围棋界面是消失的界面。你看到的是棋盘,而不是应用。
"