上一篇《雪:更快速更流畅的气象数据动画》里,我把预报变量打包成单文件容器 .xue,浏览器按索引取帧播放。同一个 GFS 时次的两个变量,逐帧预上色的 PMTiles(zoom 0–4)合计 3,205.87 MB,源 GRIB2 是 137.73 MB, .xue 压到约 65 MB。体积下来了。自定义容器的问题是只有自己的解码器能读它。

2026-09-14 我把这套格式发到 Pangeo 的论坛,第一条回复来自 Tom Nicholas,问的是:这个二进制格式不是性能的来源,需要的是选一组适合可视化的 Zarr chunk 形状,再加上浏览器端用 WASM 解码?

回答是。加速来自两件事,量化成 uint8 单字节平面(压缩前小 4 倍,解出来直接作为 WebGL2 的 R8 纹理上传),以及按播放习惯切的 chunk。两件事在标准 Zarr v3 里都能表达。

从 2026-09-15 起 .xue 退出线上发布:每个时次、每个滚动窗口轮次、每个历史个例都只发布 Zarr v3 store,由一份静态 STAC 作为数据入口。 .xue 的原始编解码代码仍然保留,用作后续优化数据编解码方案的参考。

现在的链路按数据流向分为几段:

  1. 生产:GitHub Actions 定时取上游数据,量化、编码成 Zarr store 并发布;
  2. 目录:静态 STAC 目录列出每个源的当前时次;
  3. 分发:Cloudflare R2 存数据,CDN 缓存,客户端用 HTTP Range 按需读取;
  4. 渲染:浏览器里 Rust WebAssembly 解码,WebGL2 投影和着色;
  5. 分析:Python 脚本直接读 store,synoptic-analysis Skill 生成天气形势报告。

数据源

源网格与节奏
NOAA GFS0.25°,逐小时至 F120,之后 3 小时至 F240
NOAA GFS Sflux约 13 km 高斯网格,同 GFS
ECMWF IFS 开放数据0.25°,3 小时至 F144,6 小时至 F240
ECMWF AIFS Single0.25°,6 小时至 F360
ECMWF IFS HRES(经 Open-Meteo)0.1°,逐小时至 F090、3 小时至 F144、6 小时至 F360,只有 00Z 与 12Z
NOAA HRRR0.03° 美国本土,逐小时至 F18
NOAA GEFS-Aerosols0.25°,3 小时至 F120
NCEP CFSv2地面 0.9375°、高空 1°,6 小时至 F6552(39 周),只有 00Z 与 12Z
NOAA MRMS 雷达0.02° 美国本土,每 2 分钟,滚动 4 小时
JMA 降水短时预报0.005° 日本,每 5 分钟,滚动 3 小时
CMA 雷达拼图0.0439° 中国,每 6 分钟,滚动 3 小时
Himawari-90.04°,每 10 分钟,滚动 6 小时
GOES-19 / GOES-180.04°,每 10 分钟,滚动 6 小时
Meteosat-120.04°,逐小时,滚动 24 小时

另有三个点产品:探空(一站一次升空)、机场(METAR 与 TAF)、热带气旋(逐场路径与各机构及模式的预报)。点产品是 JSON,按站点或风暴用 Range 取一行,不进栅格格式。

源分两类。预报源按起报时次发布,一个时次一个目录,新时次到了替换旧的。观测源(雷达与静止卫星)是滚动窗口,每隔几分钟构建一轮,窗口向前推一段,已经取过的帧不重下。没有哪个源从头到尾只有一个步长,所以时间轴直接声明单位与每一帧的偏移量,不假设等间距。

这里的实时指持续更新的预报和近实时观测:预报在起报后 4–8 小时之间落地,观测窗口的末端落后实时几分钟到半小时不等,取决于上游的发布延迟。

各源的差异在取数阶段消化掉。静止卫星从地球静止投影落到经纬网格、IFS HRES 从缩减高斯网格重采样、HRRR 从兰伯特投影重采样,都在取数侧做一次,之后的量化、分组、写 store 对所有源是同一条路径。

以 MRMS 为例,产品每两分钟发一帧,但雷达体扫四到六分钟一圈,新的低仰角扫描没到,这一帧在该雷达的覆盖内就是上一帧的逐值重复。带来的视觉效果是虽然帧率高,但是拼图上回波的移动断断续续,能看到回波的色块虽然整体有一致的方向,但是细节处各走各的,呈现出一种跳跃感。

格式与量化

访问方式是连续播放一整轮全球预报,加时间轴任意拖动,这个约束决定了格式的取舍。

每个变量量化成单字节:2 米温度 0.5 °C 一档,覆盖 −60 到 50 °C,最大误差 0.25 °C;降水用对数码本,上限 128 mm/h,保住小雨的分档。量化之后数据保留数值含义,浏览器既能上色播放,也能查格点的值、比较模式。

数据放在模式的原生网格上,不重投影、不预上色,投影和调色板都交给 GPU。时间上 6 帧一组,平滑场组内做帧间差分,降水按原值存,因为降水区跟着天气系统移动,帧间差反而更大。空间上切成瓦片,瓦片尺寸按源设定(GFS 与 ECMWF 是 52 × 48),浏览器只取视口覆盖的瓦片。

布局

一个 bundle 是一个 Zarr group,里面一个变量一个 uint8 数组,形状 [帧, 纬度, 经度],另有时间、纬度、经度三个坐标数组。内部的 chunk 是 6 帧乘一个瓦片,每个 chunk 一个独立的 Zstandard 帧。

一个数组只切一个 shard,覆盖整条时间轴和整个网格,所以一个数组就是桶上的一个对象。一个标量 store 是 9 个对象,与帧数无关,GFS 一个时次约 1,200 个对象。 shard 的索引放在对象尾部,客户端用一次 HTTP 后缀 range 取回,之后每一帧、每个视口都靠它定位。

发布的 codec 链是标准的 [bytes, zstd],任何 Zarr 客户端不装插件就能读。 store 里带着 bundle 的元数据和 CF 的 scale_factor / add_offset,xarray.open_zarr 读出来就是物理量。

目录

每个时次写一份 manifest.json,旁边派生一份 STAC 1.1.0 目录:根 Catalog 下一个数据源一个 Collection,一个时次一个 Item,滚动窗口按轮次各一个 Item,点产品同样三层。目录由 manifest 和源注册表生成,不含时间戳和主机名,任何一份文档都能从磁盘上的产物重新生成;链接都是相对的。

桶上每个源只留最新时次,所以每个 Collection 带一个固定路径的 item.json,指向当前时次。

读的一端用现成工具:pystac 走目录,xpystac / odc-stac 把 Item 里的 Zarr 资产开成 xarray。 group 文档里带了内联的 consolidated metadata,不能列目录的纯 HTTP 桶上 xr.open_zarr(url) 也能直接打开。

分发

站点是 Cloudflare Pages 上的静态页面,数据在公开的 R2 桶上,中间是 CDN,没有 API 层。 R2 的出口流量和边缘命中都不收费,请求数的多少主要影响延迟,不影响账单。

客户端用 HTTP Range 读它需要的 chunk,同一对象内相邻的 range 合并成一次请求。全球视图一帧是每个时间块一次请求,视口是每个瓦片行一次,一个格点的整条序列是每个时间块一个 chunk。播放头前后的帧预取进一个按字节预算的缓存。

新时次上线前先预热一遍 CDN,再切换指针,让第一个访问者拿到的就是边缘缓存。

计算集中在构建阶段,线上只提供静态文件。空间聚合、跨时次拼接这类计算都由编码器预先算成产物。

渲染

浏览器里一个 Rust WebAssembly worker 按需解码 chunk, WebGL2 的片元着色器对每个像素做反 Web Mercator 投影,再查调色板。瓦片是格点空间的矩形,拼回平面只是一次 texSubImage2D,未到的瓦片在着色器里跳过。相邻两帧在 GPU 上混合,播放时过渡连续。

等值线在同一个着色器里画,不需要第二遍渲染;风场用 GPU 粒子层。另有一条可选的 WebCodecs H.264 路线(?use_h264=true),用无损编码的视频承载同一批码值,文件更小。

分析

数据是开放 CORS 的静态文件,没有密钥,读它不经过封装接口。仓库里带了 synoptic-analysis Skill,配合能跑 Python 的 AI 助手,从 STAC 目录读实际数据、做天气形势分析、写报告。例如:

请分析东京未来三天的天气形势,比较 GFS 与 ECMWF 对降水和风的预报,结合最新卫星、雷达和探空资料,生成包含图表、数据时间和不确定性说明的中文报告。

Skill 的工作路径是:确定区域与有效时间,读模式场,分析高空槽脊、急流、地面系统、水汽输送和降水条件,用观测检查模式表现,比较不同模式的分歧。一次分析用到的数据先落到本地,再用于计算、绘图和复核,报告里的数字和图表来自同一份数据。关注区域、分析重点、报告语言和输出格式都可以改。

一份实际产出的报告是台风杜鹃即将影响东京时的分析:東京都台风天气过程分析(PDF),生成过程见《用 LLM 进行多数据源天气分析》。

自部署

需要独立运行时,选上游的开放数据,跑对应的处理流水线。本地一条命令构建一个时次:

1
2
make mvp MODEL=gfs    # 也可 ecmwf / aifs / ifshres / sflux / hrrr / gefsaero / cfs
make serve

依赖是 Python、带 GRIB driver 的 GDAL、Node.js,构建 WASM 解码器需要 Rust 工具链。前端的数据地址、R2 桶和路径都有配置入口。迁到别的对象存储或部署到内网,需要改发布流程、网络访问和权限配置,格式、目录、解码器不变。

部署之后可调整的范围包括数据(从一个模式、几个常用变量开始,按需扩展)、界面(配色、图层、面向特定地区组织内容)和分析(Skill 的分析逻辑与报告格式)。

代码是 Apache 2.0 OR MIT 双许可,任选其一,都允许商业使用、修改和闭源再分发,只要求保留版权与许可声明。数据许可按源不同:NOAA 的产品属公有领域,ECMWF 开放数据与经 Open-Meteo 转发的 IFS HRES 是 CC BY 4.0,需署名;底图 © OpenStreetMap 贡献者。