GeoCore · 矢量点阵渲染 · 完整攻略

← 承接自 地图纹理指南 / Part II

矢量点阵
地图渲染
数据获取与制作全流程

从 Natural Earth GeoJSON 数据下载到生产级 Canvas 2D 点阵渲染—— 精确多边形投影、多密度模式、文化圈着色,完整集成到 OWMap 地球仪渲染管线。

开始阅读
6
章节
12
数据源
30+
代码示例
Chapter 09 · Part II

点阵渲染哲学与使用场景

矢量点阵(Vector Dot-Matrix)渲染是一种将连续的多边形地理边界离散化为有规律排列点集的技术。 它不追求像素级精确,而是用视觉节奏感和信息密度的调制,构建出独特的艺术性地图风格—— 这正是 OWMap「深空文学地球仪」视觉语言的核心组件之一。

9.1 为什么选择点阵而非位图纹理?

位图纹理(如 4K WebP 卫星影像)在高端设备上效果极佳,但点阵渲染有其不可替代的优势:

维度 矢量点阵 位图纹理
文件体积0 字节(纯算法生成)200 KB – 4 MB
任意分辨率✓ 无损缩放受限于原始分辨率
风格可定制性极高:密度/颜色/形状均可编程低:需重新制作素材
渲染首帧速度~5–30ms(Canvas 2D)取决于网络 + GPU 上传
降级兼容性任意设备 Canvas 2D 即可运行需要 WebGL / GPU 支持
动态数据叠加天然支持逐点着色需要额外着色器
艺术风格独特性极强(科技感 / 诗意感)写实风格为主

9.2 核心使用场景

WebGL 降级回退
FALLBACK LAYER
当设备不支持 WebGL 或 Three.js 加载失败时,点阵渲染作为零依赖的 Canvas 2D 备用方案,确保地球仪始终可用。
兼容性零依赖
小地图 / 全局导航
MINIMAP
新闻详情页、故事阅读页的小尺寸地图定位组件,轻量渲染,极低内存占用。
轻量级导航
数据热力叠加底图
DATA LAYER BASE
以点阵为底图,在其上叠加热力圈、故事标记点、弧线路由等数据层,点的颜色可直接编码数据值。
数据可视化
艺术风格主题模式
THEME MODE
「深空点阵」「赛博格网」「文化圈彩色」等主题模式,可作为应用的视觉签名风格。
品牌主题
设计哲学:点阵不是精确地图的妥协,而是另一种叙事语言。 OWMap 的「深空文学」视觉语言中,均匀排列的发光点阵与星空背景形成共鸣—— 大陆轮廓是浮现而非刻划出来的,这契合平台「地理是故事发生的场域」的核心哲学。
Chapter 10

数据获取与处理管线

高质量的点阵渲染依赖精确的地理多边形数据。本章覆盖从权威数据源下载原始 GeoJSON, 到针对 Canvas 渲染优化的完整数据处理管线。

10.1 Natural Earth — 首选数据源

Natural Earth 是由 NACIS(北美制图信息学会)维护的公共域矢量地图数据集,提供三种精度级别, 可按需选择体积与精度的平衡点。

精度级别比例尺GeoJSON 体积多边形顶点数推荐用途
1:110m(低精度)1:110,000,000~500 KB~2,000 顶点全球概览、缩略图、移动端
1:50m(中精度)1:50,000,000~2 MB~8,000 顶点桌面端全球地图(推荐)
1:10m(高精度)1:10,000,000~25 MB~80,000 顶点区域级详图、高端设备
OWMap 推荐:点阵渲染使用 1:110m 数据。 由于最终渲染为离散点而非连续轮廓线,超精细多边形顶点在视觉上无法体现, 只会增加计算负担。1:110m 的约 2,000 顶点在 4K 画布上已足够精准。

10.2 数据下载地址

Data URLs# Natural Earth 1:110m 陆地多边形(最常用)
https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/
  ne_110m_land.geojson                    # 全球陆地 · 501 KB · 推荐起点
  ne_110m_admin_0_countries.geojson       # 国家边界(含属性)· 600 KB
  ne_110m_coastline.geojson               # 海岸线(线要素)

# 1:50m(桌面端更佳效果)
  ne_50m_land.geojson                     # 全球陆地 · 2 MB
  ne_50m_admin_0_countries.geojson        # 国家 · 2.4 MB

# 官方下载入口(ZIP 格式,含 SHP/GeoJSON/KMZ)
https://www.naturalearthdata.com/downloads/110m-physical-vectors/

10.3 运行时 vs 预烘焙策略

运行时 fetch + 渲染
RUNTIME STRATEGY
页面加载后 fetch GeoJSON,解析后直接渲染。适合需要动态切换精度或数据源的场景。 首次加载慢(需等待网络),但后续缓存命中后与预烘焙相当。
灵活CDN 缓存
预烘焙点云数组
BUILD-TIME BAKE
构建时将 GeoJSON 多边形采样为 Float32Array 点云,序列化为二进制或紧凑 JSON 内联到 JS。 运行时零网络请求,渲染只需迭代数组。OWMap 生产环境推荐方案。
零延迟生产推荐

10.4 Node.js 数据处理工具链

以下脚本将 Natural Earth GeoJSON 转换为内联点云数组,可直接粘贴到 globe.js。

Node.js · bake-dotmap.js/**
 * bake-dotmap.js
 * Usage: node bake-dotmap.js ne_110m_land.geojson 5 > baked-dots.js
 * Args:  [geojsonPath] [gridStep=5px at 1024px width]
 */
const fs   = require('fs');
const path = require('path');

const [,,srcPath, stepArg] = process.argv;
const step   = parseFloat(stepArg) || 5;
const W = 1024, H = 512;
const geojson = JSON.parse(fs.readFileSync(srcPath, 'utf8'));

/* Point-in-polygon (ray-casting) */
function pip(px, py, ring) {
  let inside = false;
  for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
    const [xi, yi] = ring[i], [xj, yj] = ring[j];
    if (((yi > py) !== (yj > py)) &&
        (px < (xj - xi) * (py - yi) / (yj - yi) + xi))
      inside = !inside;
  }
  return inside;
}

function isLandGeo(lng, lat) {
  for (const feat of geojson.features) {
    const geo = feat.geometry;
    const polys = geo.type === 'Polygon'
      ? [geo.coordinates]
      : geo.type === 'MultiPolygon'
        ? geo.coordinates
        : [];
    for (const poly of polys) {
      if (pip(lng, lat, poly[0])) return true; // outer ring only
    }
  }
  return false;
}

/* Sample the grid */
const pts = [];
for (let px = 0; px < W; px += step) {
  for (let py = 0; py < H; py += step) {
    const lng = (px / W) * 360 - 180;
    const lat = 90  - (py / H) * 180;
    if (isLandGeo(lng, lat)) pts.push(px / W, py / H); // normalized coords
  }
  process.stderr.write(`\rSampling... ${Math.round(px/W*100)}%`);
}
process.stderr.write('\n');

/* Output as JS module */
console.log(`/* Auto-generated — do not edit. Baked from ${path.basename(srcPath)} at step=${step} */`);
console.log(`export const BAKED_DOTS = new Float32Array([${pts.join(',')}]);`);
console.log(`export const BAKED_STEP = ${step};`);
console.log(`export const BAKED_COUNT = ${pts.length / 2};`);
1

下载 GeoJSON 数据

从 Natural Earth GitHub 仓库下载 ne_110m_land.geojson(约 500 KB)。可用 curl 或直接浏览器下载。

2

运行烘焙脚本

node bake-dotmap.js ne_110m_land.geojson 5 > baked-dots.js(约 30 秒)。输出包含归一化坐标的 Float32Array,step=5 对应 1024px 宽画布约 5px 间距。

3

集成到 globe.js

将输出的 BAKED_DOTS 数组内联到 globe.js(约 30 KB),替换当前的 isLand() bbox 检测,渲染时直接迭代点数组即可,无需再做 PIP 运算。

4

多精度版本管理

分别生成 step=3(密集)、step=5(标准)、step=10(稀疏)三个烘焙文件,按设备性能动态选择。

10.5 精度对比速查

数据源 + 精度PIP 准确率处理时间烘焙体积适用设备
当前 bbox(8个矩形)~70%0.01ms/点内联全设备
Globe.js 内置多边形(本次更新)~88%0.1ms/点内联 ~15KB全设备
NE 1:110m 烘焙点云~95%0.001ms/点~25 KB全设备(推荐)
NE 1:50m 运行时 PIP~98%0.5ms/点2 MB GeoJSON桌面端
NE 1:10m 运行时 PIP~99.5%3ms/点25 MB GeoJSON高端桌面
Chapter 11

Canvas 2D 渲染算法详解

本章深入点阵渲染的核心算法:等经纬度投影、网格采样、陆地判定, 以及在 Canvas 2D 上实现毫秒级渲染的关键优化技巧。

11.1 等经纬度投影(Equirectangular)

点阵地图使用最简单也最通用的等经纬度投影:经度线性映射到 X 轴,纬度线性映射到 Y 轴。 虽然极区面积失真较大,但对于全球概览地图完全足够,且计算极快。

投影公式/* 从地理坐标 → Canvas 像素坐标 */
function geoToCanvas(lat, lng, W, H) {
  const x = ((lng + 180) / 360) * W;   // lng ∈ [-180, 180] → [0, W]
  const y = ((90  - lat) / 180) * H;   // lat ∈ [ -90,  90] → [H, 0]
  return { x, y };
}

/* 从 Canvas 像素 → 地理坐标(渲染时使用这个方向) */
function canvasToGeo(px, py, W, H) {
  const lng = (px / W) * 360 - 180;
  const lat = 90  - (py / H) * 180;
  return { lat, lng };
}

11.2 点阵采样策略

渲染循环遍历 Canvas 上均匀分布的网格点,对每个点做陆地判定。 关键参数是 step(网格间距,单位 px),它直接决定点阵密度和渲染时间。

Core Loopfunction renderDotMatrix(canvas, cfg = {}) {
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  const {
    step   = 5,
    radius = 0.85,
    color  = 'rgba(140,180,230,0.22)',
    getColor = null,         // optional: (lat,lng)=>color for per-dot coloring
    bakedDots = null,        // optional: Float32Array of [u,v,u,v,...] normalized
    isLand = null,           // optional: (lat,lng)=>boolean
  } = cfg;

  ctx.clearRect(0, 0, W, H);

  if (bakedDots) {
    /* Fast path: iterate pre-baked point list */
    for (let i = 0; i < bakedDots.length; i += 2) {
      const px = bakedDots[i]   * W;
      const py = bakedDots[i+1] * H;
      const lat = 90  - bakedDots[i+1] * 180;
      const lng = bakedDots[i]   * 360 - 180;
      ctx.fillStyle = getColor ? getColor(lat, lng) : color;
      ctx.beginPath(); ctx.arc(px, py, radius, 0, Math.PI * 2); ctx.fill();
    }
  } else if (isLand) {
    /* Grid-scan path: test each cell */
    for (let px = 0; px < W; px += step) {
      for (let py = 0; py < H; py += step) {
        const lng = (px / W) * 360 - 180;
        const lat = 90  - (py / H) * 180;
        if (!isLand(lat, lng)) continue;
        ctx.fillStyle = getColor ? getColor(lat, lng) : color;
        ctx.beginPath(); ctx.arc(px, py, radius, 0, Math.PI * 2); ctx.fill();
      }
    }
  }
}

11.3 Ray-Cast 陆地判定(Point-in-Polygon)

对于运行时 GeoJSON 判定,使用经典 Ray-Casting 算法: 从测试点向右发射一条水平射线,统计与多边形边的交叉次数,奇数为内部,偶数为外部。

Point-in-Polygon/**
 * Ray-cast PIP against a GeoJSON Polygon or MultiPolygon.
 * ring: [[lng,lat],...] — outer ring of a polygon
 */
function pointInRing(lng, lat, ring) {
  let inside = false;
  const n = ring.length;
  for (let i = 0, j = n - 1; i < n; j = i++) {
    const [xi, yi] = ring[i], [xj, yj] = ring[j];
    if (((yi > lat) !== (yj > lat)) &&
        (lng < (xj - xi) * (lat - yi) / (yj - yi) + xi)) {
      inside = !inside;
    }
  }
  return inside;
}

function isLandGeoJSON(lat, lng, features) {
  for (const f of features) {
    const g = f.geometry;
    const polys = g.type === 'Polygon'     ? [g.coordinates]
                 : g.type === 'MultiPolygon' ?  g.coordinates : [];
    for (const poly of polys) {
      if (pointInRing(lng, lat, poly[0])) {
        /* Check holes (inner rings) */
        let inHole = false;
        for (let h = 1; h < poly.length; h++)
          if (pointInRing(lng, lat, poly[h])) { inHole = true; break; }
        if (!inHole) return true;
      }
    }
  }
  return false;
}
性能说明:PIP 对每个网格点最坏情况需要遍历所有多边形的所有顶点。 1:110m 数据约 2,000 顶点,step=5px 约 10,240 个网格点,总运算量约 2,000 万次浮点比较。 现代浏览器 JS 引擎可在 10–30ms 内完成。 如需更快,使用预烘焙点云(0.5ms 内完成)。
Chapter 12

多密度模式与视觉预设

点阵密度(step)和点半径(radius)的组合决定了地图的视觉风格,从极简稀疏到细腻密集均可实现。 OWMap 内置以下预设,兼顾视觉效果与渲染性能。

稀疏星点
step=10 · r=1.2 · mono
标准点阵
step=5 · r=0.85 · cultural
密集精细
step=3 · r=0.6 · cultural
赛博格网
step=4 · r=1.0 · neon
余烬暖光
step=5 · r=1.1 · warm
幽灵水墨
step=6 · r=1.5 · fade
预设名称step (px)radius (px)coloring渲染时间(1024px)适用场景
稀疏星点101.2单色< 2ms移动端、缩略图
标准点阵50.85文化圈5–8ms桌面主地图(默认)
密集精细30.6文化圈15–25ms高端设备特效模式
赛博格网41.0霓虹绿8–12ms科技主题 / 游戏风
余烬暖光51.1暖金色5–8ms旅行叙事主题
幽灵水墨61.5渐隐白4–6ms极简 / 文学主题
Chapter 13

文化圈配色系统

文化圈配色(Cultural Zone Coloring)是 OWMap 点阵渲染的标志性特色: 每个大陆或文化圈对应一种经过设计的色调,让地图在视觉上传达地理与文化的多样性。

13.1 色系设计原则

文化圈色系基于深空黑背景(#050D18)设计,所有颜色均为低饱和度、低亮度, 以 Alpha 透明度控制可见性。避免强对比刺眼,追求「星云中浮现」的视觉感受。

东亚 · rgba(200,120,60,0.55)
东南亚 · rgba(180,150,60,0.50)
南亚 · rgba(210,100,80,0.50)
中东 · rgba(220,160,50,0.50)
西欧 · rgba(100,140,210,0.55)
东欧/俄罗斯 · rgba(140,160,200,0.50)
非洲 · rgba(200,130,60,0.50)
北美洲 · rgba(100,170,200,0.50)
南美洲 · rgba(120,190,120,0.50)
大洋洲 · rgba(160,200,180,0.50)
globe.js · CULTURAL_ZONESconst CULTURAL_ZONES = [
  { name:'东亚',    bbox:[96,18,146,54],   color:'rgba(200,120,60,0.55)'  },
  { name:'东南亚',  bbox:[92,-10,142,28],  color:'rgba(180,150,60,0.50)'  },
  { name:'南亚',    bbox:[60,6,100,38],    color:'rgba(210,100,80,0.50)'  },
  { name:'中东',    bbox:[25,12,62,40],    color:'rgba(220,160,50,0.50)'  },
  { name:'东欧',    bbox:[28,45,60,72],    color:'rgba(140,160,200,0.50)' },
  { name:'西欧',    bbox:[-10,35,28,72],   color:'rgba(100,140,210,0.55)' },
  { name:'非洲',    bbox:[-18,-35,52,38],  color:'rgba(200,130,60,0.50)'  },
  { name:'北美洲',  bbox:[-168,7,-52,72],  color:'rgba(100,170,200,0.50)' },
  { name:'南美洲',  bbox:[-82,-56,-34,12], color:'rgba(120,190,120,0.50)' },
  { name:'大洋洲',  bbox:[114,-47,180,-10],color:'rgba(160,200,180,0.50)' },
  { name:'俄罗斯',  bbox:[28,50,190,73],   color:'rgba(140,160,180,0.45)' },
];

function getCulturalColor(lat, lng) {
  for (const z of CULTURAL_ZONES) {
    const [lo1,la1,lo2,la2] = z.bbox;
    if (lng >= lo1 && lng <= lo2 && lat >= la1 && lat <= la2) return z.color;
  }
  return 'rgba(140,180,230,0.22)'; // default ocean-blue
}
Chapter 14

交互式配置面板

本章提供生产就绪的交互式点阵配置面板实现—— 初级用户使用预设一键切换,高级用户通过参数面板全量定制, 所有更改实时预览,支持一键导出配置代码。

点阵渲染配置器 (实时预览)

选择一个预设,点阵效果将实时更新:

5 px
0.85
0.22
#050D18

当前配置的 JavaScript 代码,可直接复制到项目中:

// 当前点阵配置
window.OWMap.dotMatrixConfig = {
  step:      5,
  radius:    0.85,
  mode:      'cultural',
  bgColor:   '#050D18',
  monoColor: 'rgba(140,180,230,0.22)',
};
准备渲染...

14.1 globe.js 全局配置接口

当前版本 globe.js 已集成配置接口,支持通过 window.OWMap.dotMatrixConfig 在任何页面覆盖默认参数:

globe.js API/* 在页面脚本中(globe.js 加载之后)设置配置 */
window.OWMap = window.OWMap || {};
window.OWMap.dotMatrixConfig = {
  step:       5,            // 网格间距(px)  默认: 5
  radius:     0.85,         // 点半径(px)   默认: 0.85
  mode:       'cultural',   // 'cultural' | 'mono'
  bgColor:    '#050D18',    // 背景色
  monoColor:  'rgba(140,180,230,0.22)', // 单色模式下的颜色
  showStories: true,        // 是否显示故事标记点
};

/* 调用绘制 */
OWMap.drawWorldMap('your-canvas-id');

/* 或传入内联参数(高优先级,覆盖全局配置) */
OWMap.drawWorldMap('your-canvas-id', {
  step: 3, mode: 'cultural'
});

14.2 性能优化清单

OffscreenCanvas 异步渲染:将点阵渲染移入 Web Worker,避免阻塞主线程。 对于 step < 4 的密集模式(渲染时间 >15ms),此优化可消除掉帧风险。 const off = canvas.transferControlToOffscreen(); 传入 worker 即可。
技巧效果适用场景
预烘焙点云渲染时间从 20ms → 0.5ms所有场景(强烈推荐)
OffscreenCanvas + Worker消除主线程阻塞step ≤ 4 密集模式
ImageData 直写像素比 arc() 快 3–5×极致性能场景
devicePixelRatio 缩放减少 HiDPI 设备的点数Retina 屏
requestIdleCallback 渲染不阻塞 LCP / FID非首屏地图组件
脏标记 + 条件重绘避免无效重渲染参数实时调节时
Pixel-Direct Fast Path/* 直接写 ImageData,比 arc() 快约 4× */
function renderDotMatrixFast(canvas, bakedDots, color = [140,180,230,55]) {
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  const img = ctx.createImageData(W, H);
  const d = img.data;

  /* Fill background */
  for (let i = 0; i < d.length; i += 4) {
    d[i]=5; d[i+1]=13; d[i+2]=24; d[i+3]=255;
  }

  /* Plot dots as single pixels */
  for (let i = 0; i < bakedDots.length; i += 2) {
    const px = Math.round(bakedDots[i]   * W);
    const py = Math.round(bakedDots[i+1] * H);
    if (px < 0 || px >= W || py < 0 || py >= H) continue;
    const idx = (py * W + px) * 4;
    d[idx]   = color[0];
    d[idx+1] = color[1];
    d[idx+2] = color[2];
    d[idx+3] = color[3];
  }

  ctx.putImageData(img, 0, 0);
}

把点阵地图用到你的项目中

所有代码已集成到 globe.js,drawWorldMap() 直接支持文化圈配色与多密度预设。 运行上方配置器导出参数,粘贴到项目即可使用。

体验地球仪效果 ← 纹理指南 Ch01–08