@meteo-layer/core
v0.3.5
Published
meteo-layer 渲染器无关内核:解码(含 Go wasm 内核与 Worker)、调色板、格式注册、数据源注册、色标组件。可被 Cesium / MapLibre 等渲染器包复用。
Maintainers
Readme
@meteo-layer/core
气象图层渲染器无关内核。提供风粒子 GPU 引擎、二进制栅格解码(含 Go wasm 内核)、调色板、图像格式注册、数据源注册、掩膜运算,可被 Cesium / MapLibre 等渲染器包复用。
直接面向应用开发通常装渲染器包
@meteo-layer/cesium或@meteo-layer/maplibre(它们已依赖 core 并重导出公共 API)。core 供二次开发 / 自接渲染器的场景使用。
特性
- GpuWindField 引擎:原生 WebGL2 Transform Feedback,渲染器无关;投影由适配器通过
prelude注入(支持 MapLibre mercator/globe 与 Cesium ECEF)。 - 解码内核:Go wasm 解码 + Web Worker,主线程只做纹理上传;支持 uint8 / int16 / float32 二进制栅格(可选 gzip)。
- 图像格式抽象:内置
scalar/wind-rg/truecolor,以及高精度scalar-f16/scalar-f32(避免 8 位精度丢失),可registerFormat自定义。 - 数据源注册:统一
defineSource描述栅格元信息(投影、量程、色板、相机),支持时序frames、雷达分层slices、垂直剖面profiles三种扩展点。 - 掩膜运算:渲染器无关的点-多边形测试(射线法,含孔洞)+ mercator 纹理栅格化,供两端 GL 层做片元
discard。
安装
npm install @meteo-layer/core协议:Apache-2.0(详见仓库根目录 LICENSE)。core 无对等依赖(零渲染引擎依赖)。
快速开始
1. 注册并使用一个数据源
import { defineSource, registerSource, listSources } from '@meteo-layer/core'
defineSource({
id: 'temp', title: '2m 温度', format: 'scalar',
url: 'https://.../temp.png', bounds: [70, 0, 140, 60],
imageUnscale: [-20, 40], unit: '℃',
palette: [
[-20, [30, 60, 160, 0.8]],
[0, [60, 160, 220, 0.8]],
[20, [240, 200, 60, 0.8]],
[40, [220, 60, 40, 0.8]],
],
})
const src = getSource('temp')
console.log(listSources()) // ['temp']2. 自定义图像格式
import { registerFormat } from '@meteo-layer/core'
registerFormat({
id: 'radar-dbz',
shader: 'scalar',
sampleField: (raw, cfg, lon, lat) => { /* 返回 { value, ... } */ },
describe: (s) => ({ text: `${s.value.toFixed(0)} dBZ` }),
})3. 解码二进制栅格(bin)
import { binLoader, createBinLoader } from '@meteo-layer/core'
// 单次:直接用 binLoader 作为 source.loader
// 可配置:createBinLoader({ headers: { Authorization: 'Bearer ...' } })核心 API
数据源与格式
| 导出 | 说明 |
| --- | --- |
| defineSource(cfg) / registerSource(cfg) | 定义 / 注册数据源 |
| getSource(id) / hasSource(id) / listSources() | 数据源查询 |
| normalizePalette(stops) | 归一化色板(补全 alpha 等) |
| defineProfile(p, parent) | 定义垂直剖面(雷达 RHI 等) |
| registerFormat(fmt) / getFormat(id) / hasFormat(id) / listFormats() | 格式注册与查询 |
| scalarFormat / windRGFormat / trueColorFormat | 内置格式实例 |
| degToCompass(deg) | 度数转汉字方位 |
图像加载与解码
| 导出 | 说明 |
| --- | --- |
| loadImage(cfg) | 默认 PNG 加载器 |
| binLoader / createBinLoader(opts) | 二进制栅格加载器(uint8/int16/float32,可选 gzip) |
| parseBinBuffer(buf, cfg?) / parseBinBufferRaw(buf, cfg?) | 直接解析 bin 缓冲 |
| floatMockLoader / seriesMockLoader / makeFloatMockRaw / makeSeriesMockRaw | 模拟数据(开发/演示) |
| createDecodeWorker() | 解码 Worker 实例(wasm 内核) |
| decodeBinWasm(buf, cfg?, engine?) | wasm 解码入口 |
| openZ4D(url) / readRegion(meta, region) | Z4D1 4D 二进制:读元数据 + 任意区域取块解码(blosc/gzip/raw,支持 HTTP Range) |
| createZ4DImageLoader / createZ4DSliceLoader / createZ4DProfileLoader / createZ4DWindLoader / createZ4DVolumeLoader / createZ4DWindMagSliceLoader / createZ4DWindMagProfileLoader | Z4D1 七类 loader 工厂(填色 / 分层切片 / 垂直剖面 / 时序风场 / 真·体渲染 / 风速幅值单层填色 / 风速幅值垂直剖面) |
| openZarrArray(storeUrl, arrayPath) / readRegion(arr, region) | Zarr v2 纯前端读取:读元数据 + 任意区域取块解码 |
| createZarrImageLoader / createZarrSliceLoader / createZarrProfileLoader | Zarr 三类 loader 工厂(单图填色 / 分层切片 / 垂直剖面) |
调色板
| 导出 | 说明 |
| --- | --- |
| usePalette() | 调色板 API(共享实例,含 rebuild / colorizeValue / onChange) |
色标 UI 组件(
ColorLegend.vue)已迁移至样例工程,库不再内嵌 Vue 组件。
GpuWindField 引擎
| 导出 | 说明 |
| --- | --- |
| GpuWindField | 默认导出,渲染器无关的 GPU 风粒子引擎 |
| assembleGpuWindFieldUpdateVs(opts) | 组装粒子 update VS(注入投影 prelude) |
| DRAW_VS_BODY / DRAW_FS / FALLBACK_PRELUDE / CESIUM_ECEF_PRELUDE | 着色器源码片段与投影 prelude |
| WIND_RAINBOW_RAMP / makeWindRainbowLUT(range?, size?, ramp?) / buildWindPaletteLUT(palette, size?, range?) | 风速默认彩虹色标 + LUT 构造(useWhite=false 且源无 palette 时的统一兜底) |
掩膜(渲染器无关)
| 导出 | 说明 |
| --- | --- |
| MaskType | inside = 1(区域内显示)/ outside = 0(区域外显示) |
| rectMask(bounds) / normalizeMask(mask) | 构造 / 归一化掩膜几何 |
| pointInMaskPolygon(lng, lat, rings) | 点-多边形测试(射线法) |
| pointVisibleUnderMask(lng, lat, mask, type) | 点是否在可见区 |
| lngLatToMerc01(lng, lat) | 经纬度 → mercator [0,1] |
| rasterizeMask / rasterizeMaskToCanvas / rasterizeMaskEquirectToCanvas | 掩膜纹理栅格化 |
| createMaskTexture(gl, mask) | 生成 GL 掩膜纹理(供片元 discard) |
Mock fetcher(矩形查询多层)
| 导出 | 说明 |
| --- | --- |
| mockRadarFetcher / mockRadarFetcherBin / mockRadarFetcherF32 / mockRadarFetcherF16 | 3D 体视演示用的假数据 fetcher |
Z4D1 自定义 4D 二进制格式
单文件自包含(头部 + 块索引 + 块载荷),替代/补充 Zarr 与多帧 bin。任何 Z4D1 文件都能被读成标准
RawImage,直接喂现有渲染管线(单图层填色 / 分层切片slices/ 垂直剖面profiles/ 时序帧frames/ 风场矢量 / 真·体渲染),渲染·纹理·材质·动画零改动。
字节布局(小端 Little-Endian)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| magic | 4B ASCII | 'Z4D1' 校验 |
| version | u8 | 版本 |
| dtype | u8 | 0=float64 / 1=float32(解码统一转 float32) |
| ndim | u8 | 维数(4=4D,2=2D) |
| ncomp | u8 | 分量数:0=旧标量布局(56B 头);1=标量;2=uv 风;3=uvw |
| shape | 4×i32 | [time, level, lat, lon] |
| chunks | 4×i32 | 各维分块大小 |
| u/v/w_range | 各 2×f32 | 物理量程(u=标量/第0分量;v/w 仅矢量用) |
| codec/cname/clevel/shuffle | u8×4 | 压缩(blosc: lz4/zstd/blosclz/zlib/snappy;gzip;raw) |
| nchunks | i32 | 块数量 |
| 块索引 | nchunks×28B | 每块 [coords(4×i32) | offset(i64) | len(i32)] |
- 旧布局(56B,
ncomp=0):imageUnscale在 40–47,codec/nchunks在 48–55。 - 新布局(72B,
ncomp≥1):u/v/w_range在 40–63,codec/nchunks在 64–71。
维度契约:shape=[time, level, lat, lon](2D 文件 [lat, lon]);矢量末维每网格点连续存 ncomp 个 float32,风 ncomp=2 即 R=U(东向)/G=V(北向) 且 u/v 同量程;lat 索引 0 = 南,渲染 row0 = 北(与 Cesium 纹理排列一致)。
Loader 工厂(core 导出)
openZ4D(url) 读取并缓存元数据,readRegion(meta, region) 取任意 [start,end) 子块并解码。以下工厂返回标准 loader,直接赋给 defineSource({ loader }) 或 frames[].loader:
| 工厂 | 取数维度 | 输出 | 用途 |
| --- | --- | --- | --- |
| createZ4DImageLoader(cfg) | (timeIndex, levelIndex) 单块 → 2D | 标量/矢量色斑图 | 单图层填色 |
| createZ4DSliceLoader(cfg) | 指定 levelIndex 切层 | 2D 分层切片 | slices 雷达分层 |
| createZ4DProfileLoader(cfg) | 沿 line 读全 level → 沿程×高度幕布 | 垂直剖面 | profiles(RHI) |
| createZ4DWindLoader(cfg) | (timeIndex, levelIndex) → wind-rg(R=U G=V) | 风场矢量图 | frames 时序风场(frameIndex=timeIndex 自动播放) |
| createZ4DVolumeLoader(cfg) | 固定 time,取 levelRange 3D 块 | RGBA 体数据(data3D/width/height/depth) | 真·体渲染(volume:true) |
| createZ4DWindMagSliceLoader(cfg, levelIndex?) | 指定层 → 风速幅值单层 2D | scalar 色斑图 | 风场填色(按风速上色) |
| createZ4DWindMagProfileLoader(cfg) | 沿 line 读全 level → sqrt(u²+v²) 幕布 | scalar 色斑图 | 风速垂直剖面(profiles,cfg 取 magRange 统一量程) |
cfg:url、timeIndex?、levelIndex?、levelRange?、bounds、imageUnscale?、palette?、precision?('float32');loader 接收可选 bbox 做空间裁剪。
用法示例
import { defineSource, createZ4DVolumeLoader, createZ4DWindLoader } from '@meteo-layer/core'
// 1) 真·体渲染:把 3D 子块 loader 交给矩形框选的 fetcher
const meta = await mgr.queryByRectangle({
fetcher: async (bounds) => ({
layers: [{
format: 'scalar', bounds, imageUnscale: [0, 70], palette: [...],
loader: createZ4DVolumeLoader({ url: '/data/radar.z4d', levelRange: [0, 19], bounds }),
}],
}),
})
mgr.setVerticalExaggeration(10) // computeVolumeModelMatrix 自带垂直夸张(默认 10)
// 2) 时序风场:frameIndex = timeIndex,天然支持播放
await mgr.setSource(defineSource({
id: 'z4d-wind', format: 'wind-rg', bounds: [70, 0, 140, 60],
imageUnscale: [-30, 30], unit: 'm/s',
loader: createZ4DWindLoader({ url: '/data/wind.z4d', levelIndex: 0, bounds: [70, 0, 140, 60] }),
frames: Array.from({ length: 24 }, (_, t) => ({
loader: createZ4DWindLoader({ url: '/data/wind.z4d', levelIndex: 0, timeIndex: t, bounds: [70, 0, 140, 60] }),
time: `T+${t}h`,
})),
}))服务端需支持
Range请求(返回206 Partial Content)以按需取块;不支持时降级为整文件下载(200)后本地切片,功能仍正确。dev server 已内置/z4d中间件支持 Range 206。
场景化封装(标量/矢量 × 剖面/切片)
在底层 createZ4D*Loader 之上的一层封装:约定好气压层表 + 标准大气压高 + 默认色标 + cfg 契约,页面只传 { url, bounds, line(剖面) / bounds(切片) },无需手写 loader 闭包 / altitude 映射 / 色标。标量(温度/雷达)与矢量(风)分开。
| 工厂 | 用途 | 返回 |
| --- | --- | --- |
| createScalarProfileSource(opts) | 标量垂直剖面(温度/雷达):折线 → 取全 level 第 0 分量 → 沿程×高度幕布 | LayerSource(profiles[],可直接 MeteoScene.addLayer(createDataSource(source))) |
| createVectorProfileSource(opts) | 矢量垂直剖面(风):折线 → (U,V) → sqrt(u²+v²) 风速模长幕布 | 同上 |
| createScalarSliceFetcher(opts) | 标量切片堆叠(温度,8 等压面):矩形框选 → 多层按高度堆叠 3D 色斑 | queryByRectangle 的 fetcher (bounds) => layerCfg[] |
| createVectorSliceFetcher(opts) | 矢量切片堆叠(风,8 等压面风速模长) | 同上 |
统一 opts:{ url, bounds, imageUnscale?, valueRange?, unit?, palette?, levels?(默认 GFS 8 层), levelHeightsKm?, alongN?(默认 256), heightRange?(默认 [0,16000]), precision?, id?, title? };剖面专用再加 line: [[lon,lat],...]。
- 默认气压层
[1000,925,850,700,500,300,200,100]mb,标准大气压高近似[0,0.8,1.5,3,5.5,9,12,16]km(切片altitude/ 剖面heightRange由此推导)。 - 风默认风速色标(蓝→绿→黄→红→紫),温度默认气温色标(紫→蓝→绿→黄→红)。
import { createVectorProfileSource, createScalarSliceFetcher, createDataSource } from '@meteo-layer/core'
// 风剖面:画折线 → 风速模长垂直幕布
const src = createVectorProfileSource({
url: '/z4d/gfs_wind.z4d', bounds: [-180,-90,180,90],
line: [[100,30],[125,45]], valueRange: [0,60], unit: 'm/s',
})
scene.addLayer(createDataSource(src)) // MeteoScene 自动接线 profile 幕布
// 温度切片:画矩形 → 8 等压面 3D 堆叠
const fetcher = createScalarSliceFetcher({
url: '/z4d/gfs_tmp.z4d', bounds: [-180,-90,180,90],
imageUnscale: [-88.9,48.6], valueRange: [-90,50], unit: '℃', precision: 'float32',
})
const meta = await scene.queryByRectangle({ fetcher, defaultOpacity: 0.8 }) // → 每层 meta[]等值线 / 等值域(ContourGenerator)
渲染器无关的等值线生成器(marching squares 追踪版,参考业界主流 WContour 思路)。输入规则经纬网格,输出描边线(lines) 与 带孔洞等值域(bands),供 Cesium / MapLibre 各自绘制。
- 算法:逐格遍历 → 对跨阈值的网格边线性插值求交点 → 段按端点邻接连接成连续环;相邻 level 的环配对成 isoband(outer + holes,渲染器用 even-odd 填充规则挖洞)。
- undef 网格(NaN 或指定值)所在 cell 整体跳过,等值线不进入 undef 区。
- 纯同步无 DOM 依赖,可整体搬进 Web Worker 跑(大网格建议如此,参考业界多线程等值线思路)。
- 与色斑图严格对齐:坐标用 texel-center 约定(与 GPU 纹理采样一致);
interpMode >= 2时先以 Cardinal(Catmull-Rom c=0.5) 重采样出密网格再做 marching squares,等值线贴合 GPU 三次平滑场的真实等值点(< 2走原始线性边插值)。
import { generateContours } from '@meteo-layer/core'
// values: number[][] / Float32Array+{width,height} / {data,width,height}
const { lines, bands } = generateContours({
values, // 标量场(ny × nx)
width: nx, height: ny, // 仅 TypedArray 形态需要
west, south, east, north, // 经纬度范围(规则网格)
levels: [2, 6, 10, 14, 18], // 升序阈值
undef: -9999, // 可选,缺失值标记(默认 NaN)
row0IsNorth: true, // values[0] 是否为北纬
interpMode: 3, // 可选,与色斑图一致的插值模式(1=线性边插值[默认],>=2=Cardinal 重采样)
maxResampleDim: 480, // 可选,Cardinal 重采样密网格单边上限(默认 480)
withLines: true, withFill: true,
})
// lines: [{ level, rings: [[[lon,lat], ...], ...] }] → 渲染器画 Polyline
// bands: [{ min, max, outer: rings, holes: rings }] → 渲染器以 even-odd 填充 outer、挖 holes导出:generateContours / marchingSquaresSegments / linkSegmentsToRings / buildNiceLevels / sampleRamp / cardinalSampleField。
多源叠加统一 API(DataSource + AnimationService)
核心思路:把静态 LayerSource cfg 包成有状态广播源 DataSource,多个图层(色斑图 / 风粒子 / 格点)绑定同一 DataSource 时,时间轴一变全员刷新。渲染器包(cesium / maplibre)的 MeteoScene 基于此实现多图层同步播放。
import { createDataSource, AnimationService } from '@meteo-layer/core'
const ds = createDataSource('temp') // 传 cfg 或已注册的 id
await ds.ready() // 等首帧解码
ds.onDecoded((raw, frameIndex) => { /* ... */ })
ds.setFrameIndex(2) // 改帧 → 广播 onTChanged → 绑定层刷新
const anim = new AnimationService(ds, { speed: 1, frameDuration: 600, loop: true })
anim.onFrameChange = (i, time, total) => { /* ... */ }
anim.play()DataSource
| 成员 | 说明 |
| --- | --- |
| 构造 new DataSource(sourceCfg, opts?) / createDataSource(cfgOrId, opts?) | cfgOrId 传 LayerSource 对象或已注册 id;opts.seriesWindow 控制时序解码窗口 |
| get cfg / get id | 静态源配置 / id |
| get isSeries / get frameCount | 是否时序 / 总帧数 |
| frameIndex | 当前帧索引(可写) |
| get frameTime | 当前帧时间标签 |
| ready() | 等待首帧就绪的 Promise |
| onTChanged(fn) | 订阅帧变化(返回退订函数) |
| onDecoded(fn) | 订阅解码完成(返回退订函数) |
| setFrameIndex(i) | 切帧并广播 |
| loadCurrentFrame() / preloadFrame(i) | 加载当前 / 预加载指定帧 |
| clearCache() / destroy() | 清缓存 / 销毁 |
AnimationService
对应 QE 的 LDataAnimationService:驱动 DataSource.frameIndex 变化 → 广播 → 绑定层刷新。
| 成员 | 说明 |
| --- | --- |
| 构造 new AnimationService(dataSource, opts?) | opts = { speed, frameDuration(ms), loop } |
| get ds / speed / frameDuration / loop | 关联数据源与播放参数 |
| onFrameChange | 帧回调 (frameIndex, frameTime, frameCount) => void |
| get isPlaying | 是否播放中 |
| play() / pause() / seek(i) | 播放 / 暂停 / 跳帧 |
| setSpeed(s) / setLoop(b) / setFrameDuration(ms) | 调速 / 循环 / 帧间隔 |
| destroy() | 销毁定时器 |
TypeScript
类型声明随包发布(dist/index.d.ts)。运行时为 JS(带 JSDoc)。
