/** 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, } }