| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- /** Cesium streaming policy; pure so configuration and the actual engine call share tests. */
- import { DEFAULT_LOD_TIERS, normalizeLodTiers, fineThresholdFromTiers, tierForDistance } from '../domain/lodTiers.js'
- /**
- * 瓦片流式策略的可调档(票 35 第二轮)。
- *
- * **默认值 = 原口径**(SSE 按配置、缓存 64/32 MiB、离散/连续由 `progressiveRefinement` 决定),
- * 因此既有验收脚本 `tools/city-massing/verify-cell-coarse-city.mjs` 的断言不受影响。
- * 弱机想再省,可在 `map.performance` 里设置:
- * `tilesetScreenSpaceErrorScale: 1.3`(SSE 放大 → 少选/少解内容)
- * `tilesetCacheBytes: 50331648`(48MiB,少驻留)
- * `discreteLod: true`(离散 LOD,CPU 遍历最省,但会有 LOD 跳变)
- */
- const tuning = {
- screenSpaceErrorScale: 1,
- cacheBytes: 64 * 1024 * 1024,
- maximumCacheOverflowBytes: 32 * 1024 * 1024,
- // 维护者反馈「中距离变精模、继续靠近反而变粗糙」后**回退为配置驱动**:
- // 连续/离散仍由 `progressiveRefinement` 决定(与整改前一致)。
- // 离散档仍是运维可选项:`map.performance.discreteLod: true`。
- discreteLod: null,
- // 同一批反馈:动态 SSE 会让远处瓦片提前"满足"、近处再补,观感上就是忽粗忽细;
- // 回退为关闭(整改前口径)。
- dynamicScreenSpaceError: false,
- }
- /**
- * 细模区域批次的专用档(票 61 第二轮)。
- *
- * ## 为什么细模不能复用粗模的 tilesetOptions
- *
- * 细模区域 tileset 的层级是:`root ge=1000000(空) → preview ge=48 → fine ge=0`。
- * 也就是说 —— **窗口/立面这些细节全在 fine 层,preview 只是"有屋顶、没窗"的替身**。
- * Cesium 判"要不要继续往 fine 下钻"用的是同一套屏幕空间误差:
- *
- * sse = geometricError × drawingBufferHeight ÷ (distance × 2·tan(fovy/2))
- *
- * 而 `loadSiblings` 那处调用传的是**粗模批次**的 plan,因此细模继承了粗模的
- * `maximumScreenSpaceError = 115`。代入 ge=48、H=810、fovy=60°:
- *
- * SSE=115 → 只有 293m 以内才细化到 fine(有窗);之外停在 preview(无窗的灰盒子)
- * SSE= 96 → 只有 351m
- * SSE= 32 → 只有 1052m
- *
- * 这就是"稍远一点就变成没窗户的火柴盒"的原因。粗模的 115 是为**全城批次**定的
- * (少选瓦片、保帧率),对"近景细模"这个用途根本不合适。
- *
- * ## 另一个来源:foveatedScreenSpaceError
- *
- * 粗模档开着 `foveatedScreenSpaceError: true` —— 屏幕中心优先细化、边缘放宽阈值。
- * 对全城批次这是好事(人眼看中心),但对细模区域它造成一个**反直觉**的现象:
- * **同样距离的建筑,屏幕中间的变精模、屏幕边上的还是火柴盒**。维护者报的
- * "同样距离一半精模一半火柴盒"正是这条 —— 与距离无关,与屏幕位置有关。
- * 细模批次改为**关闭**,让同一距离的建筑获得同等细节。
- */
- const DETAIL_DEFAULTS = {
- screenSpaceError: 56,
- foveated: false,
- /** 层级表(票 61 第十六轮)。五级默认见 domain/lodTiers.js。 */
- tiers: null,
- /**
- * 2026-10-30 内存优化:细模是**每个区域一个 tileset**,每个 tileset 各自持有一份
- * Cesium 内容缓存。旧默认沿用粗模的 64MB + 32MB,区域内同时驻留多个区域时
- * 缓存上限会按区域数线性放大(密集点可同时有十几个区域)。
- * 单个区域只是几百米见方,24MB + 8MB 足够覆盖它的 fine 内容;
- * 需要更高保真/更少回流的部署可经 `map.detail.tilesetCacheBytes` 调回。
- */
- cacheBytes: 24 * 1024 * 1024,
- maximumCacheOverflowBytes: 8 * 1024 * 1024,
- }
- const detailTuning = {
- /**
- * 细模细化阈值 —— 这条**就是"精模覆盖到多远"**的旋钮(可按部署覆盖)。
- *
- * 区域 tileset 是三级:coarse(全城白盒) → preview(ge=48, **与 fine 同一套材质**)
- * → fine(ge=0, 窗户/立面细节)。Cesium 按屏幕空间误差逐级下钻,所以阈值直接决定:
- *
- * 阈值 24 → fine 覆盖到 ~1403m(近中景全是精模 ⇒ 太重,近景掉帧的主因)
- * 阈值 40 → ~842m
- * 阈值 56 → ~601m ← 新默认:精模只保近景,中景由 preview 承担
- * 阈值 96 → ~351m(精模只贴脸,中景偏粗)
- *
- * 取 56:把"精模"限定在约 600m 内,600m~覆盖前沿之间显示 preview —— preview 与 fine
- * **材质同名同色**(Plaster_Sand / Plaster_Ivory / Glass_Dark / Door_Walnut…),屋顶
- * 也共用同一张 SatelliteRoofAtlas,所以中→近的切换是"同一栋楼变细",不是"换了个东西"。
- *
- * 注意这不是"降质":**总覆盖范围由 detailFrontier 的半径控制,这里只决定分层**。
- * 想要更多精模就调小(更吃性能),想要更稳就调大。
- */
- ...DETAIL_DEFAULTS,
- }
- /**
- * `map.detail` 覆盖细模批次档。
- *
- * **可复位**:作为"配置归一化 + 热更新"的落点,它必须能把值改回来。若只认
- * `Number.isFinite`,传 `{}` 或 `null` 就永远改不回默认 —— 部署侧把阈值调大试一次,
- * 就再也回不去了。这里显式接受 null/undefined = 回默认。
- */
- export function setDetailTilesetPerformance(options = {}) {
- const next = options && typeof options === 'object' ? options : {}
- if (next.fineScreenSpaceError === null || next.fineScreenSpaceError === undefined) {
- detailTuning.screenSpaceError = DETAIL_DEFAULTS.screenSpaceError
- } else if (Number.isFinite(next.fineScreenSpaceError)) {
- detailTuning.screenSpaceError = Math.max(2, Math.min(256, next.fineScreenSpaceError))
- }
- if (next.fineFoveated === null || next.fineFoveated === undefined) detailTuning.foveated = DETAIL_DEFAULTS.foveated
- else if (typeof next.fineFoveated === 'boolean') detailTuning.foveated = next.fineFoveated
- // 层级表:传数组就归一化(非法整表退回默认),传 null 就清掉(回落到显式阈值)。
- if (next.tiers === null || next.tiers === undefined) detailTuning.tiers = null
- else detailTuning.tiers = normalizeLodTiers(next.tiers)
- // 细模缓存:null/undefined 回默认;给数则夹在安全区间(不小于 16MiB,防回流抖动)。
- if (next.tilesetCacheBytes === null || next.tilesetCacheBytes === undefined) {
- detailTuning.cacheBytes = DETAIL_DEFAULTS.cacheBytes
- } else if (Number.isFinite(next.tilesetCacheBytes)) {
- detailTuning.cacheBytes = Math.max(16 << 20, Math.min(256 << 20, next.tilesetCacheBytes))
- }
- if (next.tilesetCacheOverflowBytes === null || next.tilesetCacheOverflowBytes === undefined) {
- detailTuning.maximumCacheOverflowBytes = DETAIL_DEFAULTS.maximumCacheOverflowBytes
- } else if (Number.isFinite(next.tilesetCacheOverflowBytes)) {
- detailTuning.maximumCacheOverflowBytes = Math.max(0, Math.min(64 << 20, next.tilesetCacheOverflowBytes))
- }
- }
- /** 当前生效的层级表(没配置就是默认五级)。给诊断与闸门读同一份。 */
- export function lodTiersInEffect() {
- return detailTuning.tiers || DEFAULT_LOD_TIERS
- }
- /** 某个距离当前会落在哪一级(诊断/闸门用;纯函数委托给 domain)。 */
- export function tierAtDistance(distanceM) {
- return tierForDistance(distanceM, lodTiersInEffect())
- }
- export function detailTilesetPerformance() {
- return { ...detailTuning }
- }
- /**
- * 细模区域批次的 options。与粗模共用流式纪律(缓存/预取/连续 LOD),
- * 但**误差阈值与中心偏置独立** —— 这两项直接决定"近景是不是精模"。
- */
- export function detailTilesetOptions(plan = {}) {
- const continuous = tuning.discreteLod === null ? plan.progressiveRefinement === true : tuning.discreteLod !== true
- // 维护者可用 `tilesetScreenSpaceErrorScale` 整体降载;细模同样吃这个系数(弱机少解内容),
- // 但基数从 detailTuning.screenSpaceError 起算,绝不继承粗模的 115。
- // 票 61 第十六轮:阈值优先由**层级表**推导(这样"精模进到多近"只在一处声明,
- // 加级/改级不需要再发明旋钮);没有层级表时才回落到显式的 fineScreenSpaceError。
- const derived = detailTuning.tiers ? fineThresholdFromTiers(detailTuning.tiers) : null
- const baseThreshold = Number.isFinite(derived) ? derived : detailTuning.screenSpaceError
- const scaled = Math.round(baseThreshold * tuning.screenSpaceErrorScale)
- return {
- skipLevelOfDetail: !continuous,
- baseScreenSpaceError: 4096,
- skipScreenSpaceErrorFactor: 32,
- skipLevels: 2,
- immediatelyLoadDesiredLevelOfDetail: false,
- loadSiblings: false,
- cullRequestsWhileMoving: true,
- cullRequestsWhileMovingMultiplier: continuous ? 30 : 90,
- dynamicScreenSpaceError: tuning.dynamicScreenSpaceError,
- preloadWhenHidden: false,
- preloadFlightDestinations: false,
- // 同距离同细节:细模关掉中心偏置(粗模仍保留,那是全城批次的取舍)。
- foveatedScreenSpaceError: detailTuning.foveated === true,
- foveatedConeSize: 0.1,
- foveatedTimeDelay: 0.6,
- foveatedMinimumScreenSpaceErrorRelaxation: 0,
- maximumScreenSpaceError: scaled,
- // 细模用**自己的**缓存档(每个区域一个 tileset);全局降载仍可把它压得更低。
- cacheBytes: Math.min(detailTuning.cacheBytes, tuning.cacheBytes),
- maximumCacheOverflowBytes: Math.min(detailTuning.maximumCacheOverflowBytes, tuning.maximumCacheOverflowBytes),
- }
- }
- /** 由服务层按 `map.performance` 覆盖;未给的项保持默认档。 */
- export function setTilesetPerformance(options = {}) {
- const next = options || {}
- if (Number.isFinite(next.tilesetScreenSpaceErrorScale)) {
- tuning.screenSpaceErrorScale = Math.min(2, Math.max(1, next.tilesetScreenSpaceErrorScale))
- }
- if (Number.isFinite(next.tilesetCacheBytes)) tuning.cacheBytes = Math.max(16 << 20, next.tilesetCacheBytes)
- if (Number.isFinite(next.tilesetCacheOverflowBytes)) {
- tuning.maximumCacheOverflowBytes = Math.max(0, next.tilesetCacheOverflowBytes)
- }
- // `null` = 交回 `progressiveRefinement` 决定(默认);true/false = 强制离散/连续。
- if (next.discreteLod === null || typeof next.discreteLod === 'boolean') tuning.discreteLod = next.discreteLod
- if (typeof next.dynamicScreenSpaceError === 'boolean') tuning.dynamicScreenSpaceError = next.dynamicScreenSpaceError
- }
- export function tilesetPerformance() {
- return { ...tuning }
- }
- export function tilesetOptions(plan = {}) {
- // `discreteLod: true` 可强制走离散 LOD(skipLevelOfDetail),CPU 遍历更省但会有 LOD 跳变。
- const continuous = tuning.discreteLod === null ? plan.progressiveRefinement === true : tuning.discreteLod !== true
- const plannedError = Number.isFinite(plan.maximumScreenSpaceError) ? plan.maximumScreenSpaceError : 96
- return {
- skipLevelOfDetail: !continuous,
- baseScreenSpaceError: 4096,
- skipScreenSpaceErrorFactor: 32,
- skipLevels: 2,
- immediatelyLoadDesiredLevelOfDetail: false,
- loadSiblings: false,
- cullRequestsWhileMoving: true,
- cullRequestsWhileMovingMultiplier: continuous ? 30 : 90,
- // 第三轮:让"远离相机"的瓦片按更宽的 SSE 判定已满足 —— 飞行/加载时少请求一批内容,
- // 单帧集中上传与着色器编译的尖峰随之变小。准入半径内的近景判定不受影响。
- dynamicScreenSpaceError: tuning.dynamicScreenSpaceError,
- preloadWhenHidden: false,
- preloadFlightDestinations: false,
- foveatedScreenSpaceError: true,
- foveatedConeSize: 0.1,
- // Base REPLACE bypasses foveated priority deferral; keep the legacy value.
- foveatedTimeDelay: 0.6,
- foveatedMinimumScreenSpaceErrorRelaxation: 0,
- maximumScreenSpaceError: Math.round(plannedError * tuning.screenSpaceErrorScale),
- // 缓存上限同步下调:少驻留内容、少大块分配,弱机上 GC 峰值更低。
- cacheBytes: tuning.cacheBytes,
- maximumCacheOverflowBytes: tuning.maximumCacheOverflowBytes,
- }
- }
|