tilesetOptions.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. /** Cesium streaming policy; pure so configuration and the actual engine call share tests. */
  2. import { DEFAULT_LOD_TIERS, normalizeLodTiers, fineThresholdFromTiers, tierForDistance } from '../domain/lodTiers.js'
  3. /**
  4. * 瓦片流式策略的可调档(票 35 第二轮)。
  5. *
  6. * **默认值 = 原口径**(SSE 按配置、缓存 64/32 MiB、离散/连续由 `progressiveRefinement` 决定),
  7. * 因此既有验收脚本 `tools/city-massing/verify-cell-coarse-city.mjs` 的断言不受影响。
  8. * 弱机想再省,可在 `map.performance` 里设置:
  9. * `tilesetScreenSpaceErrorScale: 1.3`(SSE 放大 → 少选/少解内容)
  10. * `tilesetCacheBytes: 50331648`(48MiB,少驻留)
  11. * `discreteLod: true`(离散 LOD,CPU 遍历最省,但会有 LOD 跳变)
  12. */
  13. const tuning = {
  14. screenSpaceErrorScale: 1,
  15. cacheBytes: 64 * 1024 * 1024,
  16. maximumCacheOverflowBytes: 32 * 1024 * 1024,
  17. // 维护者反馈「中距离变精模、继续靠近反而变粗糙」后**回退为配置驱动**:
  18. // 连续/离散仍由 `progressiveRefinement` 决定(与整改前一致)。
  19. // 离散档仍是运维可选项:`map.performance.discreteLod: true`。
  20. discreteLod: null,
  21. // 同一批反馈:动态 SSE 会让远处瓦片提前"满足"、近处再补,观感上就是忽粗忽细;
  22. // 回退为关闭(整改前口径)。
  23. dynamicScreenSpaceError: false,
  24. }
  25. /**
  26. * 细模区域批次的专用档(票 61 第二轮)。
  27. *
  28. * ## 为什么细模不能复用粗模的 tilesetOptions
  29. *
  30. * 细模区域 tileset 的层级是:`root ge=1000000(空) → preview ge=48 → fine ge=0`。
  31. * 也就是说 —— **窗口/立面这些细节全在 fine 层,preview 只是"有屋顶、没窗"的替身**。
  32. * Cesium 判"要不要继续往 fine 下钻"用的是同一套屏幕空间误差:
  33. *
  34. * sse = geometricError × drawingBufferHeight ÷ (distance × 2·tan(fovy/2))
  35. *
  36. * 而 `loadSiblings` 那处调用传的是**粗模批次**的 plan,因此细模继承了粗模的
  37. * `maximumScreenSpaceError = 115`。代入 ge=48、H=810、fovy=60°:
  38. *
  39. * SSE=115 → 只有 293m 以内才细化到 fine(有窗);之外停在 preview(无窗的灰盒子)
  40. * SSE= 96 → 只有 351m
  41. * SSE= 32 → 只有 1052m
  42. *
  43. * 这就是"稍远一点就变成没窗户的火柴盒"的原因。粗模的 115 是为**全城批次**定的
  44. * (少选瓦片、保帧率),对"近景细模"这个用途根本不合适。
  45. *
  46. * ## 另一个来源:foveatedScreenSpaceError
  47. *
  48. * 粗模档开着 `foveatedScreenSpaceError: true` —— 屏幕中心优先细化、边缘放宽阈值。
  49. * 对全城批次这是好事(人眼看中心),但对细模区域它造成一个**反直觉**的现象:
  50. * **同样距离的建筑,屏幕中间的变精模、屏幕边上的还是火柴盒**。维护者报的
  51. * "同样距离一半精模一半火柴盒"正是这条 —— 与距离无关,与屏幕位置有关。
  52. * 细模批次改为**关闭**,让同一距离的建筑获得同等细节。
  53. */
  54. const DETAIL_DEFAULTS = {
  55. screenSpaceError: 56,
  56. foveated: false,
  57. /** 层级表(票 61 第十六轮)。五级默认见 domain/lodTiers.js。 */
  58. tiers: null,
  59. /**
  60. * 2026-10-30 内存优化:细模是**每个区域一个 tileset**,每个 tileset 各自持有一份
  61. * Cesium 内容缓存。旧默认沿用粗模的 64MB + 32MB,区域内同时驻留多个区域时
  62. * 缓存上限会按区域数线性放大(密集点可同时有十几个区域)。
  63. * 单个区域只是几百米见方,24MB + 8MB 足够覆盖它的 fine 内容;
  64. * 需要更高保真/更少回流的部署可经 `map.detail.tilesetCacheBytes` 调回。
  65. */
  66. cacheBytes: 24 * 1024 * 1024,
  67. maximumCacheOverflowBytes: 8 * 1024 * 1024,
  68. }
  69. const detailTuning = {
  70. /**
  71. * 细模细化阈值 —— 这条**就是"精模覆盖到多远"**的旋钮(可按部署覆盖)。
  72. *
  73. * 区域 tileset 是三级:coarse(全城白盒) → preview(ge=48, **与 fine 同一套材质**)
  74. * → fine(ge=0, 窗户/立面细节)。Cesium 按屏幕空间误差逐级下钻,所以阈值直接决定:
  75. *
  76. * 阈值 24 → fine 覆盖到 ~1403m(近中景全是精模 ⇒ 太重,近景掉帧的主因)
  77. * 阈值 40 → ~842m
  78. * 阈值 56 → ~601m ← 新默认:精模只保近景,中景由 preview 承担
  79. * 阈值 96 → ~351m(精模只贴脸,中景偏粗)
  80. *
  81. * 取 56:把"精模"限定在约 600m 内,600m~覆盖前沿之间显示 preview —— preview 与 fine
  82. * **材质同名同色**(Plaster_Sand / Plaster_Ivory / Glass_Dark / Door_Walnut…),屋顶
  83. * 也共用同一张 SatelliteRoofAtlas,所以中→近的切换是"同一栋楼变细",不是"换了个东西"。
  84. *
  85. * 注意这不是"降质":**总覆盖范围由 detailFrontier 的半径控制,这里只决定分层**。
  86. * 想要更多精模就调小(更吃性能),想要更稳就调大。
  87. */
  88. ...DETAIL_DEFAULTS,
  89. }
  90. /**
  91. * `map.detail` 覆盖细模批次档。
  92. *
  93. * **可复位**:作为"配置归一化 + 热更新"的落点,它必须能把值改回来。若只认
  94. * `Number.isFinite`,传 `{}` 或 `null` 就永远改不回默认 —— 部署侧把阈值调大试一次,
  95. * 就再也回不去了。这里显式接受 null/undefined = 回默认。
  96. */
  97. export function setDetailTilesetPerformance(options = {}) {
  98. const next = options && typeof options === 'object' ? options : {}
  99. if (next.fineScreenSpaceError === null || next.fineScreenSpaceError === undefined) {
  100. detailTuning.screenSpaceError = DETAIL_DEFAULTS.screenSpaceError
  101. } else if (Number.isFinite(next.fineScreenSpaceError)) {
  102. detailTuning.screenSpaceError = Math.max(2, Math.min(256, next.fineScreenSpaceError))
  103. }
  104. if (next.fineFoveated === null || next.fineFoveated === undefined) detailTuning.foveated = DETAIL_DEFAULTS.foveated
  105. else if (typeof next.fineFoveated === 'boolean') detailTuning.foveated = next.fineFoveated
  106. // 层级表:传数组就归一化(非法整表退回默认),传 null 就清掉(回落到显式阈值)。
  107. if (next.tiers === null || next.tiers === undefined) detailTuning.tiers = null
  108. else detailTuning.tiers = normalizeLodTiers(next.tiers)
  109. // 细模缓存:null/undefined 回默认;给数则夹在安全区间(不小于 16MiB,防回流抖动)。
  110. if (next.tilesetCacheBytes === null || next.tilesetCacheBytes === undefined) {
  111. detailTuning.cacheBytes = DETAIL_DEFAULTS.cacheBytes
  112. } else if (Number.isFinite(next.tilesetCacheBytes)) {
  113. detailTuning.cacheBytes = Math.max(16 << 20, Math.min(256 << 20, next.tilesetCacheBytes))
  114. }
  115. if (next.tilesetCacheOverflowBytes === null || next.tilesetCacheOverflowBytes === undefined) {
  116. detailTuning.maximumCacheOverflowBytes = DETAIL_DEFAULTS.maximumCacheOverflowBytes
  117. } else if (Number.isFinite(next.tilesetCacheOverflowBytes)) {
  118. detailTuning.maximumCacheOverflowBytes = Math.max(0, Math.min(64 << 20, next.tilesetCacheOverflowBytes))
  119. }
  120. }
  121. /** 当前生效的层级表(没配置就是默认五级)。给诊断与闸门读同一份。 */
  122. export function lodTiersInEffect() {
  123. return detailTuning.tiers || DEFAULT_LOD_TIERS
  124. }
  125. /** 某个距离当前会落在哪一级(诊断/闸门用;纯函数委托给 domain)。 */
  126. export function tierAtDistance(distanceM) {
  127. return tierForDistance(distanceM, lodTiersInEffect())
  128. }
  129. export function detailTilesetPerformance() {
  130. return { ...detailTuning }
  131. }
  132. /**
  133. * 细模区域批次的 options。与粗模共用流式纪律(缓存/预取/连续 LOD),
  134. * 但**误差阈值与中心偏置独立** —— 这两项直接决定"近景是不是精模"。
  135. */
  136. export function detailTilesetOptions(plan = {}) {
  137. const continuous = tuning.discreteLod === null ? plan.progressiveRefinement === true : tuning.discreteLod !== true
  138. // 维护者可用 `tilesetScreenSpaceErrorScale` 整体降载;细模同样吃这个系数(弱机少解内容),
  139. // 但基数从 detailTuning.screenSpaceError 起算,绝不继承粗模的 115。
  140. // 票 61 第十六轮:阈值优先由**层级表**推导(这样"精模进到多近"只在一处声明,
  141. // 加级/改级不需要再发明旋钮);没有层级表时才回落到显式的 fineScreenSpaceError。
  142. const derived = detailTuning.tiers ? fineThresholdFromTiers(detailTuning.tiers) : null
  143. const baseThreshold = Number.isFinite(derived) ? derived : detailTuning.screenSpaceError
  144. const scaled = Math.round(baseThreshold * tuning.screenSpaceErrorScale)
  145. return {
  146. skipLevelOfDetail: !continuous,
  147. baseScreenSpaceError: 4096,
  148. skipScreenSpaceErrorFactor: 32,
  149. skipLevels: 2,
  150. immediatelyLoadDesiredLevelOfDetail: false,
  151. loadSiblings: false,
  152. cullRequestsWhileMoving: true,
  153. cullRequestsWhileMovingMultiplier: continuous ? 30 : 90,
  154. dynamicScreenSpaceError: tuning.dynamicScreenSpaceError,
  155. preloadWhenHidden: false,
  156. preloadFlightDestinations: false,
  157. // 同距离同细节:细模关掉中心偏置(粗模仍保留,那是全城批次的取舍)。
  158. foveatedScreenSpaceError: detailTuning.foveated === true,
  159. foveatedConeSize: 0.1,
  160. foveatedTimeDelay: 0.6,
  161. foveatedMinimumScreenSpaceErrorRelaxation: 0,
  162. maximumScreenSpaceError: scaled,
  163. // 细模用**自己的**缓存档(每个区域一个 tileset);全局降载仍可把它压得更低。
  164. cacheBytes: Math.min(detailTuning.cacheBytes, tuning.cacheBytes),
  165. maximumCacheOverflowBytes: Math.min(detailTuning.maximumCacheOverflowBytes, tuning.maximumCacheOverflowBytes),
  166. }
  167. }
  168. /** 由服务层按 `map.performance` 覆盖;未给的项保持默认档。 */
  169. export function setTilesetPerformance(options = {}) {
  170. const next = options || {}
  171. if (Number.isFinite(next.tilesetScreenSpaceErrorScale)) {
  172. tuning.screenSpaceErrorScale = Math.min(2, Math.max(1, next.tilesetScreenSpaceErrorScale))
  173. }
  174. if (Number.isFinite(next.tilesetCacheBytes)) tuning.cacheBytes = Math.max(16 << 20, next.tilesetCacheBytes)
  175. if (Number.isFinite(next.tilesetCacheOverflowBytes)) {
  176. tuning.maximumCacheOverflowBytes = Math.max(0, next.tilesetCacheOverflowBytes)
  177. }
  178. // `null` = 交回 `progressiveRefinement` 决定(默认);true/false = 强制离散/连续。
  179. if (next.discreteLod === null || typeof next.discreteLod === 'boolean') tuning.discreteLod = next.discreteLod
  180. if (typeof next.dynamicScreenSpaceError === 'boolean') tuning.dynamicScreenSpaceError = next.dynamicScreenSpaceError
  181. }
  182. export function tilesetPerformance() {
  183. return { ...tuning }
  184. }
  185. export function tilesetOptions(plan = {}) {
  186. // `discreteLod: true` 可强制走离散 LOD(skipLevelOfDetail),CPU 遍历更省但会有 LOD 跳变。
  187. const continuous = tuning.discreteLod === null ? plan.progressiveRefinement === true : tuning.discreteLod !== true
  188. const plannedError = Number.isFinite(plan.maximumScreenSpaceError) ? plan.maximumScreenSpaceError : 96
  189. return {
  190. skipLevelOfDetail: !continuous,
  191. baseScreenSpaceError: 4096,
  192. skipScreenSpaceErrorFactor: 32,
  193. skipLevels: 2,
  194. immediatelyLoadDesiredLevelOfDetail: false,
  195. loadSiblings: false,
  196. cullRequestsWhileMoving: true,
  197. cullRequestsWhileMovingMultiplier: continuous ? 30 : 90,
  198. // 第三轮:让"远离相机"的瓦片按更宽的 SSE 判定已满足 —— 飞行/加载时少请求一批内容,
  199. // 单帧集中上传与着色器编译的尖峰随之变小。准入半径内的近景判定不受影响。
  200. dynamicScreenSpaceError: tuning.dynamicScreenSpaceError,
  201. preloadWhenHidden: false,
  202. preloadFlightDestinations: false,
  203. foveatedScreenSpaceError: true,
  204. foveatedConeSize: 0.1,
  205. // Base REPLACE bypasses foveated priority deferral; keep the legacy value.
  206. foveatedTimeDelay: 0.6,
  207. foveatedMinimumScreenSpaceErrorRelaxation: 0,
  208. maximumScreenSpaceError: Math.round(plannedError * tuning.screenSpaceErrorScale),
  209. // 缓存上限同步下调:少驻留内容、少大块分配,弱机上 GC 峰值更低。
  210. cacheBytes: tuning.cacheBytes,
  211. maximumCacheOverflowBytes: tuning.maximumCacheOverflowBytes,
  212. }
  213. }