第55篇:空间分析-剖面分析——红线走大圆,剖面却按经纬直线采样

上一篇结尾我撂下一句话:

这条空间分析线才刚开头。地形挖开了,下一步自然就是沿着一条线把它切开看看里面长什么样------剖面分析,我们下一篇见。

坑挖完了,现在该切了。

从 #53 的标记标绘,到 #54 的地形开挖,这条空间分析线走的其实是同一个逻辑:**先把球上看得见的东西,变成算得准的东西。**点位标记那篇,我们把"一个点"变成了清单里的一行;地形开挖那篇,我们把"一块地"变成了一个洞。今天轮到"一条线"------把球上那条剖面线,变成一张高程曲线图。

剖面分析是空间分析里最不花哨、但最常被要求交付的功能。选线、定管线、算输电通道、评估徒步路线,最后交出去的东西往往就是一张剖面图:横轴距离、纵轴高程,一眼看出一条线是爬升还是下降、哪里最陡、哪里有个垭口。它是那种做对了没人夸、画歪了立刻被指着鼻子问的功能。

但这条线比看起来难搞。球上那条红线有它自己走的路径,剖面图有它自己采的路径,中间还夹着一个类别轴------本猿一开始以为这三件事是一回事,量完之后发现是三回事。

大家好,我是 Cesium 酱(也可以叫我"本猿"),一名在 WebGIS 领域摸爬滚打多年的前端开发者。

写这篇的时候是凌晨,我把 demo 里的默认路径来回画了十几遍,就为了确认一个反直觉的现象:球上那条红线中间明明没拐弯,剖面曲线却在中间鼓了个包。后来发现那不是我画歪了------是红线和剖面采的,根本不是同一条线。

本文目标:把剖面分析这条链路切成四段------密化路径、挑采样点、取高程、画曲线,然后把每一段的误差都量出来:剖面曲线上的每个点到底在哪儿、横轴上那个"距离"是不是真的距离、那条 10° 的斜线上两条线能差出多远。💪

💡 本文涉及的全部代码均已开源,完整的仓库地址见文末,可自行取用、随意魔改。


一、痛点与场景:为什么"切条线看剖面"这么容易翻车?

🎯 背景现状 :剖面分析干的事很朴素------沿着一条线,每隔一段取一次地面高程,再把"距离---高程"画成二维曲线。它是把一个三维问题压成二维,而压的过程里每一步都在丢信息:路径被简化成折点、折点之间被插成采样点、采样点被取成高程、高程被画进一个横轴等距的画布里。四步走下来,图是圆的,数据是尖的,差在哪儿全靠底层实现。

典型场景里,它要同时满足这些需求:

  • 🛣️ 选线:一条拟建公路 / 管廊 / 输电线路,要先知道它翻几道梁、最深挖多少
  • 🚰 管线纵断面:市政管线要按坡度走,剖面图是唯一的校核依据
  • 🥾 徒步 / 越野路线评估:累计爬升、最大坡度、补给点落差
  • 📊 填挖方粗算:沿线的地面线与设计线一对比,粗略知道哪段是挖、哪段是填
  • 🧪 科研 / 教学:地形起伏的可视化表达,丘陵、河谷、阶地一眼可见

🤦 传统痛点:本猿把常见的几种做法列了一下,每一条都踩过:

方案 问题
桌面 GIS 切剖面,再导出图表 数据要出浏览器、出进程、出软件,来回三趟;改一次参数重跑一遍
直接 sampleTerrain 猛打点 拿到的是一串高程,但它不知道"这条线的哪一段最陡"------因为没算累积距离
采完样随手用 Excel 连线 横轴按"第几个点"排,不是按距离排;点密的地方被压扁、点疏的地方被撑开
拿球上那条红线当基准 那条线走的是测地线(大圆) ,而采样走的是经纬度线性插值,长线上根本不在一条沟里
用 globe.getHeight 取高程 它返回 undefined 的时候你未必看得见------demo 里一句 ?? 0 就把它变成海平面了

💡 本文方案:这个案例的实现链路其实清清楚楚,就四段:

1️⃣ 密化路径 :把用户给的折点(或内置的演示路径)按每段固定 500 个插值点铺成一条密集折线,同时给每个点算一个累积距离

2️⃣ 挑采样点 :按"每段步数 / 固定间距 / 总点数"三种方式,从密化折线上挑出真正要取高程的位置

3️⃣ 取高程 :对每个采样点调 viewer.scene.globe.getHeight(),拿回三角网格上的高度

4️⃣ 画曲线:把(累积距离,高程)丢给 ECharts,画一条带面积填充的曲线

✨ 一句话总结:把一条剖面线当成一条带累积距离的密集折线 ,用地形三角网格的射线求交 当高程源,最后把结果按采样序号等距 摆进一张二维图里------每一步都对,可每一步都在丢东西。问题不在于哪一步算错了,在于没人告诉你丢的是什么。


二、核心技术原理:一条剖面,四段拼出来

2.1 路径与采样的数据结构

整个案例前后跑动的其实只有两个结构,一个比一个大:

ts 复制代码
// 密化折线上的一个点:位置 + 累积距离
type SamplePoint = { lon: number; lat: number; cumDistM: number }

// demo 里路径本身只用 [lon, lat] 两个数表示
const DEFAULT_PATH: [number, number][] = [
  [90.5, 30.5], [91, 31], [91.5, 31.5], [90.5, 31.5]
]

// 剖面数组里每一行的含义:经度 / 纬度 / 高程 / 累积距离
type ProfilePoint = [number, number, number, number]

// 源码级剧透:buildDenseSamples 里那个 DENSE_PER_SEGMENT = 500 是每段 插 500 个点,不是"整条线插 500 个点"。所以默认 3 段路径的密化数组长度是 1 + 500 × 3 = 1501;而你手绘一条 20 个折点的路径,它会涨到 1 + 500 × 19 = 9501。

这个 500 是写死的,跟段长一点关系都没有:一段 100 米的线和一段 1500 公里的线,都插 500 个点。这笔账第五节还要再算一次。

2.2 完整技术路线

我们设计的完整技术路线:

核心亮点:第 ④ 步"取高程"是本文的杀手锏 🔪------它的返回值只有三种可能:一个数字、undefined、或者抛异常。而 demo 里只接住了第一种。

2.3 三种采样方式的差别

面板上那个下拉框,看着只是"参数习惯不同",实际决定的是横轴上一格代表多少米 。三种方式的算法(源码里就是一个 if / else if / else):

采样方式 原理 默认路径点数 一格代表
每段步数(默认) 每段独立按 stepCount 等分,stepCount 20~300 3×100+1−2 = 301 每段不同(733.080 / 738.842 / 957.667 m)
固定间距 n = floor(总长 / 间距),从 0 取到 n×间距 5000 m 档 → 49 点 恒定(5000 m)
总采样点数 n = 总点数,直接等分全长 20~1000 恒定(总长 ÷ (n−1))

前两种的差别很微妙但很要命:

  • 每段步数 :每段都给同样多的点,所以段越长、每格越大。这条路径的段 3 就是活证据。
  • 固定间距 :每格一样宽,但它是从起点往后数整格,最后一格走不满就被丢掉------totalLength 报的是 n × 间距,不是真实全长。

🎯 一句必须记住的话:**"固定间距"下报告的总长永远 ≤ 真实总长,缺口最大是一个间距。**默认路径实测:100 m 档丢 62.471 m,2000 m 档丢 1262.471 m,5000 m 档丢 1262.471 m。而"总采样点数"和"每段步数"两个模式不截断,最后一点就压在终点上。


三、代码实战:三个关键实现

⚠️ 温馨提示 :下面只贴关键片段,完整的单文件组件在仓库 src/cases/profile-analysis/ProfileAnalysisDemo.vue。这个 demo 依赖 Bing 影像 + Cesium World Terrain,把 Bing 换成自己的底图就能跑。

3.1 密化 + 采样:两个数组,一次去重

ts 复制代码
const DENSE_PER_SEGMENT = 500

// 🔑 距离用球面大圆公式,半径取的是 IUGG 平均半径,不是 WGS84
function haversineMeters(lon1, lat1, lon2, lat2) {
  const R = 6371008.8
  const dLat = CesiumMath.toRadians(lat2 - lat1)
  const dLon = CesiumMath.toRadians(lon2 - lon1)
  const a = Math.sin(dLat / 2) ** 2 +
            Math.cos(CesiumMath.toRadians(lat1)) * Math.cos(CesiumMath.toRadians(lat2)) *
            Math.sin(dLon / 2) ** 2
  return 2 * R * Math.asin(Math.sqrt(a))
}

// 🌰 密化的核心:位置各自线性插值,距离按段长等比分配
for (let i = 1; i <= DENSE_PER_SEGMENT; i++) {
  const t = i / DENSE_PER_SEGMENT
  dense.push({
    lon: CesiumMath.lerp(sx, ex, t),
    lat: CesiumMath.lerp(sy, ey, t),
    cumDistM: cum + segLen * t
  })
}

// 🔑 "每段步数"模式下,第 i 段的终点和第 i+1 段的起点是同一个累积距离
for (let i = 0; i <= stepCount; i++) targets.push(cum + (segLen * i) / stepCount)
targets = targets.filter((v, i) => i === 0 || v !== targets[i - 1])

🔑 关键点:

  • 位置和距离用的是两套算法 。位置是 lon / lat 各自线性插值,距离是起点到终点的 haversine 距离乘以 t。两者在短段上贴合得很好,段一长就分家。
  • "每段步数"模式下 targets 天然有重复 ,靠上面那句 filter 去重。本猿把 stepCount 从 20 到 300 逐个试了一遍,重复对恒等于"段数 − 1",去重是真的生效的 ------因为 segLen × stepCount / stepCount 在这个量级上恰好浮点相等。
  • 但"重复的折点"去不掉 。如果你在地图上手抖,把同一个位置点了两次,那一整段的 targets 全部相等,去重后只剩 1 个------少采一整段 。实测 4 个折点里首两重合、stepCount = 100 时,采样点从 301 掉到 201。

3.2 取高程:一个 ?? 0 掩盖了三种情况

ts 复制代码
for (const sample of samples) {
  let height = 0
  try {
    const heightValue = viewer.scene.globe.getHeight(
      Cartographic.fromDegrees(sample.lon, sample.lat)
    )
    height = heightValue ?? 0     // 🔑 未加载 / 出界 / 没渲染 → 一律 0 m
  } catch {
    height = 0                    // 🔑 连异常也吃掉
  }
  profile.push([sample.lon, sample.lat, height, sample.cumDistM])
}

🔑 关键点:

  • getHeight 的返回值只有三种,demo 只接住了一种 。Cesium 未压缩源码里 Globe.prototype.getHeight 有三处 return undefined:零级瓦片还没建好(L866)、经纬度不在任何零级瓦片里(L874)、以及往下找到的最深那层瓦片没有 renderedMesh(L912)。
  • 它是"逐级往下钻"的 :从零级瓦片沿四叉树一路往下,只把"有 renderedMesh"的瓦片记成 tileWithMesh,最后在这块瓦片上取交点。也就是说------剖面精度取决于当前渲染到哪一层。你把相机拉近、瓦片加载得更细,同一条剖面线的高程会变。
  • 高程来自三角面,不是高程栅格 。链路是 GlobeSurfaceTile.pick → TerrainMesh.pick → terrainPicker.rayIntersect,本质是沿椭球面法线打一条射线、和三角网格求交,落在三角形内部就是线性插值。所以采样点比网格密的那部分,是白采的。

3.3 画曲线:三个"看起来无害"的选项

ts 复制代码
chart.setOption({
  grid: { left: 40, right: 16, top: 24, bottom: 28 },
  xAxis: {
    type: 'category',                       // 🔑 类别轴:等距,与距离数值无关
    boundaryGap: false,
    name: '距离 (km)',
    data: distances,                        // 🌰 距离字符串,只当刻度标签用
    axisLabel: {
      showMinLabel: true, showMaxLabel: true,
      interval: Math.max(1, Math.floor(distances.length / 6))
    }
  },
  yAxis: { type: 'value', name: '高程 (m)', scale: true },   // 🔑 scale 不从 0 起
  series: [{
    data: heights, type: 'line', smooth: true, showSymbol: false,
    lineStyle: { color: '#2f80ed', width: 2 },
    areaStyle: { color: 'rgba(47, 128, 237, 0.35)' }
  }]
})

🔑 关键点:

  • type: 'category' 把 301 个采样点摆成 301 个等宽刻度 。distances 数组里的 "0.00 / 41.01 / 82.03 ..." 只是标签,不参与定位。这一点是后面所有横轴失真的总源头。
  • interval 是"跳过几个",不是"每几个显示一个" 。写 interval: 50,实测显示的是第 0、51、102、153、204、255 个刻度;再加上 showMaxLabel: true 补的终点,一共 7 个标签。
  • scale: true 让 y 轴不从 0 起 。默认数据范围 2760~4210 m,实测轴范围被算成 2700~4500 m ------那块蓝色面积的基线是 2700 m,不是海平面。
  • smooth: true 不会制造假峰 。本猿本来攒了一堆"平滑把尖峰冲过头"的素材,实测发现 ECharts 把控制点钳制在了相邻两点的包围盒里(源码注释写得很直白:Avoid exceeding extreme after smoothing),实测最大越界只有 0.74 m------那是像素取整的噪声(172 px 对应 2200 m,1 px = 12.79 m)。所以这一条不用避。

四、功能亮点与性能优势

4.1 三种采样方式的点数与代价

本猿把三种方式在默认路径上都跑了一遍(Node 里复刻了同一套算法,@cesium/engine 26.2.0 + ECharts 6.1.0,不是估算):

采样方式 档位 采样点数 密化数组 报告总长 与真实总长 241262.471 m 的缺口
每段步数 20 61 1501 241262.471 m 0
每段步数 100(默认) 301 1501 241262.471 m 0
每段步数 300 901 1501 241262.471 m 0
固定间距 100 m 2413 1501 241200 m −62.471 m
固定间距 500 m 483 1501 241000 m −262.471 m
固定间距 2000 m 121 1501 240000 m −1262.471 m
固定间距 5000 m 49 1501 240000 m −1262.471 m
总采样点数 20 20 1501 241262.471 m 0
总采样点数 1000 1000 1501 241262.471 m 0

注意最后一列:"固定间距"永远差那么一点,而且差多少由间距决定,不由路径决定 ------缺口精确等于 真实总长 mod 间距。

再看采样点数和绘制对象数的关系------折点数会让采样点数爆炸:

路径 密化数组 每段步数 20 每段步数 100 每段步数 300
默认 4 折点 1501 61 301 901
手绘 20 折点 9501 381 1901 5701

4.2 横坐标:等距的画布,不等距的现实

这是本猿最想让大家看一眼的一张图:

实测三段的真实长度(haversine,R = 6371008.8):73308.018 / 73145.390 / 94809.063 m ,占全长的 30.39 % / 30.32 % / 39.30 %。

而图上它们占的宽度是 33.33 % / 33.00 % / 33.00 %。于是:

段 真实长度 真实占比 图上占比 每格米数 失真系数
段 1 73308.018 m 30.39 % 33.33 % 733.080 m 1.0970×
段 2 73145.390 m 30.32 % 33.00 % 738.842 m 1.0885×
段 3 94809.063 m 39.30 % 33.00 % 957.667 m 0.8398×

最疏的一格比最密的一格宽 1.3064 倍 。段 3 比段 1 长 21501.045 m ,在图上却一样宽------它的每一段爬升被横向压扁到 76.5 %,看着比实际平缓 23.5 %。

简单说:**类别轴画出来的是"采样序号的曲线",不是"距离的曲线"。**采样均匀(固定间距模式)时两者一致;采样不均匀时(每段步数模式、或折点分布不均时),图上的坡度就是假的。

顺带说一句 y 轴。scale: true 把轴范围定在 2700~4500 m,面积图的基线落在这 2700 m 上------那块看着像"山体截面"的蓝色填充,底座其实是悬空的 。真要表达"填挖方体积"这类量,scale 必须关掉(关掉后轴范围实测是 0~5000 m)。

4.3 那条红线到底走的是哪条线

球上那条红线是 polyline.clampToGround = true,没有指定 arcType。查源码,GroundPolylineGeometry 的默认值是这么写的:

js 复制代码
// @cesium/engine · Source/Core/GroundPolylineGeometry.js
// @param {ArcType} [options.arcType=ArcType.GEODESIC]
// @param {number} [options.granularity=CesiumMath.RADIANS_PER_DEGREE]
this.arcType = options.arcType ?? ArcType.GEODESIC

granularity 的默认值是 1°,也就是 111.195 km 。实测 PolylinePipeline.generateArc 在不同跨度上输出的顶点数:

路径跨度 generateArc 顶点数
0.5° 2
1° 2
2° 3
5° 6
10° 10

默认路径三段全都不够 1°,红线中间一个点都没插 ------它就是两个端点之间的一条直线。而剖面的采样点是在 lon / lat 上线性插值的,画在球上是一条"经纬线"。两者中间差多少?

路径 haversine 段长 采样点偏离测地线最大距离
默认段 1(0.5° 斜线) 73.308 km 67.567 m
默认段 3(1.0° 纬线) 94.809 km 108.297 m
手绘 5° 纬线 476.524 km 2684.266 m
手绘 10° 纬线 952.806 km 10754.602 m
手绘 10° 斜线 1435.335 km 30229.783 m

默认那两段上,一百米上下的偏差在球上确实看不出来。但只要你画的线超过 1°------一条从北京到天津的剖面线就差不多 1.2° ------偏差立刻进入"米"级;到了省级跨度(5°~10°),剖面采的点和球上红线的最远处能差 30 公里。那时候你指着球上那条红线说"我要看这条线的剖面",实际上切的是另一条山脊。

核心优势

  • 🚀 纯前端闭环:影像、地形、采样、绘图全在浏览器里,没有任何服务端依赖
  • 📏 三种采样口径:等步数 / 等间距 / 等点数,分别对应"每段都要看细"、"我要固定物理间距"、"我只想要 N 个点"
  • 🖐️ 手绘即算 :ScreenSpaceEventHandler 收 LEFT_CLICK 攒折点,RIGHT_CLICK 或 LEFT_DOUBLE_CLICK 收尾,边画边看
  • 📈 图随线动:剖面图和球上红线共用同一份路径数据,改折点两边一起变

五、小贴士:避坑指南

🎯 关于长度:haversine 用的是平均半径

haversineMeters 里 R = 6371008.8(IUGG 平均半径),而 Cesium 的 Ellipsoid.WGS84 是椭球。两者在同一段上的差不是常数符号,是会互相抵消的------默认路径三段实测:

  • 段 1:haversine 比测地线 少 62.719 m(−0.0856 %)
  • 段 2:少 59.448 m(−0.0813 %)
  • 段 3:多 192.929 m(+0.2035 %)------因为它是纯纬线,纬圈半径在 lat 31.5° 上约为 95 km/°,平均半径算不出来这一段
  • 合计:haversine 241262.471 m ,测地线 241333.234 m ,短 70.763 m

⚠️ 重要提示 :三项误差恰好抵消掉一大半,纯属这条路径运气好。别把"这次对了"当成"这个公式对" 。要跟 Cesium 内部完全一致,直接用 EllipsoidGeodesic.surfaceDistance:

ts 复制代码
const g = new EllipsoidGeodesic(
  Cartographic.fromDegrees(a[0], a[1]),
  Cartographic.fromDegrees(b[0], b[1])
)
const meters = g.surfaceDistance     // 🔑 Cesium 自己就是这么算的

📐 关于横轴:interval 是"跳过几个"

axisLabel.interval 的语义容易反着记:写 n 表示每 n+1 个刻度显示一个 。demo 写的是 Math.max(1, Math.floor(点数 / 6)),实测在 20 ~ 1000 个点上,显示出来的标签永远是 6~7 个 (showMinLabel 和 showMaxLabel 还会各补一个)。

⚠️ 重要提示 :想让横轴真的是距离轴,把 xAxis.type 换成 'value',data 换成 [[距离, 高程], ...] 的点对,再把 boundaryGap / interval 一起删掉。收益是坡度立刻变真;代价是点密的地方会被挤在一起------而这恰恰是真实情况。

⛰️ 关于高程:先确认它真的取到了

剖面图最怕的不是"不准",是"静默地不准"。getHeight 返回 undefined 时,?? 0 会给你一条 0 米高的假平台,而 try / catch 连异常都吞了。

⚠️ 重要提示 :把 undefined 和"真的 0 米"分开报出来。最小改动就是if (h === undefined) missed++,然后把 ?? 0 留在后面,最后在面板上加一行"缺失点 N 个"。顺带提醒:分析前先把相机飞到路径上方、等它加载完 。getHeight 只认已经渲染出网格的瓦片,相机没看过的地方就是 undefined。

⚡ 关于采样:密不等于准

Cesium World Terrain 的 CesiumTerrainProvider 默认用 GeographicTilingScheme(源码 L186),level L 的瓦片经纬度跨度都是 180 / 2^L 度:

层级 角度跨度 纬向边长 经向边长(lat 31°)
10 0.175781° 19489.0 m 16787.9 m
11 0.087891° 9744.5 m 8393.9 m
12 0.043945° 4872.2 m 4197.0 m

而采样点间距实测:每段步数 300 → 268.07 m;固定间距 100 m 档 → 100.03 m。这些点里绝大多数两两落在同一个三角形内部,取到的是同一张平面上的线性插值。

⚠️ 重要提示 :与其把采样点加密,不如先把相机拉近、让瓦片加载到更细的一层------那才真的增加了信息。采样密度真正解决的是另一个问题:**别把山脊采漏。**本猿用一条构造的地形(山脊半宽约 2412 m)做了 40 次相位扫描,同一个山脊,不同采样间距能踩到的最坏情况是:

采样点数 采样间距 最坏漏掉的山脊高度
20 12698.0 m 725.08 m
100 2437.0 m 564.04 m
301 804.2 m 94.88 m
901 268.1 m 15.78 m

同一个山头,采样点少的时候能不能看见它,全看它是不是恰好落在某个采样点上。

📦 关于几个上限

  • Math.max(...heights) 的实参上限 :实测在本机 V8 上是 124000 ~ 125000 ,超了直接 RangeError: Maximum call stack size exceeded。这个 demo 的最坏情况是"每段步数 300 + 415 个折点",日常够用;但如果哪天把上限放开,记得改成 reduce。
  • "固定间距"没有点数上限 :间距滑到最小 100 m,一条 241 km 的路径就是 2413 个点、2413 次同步射线求交。Cartographic.fromDegrees 本身的构造成本只有 0.3729 ms (2413 次,已预热),但 getHeight 才是大头------而且它是同步的,全压在主线程上。
  • 密化数组只跟折点数有关 :1 + 500 × (N−1)。手绘 20 个折点就是 9501 个对象,跟路径多长无关。

目前的局限与后续可以做的:

  1. 采样循环是同步 for,采样点上千时会卡一下 UI------按 200 个一批切开,或挪进 Worker
  2. DENSE_PER_SEGMENT = 500 写死------应该按段长自适应(ceil(段长 / 目标间距))
  3. 累积距离和位置用了两套算法------统一成 EllipsoidGeodesic 就干净了
  4. 横轴是类别轴------换 type: 'value' 才是真距离轴
  5. 缺采样点没有上报------undefined 和 0 米现在长得一模一样
  6. 只能画一条剖面------没法两条线对比填挖方

六、写在最后

剖面分析这件事,做久了会有一种错觉:/ 以为自己切开的是地形,/ 其实切开的是这个引擎的一层层抽象------/ 你手里那张曲线,只是抽象漏下来的一点影子。

从 #53 把点变成清单,到 #54 把地挖出一个洞,再到今天顺着一条线把地形切开------空间分析这条线终于从"看"走到了"量"。🎉

现在,Cesium 不仅能告诉你"那里有多高",还能告诉你"这一路是怎么爬上来的"了。虽然它爬的可能是另一条山脊。

这个案例还不完美:横轴是类别轴、密化常数写死、缺采样点静默变 0、长线上红线和采样线各走各的。但它把"一条剖面是怎么拼出来的"这件事讲清楚了,这就够了。

这条线还能继续往下切。下一次我们不停在地表上了------站在一个点上往四周看:哪些地方看得见,哪些地方被挡着。通视分析,我们下一篇见。

如果你在自己的项目里也画过这种"看着对、量着歪"的剖面图,欢迎留言说说你最后是怎么校准的------是靠改用数值轴,还是干脆在服务端算完再丢给前端画。


🌟 项目已开源,欢迎 Star!(你的 Star ⭐ 就是对我最大的支持!)

仓库地址:https://github.com/jianlei-wang/CESIUM-TREASURE-BOX

代码在 src/cases/profile-analysis/ 目录下,是一个独立的 Vue 单文件组件,复制进你的项目、把 Bing 影像换成自己的底图就能跑。


Cesium酱の百宝箱 · 第 55 篇

红线绕球走大圆,剖面按格子走直线

2026 年的秋夜,与你共探"一条线的高程"

相关推荐
:mnong5 小时前
Web 三维可视化技术对比:ParaView Web vs 本项目 wasm + Three.js
前端·webgl·opengl·opencl·科学可视化
疯狂敲代码的老刘6 小时前
my-fingerprint浏览器扩展:1.4K星开源指纹防护神器,Canvas/WebGL/WebGPU伪装一键开启
webgl
Amos_Web7 小时前
Rspack 源码解析(二十一):Cacheable 与 Storage 如何持久化编译结果
前端·rust·前端框架
Amos_Web7 小时前
Rspack 源码解析(二十二):ExternalsPlugin 如何在 Factorize 阶段换掉整个模块
前端·rust·前端框架
总要学点什么1 天前
第51篇:数据分析-热力图——笔刷画的是圆,落到地上却成了椭圆
3d·数据挖掘·数据分析·前端框架·gis·webgl·cesium
Amos_Web1 天前
Rspack 源码解析(二十):Typed Hook 系统如何靠宏生成
前端·rust·前端框架
子龙_1 天前
来在网页上用threejs画个蛋白质吧
前端·webgl·three.js
To_OC2 天前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
WayneX3 天前
开源 Vue 3 组件库 Morya UI:把组件、文档、AI 工具链一起做进一个包
前端·vue.js·前端框架