@navaramap/three 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/LICENSE +8 -0
  2. package/dist/assets/atmosphere/README.md +1 -0
  3. package/dist/assets/atmosphere/higher_order_scattering.bin +0 -0
  4. package/dist/assets/atmosphere/higher_order_scattering.exr +0 -0
  5. package/dist/assets/atmosphere/irradiance.bin +0 -0
  6. package/dist/assets/atmosphere/irradiance.exr +0 -0
  7. package/dist/assets/atmosphere/scattering.bin +0 -0
  8. package/dist/assets/atmosphere/scattering.exr +0 -0
  9. package/dist/assets/atmosphere/single_mie_scattering.bin +0 -0
  10. package/dist/assets/atmosphere/single_mie_scattering.exr +0 -0
  11. package/dist/assets/atmosphere/stars.bin +0 -0
  12. package/dist/assets/atmosphere/transmittance.bin +0 -0
  13. package/dist/assets/atmosphere/transmittance.exr +0 -0
  14. package/dist/assets/cloud/README.md +1 -0
  15. package/dist/assets/cloud/local_weather.png +0 -0
  16. package/dist/assets/cloud/shape.bin +1 -0
  17. package/dist/assets/cloud/shape_detail.bin +1 -0
  18. package/dist/assets/cloud/turbulence.png +0 -0
  19. package/dist/assets/fontWorker-CEJEfRpb.js +2 -0
  20. package/dist/assets/fontWorker-CEJEfRpb.js.map +1 -0
  21. package/dist/assets/index-BUB-ijMO.js +12 -0
  22. package/dist/assets/index-BUB-ijMO.js.map +1 -0
  23. package/dist/assets/navara_wasm_font_worker_bg-665OLxjy.wasm +0 -0
  24. package/dist/assets/navara_wasm_worker_bg-BXhKC5bE.wasm +0 -0
  25. package/dist/assets/noise/README.md +1 -0
  26. package/dist/assets/noise/stbn.bin +0 -0
  27. package/dist/assets/water/README.md +3 -0
  28. package/dist/assets/water/waternormals.jpg +0 -0
  29. package/dist/index.d.ts +10114 -0
  30. package/dist/index.js +34481 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/index.umd.cjs +6345 -0
  33. package/dist/index.umd.cjs.map +1 -0
  34. package/package.json +95 -0
  35. package/src/Color.ts +111 -0
  36. package/src/ShadowMapViewers.ts +49 -0
  37. package/src/atmosphere.test.ts +76 -0
  38. package/src/atmosphere.ts +364 -0
  39. package/src/bufferView.ts +90 -0
  40. package/src/camera.ts +182 -0
  41. package/src/concurrency.ts +15 -0
  42. package/src/constants/assets.ts +30 -0
  43. package/src/constants/decoder.ts +7 -0
  44. package/src/constants/index.ts +2 -0
  45. package/src/core/BaseDesc.ts +142 -0
  46. package/src/core/BaseHandle.ts +126 -0
  47. package/src/core/DescRegistry.ts +88 -0
  48. package/src/core/EffectDesc.ts +343 -0
  49. package/src/core/EffectDescRegistry.ts +31 -0
  50. package/src/core/InstancedMeshDesc.ts +413 -0
  51. package/src/core/LightDesc.ts +216 -0
  52. package/src/core/LightDescRegistry.ts +32 -0
  53. package/src/core/MeshDesc.ts +457 -0
  54. package/src/core/MeshDescRegistry.ts +32 -0
  55. package/src/core/MeshDescWithSelectiveEffect.ts +129 -0
  56. package/src/core/Registries.ts +59 -0
  57. package/src/core/SelectiveEffectRegistry.ts +90 -0
  58. package/src/core/ViewContext.ts +235 -0
  59. package/src/core/errors.ts +16 -0
  60. package/src/core/index.ts +50 -0
  61. package/src/device.ts +414 -0
  62. package/src/effects/CustomEffectPass.ts +28 -0
  63. package/src/effects/copy.ts +10 -0
  64. package/src/effects/effect.ts +87 -0
  65. package/src/effects/index.ts +4 -0
  66. package/src/effects/render.ts +10 -0
  67. package/src/evaluations/FeatureEvaluator.ts +529 -0
  68. package/src/evaluations/index.ts +1 -0
  69. package/src/event/HillshadeContext.test.ts +232 -0
  70. package/src/event/HillshadeContext.ts +299 -0
  71. package/src/event/context.ts +279 -0
  72. package/src/event/feature.ts +357 -0
  73. package/src/event/featureEvent.ts +81 -0
  74. package/src/event/features/billboard.ts +30 -0
  75. package/src/event/features/model.ts +192 -0
  76. package/src/event/features/modelGpuBytes.test.ts +69 -0
  77. package/src/event/features/modelGpuBytes.ts +87 -0
  78. package/src/event/features/point.ts +25 -0
  79. package/src/event/features/polygon.ts +23 -0
  80. package/src/event/features/polygonOutline.ts +19 -0
  81. package/src/event/features/polyline.ts +19 -0
  82. package/src/event/features/text.ts +71 -0
  83. package/src/event/hillshade.test.ts +842 -0
  84. package/src/event/hillshade.ts +588 -0
  85. package/src/event/index.ts +700 -0
  86. package/src/event/loaders.ts +53 -0
  87. package/src/event/tile.ts +19 -0
  88. package/src/event/worker.ts +999 -0
  89. package/src/event/workerTaskSettlement.test.ts +243 -0
  90. package/src/event/workerTaskSettlement.ts +128 -0
  91. package/src/inMemoryBufferStore.test.ts +60 -0
  92. package/src/inMemoryBufferStore.ts +67 -0
  93. package/src/index.iife.ts +11 -0
  94. package/src/index.ts +2556 -0
  95. package/src/input.ts +144 -0
  96. package/src/layer.ts +183 -0
  97. package/src/layers/effect/FinalCopyEffectDesc.ts +29 -0
  98. package/src/layers/effect/MRTPassEffectDesc.ts +105 -0
  99. package/src/layers/effect/README.md +9 -0
  100. package/src/layers/effect/SelectiveEffectDesc.ts +113 -0
  101. package/src/layers/effect/SkyEnvMapEffectDesc.ts +58 -0
  102. package/src/layers/effect/TransparentPassEffectDesc.ts +62 -0
  103. package/src/layers/effect/index.ts +5 -0
  104. package/src/layers/index.ts +1 -0
  105. package/src/layersManager.ts +63 -0
  106. package/src/light.ts +15 -0
  107. package/src/loaders/AbortableImageLoader.ts +203 -0
  108. package/src/loaders/AbortableTextureLoader.ts +74 -0
  109. package/src/loaders/index.ts +5 -0
  110. package/src/material/enhancer/DESIGN.md +300 -0
  111. package/src/material/enhancer/MaterialEnhancer.ts +112 -0
  112. package/src/material/enhancer/ShaderReplacer.test.ts +114 -0
  113. package/src/material/enhancer/ShaderReplacer.ts +97 -0
  114. package/src/material/enhancer/batchTexture.ts +8 -0
  115. package/src/material/enhancer/index.ts +46 -0
  116. package/src/material/enhancer/instancedSprite/index.ts +7 -0
  117. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/index.test.ts +214 -0
  118. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/index.ts +134 -0
  119. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/markers.ts +21 -0
  120. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/material.ts +32 -0
  121. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +277 -0
  122. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +151 -0
  123. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/shader.test.ts +114 -0
  124. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/shader.ts +49 -0
  125. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/state.test.ts +82 -0
  126. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/state.ts +80 -0
  127. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +146 -0
  128. package/src/material/enhancer/instancedSprite/instancedSpriteMaterialEnhancer.ts +105 -0
  129. package/src/material/enhancer/model/index.ts +25 -0
  130. package/src/material/enhancer/model/modelBaseEnhancer/index.test.ts +74 -0
  131. package/src/material/enhancer/model/modelBaseEnhancer/index.ts +110 -0
  132. package/src/material/enhancer/model/modelBaseEnhancer/markers.ts +27 -0
  133. package/src/material/enhancer/model/modelBaseEnhancer/material.test.ts +66 -0
  134. package/src/material/enhancer/model/modelBaseEnhancer/material.ts +44 -0
  135. package/src/material/enhancer/model/modelBaseEnhancer/mutates.test.ts +84 -0
  136. package/src/material/enhancer/model/modelBaseEnhancer/mutates.ts +47 -0
  137. package/src/material/enhancer/model/modelBaseEnhancer/shader.test.ts +24 -0
  138. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +162 -0
  139. package/src/material/enhancer/model/modelBaseEnhancer/state.test.ts +103 -0
  140. package/src/material/enhancer/model/modelBaseEnhancer/state.ts +52 -0
  141. package/src/material/enhancer/model/modelBaseEnhancer/types.ts +89 -0
  142. package/src/material/enhancer/model/modelMaterialEnhancer.ts +91 -0
  143. package/src/material/enhancer/model/modelWaterEnhancer/index.test.ts +71 -0
  144. package/src/material/enhancer/model/modelWaterEnhancer/index.ts +175 -0
  145. package/src/material/enhancer/model/modelWaterEnhancer/material.test.ts +48 -0
  146. package/src/material/enhancer/model/modelWaterEnhancer/material.ts +25 -0
  147. package/src/material/enhancer/model/modelWaterEnhancer/mutates.test.ts +89 -0
  148. package/src/material/enhancer/model/modelWaterEnhancer/mutates.ts +89 -0
  149. package/src/material/enhancer/model/modelWaterEnhancer/shader.test.ts +34 -0
  150. package/src/material/enhancer/model/modelWaterEnhancer/shader.ts +111 -0
  151. package/src/material/enhancer/model/modelWaterEnhancer/state.test.ts +105 -0
  152. package/src/material/enhancer/model/modelWaterEnhancer/state.ts +59 -0
  153. package/src/material/enhancer/model/modelWaterEnhancer/types.ts +141 -0
  154. package/src/material/enhancer/model/pntsEnhancer/index.test.ts +96 -0
  155. package/src/material/enhancer/model/pntsEnhancer/index.ts +82 -0
  156. package/src/material/enhancer/model/pntsEnhancer/markers.ts +6 -0
  157. package/src/material/enhancer/model/pntsEnhancer/material.test.ts +37 -0
  158. package/src/material/enhancer/model/pntsEnhancer/material.ts +21 -0
  159. package/src/material/enhancer/model/pntsEnhancer/mutates.test.ts +80 -0
  160. package/src/material/enhancer/model/pntsEnhancer/mutates.ts +29 -0
  161. package/src/material/enhancer/model/pntsEnhancer/shader.test.ts +32 -0
  162. package/src/material/enhancer/model/pntsEnhancer/shader.ts +56 -0
  163. package/src/material/enhancer/model/pntsEnhancer/state.test.ts +87 -0
  164. package/src/material/enhancer/model/pntsEnhancer/state.ts +29 -0
  165. package/src/material/enhancer/model/pntsEnhancer/types.ts +38 -0
  166. package/src/material/enhancer/polygon/index.ts +22 -0
  167. package/src/material/enhancer/polygon/polygonBaseEnhancer/index.test.ts +101 -0
  168. package/src/material/enhancer/polygon/polygonBaseEnhancer/index.ts +130 -0
  169. package/src/material/enhancer/polygon/polygonBaseEnhancer/markers.ts +23 -0
  170. package/src/material/enhancer/polygon/polygonBaseEnhancer/material.test.ts +19 -0
  171. package/src/material/enhancer/polygon/polygonBaseEnhancer/material.ts +38 -0
  172. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.test.ts +75 -0
  173. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +122 -0
  174. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.test.ts +25 -0
  175. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +249 -0
  176. package/src/material/enhancer/polygon/polygonBaseEnhancer/state.test.ts +102 -0
  177. package/src/material/enhancer/polygon/polygonBaseEnhancer/state.ts +92 -0
  178. package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +150 -0
  179. package/src/material/enhancer/polygon/polygonMaterialEnhancer.ts +97 -0
  180. package/src/material/enhancer/polygon/polygonWaterEnhancer/index.test.ts +72 -0
  181. package/src/material/enhancer/polygon/polygonWaterEnhancer/index.ts +164 -0
  182. package/src/material/enhancer/polygon/polygonWaterEnhancer/material.test.ts +48 -0
  183. package/src/material/enhancer/polygon/polygonWaterEnhancer/material.ts +35 -0
  184. package/src/material/enhancer/polygon/polygonWaterEnhancer/mutates.ts +61 -0
  185. package/src/material/enhancer/polygon/polygonWaterEnhancer/shader.test.ts +34 -0
  186. package/src/material/enhancer/polygon/polygonWaterEnhancer/shader.ts +96 -0
  187. package/src/material/enhancer/polygon/polygonWaterEnhancer/state.test.ts +98 -0
  188. package/src/material/enhancer/polygon/polygonWaterEnhancer/state.ts +57 -0
  189. package/src/material/enhancer/polygon/polygonWaterEnhancer/types.ts +115 -0
  190. package/src/material/enhancer/polyline/index.ts +7 -0
  191. package/src/material/enhancer/polyline/polylineBaseEnhancer/index.test.ts +157 -0
  192. package/src/material/enhancer/polyline/polylineBaseEnhancer/index.ts +154 -0
  193. package/src/material/enhancer/polyline/polylineBaseEnhancer/markers.ts +19 -0
  194. package/src/material/enhancer/polyline/polylineBaseEnhancer/material.test.ts +42 -0
  195. package/src/material/enhancer/polyline/polylineBaseEnhancer/material.ts +38 -0
  196. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.test.ts +120 -0
  197. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +169 -0
  198. package/src/material/enhancer/polyline/polylineBaseEnhancer/shader.test.ts +67 -0
  199. package/src/material/enhancer/polyline/polylineBaseEnhancer/shader.ts +75 -0
  200. package/src/material/enhancer/polyline/polylineBaseEnhancer/state.test.ts +55 -0
  201. package/src/material/enhancer/polyline/polylineBaseEnhancer/state.ts +91 -0
  202. package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +170 -0
  203. package/src/material/enhancer/polyline/polylineMaterialEnhancer.ts +102 -0
  204. package/src/material/enhancer/sdfText/index.ts +7 -0
  205. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/index.ts +125 -0
  206. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/markers.ts +19 -0
  207. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/material.test.ts +35 -0
  208. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/material.ts +32 -0
  209. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.test.ts +381 -0
  210. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +232 -0
  211. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +36 -0
  212. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.test.ts +118 -0
  213. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.ts +107 -0
  214. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +184 -0
  215. package/src/material/enhancer/sdfText/sdfTextMaterialEnhancer.ts +100 -0
  216. package/src/material/enhancer/shadowMap/index.ts +7 -0
  217. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/index.ts +97 -0
  218. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/markers.ts +10 -0
  219. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/material.ts +31 -0
  220. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/mutates.ts +8 -0
  221. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/shader.test.ts +17 -0
  222. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/shader.ts +53 -0
  223. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/state.ts +17 -0
  224. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/types.ts +26 -0
  225. package/src/material/enhancer/test-utils/shaderCompatibility.ts +124 -0
  226. package/src/material/enhancer/tileComposite/compose.test.ts +95 -0
  227. package/src/material/enhancer/tileComposite/compose.ts +112 -0
  228. package/src/material/enhancer/tileComposite/index.ts +23 -0
  229. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/index.ts +11 -0
  230. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +132 -0
  231. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/shader.ts +245 -0
  232. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/types.ts +43 -0
  233. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/index.ts +29 -0
  234. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/mutates.ts +68 -0
  235. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +24 -0
  236. package/src/material/enhancer/tileComposite/tileHillshadeEnhancer/index.ts +29 -0
  237. package/src/material/enhancer/tileComposite/tileHillshadeEnhancer/mutates.ts +34 -0
  238. package/src/material/enhancer/tileComposite/tileHillshadeEnhancer/shader.ts +36 -0
  239. package/src/material/enhancer/tileComposite/tileWaterEnhancer/index.ts +23 -0
  240. package/src/material/enhancer/tileComposite/tileWaterEnhancer/mutates.ts +23 -0
  241. package/src/material/enhancer/tileComposite/tileWaterEnhancer/shader.ts +8 -0
  242. package/src/material/enhancer/tileComposite/tileWatermaskEnhancer/index.ts +23 -0
  243. package/src/material/enhancer/tileComposite/tileWatermaskEnhancer/mutates.ts +34 -0
  244. package/src/material/enhancer/tileComposite/tileWatermaskEnhancer/shader.ts +13 -0
  245. package/src/material/enhancer/tileComposite/types.ts +58 -0
  246. package/src/material/index.ts +3 -0
  247. package/src/material/macro/index.ts +1 -0
  248. package/src/material/macro/tileShader.test.ts +136 -0
  249. package/src/material/macro/tileShader.ts +287 -0
  250. package/src/material/overrideMaterialsForMRT.ts +439 -0
  251. package/src/material/selectiveEffectMaterialSetup.test.ts +52 -0
  252. package/src/material/selectiveEffectMaterialSetup.ts +53 -0
  253. package/src/material/types/index.ts +1 -0
  254. package/src/material/types/uniform.ts +1 -0
  255. package/src/mesh/DrapedMesh.ts +101 -0
  256. package/src/mesh/batchTexture.test.ts +646 -0
  257. package/src/mesh/batchTexture.ts +437 -0
  258. package/src/mesh/batchedFeature.ts +186 -0
  259. package/src/mesh/batchedSdfText.ts +484 -0
  260. package/src/mesh/featureMesh.ts +43 -0
  261. package/src/mesh/index.ts +16 -0
  262. package/src/mesh/instanced.ts +117 -0
  263. package/src/mesh/model.ts +483 -0
  264. package/src/mesh/pickableMesh.ts +55 -0
  265. package/src/mesh/pickableMeshWrapper.ts +529 -0
  266. package/src/mesh/polygon.ts +699 -0
  267. package/src/mesh/polygonOutline.ts +393 -0
  268. package/src/mesh/polyline.ts +523 -0
  269. package/src/mesh/releaseGeometryArrays.ts +57 -0
  270. package/src/mesh/rtcRteHelper.ts +211 -0
  271. package/src/mesh/sdfText.test.ts +200 -0
  272. package/src/mesh/sdfText.ts +1019 -0
  273. package/src/mesh/sprite/billboardAtlas.test.ts +177 -0
  274. package/src/mesh/sprite/billboardAtlas.ts +210 -0
  275. package/src/mesh/sprite/billboardAtlasImageLoader.ts +29 -0
  276. package/src/mesh/sprite/index.ts +1 -0
  277. package/src/mesh/sprite/instancedSprite.ts +694 -0
  278. package/src/mesh/tile.test.ts +393 -0
  279. package/src/mesh/tile.ts +1790 -0
  280. package/src/object3DEvent.ts +6 -0
  281. package/src/orchestrators/RenderPassOrchestrator.ts +147 -0
  282. package/src/orchestrators/index.ts +1 -0
  283. package/src/passes/AllDepthCopyPass.ts +165 -0
  284. package/src/passes/CustomRenderPass.ts +279 -0
  285. package/src/passes/NormalCopyPass.ts +125 -0
  286. package/src/passes/RenderTargetCopyPass.ts +91 -0
  287. package/src/passes/SkyEnvMapPass.ts +58 -0
  288. package/src/passes/index.ts +5 -0
  289. package/src/pick/pickHelper.ts +273 -0
  290. package/src/pick/pickTerrain.ts +254 -0
  291. package/src/plugins/AttributionPlugin/AttributionPlugin.test.ts +210 -0
  292. package/src/plugins/AttributionPlugin/attribution.test.ts +225 -0
  293. package/src/plugins/AttributionPlugin/attribution.ts +355 -0
  294. package/src/plugins/AttributionPlugin/attributionStyles.ts +163 -0
  295. package/src/plugins/AttributionPlugin/index.ts +702 -0
  296. package/src/plugins/index.ts +1 -0
  297. package/src/quality.ts +1 -0
  298. package/src/renderOrder.ts +3 -0
  299. package/src/scene.test.ts +164 -0
  300. package/src/scene.ts +127 -0
  301. package/src/shaders/index.ts +1 -0
  302. package/src/solar.test.ts +81 -0
  303. package/src/solar.ts +101 -0
  304. package/src/source.ts +80 -0
  305. package/src/stats.ts +102 -0
  306. package/src/tasks/computeVertexNormalsAsync.ts +15 -0
  307. package/src/tasks/constructPolygonBatchedFeature.ts +34 -0
  308. package/src/tasks/constructPolylineBatchedFeature.ts +27 -0
  309. package/src/tasks/constructQuantizedMeshTerrainMesh.ts +24 -0
  310. package/src/tasks/constructTerrainMesh.ts +25 -0
  311. package/src/tasks/getImageDataFromBlob.ts +13 -0
  312. package/src/tasks/getImageDataFromImageBitmap.ts +12 -0
  313. package/src/tasks/parseMvtTile.ts +22 -0
  314. package/src/tasks/queueTask.ts +19 -0
  315. package/src/tasks/toCreasedNormalsAsync.ts +18 -0
  316. package/src/tasks/upsampleQuantizedMeshTerrainMesh.ts +44 -0
  317. package/src/tasks/upsampleTerrainMesh.ts +39 -0
  318. package/src/texture.ts +19 -0
  319. package/src/textures.ts +13 -0
  320. package/src/tileTexture/CompositeShader.snapshot.test.ts +59 -0
  321. package/src/tileTexture/CompositeShader.test.ts +168 -0
  322. package/src/tileTexture/CompositeShader.ts +53 -0
  323. package/src/tileTexture/SlotPlanner.test.ts +105 -0
  324. package/src/tileTexture/SlotPlanner.ts +102 -0
  325. package/src/tileTexture/TileTextureCache.test.ts +227 -0
  326. package/src/tileTexture/TileTextureCache.ts +192 -0
  327. package/src/tileTexture/TileTextureCompositor.test.ts +260 -0
  328. package/src/tileTexture/TileTextureCompositor.ts +421 -0
  329. package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +2649 -0
  330. package/src/tileTexture/index.ts +8 -0
  331. package/src/tileTexture/types.ts +150 -0
  332. package/src/type/index.ts +261 -0
  333. package/src/uniforms.ts +20 -0
  334. package/src/utils/arraysEqual.ts +9 -0
  335. package/src/utils/blendModes.ts +80 -0
  336. package/src/utils/geometry.ts +53 -0
  337. package/src/utils/hillshadeNormalMapGenerator.ts +193 -0
  338. package/src/utils/index.ts +8 -0
  339. package/src/utils/object.test.ts +14 -0
  340. package/src/utils/object.ts +7 -0
  341. package/src/utils/replacer.test.ts +42 -0
  342. package/src/utils/replacer.ts +89 -0
  343. package/src/utils/screen.ts +29 -0
  344. package/src/utils/textureFragmentIndex.test.ts +224 -0
  345. package/src/utils/textureFragmentIndex.ts +97 -0
  346. package/src/utils/worker.ts +3 -0
  347. package/src/vite-env.d.ts +2 -0
  348. package/src/worker/index.ts +13 -0
  349. package/src/worker/tasks/computeVertexNormals.ts +15 -0
  350. package/src/worker/tasks/toCreasedNormals.ts +22 -0
@@ -0,0 +1,1790 @@
1
+ import { generate_id_from_entity, type TileHandle } from "@navaramap/core";
2
+ import type {
3
+ MeshAdded,
4
+ Mesh as EventMesh,
5
+ RasterTileInternalMaterial,
6
+ Transform,
7
+ TextureFragment,
8
+ MeshChanged,
9
+ Globe,
10
+ } from "@navaramap/engine";
11
+ import BranchFreeTernary from "@shaders/glsl/chunks/branchFreeTernary.glsl?raw";
12
+ import SpecularParsFragment from "@shaders/glsl/chunks/spucular_pars_fragment.glsl";
13
+ import WaterParsFragment from "@shaders/glsl/chunks/water_pars_fragment.glsl?raw";
14
+ import {
15
+ BufferAttribute,
16
+ BufferGeometry,
17
+ Color,
18
+ DataTexture,
19
+ NearestFilter,
20
+ Mesh,
21
+ MeshLambertMaterial,
22
+ RedFormat,
23
+ RGBAFormat,
24
+ SRGBColorSpace,
25
+ Texture,
26
+ UnsignedByteType,
27
+ Vector2,
28
+ Vector3,
29
+ Vector4,
30
+ AddOperation,
31
+ Object3D,
32
+ WebGLRenderTarget,
33
+ type MagnificationTextureFilter,
34
+ type MinificationTextureFilter,
35
+ ShaderChunk,
36
+ Box3,
37
+ Box3Helper,
38
+ Sphere,
39
+ NoColorSpace,
40
+ MeshBasicMaterial,
41
+ LinearFilter,
42
+ } from "three";
43
+
44
+ import { PolygonMesh, PolylineMesh } from "..";
45
+ import { setTransform } from "../event";
46
+ import type { EventContext, TileHandler } from "../event/context";
47
+ import {
48
+ generateTileCommonInjection,
49
+ generateTileMapFragment,
50
+ generateTileNormalFragmentMaps,
51
+ TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT,
52
+ TILE_NORMAL_BUFFER_REPLACEMENT,
53
+ TILE_PICK_FRAGMENT_OVERRIDE,
54
+ TILE_VERTEX_INJECTIONS,
55
+ } from "../material";
56
+ import { deriveCompositeFeatures } from "../material/enhancer/tileComposite";
57
+ import type { CustomObject3DEventMap } from "../object3DEvent";
58
+ import type { TexturizedSceneByTileCoordinates } from "../scene";
59
+ import type { TextureOptions } from "../textures";
60
+ import {
61
+ planSlots,
62
+ type CompositeFeatures,
63
+ type CompositeGlobals,
64
+ type CompositeLayer,
65
+ type TileTextureCompositor,
66
+ } from "../tileTexture";
67
+ import type { TileMapByHandle } from "../type";
68
+ import type { CommonUniforms } from "../uniforms";
69
+ import { createReplacer } from "../utils";
70
+ import {
71
+ type TextureSlot,
72
+ updateTextureFragmentIndex,
73
+ removeTextureFragmentIndex,
74
+ } from "../utils/textureFragmentIndex";
75
+
76
+ import type { PickableMesh } from "./pickableMesh";
77
+ import { releaseGeometryArraysAfterUpload } from "./releaseGeometryArrays";
78
+
79
+ export type TileMaterial = MeshBasicMaterial | MeshLambertMaterial;
80
+
81
+ /**
82
+ * One WebMercator vector source tile baked into a layer's render target.
83
+ * `tileHandle` keys the cached offscreen scene; `uvOffset`/`uvScale` are the
84
+ * **mercator** affine framing the terrain tile's sub-region of that (possibly
85
+ * coarser, ancestor-resolved) source tile, driving the bake camera (identity for
86
+ * an exact same-tile drape).
87
+ */
88
+ type VectorSource = {
89
+ tileHandle: TileHandle;
90
+ uvOffset: [number, number];
91
+ uvScale: [number, number];
92
+ };
93
+
94
+ /**
95
+ * One clamp-to-ground vector layer draped on this terrain tile, resolved by Rust.
96
+ * A layer is backed by one source on WebMercator terrain, but by several when the
97
+ * terrain is Geographic (N:M): every overlapping WM vector tile is baked into the
98
+ * layer's single render target, each framed by its own `uvOffset`/`uvScale`, so
99
+ * the RT ends up spanning the terrain tile's extent. `reproject` carries the
100
+ * terrain `[south, north]` band on Geographic terrain (undefined on WM) so the
101
+ * composite paste can remap the latitude axis WebMercator→Geographic, exactly
102
+ * like a raster slot.
103
+ */
104
+ type VectorSlot = {
105
+ layerId: string;
106
+ sources: VectorSource[];
107
+ reproject?: [number, number];
108
+ };
109
+
110
+ /** The MRT composite atlas has three RGBA attachments (color, attr, normal). */
111
+ const ATLAS_ATTACHMENTS = 3;
112
+ /** GPU bytes for one drape render target at side length `size`: size² RGBA. */
113
+ function drapeRtBytes(size: number): number {
114
+ return size * size * 4;
115
+ }
116
+ /** GPU bytes for the per-tile composite atlas at side length `size`: one MRT
117
+ * with three size² RGBA attachments. A terrain tile acquires this atlas in its
118
+ * constructor and holds it for its whole lifetime (raster-only tiles
119
+ * included), so Rust seeds it into the tile's cost at mesh-attach time and
120
+ * every drape report from here must keep including it. The side length is 512²
121
+ * on both desktop and mobile (so ~3MB per tile with three attachments) and
122
+ * flows in from the compositor, which is the SAME source of truth
123
+ * `getDefaultMemoryCostHints` feeds to the Rust ledger — so the reported bytes
124
+ * always match the seed. */
125
+ function compositeAtlasBytes(size: number): number {
126
+ return size * size * 4 * ATLAS_ATTACHMENTS;
127
+ }
128
+
129
+ /** Shared 1×1 transparent texture bound to texturized sampler slots that have
130
+ * no drape render target yet (they are allocated lazily). WebGL requires every
131
+ * sampler to reference a valid texture; the shader gates sampling with `shows`,
132
+ * so this placeholder's content is never visible. */
133
+ const EMPTY_TEXTURIZED_TEXTURE = new DataTexture(
134
+ new Uint8Array([0, 0, 0, 0]),
135
+ 1,
136
+ 1,
137
+ RGBAFormat,
138
+ );
139
+ EMPTY_TEXTURIZED_TEXTURE.needsUpdate = true;
140
+
141
+ /** Apply the shared texturized-slot sampler settings to a drape render target's
142
+ * texture. Returns the texture for call-site chaining. */
143
+ function configureTexturizedTexture(
144
+ tex: Texture,
145
+ textureOptions: TextureOptions,
146
+ ): Texture {
147
+ tex.minFilter = textureOptions.minFilter as MinificationTextureFilter;
148
+ tex.magFilter = textureOptions.magFilter as MagnificationTextureFilter;
149
+ tex.anisotropy = textureOptions.maxAnisotropy;
150
+ tex.generateMipmaps = textureOptions.useMipmaps;
151
+ tex.needsUpdate = true;
152
+ return tex;
153
+ }
154
+
155
+ export class TileMesh
156
+ extends Mesh<BufferGeometry, TileMaterial, CustomObject3DEventMap>
157
+ implements PickableMesh
158
+ {
159
+ handle: TileHandle;
160
+ tileHandler: TileHandler;
161
+ maxTextures: number;
162
+ texturizedSceneIndexFrom: number;
163
+ numTexturizedVector: number;
164
+
165
+ // Per-layer texturized-vector slots resolved by Rust: one slot per clamp-to-ground
166
+ // layer, keyed by the WM vector tile whose offscreen scene backs it. Ancestor
167
+ // fallback is already resolved in Rust, so the web side just caches + bakes.
168
+ private vectorSlots: VectorSlot[] = [];
169
+ // Signature of the last baked slot set; a change drives a re-bake + re-bind.
170
+ private prevVectorSignature = "";
171
+ // The vector resolution revision at which `vectorSlots` was last fetched. Re-fetching
172
+ // is skipped while the global revision is unchanged (the resolve result can only change
173
+ // when a traverse runs or a scene becomes ready). `-1` forces a fetch on the first frame.
174
+ private _lastVectorRevision = -1;
175
+
176
+ // Separate mesh for shadow casting (uses terrain-only geometry without skirt)
177
+ private shadowMesh?: Mesh<BufferGeometry, TileMaterial>;
178
+
179
+ private texturizedSceneByTileCoordinates: TexturizedSceneByTileCoordinates;
180
+ private compositor: TileTextureCompositor;
181
+
182
+ texturizedSceneRenderTargets: WebGLRenderTarget[] = [];
183
+ // Drape RT side length + atlas byte cost, read from the compositor (the
184
+ // single source of truth shared with the Rust cost hints) in the constructor.
185
+ // Per-instance rather than module constants because the compositor size is a
186
+ // runtime value (512² on both desktop and mobile today, but configurable).
187
+ private readonly drapeRtSize: number;
188
+ private readonly atlasBytes: number;
189
+ // Drape footprint (bytes) last reported to the memory ledger, so a report
190
+ // only crosses the WASM boundary when it actually changes. Starts at the
191
+ // atlas cost to match the seed Rust charges at mesh-attach time (the atlas
192
+ // is acquired in the constructor) — a tile that never drapes therefore
193
+ // never reports. Assigned in the constructor once `atlasBytes` is known.
194
+ private reportedDrapeGpuBytes: number;
195
+
196
+ private warnedExceededTextures = false;
197
+
198
+ readonly ctx: EventContext;
199
+
200
+ constructor(ctx: EventContext, mesh: MeshAdded) {
201
+ super();
202
+ this.ctx = ctx;
203
+
204
+ const {
205
+ texturizedSceneByTileCoordinates,
206
+ tileTextureCompositor,
207
+ textureOptions,
208
+ tileMapByHandle,
209
+ tileHandler,
210
+ } = ctx;
211
+
212
+ const handle = mesh.tile_handle;
213
+ this.handle = handle;
214
+
215
+ this.texturizedSceneByTileCoordinates = texturizedSceneByTileCoordinates;
216
+ this.compositor = tileTextureCompositor;
217
+
218
+ // Device-dependent atlas / drape RT sizing, sourced from the compositor so
219
+ // it always matches the ledger cost hints (see device.ts).
220
+ this.drapeRtSize = this.compositor.size;
221
+ this.atlasBytes = compositeAtlasBytes(this.compositor.size);
222
+ this.reportedDrapeGpuBytes = this.atlasBytes;
223
+
224
+ // Acquire the per-tile composite atlas from the compositor. Pairing the
225
+ // acquire with the mesh keeps the atlas lifecycle tied to the tile and lets
226
+ // the compositor allocate its private OrthographicCamera for this handle.
227
+ // The atlas Textures are bound to the main shader later in initMaterial.
228
+ this.compositor.acquire(handle);
229
+
230
+ this.maxTextures = textureOptions.maxTextures;
231
+
232
+ // Calculate numAdditionalTextures based on which additional textures are in use
233
+ const additionalTexturesInUse =
234
+ textureOptions.additionalTexturesInUse ?? {};
235
+ let numAdditionalTextures = 0;
236
+ if (additionalTexturesInUse.waterTexture) numAdditionalTextures++;
237
+ if (additionalTexturesInUse.colorMapTexture) numAdditionalTextures++;
238
+
239
+ this.numTexturizedVector =
240
+ Math.floor(textureOptions.maxTextures / 2) - numAdditionalTextures;
241
+ this.texturizedSceneIndexFrom = this.maxTextures - this.numTexturizedVector;
242
+
243
+ // Drape render targets are allocated lazily (see `ensureRenderTargets`),
244
+ // one per live clamp-to-ground layer draped onto this tile — most terrain
245
+ // tiles drape 0–2 layers, so eagerly reserving `numTexturizedVector` (~5)
246
+ // drape targets per tile wasted GPU memory and, worse, scaled with terrain
247
+ // subdivision past the vector maxZoom (a mobile OOM driver).
248
+
249
+ this.tileHandler = tileHandler;
250
+
251
+ tileMapByHandle.set(handle, this);
252
+
253
+ this.onBeforeRender = this._onBeforeRender;
254
+ }
255
+
256
+ /**
257
+ * Pull the Rust-resolved texturized-vector slots for this terrain tile into
258
+ * {@link vectorSlots}: one bake slot per clamp-to-ground layer, keyed by the
259
+ * rendered WM vector tile that backs it (ancestor fallback already resolved in
260
+ * Rust). Only layers with an actual draped scene take a slot — Rust also returns
261
+ * non-draped layers (rendered in the MRT scene), which must not consume the budget.
262
+ * Capped to the GPU slot budget; warns once when more draped layers are requested.
263
+ */
264
+ private refreshVectorSlots() {
265
+ const states = this.tileHandler.getVectorTileStates(this.handle) ?? [];
266
+
267
+ // Group the flattened per-(layer, source) states back into one slot per
268
+ // layer, each carrying every overlapping WM source tile (N:M on Geographic
269
+ // terrain). First-seen layer order assigns the render-target index, kept
270
+ // consistent by renderVectorScenes / bindVectorSlots / buildCompositeLayers.
271
+ const byLayer = new Map<string, VectorSlot>();
272
+ const droppedLayers = new Set<string>();
273
+ for (const state of states) {
274
+ const layerId = state.layer_id;
275
+ let slot = byLayer.get(layerId);
276
+ if (!slot) {
277
+ // Rust returns a state for every vector layer with a rendered tile, including
278
+ // non-draped layers (those render in the MRT scene and never create a texturized
279
+ // scene). Only a layer that actually has a draped scene may consume a bake slot;
280
+ // otherwise non-draped layers would occupy the limited slot budget and crowd out
281
+ // genuinely-draped layers past the cap. A draped layer whose scene isn't ready yet
282
+ // simply gets no slot this pass and is picked up once its scene exists.
283
+ const hasScene =
284
+ this.texturizedSceneByTileCoordinates.findSceneByLayerId(
285
+ state.tile_handle,
286
+ layerId,
287
+ ) != null;
288
+ if (!hasScene) continue;
289
+ // Cap the layer fan-out to the GPU slot budget; extra layers are dropped.
290
+ if (byLayer.size >= this.numTexturizedVector) {
291
+ droppedLayers.add(layerId);
292
+ continue;
293
+ }
294
+ const reproject = state.reproject_terrain_lat;
295
+ slot = {
296
+ layerId,
297
+ sources: [],
298
+ reproject:
299
+ reproject.length >= 2 ? [reproject[0], reproject[1]] : undefined,
300
+ };
301
+ byLayer.set(layerId, slot);
302
+ }
303
+ slot.sources.push({
304
+ tileHandle: state.tile_handle,
305
+ uvOffset: [state.uv_offset[0] ?? 0, state.uv_offset[1] ?? 0],
306
+ uvScale: [state.uv_scale[0] ?? 1, state.uv_scale[1] ?? 1],
307
+ });
308
+ }
309
+
310
+ if (droppedLayers.size > 0) {
311
+ if (!this.warnedExceededTextures) {
312
+ this.warnedExceededTextures = true;
313
+ console.warn(
314
+ `[TileMesh] Exceeded maximum MVT texture slots: ${byLayer.size + droppedLayers.size} layers requested, ` +
315
+ `but only ${this.numTexturizedVector} slots available. ` +
316
+ `Some MVT layers will not be rendered.`,
317
+ );
318
+ }
319
+ } else {
320
+ this.warnedExceededTextures = false;
321
+ }
322
+
323
+ this.vectorSlots = [...byLayer.values()];
324
+ this.syncDrapeRenderTargets();
325
+ }
326
+
327
+ /**
328
+ * Size the drape render-target pool to the live slot count (already capped to
329
+ * `numTexturizedVector` by {@link refreshVectorSlots}): allocate the ones a
330
+ * newly-draped layer needs, dispose the ones a vanished layer freed. Then
331
+ * report the pool's GPU footprint to the memory ledger so it tracks drape
332
+ * memory that scales with terrain subdivision past the vector maxZoom — a cost
333
+ * per-vector-tile accounting cannot see. Reports only when the count changes.
334
+ */
335
+ private syncDrapeRenderTargets() {
336
+ const target = this.vectorSlots.length;
337
+ const rts = this.texturizedSceneRenderTargets;
338
+ while (rts.length < target) {
339
+ const rt = new WebGLRenderTarget(this.drapeRtSize, this.drapeRtSize, {
340
+ format: RGBAFormat,
341
+ });
342
+ // Match the sampler settings setupTextures applies to eager RTs, so the
343
+ // lazily-grown one binds identically when bindVectorSlots picks it up.
344
+ configureTexturizedTexture(rt.texture, this.ctx.textureOptions);
345
+ rts.push(rt);
346
+ }
347
+ while (rts.length > target) {
348
+ rts.pop()?.dispose();
349
+ }
350
+
351
+ // The composite atlas is acquired in the constructor and held for the
352
+ // tile's lifetime regardless of draping, so it is always part of the
353
+ // report — otherwise a tile whose last vector layer vanished would report
354
+ // 0 and wipe the atlas cost Rust seeds at mesh-attach time. The render
355
+ // targets on top of it scale with terrain subdivision past the vector
356
+ // maxZoom — a cost per-vector-tile accounting cannot see.
357
+ const bytes = this.atlasBytes + rts.length * drapeRtBytes(this.drapeRtSize);
358
+ if (bytes === this.reportedDrapeGpuBytes) return;
359
+ // Only the mesh currently registered for this position-stable handle may
360
+ // report. During a terrain mesh replacement the outgoing mesh briefly
361
+ // coexists with the incoming one, which has already claimed the handle
362
+ // (constructor's `tileMapByHandle.set`) and been seeded by
363
+ // `attach_terrain_mesh_cost`. A stray report from the outgoing mesh would
364
+ // resolve on the Rust side (via `Without<Deleted>`) to the NEW live entity
365
+ // and overwrite its freshly-seeded atlas cost — the same hazard the
366
+ // dispose() 0-report guards against, here on the non-zero path.
367
+ if (this.ctx.tileMapByHandle.get(this.handle) !== this) return;
368
+ this.reportedDrapeGpuBytes = bytes;
369
+ this.tileHandler.reportDrapeGpuBytes(this.handle, bytes);
370
+ }
371
+
372
+ /**
373
+ * Identity of the current slot set + the backing scenes' content. A change means
374
+ * the offscreen scenes must be re-baked and the per-slot material state re-bound.
375
+ */
376
+ private vectorSignature(): string {
377
+ return this.vectorSlots
378
+ .map((slot) => {
379
+ const sourceSig = slot.sources
380
+ .map((s) => {
381
+ const scene =
382
+ this.texturizedSceneByTileCoordinates.findSceneByLayerId(
383
+ s.tileHandle,
384
+ slot.layerId,
385
+ );
386
+ return `${s.tileHandle}@${s.uvOffset[0]},${s.uvOffset[1]},${s.uvScale[0]},${s.uvScale[1]}#${scene?.revision ?? -1}:${scene?.children.length ?? 0}`;
387
+ })
388
+ .join(",");
389
+ return `${slot.layerId}[${sourceSig}]`;
390
+ })
391
+ .join("|");
392
+ }
393
+
394
+ private _onBeforeRender = () => {
395
+ if (!this.visible) return;
396
+
397
+ // Re-fetch the Rust-resolved slots only when the global vector resolution revision
398
+ // changed; otherwise reuse the cached slots. The resolve result can only flip when a
399
+ // traverse runs (new/removed rendered tiles, terrain subdivision) or a scene becomes
400
+ // ready — both bump the revision. This skips a per-tile WASM-boundary resolve every
401
+ // frame, the dominant steady-state cost when many terrain tiles are visible.
402
+ const revision = this.tileHandler.vectorRevision();
403
+ if (revision !== this._lastVectorRevision) {
404
+ this._lastVectorRevision = revision;
405
+ this.refreshVectorSlots();
406
+ }
407
+
408
+ // Re-bake the offscreen vector scenes only when the resolved slots or their
409
+ // backing scenes changed; otherwise the existing render targets stay valid.
410
+ const signature = this.vectorSignature();
411
+ if (signature !== this.prevVectorSignature) {
412
+ this.prevVectorSignature = signature;
413
+ this.compositor.renderVectorScenes(
414
+ this.vectorSlots,
415
+ this.texturizedSceneRenderTargets,
416
+ );
417
+ this.bindVectorSlots();
418
+ this.compositor.markDirty(this.handle, "vector-revision");
419
+ }
420
+
421
+ // MRT composite pass: bakes N source textures into the per-tile atlas.
422
+ // Skip building the layer snapshot entirely when nothing is dirty.
423
+ if (!this.compositor.cache.isDirty(this.handle)) return;
424
+ const { layers, globals } = this.buildCompositeLayers();
425
+ const plan = planSlots(
426
+ layers,
427
+ this.texturizedSceneIndexFrom,
428
+ this.maxTextures,
429
+ );
430
+ this.compositor.runCompositePassIfDirty(
431
+ this.handle,
432
+ plan,
433
+ globals,
434
+ deriveCompositeFeatures(layers, globals),
435
+ );
436
+ };
437
+
438
+ /**
439
+ * Drive each vector slot's main-shader state per frame: texture, per-slot UV
440
+ * transform, visibility and the draped mesh's material attributes. One slot per
441
+ * layer, sourced from the WM vector tile the Rust resolve picked. The resolve
442
+ * walks up to the nearest rendered tile (readiness derived from ECS activation
443
+ * / the Rust resolve, not a JS-side scene_ready flag), so the slot's scene is
444
+ * always bakeable (its own render target was framed by the bake) and the UV is
445
+ * identity — the ancestor LOD fallback lives entirely in Rust. Replaces the old
446
+ * scene-observer `updateTexturizedSceneTextureVisibility`.
447
+ */
448
+ private bindVectorSlots() {
449
+ const m = this.material;
450
+ if (!m || !m.userData || !m.userData.textures?.value) return;
451
+ const textures = m.userData.textures.value;
452
+ const uvOffsets: Vector2[] = m.userData.layerUvOffset?.value ?? [];
453
+ const uvScales: Vector2[] = m.userData.layerUvScale?.value ?? [];
454
+
455
+ for (let i = 0; i < this.numTexturizedVector; i++) {
456
+ const lastIdx = this.texturizedSceneIndexFrom + i;
457
+ const ownRt = this.texturizedSceneRenderTargets[i];
458
+ // Slots past the live pool (lazily sized to the draped-layer count) have
459
+ // no render target: hide the slot and rebind the shared empty texture so
460
+ // the shader never samples a stale / disposed texture from a layer that
461
+ // is no longer draped here.
462
+ if (!ownRt) {
463
+ m.userData.shows.value[lastIdx] = 0;
464
+ textures[lastIdx] = EMPTY_TEXTURIZED_TEXTURE;
465
+ continue;
466
+ }
467
+
468
+ textures[lastIdx] = ownRt.texture;
469
+ // The bake framed every source into this RT so it spans the terrain tile's
470
+ // extent: the paste samples it 1:1 in longitude (identity UV here); the
471
+ // latitude axis is reprojected WebMercator→Geographic in the composite
472
+ // shader via the slot's reproject band (see buildCompositeLayers).
473
+ uvOffsets[lastIdx]?.set(0, 0);
474
+ uvScales[lastIdx]?.set(1, 1);
475
+
476
+ const slot = this.vectorSlots[i];
477
+ const mesh = slot ? this.representativeVectorMesh(slot) : undefined;
478
+ m.userData.shows.value[lastIdx] = mesh ? 1 : 0;
479
+ if (mesh) this.copyVectorMeshAttrs(lastIdx, mesh);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * First available draped mesh backing any of a layer's sources. A clamp-to-ground
485
+ * layer's material attributes (water/specular/emissive/…) are uniform across its
486
+ * tiles, so any source's mesh is a faithful representative for the slot.
487
+ */
488
+ private representativeVectorMesh(slot: VectorSlot): Object3D | undefined {
489
+ for (const source of slot.sources) {
490
+ const scene = this.texturizedSceneByTileCoordinates.findSceneByLayerId(
491
+ source.tileHandle,
492
+ slot.layerId,
493
+ );
494
+ if (scene && !scene.removed && scene.children.length) {
495
+ return scene.children[0];
496
+ }
497
+ }
498
+ return undefined;
499
+ }
500
+
501
+ /** Copy a draped mesh's material attributes into the main shader's slot `lastIdx`. */
502
+ private copyVectorMeshAttrs(lastIdx: number, mesh: Object3D) {
503
+ const m = this.material;
504
+ if (mesh instanceof PolygonMesh) {
505
+ // Use PolygonMesh getters that expose material enhancer state
506
+ m.userData.reflectivities.value[lastIdx] = mesh.reflectivity;
507
+ m.userData.roughnesses.value[lastIdx] = mesh.roughness;
508
+ m.userData.waters.value[lastIdx] = mesh.water;
509
+ m.userData.waterScaleNormals.value[lastIdx] = mesh.waterScaleNormal;
510
+ m.userData.waterSpeeds.value[lastIdx] = mesh.waterSpeed;
511
+ m.userData.shininesses.value[lastIdx] = mesh.shininess;
512
+ m.userData.specularStrengths.value[lastIdx] = mesh.specularStrength;
513
+ m.userData.applyWaterNormals.value[lastIdx] = mesh.applyWaterNormal;
514
+ m.userData.speculars.value[lastIdx] = mesh.specular;
515
+ m.userData.emissiveIntensities.value[lastIdx] = mesh.emissiveIntensity;
516
+ m.userData.emissiveColors.value[lastIdx].set(mesh.emissiveColor);
517
+ m.userData.effectIdsMasks.value[lastIdx] = mesh.effectIdsMask;
518
+ } else if (mesh instanceof PolylineMesh) {
519
+ m.userData.emissiveIntensities.value[lastIdx] = mesh.emissiveIntensity;
520
+ m.userData.emissiveColors.value[lastIdx].set(mesh.emissiveColor);
521
+ m.userData.effectIdsMasks.value[lastIdx] = mesh.effectIdsMask;
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Feature flags for the **main** TileMesh shader (atlas-sampling side): drives
527
+ * its program cache key and which normal/specular branches compile. Sourced
528
+ * from the material defines/userData, independent of the composite pass's own
529
+ * (layer-derived) feature set — see {@link deriveCompositeFeatures}.
530
+ */
531
+ private computeFeatures(): CompositeFeatures {
532
+ const ud = this.material.userData;
533
+ // Only slots that are still shown count: bindVectorSlots sets shows=0 when a
534
+ // slot loses its representative mesh but leaves the per-slot waters/heatmap
535
+ // uniforms at their previous values, so a disappeared layer would otherwise
536
+ // keep its shader feature (e.g. water) compiled in. Mirrors the shows gate in
537
+ // buildCompositeLayers.
538
+ const shows: number[] = ud.shows?.value ?? [];
539
+ const waters = ud.waters?.value as boolean[] | undefined;
540
+ const heatmaps = ud.isElevationHeatmaps?.value as boolean[] | undefined;
541
+ return {
542
+ hasHillshade: ud.defines?.USE_HILLSHADE === 1,
543
+ hasWater: !!waters?.some((v, i) => v === true && shows[i] === 1),
544
+ hasElevationHeatmap: !!heatmaps?.some(
545
+ (v, i) => v === true && shows[i] === 1,
546
+ ),
547
+ hasWatermask: this.userData.watermask != null,
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Snapshot the current per-slot uniform state into a typed layer list for the
553
+ * compositor's MRT pass. Reads directly from material.userData (the same
554
+ * source of truth the main shader uses); only slots that are both shown and
555
+ * textured become layers, so the SlotPlanner's compact counts cover exactly
556
+ * the active slots.
557
+ */
558
+ private buildCompositeLayers(): {
559
+ layers: CompositeLayer[];
560
+ globals: CompositeGlobals;
561
+ } {
562
+ const ud = this.material.userData;
563
+ const shows: number[] = ud.shows?.value ?? [];
564
+ const textures: (Texture | null)[] = ud.textures?.value ?? [];
565
+ const colors: Color[] = ud.colors?.value ?? [];
566
+ const opacities: number[] = ud.opacities?.value ?? [];
567
+ const layerUvOffset: Vector2[] = ud.layerUvOffset?.value ?? [];
568
+ const layerUvScale: Vector2[] = ud.layerUvScale?.value ?? [];
569
+ const isHillshades: boolean[] = ud.isHillshades?.value ?? [];
570
+ const isElevationHeatmaps: boolean[] = ud.isElevationHeatmaps?.value ?? [];
571
+ const waters: boolean[] = ud.waters?.value ?? [];
572
+ // Per-slot Mercator reprojection flag + the shared terrain latitude range.
573
+ const layerReproject: number[] = ud.layerReproject ?? [];
574
+ const terrainLatRange: number[] = ud.terrainLatRange ?? [];
575
+ const reproject: [number, number] | undefined =
576
+ terrainLatRange.length === 2
577
+ ? [terrainLatRange[0], terrainLatRange[1]]
578
+ : undefined;
579
+ const boundary = this.texturizedSceneIndexFrom;
580
+
581
+ const layers: CompositeLayer[] = [];
582
+ for (let absSlot = 0; absSlot < this.maxTextures; absSlot++) {
583
+ const texture = textures[absSlot] ?? null;
584
+ if (shows[absSlot] !== 1 || texture == null) continue;
585
+
586
+ const region = absSlot < boundary ? "raster" : "vector";
587
+ const uvOffset = layerUvOffset[absSlot] ?? new Vector2(0, 0);
588
+ const uvScale = layerUvScale[absSlot] ?? new Vector2(1, 1);
589
+
590
+ // Hillshade takes precedence: a slot flagged both ways has its color
591
+ // zeroed and emits a normal, so classifying it as hillshade is correct —
592
+ // the elevation sample would be zeroed anyway.
593
+ if (isHillshades[absSlot]) {
594
+ layers.push({
595
+ kind: "hillshade",
596
+ region,
597
+ absSlot,
598
+ texture,
599
+ uvOffset,
600
+ uvScale,
601
+ });
602
+ } else if (isElevationHeatmaps[absSlot]) {
603
+ layers.push({
604
+ kind: "elevationHeatmap",
605
+ region,
606
+ absSlot,
607
+ texture,
608
+ uvOffset,
609
+ uvScale,
610
+ opacity: opacities[absSlot] ?? 1,
611
+ });
612
+ } else {
613
+ // WebMercator-on-Geographic latitude reprojection. Raster slots read the
614
+ // shared per-tile flag/band stashed by setupTextures; baked-vector slots
615
+ // (whose RT spans the terrain extent) carry the Rust-resolved terrain
616
+ // `[south, north]` band directly on the slot. `undefined` on WM terrain.
617
+ const slotReproject =
618
+ region === "vector"
619
+ ? this.vectorSlots[absSlot - boundary]?.reproject
620
+ : layerReproject[absSlot] === 1
621
+ ? reproject
622
+ : undefined;
623
+ layers.push({
624
+ kind: "raster",
625
+ region,
626
+ absSlot,
627
+ texture,
628
+ uvOffset,
629
+ uvScale,
630
+ color: colors[absSlot] ?? new Color(),
631
+ opacity: opacities[absSlot] ?? 1,
632
+ water: waters[absSlot] ?? false,
633
+ reproject: slotReproject,
634
+ });
635
+ }
636
+ }
637
+
638
+ const globals: CompositeGlobals = {
639
+ hillshadeExaggeration: ud.hillshadeExaggeration?.value ?? 1,
640
+ watermask: this.userData.watermask?.texture ?? null,
641
+ colorMapTexture: this.ctx.uniforms.colorMapTexture.value ?? null,
642
+ elevationRGBScaler: ud.elevationRGBScaler?.value ?? new Vector3(),
643
+ elevationMinMaxHeightAndBoundary:
644
+ ud.elevationMinMaxHeightAndBoundary?.value ?? new Vector3(),
645
+ elevationMinMaxOffsetAndEpsilonAndOffset:
646
+ ud.elevationMinMaxOffsetAndEpsilonAndOffset?.value ?? new Vector4(),
647
+ logarithmic: ud.logarithmic?.value ?? false,
648
+ logBase: ud.logBase?.value ?? 10,
649
+ logBoundary: ud.logBoundary?.value ?? 10,
650
+ };
651
+
652
+ return { layers, globals };
653
+ }
654
+
655
+ async _init(mesh: MeshAdded) {
656
+ await this.createMesh(
657
+ generate_id_from_entity(mesh),
658
+ mesh.mesh,
659
+ mesh.material,
660
+ mesh.transform,
661
+ mesh.globe,
662
+ );
663
+
664
+ this.addEventListener("removedFromWorld", () => {
665
+ this.dispose(
666
+ this.ctx.tileMapByHandle,
667
+ this.ctx.textureFragmentIndex,
668
+ this.ctx.tileMeshToFragmentIds,
669
+ );
670
+ });
671
+ }
672
+
673
+ private async createMesh(
674
+ id: string,
675
+ mesh: EventMesh,
676
+ mat: RasterTileInternalMaterial,
677
+ transform: Transform | undefined,
678
+ globe: Globe,
679
+ ) {
680
+ const {
681
+ scenes,
682
+ meshes,
683
+ buf,
684
+ loadedTexs,
685
+ textureOptions,
686
+ uniforms,
687
+ textureFragmentIndex,
688
+ tileMeshToFragmentIds,
689
+ } = this.ctx;
690
+
691
+ // Read the AABB before grabbing the buffer views below. `mesh.aabb` (and its
692
+ // Vec3 center/extent) are wasm-bindgen getters that allocate WASM objects,
693
+ // which can grow linear memory and detach any previously-obtained buffer
694
+ // view. Reading it first keeps position/uv/indices/normals valid until
695
+ // createSkirtMesh copies them.
696
+ const aabb = mesh.aabb;
697
+ const center = aabb.center;
698
+ const extent = aabb.extent;
699
+ const aabb_center = new Vector3(center.x, center.y, center.z);
700
+ const aabb_extent = new Vector3(extent.x, extent.y, extent.z);
701
+
702
+ const position = buf.f32(mesh.vertices);
703
+ const indices = buf.u32(mesh.indices);
704
+ if (!position || !indices) return;
705
+
706
+ const uv = buf.f32(mesh.uvs);
707
+ const normals = mesh.normals != null ? buf.f32(mesh.normals) : null;
708
+
709
+ // The buf.* arrays are short-lived views into WASM memory: they must be
710
+ // consumed before any further WASM call that allocates. createSkirtMesh only
711
+ // reads non-allocating handle getters and consecutive buf.* views, then
712
+ // copies them once — into the combined buffers when a skirt exists, or via
713
+ // slice() otherwise.
714
+ // Terrain-only geometry (for shadow rendering). createSkirtMesh fills it:
715
+ // when a skirt exists it shares the combined geometry's buffers (with the
716
+ // skirt cut off via drawRange).
717
+ const terrainGeometry = new BufferGeometry();
718
+
719
+ const geometry = this.createSkirtMesh(
720
+ mesh,
721
+ terrainGeometry,
722
+ position,
723
+ uv,
724
+ indices,
725
+ normals,
726
+ );
727
+
728
+ // Watermask: 1 byte = uniform, 65536 bytes = 256x256 grid. Stored for downstream consumers.
729
+ if (mesh.watermask != null) {
730
+ const watermask = buf.u8(mesh.watermask);
731
+ if (watermask) {
732
+ const isUniform = watermask.length === 1;
733
+ const size = isUniform ? 1 : 256;
734
+ // Single-channel R8 — the composite shader only reads `.r`. RedFormat
735
+ // halves GPU memory vs. RGBA (256×256 = 64KB instead of 256KB).
736
+ // `watermask` is a short-lived view into WASM memory; copy it once and
737
+ // share the copy between the texture and `data` (both only read it).
738
+ const data = watermask.slice();
739
+ const texture = new DataTexture(
740
+ data,
741
+ size,
742
+ size,
743
+ RedFormat,
744
+ UnsignedByteType,
745
+ );
746
+ texture.flipY = true;
747
+ texture.needsUpdate = true;
748
+ this.userData.watermask = {
749
+ data,
750
+ isUniform,
751
+ texture,
752
+ };
753
+ }
754
+ }
755
+
756
+ this.userData.hasNormalAttribute = normals != null;
757
+
758
+ geometry.boundingBox = new Box3(
759
+ aabb_center.clone().sub(aabb_extent),
760
+ aabb_center.clone().add(aabb_extent),
761
+ );
762
+
763
+ geometry.boundingSphere = new Sphere(aabb_center, aabb_extent.length());
764
+
765
+ if (mat.showBoundingBox) {
766
+ const bb = new Box3Helper(geometry.boundingBox, 0x00ff00);
767
+ this.add(bb);
768
+ }
769
+ this.geometry = geometry;
770
+
771
+ // Drop the CPU-side typed arrays (position/uv/normal/index, and the skirt
772
+ // data merged into the same combined buffers above) once the GPU upload
773
+ // completes, so terrain geometry keeps a single resident (GPU) copy — the
774
+ // premise behind GPU_GEOMETRY_RESIDENCY_FACTOR=1 in the Rust ledger.
775
+ // Bounding volumes are already assigned from the WASM aabb above (no
776
+ // computeBoundingBox re-read), nothing else re-reads these arrays, and
777
+ // terrain geometry never goes through the worker `toBufferGeometryLike`
778
+ // path, so releasing is safe. CONSTRAINT: once released these geometries
779
+ // can no longer be re-uploaded on a WebGL context loss — acceptable because
780
+ // terrain is re-fetched from Rust on demand, not restored from CPU memory.
781
+ // When a skirt exists, `terrainGeometry` (shadow mesh) shares these exact
782
+ // BufferAttribute instances, so this one call releases both meshes' arrays.
783
+ releaseGeometryArraysAfterUpload(geometry);
784
+
785
+ this.material = this.initMaterial(mat, uniforms, globe);
786
+
787
+ const maxTextures = this.maxTextures;
788
+ this.setUniforms(mat, maxTextures);
789
+ this.setupTextureFragments(
790
+ mat.texture_fragments(),
791
+ textureFragmentIndex,
792
+ tileMeshToFragmentIds,
793
+ );
794
+ this.setupTextures(loadedTexs, textureOptions, maxTextures, mat);
795
+
796
+ // Create shadow mesh if we have separate terrain geometry (i.e., skirt exists)
797
+ // This prevents the skirt from casting unexpected shadows
798
+ if (geometry !== terrainGeometry) {
799
+ terrainGeometry.boundingBox = geometry.boundingBox.clone();
800
+ terrainGeometry.boundingSphere = geometry.boundingSphere.clone();
801
+
802
+ // Create shadow mesh using terrain-only geometry (without skirt)
803
+ this.shadowMesh = new Mesh(terrainGeometry, this.material);
804
+ this.shadowMesh.castShadow = !!mat.castShadow;
805
+ this.shadowMesh.receiveShadow = !!mat.receiveShadow;
806
+ this.add(this.shadowMesh);
807
+
808
+ // Main mesh with skirt doesn't cast shadow, but receives it
809
+ this.castShadow = false;
810
+ this.receiveShadow = !!mat.receiveShadow;
811
+ } else {
812
+ // No skirt - use the main mesh for both rendering and shadow
813
+ this.castShadow = !!mat.castShadow;
814
+ this.receiveShadow = !!mat.receiveShadow;
815
+ }
816
+
817
+ this.visible = false;
818
+ this.renderOrder = mesh.render_order;
819
+ if (transform) setTransform(this, transform);
820
+ scenes.globe.add(this);
821
+ meshes.set(id, this);
822
+ }
823
+
824
+ // Create combined geometry (terrain + skirt) for rendering, and populate
825
+ // `terrainGeometry` (used by shadowMesh so the skirt casts no shadow) with
826
+ // the same buffers, restricted to the terrain via drawRange.
827
+ // Normals are computed dynamically in the fragment shader from vPosition.
828
+ createSkirtMesh(
829
+ mesh: EventMesh,
830
+ terrainGeometry: BufferGeometry,
831
+ position: Float32Array,
832
+ uv: Float32Array | null,
833
+ indices: Uint32Array,
834
+ normals: Float32Array | null,
835
+ ) {
836
+ const { buf } = this.ctx;
837
+ // Check for separate skirt data
838
+ const skirtVerticesHandle = mesh.skirt_vertices;
839
+ const skirtIndicesHandle = mesh.skirt_indices;
840
+ const skirtUvsHandle = mesh.skirt_uvs;
841
+ const skirtNormalsHandle = mesh.skirt_normals;
842
+
843
+ const hasSkirt = skirtVerticesHandle != null && skirtIndicesHandle != null;
844
+ const skirtPosition =
845
+ skirtVerticesHandle != null ? buf.f32(skirtVerticesHandle) : null;
846
+ const skirtIndices =
847
+ skirtIndicesHandle != null ? buf.u32(skirtIndicesHandle) : null;
848
+ const skirtUv = skirtUvsHandle != null ? buf.f32(skirtUvsHandle) : null;
849
+ const skirtNormals =
850
+ skirtNormalsHandle != null ? buf.f32(skirtNormalsHandle) : null;
851
+
852
+ // Create combined geometry (terrain + skirt) for main rendering
853
+ let geometry: BufferGeometry;
854
+ if (hasSkirt && skirtPosition && skirtIndices) {
855
+ geometry = new BufferGeometry();
856
+
857
+ // The terrain data sits at the front of every combined buffer, so the
858
+ // terrain-only (shadow) geometry shares the combined attributes — one
859
+ // CPU array and one GPU buffer per attribute — and drawRange below cuts
860
+ // the skirt off.
861
+
862
+ // Combine vertices: terrain vertices + skirt vertices
863
+ const combinedPosition = new Float32Array(
864
+ position.length + skirtPosition.length,
865
+ );
866
+ combinedPosition.set(position);
867
+ combinedPosition.set(skirtPosition, position.length);
868
+ const positionAttribute = new BufferAttribute(combinedPosition, 3);
869
+ geometry.setAttribute("position", positionAttribute);
870
+ terrainGeometry.setAttribute("position", positionAttribute);
871
+
872
+ // Combine UVs
873
+ if (uv) {
874
+ const combinedUv = new Float32Array(uv.length + (skirtUv?.length ?? 0));
875
+ combinedUv.set(uv);
876
+ if (skirtUv) {
877
+ combinedUv.set(skirtUv, uv.length);
878
+ }
879
+ const uvAttribute = new BufferAttribute(combinedUv, 2);
880
+ geometry.setAttribute("uv", uvAttribute);
881
+ terrainGeometry.setAttribute("uv", uvAttribute);
882
+ }
883
+
884
+ // Combine normals: terrain normals + skirt normals (computed in Rust as edge normals)
885
+ if (normals) {
886
+ const combinedNormals = new Float32Array(
887
+ normals.length + (skirtNormals?.length ?? 0),
888
+ );
889
+ combinedNormals.set(normals);
890
+ if (skirtNormals) {
891
+ combinedNormals.set(skirtNormals, normals.length);
892
+ }
893
+ const normalAttribute = new BufferAttribute(combinedNormals, 3);
894
+ geometry.setAttribute("normal", normalAttribute);
895
+ terrainGeometry.setAttribute("normal", normalAttribute);
896
+ }
897
+
898
+ // Combine indices: terrain indices + skirt indices
899
+ const combinedIndices = new Uint32Array(
900
+ indices.length + skirtIndices.length,
901
+ );
902
+ combinedIndices.set(indices);
903
+ combinedIndices.set(skirtIndices, indices.length);
904
+ const indexAttribute = new BufferAttribute(combinedIndices, 1);
905
+ geometry.setIndex(indexAttribute);
906
+
907
+ terrainGeometry.setIndex(indexAttribute);
908
+ terrainGeometry.setDrawRange(0, indices.length);
909
+ } else {
910
+ // No skirt data - build the terrain geometry from the arrays. slice()
911
+ // copies out of the WASM-memory views since BufferAttribute retains them.
912
+ terrainGeometry.setAttribute(
913
+ "position",
914
+ new BufferAttribute(position.slice(), 3),
915
+ );
916
+ if (uv) {
917
+ terrainGeometry.setAttribute("uv", new BufferAttribute(uv.slice(), 2));
918
+ }
919
+ if (normals) {
920
+ terrainGeometry.setAttribute(
921
+ "normal",
922
+ new BufferAttribute(normals.slice(), 3),
923
+ );
924
+ }
925
+ terrainGeometry.setIndex(new BufferAttribute(indices.slice(), 1));
926
+ geometry = terrainGeometry;
927
+ }
928
+
929
+ return geometry;
930
+ }
931
+
932
+ private shouldUseNormal(
933
+ mat: Partial<RasterTileInternalMaterial> | RasterTileInternalMaterial,
934
+ globe: Globe,
935
+ ): boolean {
936
+ // Lambert (normal-based) shading is needed if any of these is true:
937
+ // 1. globe.useNormal is set
938
+ // 2. a per-vertex normal attribute is bound (e.g. quantized-mesh)
939
+ // 3. any hillshade slot is active
940
+ const hillshadeActive = !!mat.isHillshades?.some((v) => v !== 0);
941
+ return (
942
+ !!globe.useNormal || !!this.userData.hasNormalAttribute || hillshadeActive
943
+ );
944
+ }
945
+
946
+ private initMaterial(
947
+ _mat: RasterTileInternalMaterial,
948
+ uniforms: CommonUniforms,
949
+ globe: Globe,
950
+ ): TileMaterial {
951
+ const useNormal = this.shouldUseNormal(_mat, globe);
952
+ const m = useNormal
953
+ ? new MeshLambertMaterial({
954
+ stencilWrite: false,
955
+ color: globe.color,
956
+ })
957
+ : new MeshBasicMaterial({
958
+ stencilWrite: false,
959
+ color: globe.color,
960
+ });
961
+
962
+ m.userData.uPickable = {
963
+ value: 0,
964
+ };
965
+
966
+ m.userData.uTime = uniforms.time;
967
+
968
+ m.userData.defines = {
969
+ USE_UV: 1,
970
+ USE_SELECTIVE_EFFECT: 1,
971
+ USE_VERTEX_NORMAL: this.userData.hasNormalAttribute ? 1 : 0,
972
+ };
973
+
974
+ m.envMap = uniforms.tSkyEnvMap.value ?? null;
975
+ m.combine = AddOperation;
976
+
977
+ const maxTextures = this.maxTextures;
978
+
979
+ // Bind the compositor's atlas outputs as TileMesh-visible uniforms so the
980
+ // slim shader can sample them. Atlas Textures are created once per tile
981
+ // and persist across composite passes — `needsUpdate` is flipped by the
982
+ // compositor when a new pass writes to them.
983
+ const atlasOutputs = this.compositor.acquireOutputs(this.handle);
984
+ m.userData.uColorAtlas = { value: atlasOutputs.color };
985
+ m.userData.uAttrAtlas = { value: atlasOutputs.attr };
986
+ m.userData.uNormalAtlas = { value: atlasOutputs.normal };
987
+
988
+ m.customProgramCacheKey = () =>
989
+ "TILE" + JSON.stringify(this.computeFeatures());
990
+
991
+ m.onBeforeCompile = (shader) => {
992
+ shader.defines ??= {};
993
+ Object.assign(shader.defines, m.userData.defines);
994
+ shader.uniforms.reflectivity = { value: 0 };
995
+
996
+ const features = this.computeFeatures();
997
+
998
+ shader.uniforms.uPickable = m.userData.uPickable;
999
+ shader.uniforms.uColorAtlas = m.userData.uColorAtlas;
1000
+ shader.uniforms.uAttrAtlas = m.userData.uAttrAtlas;
1001
+ shader.uniforms.uNormalAtlas = m.userData.uNormalAtlas;
1002
+
1003
+ // Per-slot uniform arrays — indexed once per fragment by the winning
1004
+ // slot decoded from attr.a (no per-fragment loop any more).
1005
+ shader.uniforms.uColors = m.userData.colors;
1006
+ shader.uniforms.uReflectivities = m.userData.reflectivities;
1007
+ shader.uniforms.uRoughnesses = m.userData.roughnesses;
1008
+ shader.uniforms.uWaterScaleNormals = m.userData.waterScaleNormals;
1009
+ shader.uniforms.uWaterSpeeds = m.userData.waterSpeeds;
1010
+ shader.uniforms.uShininesses = m.userData.shininesses;
1011
+ shader.uniforms.uSpecularStrengths = m.userData.specularStrengths;
1012
+ shader.uniforms.uApplyWaterNormals = m.userData.applyWaterNormals;
1013
+ shader.uniforms.uSpeculars = m.userData.speculars;
1014
+ shader.uniforms.uEmissiveIntensities = m.userData.emissiveIntensities;
1015
+ shader.uniforms.uEmissiveColors = m.userData.emissiveColors;
1016
+ shader.uniforms.uEffectIdsMasks = m.userData.effectIdsMasks;
1017
+ // Satisfy MRT-injected single uniform declarations (tile uses per-slot
1018
+ // arrays + winIdx indexing instead).
1019
+ shader.uniforms.uEffectIdsMask = { value: 0 };
1020
+ shader.uniforms.uEmissiveIntensity = { value: 0 };
1021
+ shader.uniforms.uWaterNormalMap = uniforms.waterTexture;
1022
+ shader.uniforms.uHillshadeExaggeration = m.userData.hillshadeExaggeration;
1023
+ shader.uniforms.uIor = { value: 1.33333 };
1024
+ shader.uniforms.uTime = m.userData.uTime;
1025
+
1026
+ shader.vertexShader = createReplacer(shader.vertexShader)
1027
+ .replace(
1028
+ "#include <common>",
1029
+ `${TILE_VERTEX_INJECTIONS.afterCommon}
1030
+ #include <common>`,
1031
+ )
1032
+ .replace(
1033
+ "#include <uv_vertex>",
1034
+ `#include <uv_vertex>
1035
+ ${TILE_VERTEX_INJECTIONS.afterUvVertex}`,
1036
+ )
1037
+ .replace(
1038
+ "#include <envmap_vertex>",
1039
+ `#include <envmap_vertex>
1040
+ ${TILE_VERTEX_INJECTIONS.afterEnvmapVertex}`,
1041
+ ).source;
1042
+
1043
+ shader.fragmentShader = createReplacer(shader.fragmentShader)
1044
+ .replace(
1045
+ "#include <common>",
1046
+ `
1047
+ ${generateTileCommonInjection(maxTextures)}
1048
+
1049
+ #include <common>
1050
+ `,
1051
+ )
1052
+ .replaceWithCondition(
1053
+ "#include <lights_lambert_pars_fragment>",
1054
+ `
1055
+ #include <lights_lambert_pars_fragment>
1056
+
1057
+ ${WaterParsFragment}
1058
+ ${SpecularParsFragment}
1059
+ ${BranchFreeTernary}
1060
+ `,
1061
+ useNormal,
1062
+ )
1063
+ .replace(
1064
+ "#include <map_fragment>",
1065
+ generateTileMapFragment(maxTextures),
1066
+ )
1067
+ .replaceWithCondition(
1068
+ "#include <normal_fragment_maps>",
1069
+ generateTileNormalFragmentMaps(features),
1070
+ useNormal,
1071
+ )
1072
+ .replaceWithCondition(
1073
+ "vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;",
1074
+ `
1075
+ vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;
1076
+ outgoingLight += specular;
1077
+ `,
1078
+ useNormal,
1079
+ )
1080
+ .replaceWithCondition(
1081
+ "#include <envmap_fragment>",
1082
+ createReplacer(ShaderChunk.envmap_fragment).replace(
1083
+ "outgoingLight += envColor.xyz * specularStrength * reflectivity;",
1084
+ "outgoingLight += nvr_branchFreeTernary(useSpecular, vec3(0.0), envColor.xyz * specularStrength * tileReflectivity);",
1085
+ ).source,
1086
+ useNormal,
1087
+ )
1088
+ // Pick override is the LAST write of the fragment shader (after
1089
+ // tonemapping/colorspace/fog/dithering), so the encoded batchId
1090
+ // reaches the pick buffer bit-for-bit. See TILE_PICK_FRAGMENT_OVERRIDE.
1091
+ .replace(
1092
+ "#include <dithering_fragment>",
1093
+ `#include <dithering_fragment>
1094
+ ${TILE_PICK_FRAGMENT_OVERRIDE}`,
1095
+ )
1096
+ .replaceWithCondition(
1097
+ "normalBuffer = vec4(packNormalToVec2(normal), reflectivity, roughnessFactor);",
1098
+ TILE_NORMAL_BUFFER_REPLACEMENT,
1099
+ useNormal,
1100
+ )
1101
+ .replace(
1102
+ `effectIdBuffer = vec4(uEffectIdsMask, 0.0, 0.0, 1.0);
1103
+ emissiveBuffer = vec4(diffuseColor.rgb * uEmissiveIntensity + emissive, 1.0);`,
1104
+ TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT,
1105
+ ).source;
1106
+ };
1107
+
1108
+ this.ctx.viewContext?.applyShadowMaterial(m);
1109
+
1110
+ return m;
1111
+ }
1112
+
1113
+ _update(mesh: MeshChanged) {
1114
+ const {
1115
+ loadedTexs,
1116
+ textureOptions,
1117
+ textureFragmentIndex,
1118
+ tileMeshToFragmentIds,
1119
+ } = this.ctx;
1120
+ const globe = mesh.globe;
1121
+ const changedMaterial = mesh.material;
1122
+ const tileMesh = mesh.mesh;
1123
+ const active = tileMesh.active;
1124
+
1125
+ const maxTextures = textureOptions.maxTextures;
1126
+
1127
+ // TODO: Support hide entire globe.
1128
+ this.visible = active;
1129
+
1130
+ if (active) {
1131
+ this.ensureCorrectMaterialType(changedMaterial, globe);
1132
+
1133
+ this.setupTextureFragments(
1134
+ changedMaterial?.texture_fragments(),
1135
+ textureFragmentIndex,
1136
+ tileMeshToFragmentIds,
1137
+ );
1138
+
1139
+ // Set uniforms (this may switch material type for hillshade)
1140
+ this.setUniforms(changedMaterial, maxTextures);
1141
+
1142
+ this.setupTextures(
1143
+ loadedTexs,
1144
+ textureOptions,
1145
+ maxTextures,
1146
+ changedMaterial,
1147
+ );
1148
+
1149
+ // Material / texture rebind → force the vector slots to re-bind their
1150
+ // per-slot uniform state on the next frame, and repaint the composite atlas.
1151
+ this.prevVectorSignature = "";
1152
+ this.compositor.markDirty(this.handle, "material");
1153
+ this.compositor.markDirty(this.handle, "texture-binding");
1154
+ }
1155
+
1156
+ // Update shadow settings
1157
+ // If shadowMesh exists, it handles castShadow while main mesh only receives shadows
1158
+ if (this.shadowMesh) {
1159
+ if (this.shadowMesh.castShadow !== changedMaterial.castShadow) {
1160
+ this.shadowMesh.castShadow = !!changedMaterial.castShadow;
1161
+ }
1162
+ if (this.shadowMesh.receiveShadow !== changedMaterial.receiveShadow) {
1163
+ this.shadowMesh.receiveShadow = !!changedMaterial.receiveShadow;
1164
+ }
1165
+ // Main mesh with skirt never casts shadow
1166
+ this.castShadow = false;
1167
+ } else {
1168
+ if (this.castShadow !== changedMaterial.castShadow) {
1169
+ this.castShadow = !!changedMaterial.castShadow;
1170
+ }
1171
+ }
1172
+ if (this.receiveShadow !== changedMaterial.receiveShadow) {
1173
+ this.receiveShadow = !!changedMaterial.receiveShadow;
1174
+ }
1175
+ if (this.material.color.getHex() !== globe.color) {
1176
+ this.material.color.setHex(globe.color);
1177
+ }
1178
+ if (this.material.transparent !== globe.transparent) {
1179
+ this.material.transparent = globe.transparent;
1180
+ this.material.needsUpdate = true;
1181
+ }
1182
+ if (this.material.opacity !== globe.opacity) {
1183
+ this.material.opacity = globe.opacity;
1184
+ }
1185
+ if (this.material.wireframe !== globe.wireframe) {
1186
+ this.material.wireframe = globe.wireframe;
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * Ensures the material type matches the hillshade requirements.
1192
+ * Switches between MeshBasicMaterial and MeshLambertMaterial as needed.
1193
+ * - MeshLambertMaterial: Required for hillshade (supports normal-based lighting)
1194
+ * - MeshBasicMaterial: Used when no hillshade (more performant)
1195
+ */
1196
+ private ensureCorrectMaterialType(
1197
+ mat: RasterTileInternalMaterial,
1198
+ globe: Globe,
1199
+ ): void {
1200
+ // Same decision used by initMaterial — keeping them in sync avoids
1201
+ // mid-session Lambert↔Basic flips that strip normal shading from some tiles.
1202
+ const needsLambert = this.shouldUseNormal(mat, globe);
1203
+ const isLambert = this.material instanceof MeshLambertMaterial;
1204
+
1205
+ if (needsLambert !== isLambert) {
1206
+ const oldMaterial = this.material;
1207
+
1208
+ // Create new material with correct type. Per-layer UV uniforms will be
1209
+ // re-populated by setupTextures before the next render.
1210
+ this.material = this.initMaterial(mat, this.ctx.uniforms, globe);
1211
+
1212
+ if (this.shadowMesh) {
1213
+ this.shadowMesh.material = this.material;
1214
+ }
1215
+
1216
+ // Dispose old material
1217
+ this.ctx.viewContext?.removeShadowMaterial(oldMaterial);
1218
+ oldMaterial.dispose();
1219
+ }
1220
+ }
1221
+
1222
+ private setUniforms(mat: RasterTileInternalMaterial, maxTextures: number) {
1223
+ const m = this.material;
1224
+
1225
+ if (!m.userData.shows) {
1226
+ m.userData.shows = {
1227
+ value: [...new Array(maxTextures)].fill(0),
1228
+ };
1229
+ }
1230
+ if (!m.userData.colors) {
1231
+ m.userData.colors = {
1232
+ value: [...new Array(maxTextures)].map(() => new Color()),
1233
+ };
1234
+ }
1235
+ if (!m.userData.opacities) {
1236
+ m.userData.opacities = {
1237
+ value: [...new Array(maxTextures)].fill(1),
1238
+ };
1239
+ }
1240
+ if (!m.userData.reflectivities) {
1241
+ m.userData.reflectivities = {
1242
+ value: [...new Array(maxTextures)].fill(0),
1243
+ };
1244
+ }
1245
+ if (!m.userData.roughnesses) {
1246
+ m.userData.roughnesses = {
1247
+ value: [...new Array(maxTextures)].fill(0),
1248
+ };
1249
+ }
1250
+ if (!m.userData.waters) {
1251
+ m.userData.waters = {
1252
+ value: [...new Array(maxTextures)].fill(false),
1253
+ };
1254
+ }
1255
+ if (!m.userData.waterScaleNormals) {
1256
+ m.userData.waterScaleNormals = {
1257
+ value: [...new Array(maxTextures)].fill(0),
1258
+ };
1259
+ }
1260
+ if (!m.userData.waterSpeeds) {
1261
+ m.userData.waterSpeeds = {
1262
+ value: [...new Array(maxTextures)].fill(0),
1263
+ };
1264
+ }
1265
+ if (!m.userData.shininesses) {
1266
+ m.userData.shininesses = {
1267
+ value: [...new Array(maxTextures)].fill(0),
1268
+ };
1269
+ }
1270
+ if (!m.userData.specularStrengths) {
1271
+ m.userData.specularStrengths = {
1272
+ value: [...new Array(maxTextures)].fill(0),
1273
+ };
1274
+ }
1275
+ if (!m.userData.applyWaterNormals) {
1276
+ m.userData.applyWaterNormals = {
1277
+ value: [...new Array(maxTextures)].fill(0),
1278
+ };
1279
+ }
1280
+ if (!m.userData.speculars) {
1281
+ m.userData.speculars = {
1282
+ value: [...new Array(maxTextures)].fill(false),
1283
+ };
1284
+ }
1285
+ if (!m.userData.emissiveIntensities) {
1286
+ m.userData.emissiveIntensities = {
1287
+ value: [...new Array(maxTextures)].fill(0),
1288
+ };
1289
+ }
1290
+ if (!m.userData.emissiveColors) {
1291
+ m.userData.emissiveColors = {
1292
+ value: [...new Array(maxTextures)].map(() => new Color()),
1293
+ };
1294
+ }
1295
+ if (!m.userData.effectIdsMasks) {
1296
+ m.userData.effectIdsMasks = {
1297
+ value: [...new Array(maxTextures)].fill(0),
1298
+ };
1299
+ }
1300
+
1301
+ // Elevation Heatmap uniforms
1302
+ if (!m.userData.isElevationHeatmaps) {
1303
+ m.userData.isElevationHeatmaps = {
1304
+ value: [...new Array(maxTextures)].fill(false),
1305
+ };
1306
+ }
1307
+ if (!m.userData.elevationRGBScaler) {
1308
+ m.userData.elevationRGBScaler = {
1309
+ value: new Vector3(0, 0, 0),
1310
+ };
1311
+ }
1312
+ if (!m.userData.elevationMinMaxHeightAndBoundary) {
1313
+ m.userData.elevationMinMaxHeightAndBoundary = {
1314
+ value: new Vector3(0, 0, 0), // minHeight, maxHeight, boundary
1315
+ };
1316
+ }
1317
+ if (!m.userData.elevationMinMaxOffsetAndEpsilonAndOffset) {
1318
+ m.userData.elevationMinMaxOffsetAndEpsilonAndOffset = {
1319
+ value: new Vector4(0, 0, 0, 0), // minOffset, maxOffset, epsilon, offset
1320
+ };
1321
+ }
1322
+ if (!m.userData.logarithmic) {
1323
+ m.userData.logarithmic = {
1324
+ value: false,
1325
+ };
1326
+ }
1327
+ if (!m.userData.logBase) {
1328
+ m.userData.logBase = {
1329
+ value: 10,
1330
+ };
1331
+ }
1332
+ if (!m.userData.logBoundary) {
1333
+ m.userData.logBoundary = {
1334
+ value: 10,
1335
+ };
1336
+ }
1337
+
1338
+ // Hillshade uniforms
1339
+ if (!m.userData.isHillshades) {
1340
+ m.userData.isHillshades = {
1341
+ value: [...new Array(maxTextures)].fill(false),
1342
+ };
1343
+ }
1344
+ if (!m.userData.hillshadeOffset) {
1345
+ m.userData.hillshadeOffset = {
1346
+ value: 0.0,
1347
+ };
1348
+ }
1349
+ if (!m.userData.hillshadeExaggeration) {
1350
+ m.userData.hillshadeExaggeration = {
1351
+ value: 1.0,
1352
+ };
1353
+ }
1354
+ if (!m.userData.metersPerTexel) {
1355
+ m.userData.metersPerTexel = {
1356
+ value: [...new Array(maxTextures)].fill(1.0),
1357
+ };
1358
+ }
1359
+
1360
+ // Reset all texture properties
1361
+ for (let i = 0; i < m.userData.shows.value.length; i++) {
1362
+ m.userData.shows.value[i] = 0;
1363
+ m.userData.colors.value[i] = new Color();
1364
+ m.userData.opacities.value[i] = 1;
1365
+ m.userData.isElevationHeatmaps.value[i] = false; // Reset elevation heatmap flags
1366
+ m.userData.isHillshades.value[i] = false; // Reset hillshade flags
1367
+ }
1368
+
1369
+ // All properties have same length.
1370
+ const shows = mat.shows;
1371
+ const colors = mat.colors;
1372
+ const opacities = mat.opacities;
1373
+ for (let i = 0; i < shows.length; i++) {
1374
+ m.userData.shows.value[i] = shows[i];
1375
+ m.userData.colors.value[i] = new Color(colors[i]);
1376
+ m.userData.opacities.value[i] = opacities[i];
1377
+ }
1378
+
1379
+ // Update elevation heatmap parameters from Rust material
1380
+ if (mat.isElevationHeatmaps && mat.isElevationHeatmaps.length > 0) {
1381
+ for (
1382
+ let i = 0;
1383
+ i < Math.min(mat.isElevationHeatmaps.length, maxTextures);
1384
+ i++
1385
+ ) {
1386
+ m.userData.isElevationHeatmaps.value[i] =
1387
+ mat.isElevationHeatmaps[i] !== 0;
1388
+ }
1389
+ }
1390
+
1391
+ // Update hillshade parameters from Rust material
1392
+ if (mat.isHillshades && mat.isHillshades.length > 0) {
1393
+ for (let i = 0; i < Math.min(mat.isHillshades.length, maxTextures); i++) {
1394
+ m.userData.isHillshades.value[i] = mat.isHillshades[i] !== 0;
1395
+ }
1396
+ }
1397
+
1398
+ // Hillshade exaggeration (applied during normal map sampling in shader)
1399
+ m.userData.hillshadeExaggeration.value = mat.hillshadeExaggeration;
1400
+
1401
+ m.userData.elevationRGBScaler.value.set(
1402
+ mat.elevationRScaler,
1403
+ mat.elevationGScaler,
1404
+ mat.elevationBScaler,
1405
+ );
1406
+
1407
+ m.userData.elevationMinMaxHeightAndBoundary.value.set(
1408
+ mat.elevationMinHeight,
1409
+ mat.elevationMaxHeight,
1410
+ mat.elevationBoundary,
1411
+ );
1412
+
1413
+ m.userData.elevationMinMaxOffsetAndEpsilonAndOffset.value.set(
1414
+ mat.elevationMinOffset,
1415
+ mat.elevationMaxOffset,
1416
+ mat.elevationEpsilon,
1417
+ mat.elevationOffset,
1418
+ );
1419
+
1420
+ m.userData.logarithmic.value = mat.logarithmic;
1421
+ m.userData.logBase.value = Math.log(mat.logBoundary);
1422
+ m.userData.logBoundary.value = mat.logBoundary;
1423
+
1424
+ const prevHillshade = m.userData.defines.USE_HILLSHADE;
1425
+ const newHillshade = m.userData.isHillshades.value.some(
1426
+ (v: boolean) => v === true,
1427
+ )
1428
+ ? 1
1429
+ : 0;
1430
+ m.userData.defines.USE_HILLSHADE = newHillshade;
1431
+ if (prevHillshade !== newHillshade) {
1432
+ this.material.needsUpdate = true;
1433
+ }
1434
+ }
1435
+
1436
+ private setupTextureFragments(
1437
+ textureFragments: TextureFragment[] | undefined,
1438
+ textureFragmentIndex: Map<string, Set<TextureSlot>> | undefined,
1439
+ tileMeshToFragmentIds: Map<TileMesh, Set<string>> | undefined,
1440
+ ) {
1441
+ const m = this.material;
1442
+
1443
+ // Per-layer parent fallback is now resolved in Rust: when a layer's own data
1444
+ // isn't ready, the entity slot in `texture_fragments` already points at the
1445
+ // nearest ready ancestor entity for that layer. So this side has nothing to do
1446
+ // for the "no fragments / use parent" case.
1447
+ if (!textureFragments || !textureFragments.length) {
1448
+ m.userData.textureFragments = { value: [] };
1449
+ updateTextureFragmentIndex(
1450
+ textureFragmentIndex,
1451
+ tileMeshToFragmentIds,
1452
+ this,
1453
+ [],
1454
+ );
1455
+ return;
1456
+ }
1457
+
1458
+ const texturesFragmentIds = [];
1459
+ for (const fragment of textureFragments) {
1460
+ texturesFragmentIds.push(
1461
+ fragment ? generate_id_from_entity(fragment) : null,
1462
+ );
1463
+ }
1464
+
1465
+ m.userData.textureFragments = {
1466
+ value: texturesFragmentIds,
1467
+ };
1468
+
1469
+ // Update reverse index for efficient texture fragment lookups
1470
+ updateTextureFragmentIndex(
1471
+ textureFragmentIndex,
1472
+ tileMeshToFragmentIds,
1473
+ this,
1474
+ texturesFragmentIds,
1475
+ );
1476
+ }
1477
+
1478
+ setupTextures(
1479
+ loadedTexes: Map<string, Texture>,
1480
+ textureOptions: TextureOptions,
1481
+ maxTextures: number,
1482
+ mat: Partial<RasterTileInternalMaterial> | RasterTileInternalMaterial,
1483
+ preserveLayerUv = false,
1484
+ ) {
1485
+ const m = this.material;
1486
+
1487
+ if (!m.userData.textures) {
1488
+ m.userData.textures = {
1489
+ value: [...new Array(maxTextures)].fill(null),
1490
+ };
1491
+ }
1492
+
1493
+ if (!m.userData.layerUvOffset) {
1494
+ m.userData.layerUvOffset = {
1495
+ value: [...new Array(maxTextures)].map(() => new Vector2(0, 0)),
1496
+ };
1497
+ }
1498
+
1499
+ if (!m.userData.layerUvScale) {
1500
+ m.userData.layerUvScale = {
1501
+ value: [...new Array(maxTextures)].map(() => new Vector2(1, 1)),
1502
+ };
1503
+ }
1504
+
1505
+ // Reset textures (always) and per-layer UV (only if not preserving)
1506
+ for (let i = 0; i < maxTextures; i++) {
1507
+ m.userData.textures.value[i] = null;
1508
+
1509
+ // Skip resetting per-layer UV if preserving (e.g., during rebind)
1510
+ if (!preserveLayerUv) {
1511
+ m.userData.layerUvOffset.value[i].set(0, 0);
1512
+ m.userData.layerUvScale.value[i].set(1, 1);
1513
+ }
1514
+ }
1515
+
1516
+ const textureFragments = m.userData.textureFragments?.value;
1517
+ const textureFragmentsLen = textureFragments?.length ?? 0;
1518
+
1519
+ const numTexturizedVector = this.numTexturizedVector;
1520
+
1521
+ // Raster textures fill slots [0, texturizedSceneIndexFrom); texturized vector
1522
+ // scenes occupy the rest. Exactly `texturizedSceneIndexFrom` raster layers fit
1523
+ // (indices 0..from-1), so only a strictly larger count overflows. Rust caps the
1524
+ // draped-tile fan-out (RASTER_DRAPE_SLOT_BUDGET) to keep this from triggering;
1525
+ // this guard is the final safety net and the extra fragments are dropped below.
1526
+ if (textureFragmentsLen > this.texturizedSceneIndexFrom) {
1527
+ console.error(
1528
+ `Exceeded maximum textures: ${textureFragmentsLen} layers are provided. Maximum the number of textures is ${this.texturizedSceneIndexFrom}.`,
1529
+ );
1530
+ }
1531
+
1532
+ const textures = m.userData.textures.value;
1533
+
1534
+ // Per-layer UV transforms from Rust (covers regular textures and hillshades).
1535
+ // `null` entry means identity (no parent reuse).
1536
+ const layerUvTransforms = mat.layerUvTransforms?.() ?? [];
1537
+
1538
+ // Per-slot Mercator reprojection (WebMercator raster on Geographic terrain).
1539
+ // Stashed for `buildCompositeLayers`; the terrain latitude range is shared by
1540
+ // every reprojecting slot of this tile.
1541
+ m.userData.layerReproject = Array.from(mat.layerReproject ?? []);
1542
+ m.userData.terrainLatRange = Array.from(mat.terrainLatRange ?? []);
1543
+
1544
+ // Setting tile textures
1545
+ for (let i = 0; i < textureFragmentsLen; i++) {
1546
+ if (i >= this.texturizedSceneIndexFrom) {
1547
+ break;
1548
+ }
1549
+
1550
+ const textureFragment = textureFragments[i];
1551
+ const t = textureFragment ? loadedTexes.get(textureFragment) : undefined;
1552
+ if (!t) {
1553
+ textures[i] = null;
1554
+ continue;
1555
+ }
1556
+
1557
+ const isElevationHeatmap =
1558
+ mat.isElevationHeatmaps && mat.isElevationHeatmaps[i];
1559
+ const isHillshade = mat.isHillshades && mat.isHillshades[i];
1560
+
1561
+ // Per-layer UV transform: identity by default; uses Rust's resolved transform
1562
+ // when this layer is sampling a parent tile.
1563
+ if (!preserveLayerUv) {
1564
+ const uvTransform = layerUvTransforms[i];
1565
+ if (uvTransform) {
1566
+ m.userData.layerUvOffset.value[i].set(
1567
+ uvTransform.offset.x,
1568
+ uvTransform.offset.y,
1569
+ );
1570
+ m.userData.layerUvScale.value[i].set(
1571
+ uvTransform.scale.x,
1572
+ uvTransform.scale.y,
1573
+ );
1574
+ } else {
1575
+ m.userData.layerUvOffset.value[i].set(0, 0);
1576
+ m.userData.layerUvScale.value[i].set(1, 1);
1577
+ }
1578
+ }
1579
+
1580
+ const isDEMTexture = isElevationHeatmap || isHillshade;
1581
+ const targetColorSpace = isDEMTexture ? NoColorSpace : SRGBColorSpace;
1582
+
1583
+ // Update colorSpace if needed
1584
+ const colorSpaceChanged = t.colorSpace !== targetColorSpace;
1585
+ if (colorSpaceChanged) {
1586
+ t.colorSpace = targetColorSpace;
1587
+ t.needsUpdate = true;
1588
+ }
1589
+
1590
+ // CRITICAL: Elevation DEM textures must use NearestFilter to prevent interpolation artifacts.
1591
+ // Linear interpolation between ocean RGB(128,0,0) and land RGB(0,0,5)
1592
+ // produces intermediate values like RGB(64,0,2) which decode to ~42000m!
1593
+ // However, hillshade normal maps (which store normals directly in RGB, not encoded heights)
1594
+ // should use LinearFilter for smooth bilinear interpolation at tile boundaries.
1595
+ // Always apply these settings for DEM textures, independent of colorSpace change
1596
+ if (isDEMTexture) {
1597
+ if (isHillshade) {
1598
+ // Hillshade normal maps: use LinearFilter for hardware bilinear interpolation
1599
+ // Normals are stored directly in RGB [-1,1] -> [0,1], so linear filtering is safe
1600
+ if (t.minFilter !== LinearFilter) {
1601
+ t.minFilter = LinearFilter;
1602
+ t.needsUpdate = true;
1603
+ }
1604
+ if (t.magFilter !== LinearFilter) {
1605
+ t.magFilter = LinearFilter;
1606
+ t.needsUpdate = true;
1607
+ }
1608
+ }
1609
+
1610
+ if (isElevationHeatmap) {
1611
+ // Elevation DEM textures: use NearestFilter to prevent decoding artifacts
1612
+ // RGB-encoded heights must not be interpolated before decoding
1613
+ if (t.minFilter !== NearestFilter) {
1614
+ t.minFilter = NearestFilter;
1615
+ t.needsUpdate = true;
1616
+ }
1617
+ if (t.magFilter !== NearestFilter) {
1618
+ t.magFilter = NearestFilter;
1619
+ t.needsUpdate = true;
1620
+ }
1621
+ }
1622
+
1623
+ if (t.generateMipmaps !== false) {
1624
+ t.generateMipmaps = false;
1625
+ t.needsUpdate = true;
1626
+ }
1627
+ } else {
1628
+ // Regular textures: only update sampler settings when first binding
1629
+ if (t.minFilter !== textureOptions.minFilter) {
1630
+ t.minFilter = textureOptions.minFilter as MinificationTextureFilter;
1631
+ t.needsUpdate = true;
1632
+ }
1633
+ if (t.magFilter !== textureOptions.magFilter) {
1634
+ t.magFilter = textureOptions.magFilter as MagnificationTextureFilter;
1635
+ t.needsUpdate = true;
1636
+ }
1637
+ if (t.anisotropy !== textureOptions.maxAnisotropy) {
1638
+ t.anisotropy = textureOptions.maxAnisotropy;
1639
+ t.needsUpdate = true;
1640
+ }
1641
+ if (t.generateMipmaps !== textureOptions.useMipmaps) {
1642
+ t.generateMipmaps = textureOptions.useMipmaps;
1643
+ t.needsUpdate = true;
1644
+ }
1645
+ }
1646
+
1647
+ textures[i] = t;
1648
+ }
1649
+
1650
+ for (let i = 0; i < numTexturizedVector; i++) {
1651
+ // Every texturized sampler slot must reference a valid texture (GLSL/WebGL
1652
+ // requires it even for a sampler never read). Drape render targets are now
1653
+ // allocated lazily, so a slot without one binds the shared empty texture;
1654
+ // `bindVectorSlots` swaps in the real RT texture once a layer drapes here.
1655
+ const lastIndex = this.texturizedSceneIndexFrom + i;
1656
+ const rt = this.texturizedSceneRenderTargets[i];
1657
+ const texturizedSceneTexture = rt
1658
+ ? configureTexturizedTexture(rt.texture, textureOptions)
1659
+ : EMPTY_TEXTURIZED_TEXTURE;
1660
+
1661
+ textures[lastIndex] = texturizedSceneTexture;
1662
+
1663
+ // Per-slot visibility + material attributes are bound by bindVectorSlots once
1664
+ // the Rust-resolved slots are known; default to hidden here.
1665
+ m.userData.shows.value[lastIndex] = 0;
1666
+ m.userData.colors.value[lastIndex] = new Color(0xffffff);
1667
+ m.userData.opacities.value[lastIndex] = 1.0;
1668
+ }
1669
+ // Re-bind vector slots on the next frame now that render targets were rebound.
1670
+ this.prevVectorSignature = "";
1671
+ }
1672
+
1673
+ /**
1674
+ * Rebind textures for this TileMesh by calling setupTextures
1675
+ * This ensures texture updates go through the standard texture management system
1676
+ * Used by hillshade backfill and other dynamic texture updates.
1677
+ * Preserves existing per-layer UV transforms to avoid overwriting parent-reuse values.
1678
+ */
1679
+ rebindTextures(
1680
+ loadedTexs: Map<string, Texture>,
1681
+ textureOptions: TextureOptions,
1682
+ ) {
1683
+ const material = this.material;
1684
+ if (!material || !material.userData) return;
1685
+
1686
+ // Create a minimal material object with the required fields from current material
1687
+ const materialData: Partial<RasterTileInternalMaterial> = {
1688
+ isElevationHeatmaps: material.userData.isElevationHeatmaps?.value,
1689
+ isHillshades: material.userData.isHillshades?.value,
1690
+ };
1691
+
1692
+ this.setupTextures(
1693
+ loadedTexs,
1694
+ textureOptions,
1695
+ this.maxTextures,
1696
+ materialData,
1697
+ true, // preserveLayerUv
1698
+ );
1699
+ // Hillshade backfill / dynamic rebinds change the composite atlas inputs.
1700
+ this.compositor.markDirty(this.handle, "hillshade");
1701
+ }
1702
+
1703
+ onBeforePicking(): void {
1704
+ this.material.userData.uPickable.value = 1;
1705
+
1706
+ // Force the vector scenes to re-bake for the picking pass.
1707
+ this.prevVectorSignature = "";
1708
+ this.compositor.markDirty(this.handle, "vector-revision");
1709
+ }
1710
+
1711
+ onAfterPicking(): void {
1712
+ this.material.userData.uPickable.value = 0;
1713
+ this.prevVectorSignature = "";
1714
+ this.compositor.markDirty(this.handle, "vector-revision");
1715
+ }
1716
+
1717
+ getRenderable(): Object3D {
1718
+ return this;
1719
+ }
1720
+
1721
+ private _disposed = false;
1722
+
1723
+ dispose(
1724
+ tileMapByHandle?: TileMapByHandle,
1725
+ textureFragmentIndex?: Map<string, Set<TextureSlot>>,
1726
+ tileMeshToFragmentIds?: Map<TileMesh, Set<string>>,
1727
+ ) {
1728
+ // A second dispose would steal another holder's compositor refcount.
1729
+ if (this._disposed) {
1730
+ if (import.meta.env?.DEV) {
1731
+ console.error(
1732
+ `TileMesh.dispose called twice for handle ${this.handle}`,
1733
+ );
1734
+ }
1735
+ return;
1736
+ }
1737
+ this._disposed = true;
1738
+
1739
+ // Remove from texture fragment index
1740
+ if (textureFragmentIndex && tileMeshToFragmentIds) {
1741
+ removeTextureFragmentIndex(
1742
+ textureFragmentIndex,
1743
+ tileMeshToFragmentIds,
1744
+ this,
1745
+ );
1746
+ }
1747
+
1748
+ this.ctx.viewContext?.removeShadowMaterial(this.material);
1749
+
1750
+ // Note: geometry disposal (including shadowMesh.geometry) is handled by
1751
+ // disposeObject3D() in event/index.ts before removedFromWorld is dispatched.
1752
+ // We only need to remove shadowMesh from the scene graph here.
1753
+ if (this.shadowMesh) {
1754
+ this.remove(this.shadowMesh);
1755
+ this.shadowMesh = undefined;
1756
+ }
1757
+
1758
+ // Dispose WebGLRenderTargets to free GPU memory
1759
+ for (const renderTarget of this.texturizedSceneRenderTargets) {
1760
+ renderTarget.dispose();
1761
+ }
1762
+ this.texturizedSceneRenderTargets.length = 0;
1763
+ // Do NOT report a zero drape cost here. The Rust `TerrainTileGpuCost` /
1764
+ // `TileCost` live on the mesh entity, whose despawn already subtracts the
1765
+ // full cost from the ledger via the component `on_replace` hook — so the
1766
+ // report is redundant on the normal removal path. Worse, on the terrain
1767
+ // mesh-replacement path the old entity is marked `Deleted` while a NEW live
1768
+ // mesh reuses the same position-stable handle, so `reportDrapeGpuBytes(0)`
1769
+ // would resolve (via `Without<Deleted>`) to the NEW entity and wipe the
1770
+ // atlas cost `attach_terrain_mesh_cost` just seeded. Leaving accounting to
1771
+ // the entity despawn keeps both paths correct.
1772
+ this.reportedDrapeGpuBytes = 0;
1773
+
1774
+ // Release the watermask DataTexture (one per tile, RedFormat 1×1 or 256×256).
1775
+ this.userData.watermask?.texture?.dispose();
1776
+
1777
+ // Clean up from tileMapByHandle
1778
+ if (tileMapByHandle) {
1779
+ tileMapByHandle.delete(this.handle);
1780
+ }
1781
+
1782
+ // Release the per-tile composite atlas (refcounted via the compositor's
1783
+ // TileTextureCache). Mirrors the acquire() in the constructor.
1784
+ this.compositor.release(this.handle);
1785
+
1786
+ // Note: the texturized-scene cache is keyed by WM vector tile handle and is
1787
+ // shared across terrain tiles (N:M draping), so it is NOT deleted here — its
1788
+ // entries are cleared by the vector unload path in event/feature.ts.
1789
+ }
1790
+ }