@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,2649 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`generateCompositeFragmentShader snapshots > all features on, mixed raster+vector 1`] = `
4
+ "
5
+ precision highp float;
6
+ precision highp int;
7
+
8
+ #include <common>
9
+
10
+ varying vec2 vUv;
11
+
12
+
13
+
14
+ uniform int uShows[4];
15
+ uniform vec3 uColors[4];
16
+ uniform float uOpacities[4];
17
+ uniform sampler2D uTextures[4];
18
+ uniform vec2 uLayerUvOffset[4];
19
+ uniform vec2 uLayerUvScale[4];
20
+ uniform int uReproject[4];
21
+ uniform vec2 uReprojectTerrainLat[4];
22
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
23
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
24
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
25
+ uniform vec4 uReprojectMerc[4];
26
+ uniform bool uIsHillshades[4];
27
+ uniform float uHillshadeExaggeration;
28
+ uniform bool uIsElevationHeatmaps[4];
29
+ uniform bool uWaters[4];
30
+
31
+ uniform sampler2D uWatermask;
32
+
33
+
34
+ #if USE_ELEVATION_HEATMAP
35
+ float nvr_branchFreeTernary(bool comparison, float a, float b) {
36
+ float useA = float(comparison);
37
+ return a * useA + b * (1.0 - useA);
38
+ }
39
+
40
+ /**
41
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
42
+ * evaluate both possibilities for a vec2 expression.
43
+ *
44
+ * @name nvr_branchFreeTernary
45
+ * @glslFunction
46
+ *
47
+ * @param {bool} comparison A comparison statement
48
+ * @param {vec2} a Value to return if the comparison is true.
49
+ * @param {vec2} b Value to return if the comparison is false.
50
+ *
51
+ * @returns {vec2} equivalent of comparison ? a : b
52
+ */
53
+ vec2 nvr_branchFreeTernary(bool comparison, vec2 a, vec2 b) {
54
+ float useA = float(comparison);
55
+ return a * useA + b * (1.0 - useA);
56
+ }
57
+
58
+ /**
59
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
60
+ * evaluate both possibilities for a vec3 expression.
61
+ *
62
+ * @name nvr_branchFreeTernary
63
+ * @glslFunction
64
+ *
65
+ * @param {bool} comparison A comparison statement
66
+ * @param {vec3} a Value to return if the comparison is true.
67
+ * @param {vec3} b Value to return if the comparison is false.
68
+ *
69
+ * @returns {vec3} equivalent of comparison ? a : b
70
+ */
71
+ vec3 nvr_branchFreeTernary(bool comparison, vec3 a, vec3 b) {
72
+ float useA = float(comparison);
73
+ return a * useA + b * (1.0 - useA);
74
+ }
75
+
76
+ /**
77
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
78
+ * evaluate both possibilities for a vec4 expression.
79
+ *
80
+ * @name nvr_branchFreeTernary
81
+ * @glslFunction
82
+ *
83
+ * @param {bool} comparison A comparison statement
84
+ * @param {vec3} a Value to return if the comparison is true.
85
+ * @param {vec3} b Value to return if the comparison is false.
86
+ *
87
+ * @returns {vec3} equivalent of comparison ? a : b
88
+ */
89
+ vec4 nvr_branchFreeTernary(bool comparison, vec4 a, vec4 b) {
90
+ float useA = float(comparison);
91
+ return a * useA + b * (1.0 - useA);
92
+ }
93
+ #ifndef DEM_UTIL_GLSL
94
+ #define DEM_UTIL_GLSL
95
+
96
+ bool isPowerOfTwo(int n) {
97
+ return n > 0 && ((n & (n - 1)) == 0);
98
+ }
99
+
100
+ const float invalidHeight = -999999.0;
101
+ bool isValidHeight(float h) {
102
+ return h > -999998.0;
103
+ }
104
+
105
+ struct DEMBilinearData {
106
+ ivec2 p00, p10, p01, p11;
107
+ vec2 frac;
108
+ };
109
+
110
+ DEMBilinearData prepareDEMBilinear(ivec2 actualTexSize, vec2 uv) {
111
+ vec2 paddedTexSize = vec2(actualTexSize);
112
+
113
+
114
+
115
+
116
+ bool hasPadding = !isPowerOfTwo(actualTexSize.x);
117
+
118
+ vec2 contentSize = hasPadding ? paddedTexSize - vec2(2.0) : paddedTexSize;
119
+
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+ vec2 pixelCoord = hasPadding
129
+ ? uv * (contentSize - vec2(1.0)) + vec2(1.0)
130
+ : uv * (contentSize - vec2(1.0));
131
+
132
+ vec2 pixelFloor = floor(pixelCoord);
133
+ vec2 pixelFrac = fract(pixelCoord);
134
+
135
+
136
+
137
+ ivec2 maxCoord = ivec2(paddedTexSize) - ivec2(1);
138
+
139
+ DEMBilinearData data;
140
+ data.p00 = clamp(ivec2(pixelFloor), ivec2(0), maxCoord);
141
+ data.p10 = clamp(ivec2(pixelFloor + vec2(1.0, 0.0)), ivec2(0), maxCoord);
142
+ data.p01 = clamp(ivec2(pixelFloor + vec2(0.0, 1.0)), ivec2(0), maxCoord);
143
+ data.p11 = clamp(ivec2(pixelFloor + vec2(1.0, 1.0)), ivec2(0), maxCoord);
144
+ data.frac = pixelFrac;
145
+
146
+ return data;
147
+ }
148
+
149
+ float decodeDEMHeight(vec4 color, vec3 rgbScaler, float boundary, float minOffset, float maxOffset, float epsilon, float offset) {
150
+ vec3 rgb = color.rgb * 255.0;
151
+ float x = dot(rgb, rgbScaler);
152
+
153
+ float h;
154
+ float epsilon_cmp = 1.0;
155
+ if (abs(x - boundary) > epsilon_cmp) {
156
+ if (x > boundary) {
157
+ h = x + maxOffset;
158
+ } else {
159
+ h = x + minOffset;
160
+ }
161
+
162
+ h = h * epsilon + offset;
163
+ } else {
164
+ h = invalidHeight;
165
+ }
166
+
167
+ return h;
168
+ }
169
+
170
+ float interpolateDEMHeights(float h00, float h10, float h01, float h11, vec2 frac) {
171
+
172
+ if (!isValidHeight(h00)) return invalidHeight;
173
+
174
+
175
+
176
+ float maxReasonableDiff = 1000.0;
177
+
178
+
179
+ if (isValidHeight(h10) && abs(h10 - h00) > maxReasonableDiff) h10 = h00;
180
+ if (isValidHeight(h01) && abs(h01 - h00) > maxReasonableDiff) h01 = h00;
181
+ if (isValidHeight(h11) && abs(h11 - h00) > maxReasonableDiff) h11 = h00;
182
+
183
+
184
+ if (!isValidHeight(h10)) h10 = h00;
185
+ if (!isValidHeight(h01)) h01 = h00;
186
+ if (!isValidHeight(h11)) h11 = h00;
187
+
188
+
189
+ float h0 = mix(h00, h10, frac.x);
190
+ float h1 = mix(h01, h11, frac.x);
191
+ return mix(h0, h1, frac.y);
192
+ }
193
+
194
+ #endif
195
+
196
+ uniform sampler2D uColorMapTexture;
197
+ uniform vec3 uElevationRGBScaler;
198
+ uniform vec3 uElevationMinMaxHeightAndBoundary;
199
+ uniform vec4 uElevationMinMaxOffsetAndEpsilonAndOffset;
200
+ uniform bool uLogarithmic;
201
+ uniform float uLogBase;
202
+ uniform float uLogBoundary;
203
+
204
+ float pseudoLog(float value) {
205
+ return value > uLogBoundary ? log(value) / uLogBase : value < -uLogBoundary ? -log(-value) / uLogBase : value / uLogBoundary;
206
+ }
207
+
208
+
209
+ float decodeRawHeightForElevation(vec4 color) {
210
+ return decodeDEMHeight(
211
+ color,
212
+ uElevationRGBScaler,
213
+ uElevationMinMaxHeightAndBoundary.z,
214
+ uElevationMinMaxOffsetAndEpsilonAndOffset.x,
215
+ uElevationMinMaxOffsetAndEpsilonAndOffset.y,
216
+ uElevationMinMaxOffsetAndEpsilonAndOffset.z,
217
+ uElevationMinMaxOffsetAndEpsilonAndOffset.w
218
+ );
219
+ }
220
+
221
+
222
+ float normalizeElevationHeight(float h) {
223
+
224
+ float minHeight = nvr_branchFreeTernary(uLogarithmic, pseudoLog(uElevationMinMaxHeightAndBoundary.x), uElevationMinMaxHeightAndBoundary.x);
225
+ float maxHeight = nvr_branchFreeTernary(uLogarithmic, pseudoLog(uElevationMinMaxHeightAndBoundary.y), uElevationMinMaxHeightAndBoundary.y);
226
+ float value = nvr_branchFreeTernary(uLogarithmic, pseudoLog(h), h);
227
+
228
+ return clamp(
229
+ (value - minHeight) / (maxHeight - minHeight),
230
+ 0.0,
231
+ 1.0
232
+ );
233
+ }
234
+
235
+
236
+
237
+ float sampleElevationBilinear(sampler2D demTexture, vec2 uv, ivec2 texSize) {
238
+
239
+ DEMBilinearData data = prepareDEMBilinear(texSize, uv);
240
+
241
+ float h = invalidHeight;
242
+
243
+
244
+ float h00 = decodeRawHeightForElevation(texelFetch(demTexture, data.p00, 0));
245
+ if(isValidHeight(h00)) {
246
+ float h10 = decodeRawHeightForElevation(texelFetch(demTexture, data.p10, 0));
247
+ float h01 = decodeRawHeightForElevation(texelFetch(demTexture, data.p01, 0));
248
+ float h11 = decodeRawHeightForElevation(texelFetch(demTexture, data.p11, 0));
249
+
250
+
251
+ h = interpolateDEMHeights(h00, h10, h01, h11, data.frac);
252
+ }
253
+
254
+ if (!isValidHeight(h)) {
255
+ return 0.0;
256
+ }
257
+
258
+
259
+ return normalizeElevationHeight(h);
260
+ }
261
+ #endif
262
+
263
+ layout(location = 0) out vec4 colorBuffer;
264
+ layout(location = 1) out vec4 attrBuffer;
265
+ layout(location = 2) out vec4 normalBuffer;
266
+
267
+ void main() {
268
+ vec4 compositedColor = vec4(0.0);
269
+ int winningSlot = -1;
270
+ float isWater = 0.0;
271
+ float isTexturized = 0.0;
272
+
273
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
274
+ bool hasHillshadeNormal = false;
275
+
276
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
277
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
278
+ // computed lazily on first use and reused by the rest (see reproject block).
279
+ float gReprojMLat = 0.0;
280
+ bool gReprojMLatReady = false;
281
+
282
+
283
+ // ---- compact slot 0 (absolute slot 0, raster) ----
284
+ if (uShows[0] == 1) {
285
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
286
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
287
+ float inBounds0 = 1.0;
288
+
289
+ if (uReproject[0] == 1) {
290
+ // Affine UV is correct in longitude but stretches latitude. Remap this
291
+ // fragment's latitude through Mercator so it samples the source correctly.
292
+ //
293
+ // The only per-fragment transcendental left is mLat: it depends solely on
294
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
295
+ // identical for every reprojecting slot — computed once and reused. The
296
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
297
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
298
+ if (!gReprojMLatReady) {
299
+ float lat = uReprojectTerrainLat[0].x
300
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
301
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
302
+ gReprojMLatReady = true;
303
+ }
304
+ float mRs = uReprojectMerc[0].x;
305
+ float mDen = uReprojectMerc[0].y;
306
+ // Only reproject when the source's Mercator span is wide enough to be
307
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
308
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
309
+ // float32 and produces vertical-stretch noise. There the Mercator correction
310
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
311
+ if (abs(mDen) > 1e-3) {
312
+ texUv0.y = (gReprojMLat - mRs) / mDen;
313
+ }
314
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
315
+ // terrain reaches ±90°. When this slot is the band-edge source (its
316
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
317
+ // that edge texel so the last available source row stretches across the cap
318
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
319
+ // the normal drop.
320
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
321
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
322
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
323
+ // source tile covers only part of the terrain tile; outside its [0,1] range
324
+ // the sampler would smear the edge texel across the rest, so drop it there.
325
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
326
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
327
+ }
328
+
329
+ vec4 texColor0;
330
+ if (uIsElevationHeatmaps[0]) {
331
+ ivec2 demTexSize = textureSize(uTextures[0], 0);
332
+ float normalized_h = sampleElevationBilinear(uTextures[0], texUv0, demTexSize);
333
+ texColor0 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, 1.0);
334
+ } else {
335
+ texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
336
+ }
337
+
338
+ if (uIsHillshades[0]) {
339
+ texColor0 = vec4(0.0);
340
+ }
341
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
342
+ if (alpha0 > 0.01) {
343
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
344
+ winningSlot = 0;
345
+ isTexturized = 0.0;
346
+
347
+ isWater = uWaters[0] ? 1.0 : 0.0;
348
+ }
349
+ }
350
+
351
+ // ---- compact slot 1 (absolute slot 1, raster) ----
352
+ if (uShows[1] == 1) {
353
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
354
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
355
+ float inBounds1 = 1.0;
356
+
357
+ if (uReproject[1] == 1) {
358
+ // Affine UV is correct in longitude but stretches latitude. Remap this
359
+ // fragment's latitude through Mercator so it samples the source correctly.
360
+ //
361
+ // The only per-fragment transcendental left is mLat: it depends solely on
362
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
363
+ // identical for every reprojecting slot — computed once and reused. The
364
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
365
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
366
+ if (!gReprojMLatReady) {
367
+ float lat = uReprojectTerrainLat[1].x
368
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
369
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
370
+ gReprojMLatReady = true;
371
+ }
372
+ float mRs = uReprojectMerc[1].x;
373
+ float mDen = uReprojectMerc[1].y;
374
+ // Only reproject when the source's Mercator span is wide enough to be
375
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
376
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
377
+ // float32 and produces vertical-stretch noise. There the Mercator correction
378
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
379
+ if (abs(mDen) > 1e-3) {
380
+ texUv1.y = (gReprojMLat - mRs) / mDen;
381
+ }
382
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
383
+ // terrain reaches ±90°. When this slot is the band-edge source (its
384
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
385
+ // that edge texel so the last available source row stretches across the cap
386
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
387
+ // the normal drop.
388
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
389
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
390
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
391
+ // source tile covers only part of the terrain tile; outside its [0,1] range
392
+ // the sampler would smear the edge texel across the rest, so drop it there.
393
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
394
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
395
+ }
396
+
397
+ vec4 texColor1;
398
+ if (uIsElevationHeatmaps[1]) {
399
+ ivec2 demTexSize = textureSize(uTextures[1], 0);
400
+ float normalized_h = sampleElevationBilinear(uTextures[1], texUv1, demTexSize);
401
+ texColor1 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, 1.0);
402
+ } else {
403
+ texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
404
+ }
405
+
406
+ if (uIsHillshades[1]) {
407
+ texColor1 = vec4(0.0);
408
+ }
409
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
410
+ if (alpha1 > 0.01) {
411
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
412
+ winningSlot = 1;
413
+ isTexturized = 0.0;
414
+
415
+ isWater = uWaters[1] ? 1.0 : 0.0;
416
+ }
417
+ }
418
+
419
+ // ---- compact slot 2 (absolute slot 2, vector) ----
420
+ if (uShows[2] == 1) {
421
+ vec2 texUv2 = vUv * uLayerUvScale[2] + uLayerUvOffset[2];
422
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
423
+ float inBounds2 = 1.0;
424
+
425
+ if (uReproject[2] == 1) {
426
+ // Affine UV is correct in longitude but stretches latitude. Remap this
427
+ // fragment's latitude through Mercator so it samples the source correctly.
428
+ //
429
+ // The only per-fragment transcendental left is mLat: it depends solely on
430
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
431
+ // identical for every reprojecting slot — computed once and reused. The
432
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
433
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
434
+ if (!gReprojMLatReady) {
435
+ float lat = uReprojectTerrainLat[2].x
436
+ + vUv.y * (uReprojectTerrainLat[2].y - uReprojectTerrainLat[2].x);
437
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
438
+ gReprojMLatReady = true;
439
+ }
440
+ float mRs = uReprojectMerc[2].x;
441
+ float mDen = uReprojectMerc[2].y;
442
+ // Only reproject when the source's Mercator span is wide enough to be
443
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
444
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
445
+ // float32 and produces vertical-stretch noise. There the Mercator correction
446
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
447
+ if (abs(mDen) > 1e-3) {
448
+ texUv2.y = (gReprojMLat - mRs) / mDen;
449
+ }
450
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
451
+ // terrain reaches ±90°. When this slot is the band-edge source (its
452
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
453
+ // that edge texel so the last available source row stretches across the cap
454
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
455
+ // the normal drop.
456
+ if (uReprojectMerc[2].z > 0.5) texUv2.y = min(texUv2.y, 1.0);
457
+ if (uReprojectMerc[2].w > 0.5) texUv2.y = max(texUv2.y, 0.0);
458
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
459
+ // source tile covers only part of the terrain tile; outside its [0,1] range
460
+ // the sampler would smear the edge texel across the rest, so drop it there.
461
+ inBounds2 = step(0.0, texUv2.x) * step(texUv2.x, 1.0)
462
+ * step(0.0, texUv2.y) * step(texUv2.y, 1.0);
463
+ }
464
+
465
+ vec4 texColor2;
466
+ if (uIsElevationHeatmaps[2]) {
467
+ ivec2 demTexSize = textureSize(uTextures[2], 0);
468
+ float normalized_h = sampleElevationBilinear(uTextures[2], texUv2, demTexSize);
469
+ texColor2 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, 1.0);
470
+ } else {
471
+ texColor2 = texture2D(uTextures[2], texUv2) * vec4(uColors[2], 1.0);
472
+ }
473
+
474
+ if (uIsHillshades[2]) {
475
+ texColor2 = vec4(0.0);
476
+ }
477
+ float alpha2 = texColor2.a * uOpacities[2] * inBounds2;
478
+ if (alpha2 > 0.01) {
479
+ compositedColor = mix(compositedColor, vec4(texColor2.rgb, 1.0), alpha2);
480
+ winningSlot = 2;
481
+ isTexturized = 1.0;
482
+
483
+ isWater = uWaters[2] ? 1.0 : 0.0;
484
+ }
485
+ }
486
+
487
+ // ---- compact slot 3 (absolute slot 3, vector) ----
488
+ if (uShows[3] == 1) {
489
+ vec2 texUv3 = vUv * uLayerUvScale[3] + uLayerUvOffset[3];
490
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
491
+ float inBounds3 = 1.0;
492
+
493
+ if (uReproject[3] == 1) {
494
+ // Affine UV is correct in longitude but stretches latitude. Remap this
495
+ // fragment's latitude through Mercator so it samples the source correctly.
496
+ //
497
+ // The only per-fragment transcendental left is mLat: it depends solely on
498
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
499
+ // identical for every reprojecting slot — computed once and reused. The
500
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
501
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
502
+ if (!gReprojMLatReady) {
503
+ float lat = uReprojectTerrainLat[3].x
504
+ + vUv.y * (uReprojectTerrainLat[3].y - uReprojectTerrainLat[3].x);
505
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
506
+ gReprojMLatReady = true;
507
+ }
508
+ float mRs = uReprojectMerc[3].x;
509
+ float mDen = uReprojectMerc[3].y;
510
+ // Only reproject when the source's Mercator span is wide enough to be
511
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
512
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
513
+ // float32 and produces vertical-stretch noise. There the Mercator correction
514
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
515
+ if (abs(mDen) > 1e-3) {
516
+ texUv3.y = (gReprojMLat - mRs) / mDen;
517
+ }
518
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
519
+ // terrain reaches ±90°. When this slot is the band-edge source (its
520
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
521
+ // that edge texel so the last available source row stretches across the cap
522
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
523
+ // the normal drop.
524
+ if (uReprojectMerc[3].z > 0.5) texUv3.y = min(texUv3.y, 1.0);
525
+ if (uReprojectMerc[3].w > 0.5) texUv3.y = max(texUv3.y, 0.0);
526
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
527
+ // source tile covers only part of the terrain tile; outside its [0,1] range
528
+ // the sampler would smear the edge texel across the rest, so drop it there.
529
+ inBounds3 = step(0.0, texUv3.x) * step(texUv3.x, 1.0)
530
+ * step(0.0, texUv3.y) * step(texUv3.y, 1.0);
531
+ }
532
+
533
+ vec4 texColor3;
534
+ if (uIsElevationHeatmaps[3]) {
535
+ ivec2 demTexSize = textureSize(uTextures[3], 0);
536
+ float normalized_h = sampleElevationBilinear(uTextures[3], texUv3, demTexSize);
537
+ texColor3 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, 1.0);
538
+ } else {
539
+ texColor3 = texture2D(uTextures[3], texUv3) * vec4(uColors[3], 1.0);
540
+ }
541
+
542
+ if (uIsHillshades[3]) {
543
+ texColor3 = vec4(0.0);
544
+ }
545
+ float alpha3 = texColor3.a * uOpacities[3] * inBounds3;
546
+ if (alpha3 > 0.01) {
547
+ compositedColor = mix(compositedColor, vec4(texColor3.rgb, 1.0), alpha3);
548
+ winningSlot = 3;
549
+ isTexturized = 1.0;
550
+
551
+ isWater = uWaters[3] ? 1.0 : 0.0;
552
+ }
553
+ }
554
+
555
+
556
+ if (uIsHillshades[0]) {
557
+ ivec2 normalMapSize0 = textureSize(uTextures[0], 0);
558
+ if (normalMapSize0.x > 1 && normalMapSize0.y > 1) {
559
+ vec2 hillshadeUv0 = clamp(vUv * uLayerUvScale[0] + uLayerUvOffset[0], vec2(0.0), vec2(1.0));
560
+ vec3 n0 = texture2D(uTextures[0], hillshadeUv0).rgb * 2.0 - 1.0;
561
+ n0 = normalize(n0);
562
+ n0.xy *= uHillshadeExaggeration;
563
+ n0 = normalize(n0);
564
+ hillshadeNormal = n0 * 0.5 + 0.5; // store in [0,1]
565
+ hasHillshadeNormal = true;
566
+ }
567
+ }
568
+
569
+ if (uIsHillshades[1]) {
570
+ ivec2 normalMapSize1 = textureSize(uTextures[1], 0);
571
+ if (normalMapSize1.x > 1 && normalMapSize1.y > 1) {
572
+ vec2 hillshadeUv1 = clamp(vUv * uLayerUvScale[1] + uLayerUvOffset[1], vec2(0.0), vec2(1.0));
573
+ vec3 n1 = texture2D(uTextures[1], hillshadeUv1).rgb * 2.0 - 1.0;
574
+ n1 = normalize(n1);
575
+ n1.xy *= uHillshadeExaggeration;
576
+ n1 = normalize(n1);
577
+ hillshadeNormal = n1 * 0.5 + 0.5; // store in [0,1]
578
+ hasHillshadeNormal = true;
579
+ }
580
+ }
581
+
582
+ if (uIsHillshades[2]) {
583
+ ivec2 normalMapSize2 = textureSize(uTextures[2], 0);
584
+ if (normalMapSize2.x > 1 && normalMapSize2.y > 1) {
585
+ vec2 hillshadeUv2 = clamp(vUv * uLayerUvScale[2] + uLayerUvOffset[2], vec2(0.0), vec2(1.0));
586
+ vec3 n2 = texture2D(uTextures[2], hillshadeUv2).rgb * 2.0 - 1.0;
587
+ n2 = normalize(n2);
588
+ n2.xy *= uHillshadeExaggeration;
589
+ n2 = normalize(n2);
590
+ hillshadeNormal = n2 * 0.5 + 0.5; // store in [0,1]
591
+ hasHillshadeNormal = true;
592
+ }
593
+ }
594
+
595
+ if (uIsHillshades[3]) {
596
+ ivec2 normalMapSize3 = textureSize(uTextures[3], 0);
597
+ if (normalMapSize3.x > 1 && normalMapSize3.y > 1) {
598
+ vec2 hillshadeUv3 = clamp(vUv * uLayerUvScale[3] + uLayerUvOffset[3], vec2(0.0), vec2(1.0));
599
+ vec3 n3 = texture2D(uTextures[3], hillshadeUv3).rgb * 2.0 - 1.0;
600
+ n3 = normalize(n3);
601
+ n3.xy *= uHillshadeExaggeration;
602
+ n3 = normalize(n3);
603
+ hillshadeNormal = n3 * 0.5 + 0.5; // store in [0,1]
604
+ hasHillshadeNormal = true;
605
+ }
606
+ }
607
+
608
+ // Quantized-mesh watermask: 0 = land, 255 = water (per spec). Takes priority
609
+ // over per-slot uWaters — even if no winning slot flagged water, watermask
610
+ // can still mark this pixel as water. Threshold at 0.5 (byte > 127).
611
+ isWater = max(isWater, step(0.5, texture(uWatermask, vUv).r));
612
+
613
+ colorBuffer = compositedColor;
614
+
615
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
616
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
617
+ attrBuffer = vec4(
618
+ isWater,
619
+ isTexturized,
620
+ 0.0,
621
+ float(winningSlot + 1) / 255.0
622
+ );
623
+
624
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
625
+ }
626
+ "
627
+ `;
628
+
629
+ exports[`generateCompositeFragmentShader snapshots > elevation heatmap only 1`] = `
630
+ "
631
+ precision highp float;
632
+ precision highp int;
633
+
634
+ #include <common>
635
+
636
+ varying vec2 vUv;
637
+
638
+
639
+
640
+ uniform int uShows[2];
641
+ uniform vec3 uColors[2];
642
+ uniform float uOpacities[2];
643
+ uniform sampler2D uTextures[2];
644
+ uniform vec2 uLayerUvOffset[2];
645
+ uniform vec2 uLayerUvScale[2];
646
+ uniform int uReproject[2];
647
+ uniform vec2 uReprojectTerrainLat[2];
648
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
649
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
650
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
651
+ uniform vec4 uReprojectMerc[2];
652
+ uniform bool uIsElevationHeatmaps[2];
653
+
654
+
655
+
656
+
657
+ #if USE_ELEVATION_HEATMAP
658
+ float nvr_branchFreeTernary(bool comparison, float a, float b) {
659
+ float useA = float(comparison);
660
+ return a * useA + b * (1.0 - useA);
661
+ }
662
+
663
+ /**
664
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
665
+ * evaluate both possibilities for a vec2 expression.
666
+ *
667
+ * @name nvr_branchFreeTernary
668
+ * @glslFunction
669
+ *
670
+ * @param {bool} comparison A comparison statement
671
+ * @param {vec2} a Value to return if the comparison is true.
672
+ * @param {vec2} b Value to return if the comparison is false.
673
+ *
674
+ * @returns {vec2} equivalent of comparison ? a : b
675
+ */
676
+ vec2 nvr_branchFreeTernary(bool comparison, vec2 a, vec2 b) {
677
+ float useA = float(comparison);
678
+ return a * useA + b * (1.0 - useA);
679
+ }
680
+
681
+ /**
682
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
683
+ * evaluate both possibilities for a vec3 expression.
684
+ *
685
+ * @name nvr_branchFreeTernary
686
+ * @glslFunction
687
+ *
688
+ * @param {bool} comparison A comparison statement
689
+ * @param {vec3} a Value to return if the comparison is true.
690
+ * @param {vec3} b Value to return if the comparison is false.
691
+ *
692
+ * @returns {vec3} equivalent of comparison ? a : b
693
+ */
694
+ vec3 nvr_branchFreeTernary(bool comparison, vec3 a, vec3 b) {
695
+ float useA = float(comparison);
696
+ return a * useA + b * (1.0 - useA);
697
+ }
698
+
699
+ /**
700
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
701
+ * evaluate both possibilities for a vec4 expression.
702
+ *
703
+ * @name nvr_branchFreeTernary
704
+ * @glslFunction
705
+ *
706
+ * @param {bool} comparison A comparison statement
707
+ * @param {vec3} a Value to return if the comparison is true.
708
+ * @param {vec3} b Value to return if the comparison is false.
709
+ *
710
+ * @returns {vec3} equivalent of comparison ? a : b
711
+ */
712
+ vec4 nvr_branchFreeTernary(bool comparison, vec4 a, vec4 b) {
713
+ float useA = float(comparison);
714
+ return a * useA + b * (1.0 - useA);
715
+ }
716
+ #ifndef DEM_UTIL_GLSL
717
+ #define DEM_UTIL_GLSL
718
+
719
+ bool isPowerOfTwo(int n) {
720
+ return n > 0 && ((n & (n - 1)) == 0);
721
+ }
722
+
723
+ const float invalidHeight = -999999.0;
724
+ bool isValidHeight(float h) {
725
+ return h > -999998.0;
726
+ }
727
+
728
+ struct DEMBilinearData {
729
+ ivec2 p00, p10, p01, p11;
730
+ vec2 frac;
731
+ };
732
+
733
+ DEMBilinearData prepareDEMBilinear(ivec2 actualTexSize, vec2 uv) {
734
+ vec2 paddedTexSize = vec2(actualTexSize);
735
+
736
+
737
+
738
+
739
+ bool hasPadding = !isPowerOfTwo(actualTexSize.x);
740
+
741
+ vec2 contentSize = hasPadding ? paddedTexSize - vec2(2.0) : paddedTexSize;
742
+
743
+
744
+
745
+
746
+
747
+
748
+
749
+
750
+
751
+ vec2 pixelCoord = hasPadding
752
+ ? uv * (contentSize - vec2(1.0)) + vec2(1.0)
753
+ : uv * (contentSize - vec2(1.0));
754
+
755
+ vec2 pixelFloor = floor(pixelCoord);
756
+ vec2 pixelFrac = fract(pixelCoord);
757
+
758
+
759
+
760
+ ivec2 maxCoord = ivec2(paddedTexSize) - ivec2(1);
761
+
762
+ DEMBilinearData data;
763
+ data.p00 = clamp(ivec2(pixelFloor), ivec2(0), maxCoord);
764
+ data.p10 = clamp(ivec2(pixelFloor + vec2(1.0, 0.0)), ivec2(0), maxCoord);
765
+ data.p01 = clamp(ivec2(pixelFloor + vec2(0.0, 1.0)), ivec2(0), maxCoord);
766
+ data.p11 = clamp(ivec2(pixelFloor + vec2(1.0, 1.0)), ivec2(0), maxCoord);
767
+ data.frac = pixelFrac;
768
+
769
+ return data;
770
+ }
771
+
772
+ float decodeDEMHeight(vec4 color, vec3 rgbScaler, float boundary, float minOffset, float maxOffset, float epsilon, float offset) {
773
+ vec3 rgb = color.rgb * 255.0;
774
+ float x = dot(rgb, rgbScaler);
775
+
776
+ float h;
777
+ float epsilon_cmp = 1.0;
778
+ if (abs(x - boundary) > epsilon_cmp) {
779
+ if (x > boundary) {
780
+ h = x + maxOffset;
781
+ } else {
782
+ h = x + minOffset;
783
+ }
784
+
785
+ h = h * epsilon + offset;
786
+ } else {
787
+ h = invalidHeight;
788
+ }
789
+
790
+ return h;
791
+ }
792
+
793
+ float interpolateDEMHeights(float h00, float h10, float h01, float h11, vec2 frac) {
794
+
795
+ if (!isValidHeight(h00)) return invalidHeight;
796
+
797
+
798
+
799
+ float maxReasonableDiff = 1000.0;
800
+
801
+
802
+ if (isValidHeight(h10) && abs(h10 - h00) > maxReasonableDiff) h10 = h00;
803
+ if (isValidHeight(h01) && abs(h01 - h00) > maxReasonableDiff) h01 = h00;
804
+ if (isValidHeight(h11) && abs(h11 - h00) > maxReasonableDiff) h11 = h00;
805
+
806
+
807
+ if (!isValidHeight(h10)) h10 = h00;
808
+ if (!isValidHeight(h01)) h01 = h00;
809
+ if (!isValidHeight(h11)) h11 = h00;
810
+
811
+
812
+ float h0 = mix(h00, h10, frac.x);
813
+ float h1 = mix(h01, h11, frac.x);
814
+ return mix(h0, h1, frac.y);
815
+ }
816
+
817
+ #endif
818
+
819
+ uniform sampler2D uColorMapTexture;
820
+ uniform vec3 uElevationRGBScaler;
821
+ uniform vec3 uElevationMinMaxHeightAndBoundary;
822
+ uniform vec4 uElevationMinMaxOffsetAndEpsilonAndOffset;
823
+ uniform bool uLogarithmic;
824
+ uniform float uLogBase;
825
+ uniform float uLogBoundary;
826
+
827
+ float pseudoLog(float value) {
828
+ return value > uLogBoundary ? log(value) / uLogBase : value < -uLogBoundary ? -log(-value) / uLogBase : value / uLogBoundary;
829
+ }
830
+
831
+
832
+ float decodeRawHeightForElevation(vec4 color) {
833
+ return decodeDEMHeight(
834
+ color,
835
+ uElevationRGBScaler,
836
+ uElevationMinMaxHeightAndBoundary.z,
837
+ uElevationMinMaxOffsetAndEpsilonAndOffset.x,
838
+ uElevationMinMaxOffsetAndEpsilonAndOffset.y,
839
+ uElevationMinMaxOffsetAndEpsilonAndOffset.z,
840
+ uElevationMinMaxOffsetAndEpsilonAndOffset.w
841
+ );
842
+ }
843
+
844
+
845
+ float normalizeElevationHeight(float h) {
846
+
847
+ float minHeight = nvr_branchFreeTernary(uLogarithmic, pseudoLog(uElevationMinMaxHeightAndBoundary.x), uElevationMinMaxHeightAndBoundary.x);
848
+ float maxHeight = nvr_branchFreeTernary(uLogarithmic, pseudoLog(uElevationMinMaxHeightAndBoundary.y), uElevationMinMaxHeightAndBoundary.y);
849
+ float value = nvr_branchFreeTernary(uLogarithmic, pseudoLog(h), h);
850
+
851
+ return clamp(
852
+ (value - minHeight) / (maxHeight - minHeight),
853
+ 0.0,
854
+ 1.0
855
+ );
856
+ }
857
+
858
+
859
+
860
+ float sampleElevationBilinear(sampler2D demTexture, vec2 uv, ivec2 texSize) {
861
+
862
+ DEMBilinearData data = prepareDEMBilinear(texSize, uv);
863
+
864
+ float h = invalidHeight;
865
+
866
+
867
+ float h00 = decodeRawHeightForElevation(texelFetch(demTexture, data.p00, 0));
868
+ if(isValidHeight(h00)) {
869
+ float h10 = decodeRawHeightForElevation(texelFetch(demTexture, data.p10, 0));
870
+ float h01 = decodeRawHeightForElevation(texelFetch(demTexture, data.p01, 0));
871
+ float h11 = decodeRawHeightForElevation(texelFetch(demTexture, data.p11, 0));
872
+
873
+
874
+ h = interpolateDEMHeights(h00, h10, h01, h11, data.frac);
875
+ }
876
+
877
+ if (!isValidHeight(h)) {
878
+ return 0.0;
879
+ }
880
+
881
+
882
+ return normalizeElevationHeight(h);
883
+ }
884
+ #endif
885
+
886
+ layout(location = 0) out vec4 colorBuffer;
887
+ layout(location = 1) out vec4 attrBuffer;
888
+ layout(location = 2) out vec4 normalBuffer;
889
+
890
+ void main() {
891
+ vec4 compositedColor = vec4(0.0);
892
+ int winningSlot = -1;
893
+ float isWater = 0.0;
894
+ float isTexturized = 0.0;
895
+
896
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
897
+ bool hasHillshadeNormal = false;
898
+
899
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
900
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
901
+ // computed lazily on first use and reused by the rest (see reproject block).
902
+ float gReprojMLat = 0.0;
903
+ bool gReprojMLatReady = false;
904
+
905
+
906
+ // ---- compact slot 0 (absolute slot 0, raster) ----
907
+ if (uShows[0] == 1) {
908
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
909
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
910
+ float inBounds0 = 1.0;
911
+
912
+ if (uReproject[0] == 1) {
913
+ // Affine UV is correct in longitude but stretches latitude. Remap this
914
+ // fragment's latitude through Mercator so it samples the source correctly.
915
+ //
916
+ // The only per-fragment transcendental left is mLat: it depends solely on
917
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
918
+ // identical for every reprojecting slot — computed once and reused. The
919
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
920
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
921
+ if (!gReprojMLatReady) {
922
+ float lat = uReprojectTerrainLat[0].x
923
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
924
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
925
+ gReprojMLatReady = true;
926
+ }
927
+ float mRs = uReprojectMerc[0].x;
928
+ float mDen = uReprojectMerc[0].y;
929
+ // Only reproject when the source's Mercator span is wide enough to be
930
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
931
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
932
+ // float32 and produces vertical-stretch noise. There the Mercator correction
933
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
934
+ if (abs(mDen) > 1e-3) {
935
+ texUv0.y = (gReprojMLat - mRs) / mDen;
936
+ }
937
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
938
+ // terrain reaches ±90°. When this slot is the band-edge source (its
939
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
940
+ // that edge texel so the last available source row stretches across the cap
941
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
942
+ // the normal drop.
943
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
944
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
945
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
946
+ // source tile covers only part of the terrain tile; outside its [0,1] range
947
+ // the sampler would smear the edge texel across the rest, so drop it there.
948
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
949
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
950
+ }
951
+
952
+ vec4 texColor0;
953
+ if (uIsElevationHeatmaps[0]) {
954
+ ivec2 demTexSize = textureSize(uTextures[0], 0);
955
+ float normalized_h = sampleElevationBilinear(uTextures[0], texUv0, demTexSize);
956
+ texColor0 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, 1.0);
957
+ } else {
958
+ texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
959
+ }
960
+
961
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
962
+ if (alpha0 > 0.01) {
963
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
964
+ winningSlot = 0;
965
+ isTexturized = 0.0;
966
+
967
+ }
968
+ }
969
+
970
+ // ---- compact slot 1 (absolute slot 1, raster) ----
971
+ if (uShows[1] == 1) {
972
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
973
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
974
+ float inBounds1 = 1.0;
975
+
976
+ if (uReproject[1] == 1) {
977
+ // Affine UV is correct in longitude but stretches latitude. Remap this
978
+ // fragment's latitude through Mercator so it samples the source correctly.
979
+ //
980
+ // The only per-fragment transcendental left is mLat: it depends solely on
981
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
982
+ // identical for every reprojecting slot — computed once and reused. The
983
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
984
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
985
+ if (!gReprojMLatReady) {
986
+ float lat = uReprojectTerrainLat[1].x
987
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
988
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
989
+ gReprojMLatReady = true;
990
+ }
991
+ float mRs = uReprojectMerc[1].x;
992
+ float mDen = uReprojectMerc[1].y;
993
+ // Only reproject when the source's Mercator span is wide enough to be
994
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
995
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
996
+ // float32 and produces vertical-stretch noise. There the Mercator correction
997
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
998
+ if (abs(mDen) > 1e-3) {
999
+ texUv1.y = (gReprojMLat - mRs) / mDen;
1000
+ }
1001
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1002
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1003
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1004
+ // that edge texel so the last available source row stretches across the cap
1005
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1006
+ // the normal drop.
1007
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
1008
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
1009
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1010
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1011
+ // the sampler would smear the edge texel across the rest, so drop it there.
1012
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
1013
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
1014
+ }
1015
+
1016
+ vec4 texColor1;
1017
+ if (uIsElevationHeatmaps[1]) {
1018
+ ivec2 demTexSize = textureSize(uTextures[1], 0);
1019
+ float normalized_h = sampleElevationBilinear(uTextures[1], texUv1, demTexSize);
1020
+ texColor1 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, 1.0);
1021
+ } else {
1022
+ texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
1023
+ }
1024
+
1025
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
1026
+ if (alpha1 > 0.01) {
1027
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
1028
+ winningSlot = 1;
1029
+ isTexturized = 0.0;
1030
+
1031
+ }
1032
+ }
1033
+
1034
+
1035
+
1036
+ colorBuffer = compositedColor;
1037
+
1038
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
1039
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
1040
+ attrBuffer = vec4(
1041
+ isWater,
1042
+ isTexturized,
1043
+ 0.0,
1044
+ float(winningSlot + 1) / 255.0
1045
+ );
1046
+
1047
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
1048
+ }
1049
+ "
1050
+ `;
1051
+
1052
+ exports[`generateCompositeFragmentShader snapshots > hillshade only 1`] = `
1053
+ "
1054
+ precision highp float;
1055
+ precision highp int;
1056
+
1057
+ #include <common>
1058
+
1059
+ varying vec2 vUv;
1060
+
1061
+
1062
+
1063
+ uniform int uShows[2];
1064
+ uniform vec3 uColors[2];
1065
+ uniform float uOpacities[2];
1066
+ uniform sampler2D uTextures[2];
1067
+ uniform vec2 uLayerUvOffset[2];
1068
+ uniform vec2 uLayerUvScale[2];
1069
+ uniform int uReproject[2];
1070
+ uniform vec2 uReprojectTerrainLat[2];
1071
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
1072
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
1073
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
1074
+ uniform vec4 uReprojectMerc[2];
1075
+ uniform bool uIsHillshades[2];
1076
+ uniform float uHillshadeExaggeration;
1077
+
1078
+
1079
+
1080
+
1081
+
1082
+
1083
+ layout(location = 0) out vec4 colorBuffer;
1084
+ layout(location = 1) out vec4 attrBuffer;
1085
+ layout(location = 2) out vec4 normalBuffer;
1086
+
1087
+ void main() {
1088
+ vec4 compositedColor = vec4(0.0);
1089
+ int winningSlot = -1;
1090
+ float isWater = 0.0;
1091
+ float isTexturized = 0.0;
1092
+
1093
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
1094
+ bool hasHillshadeNormal = false;
1095
+
1096
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
1097
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
1098
+ // computed lazily on first use and reused by the rest (see reproject block).
1099
+ float gReprojMLat = 0.0;
1100
+ bool gReprojMLatReady = false;
1101
+
1102
+
1103
+ // ---- compact slot 0 (absolute slot 0, raster) ----
1104
+ if (uShows[0] == 1) {
1105
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
1106
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1107
+ float inBounds0 = 1.0;
1108
+
1109
+ if (uReproject[0] == 1) {
1110
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1111
+ // fragment's latitude through Mercator so it samples the source correctly.
1112
+ //
1113
+ // The only per-fragment transcendental left is mLat: it depends solely on
1114
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1115
+ // identical for every reprojecting slot — computed once and reused. The
1116
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1117
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1118
+ if (!gReprojMLatReady) {
1119
+ float lat = uReprojectTerrainLat[0].x
1120
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
1121
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1122
+ gReprojMLatReady = true;
1123
+ }
1124
+ float mRs = uReprojectMerc[0].x;
1125
+ float mDen = uReprojectMerc[0].y;
1126
+ // Only reproject when the source's Mercator span is wide enough to be
1127
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1128
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1129
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1130
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1131
+ if (abs(mDen) > 1e-3) {
1132
+ texUv0.y = (gReprojMLat - mRs) / mDen;
1133
+ }
1134
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1135
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1136
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1137
+ // that edge texel so the last available source row stretches across the cap
1138
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1139
+ // the normal drop.
1140
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
1141
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
1142
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1143
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1144
+ // the sampler would smear the edge texel across the rest, so drop it there.
1145
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
1146
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
1147
+ }
1148
+
1149
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
1150
+
1151
+ if (uIsHillshades[0]) {
1152
+ texColor0 = vec4(0.0);
1153
+ }
1154
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
1155
+ if (alpha0 > 0.01) {
1156
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
1157
+ winningSlot = 0;
1158
+ isTexturized = 0.0;
1159
+
1160
+ }
1161
+ }
1162
+
1163
+ // ---- compact slot 1 (absolute slot 1, raster) ----
1164
+ if (uShows[1] == 1) {
1165
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
1166
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1167
+ float inBounds1 = 1.0;
1168
+
1169
+ if (uReproject[1] == 1) {
1170
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1171
+ // fragment's latitude through Mercator so it samples the source correctly.
1172
+ //
1173
+ // The only per-fragment transcendental left is mLat: it depends solely on
1174
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1175
+ // identical for every reprojecting slot — computed once and reused. The
1176
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1177
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1178
+ if (!gReprojMLatReady) {
1179
+ float lat = uReprojectTerrainLat[1].x
1180
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
1181
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1182
+ gReprojMLatReady = true;
1183
+ }
1184
+ float mRs = uReprojectMerc[1].x;
1185
+ float mDen = uReprojectMerc[1].y;
1186
+ // Only reproject when the source's Mercator span is wide enough to be
1187
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1188
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1189
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1190
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1191
+ if (abs(mDen) > 1e-3) {
1192
+ texUv1.y = (gReprojMLat - mRs) / mDen;
1193
+ }
1194
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1195
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1196
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1197
+ // that edge texel so the last available source row stretches across the cap
1198
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1199
+ // the normal drop.
1200
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
1201
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
1202
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1203
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1204
+ // the sampler would smear the edge texel across the rest, so drop it there.
1205
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
1206
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
1207
+ }
1208
+
1209
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
1210
+
1211
+ if (uIsHillshades[1]) {
1212
+ texColor1 = vec4(0.0);
1213
+ }
1214
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
1215
+ if (alpha1 > 0.01) {
1216
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
1217
+ winningSlot = 1;
1218
+ isTexturized = 0.0;
1219
+
1220
+ }
1221
+ }
1222
+
1223
+
1224
+ if (uIsHillshades[0]) {
1225
+ ivec2 normalMapSize0 = textureSize(uTextures[0], 0);
1226
+ if (normalMapSize0.x > 1 && normalMapSize0.y > 1) {
1227
+ vec2 hillshadeUv0 = clamp(vUv * uLayerUvScale[0] + uLayerUvOffset[0], vec2(0.0), vec2(1.0));
1228
+ vec3 n0 = texture2D(uTextures[0], hillshadeUv0).rgb * 2.0 - 1.0;
1229
+ n0 = normalize(n0);
1230
+ n0.xy *= uHillshadeExaggeration;
1231
+ n0 = normalize(n0);
1232
+ hillshadeNormal = n0 * 0.5 + 0.5; // store in [0,1]
1233
+ hasHillshadeNormal = true;
1234
+ }
1235
+ }
1236
+
1237
+ if (uIsHillshades[1]) {
1238
+ ivec2 normalMapSize1 = textureSize(uTextures[1], 0);
1239
+ if (normalMapSize1.x > 1 && normalMapSize1.y > 1) {
1240
+ vec2 hillshadeUv1 = clamp(vUv * uLayerUvScale[1] + uLayerUvOffset[1], vec2(0.0), vec2(1.0));
1241
+ vec3 n1 = texture2D(uTextures[1], hillshadeUv1).rgb * 2.0 - 1.0;
1242
+ n1 = normalize(n1);
1243
+ n1.xy *= uHillshadeExaggeration;
1244
+ n1 = normalize(n1);
1245
+ hillshadeNormal = n1 * 0.5 + 0.5; // store in [0,1]
1246
+ hasHillshadeNormal = true;
1247
+ }
1248
+ }
1249
+
1250
+ colorBuffer = compositedColor;
1251
+
1252
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
1253
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
1254
+ attrBuffer = vec4(
1255
+ isWater,
1256
+ isTexturized,
1257
+ 0.0,
1258
+ float(winningSlot + 1) / 255.0
1259
+ );
1260
+
1261
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
1262
+ }
1263
+ "
1264
+ `;
1265
+
1266
+ exports[`generateCompositeFragmentShader snapshots > raster+single vector, boundary 24 1`] = `
1267
+ "
1268
+ precision highp float;
1269
+ precision highp int;
1270
+
1271
+ #include <common>
1272
+
1273
+ varying vec2 vUv;
1274
+
1275
+
1276
+
1277
+ uniform int uShows[3];
1278
+ uniform vec3 uColors[3];
1279
+ uniform float uOpacities[3];
1280
+ uniform sampler2D uTextures[3];
1281
+ uniform vec2 uLayerUvOffset[3];
1282
+ uniform vec2 uLayerUvScale[3];
1283
+ uniform int uReproject[3];
1284
+ uniform vec2 uReprojectTerrainLat[3];
1285
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
1286
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
1287
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
1288
+ uniform vec4 uReprojectMerc[3];
1289
+
1290
+
1291
+
1292
+
1293
+
1294
+
1295
+
1296
+ layout(location = 0) out vec4 colorBuffer;
1297
+ layout(location = 1) out vec4 attrBuffer;
1298
+ layout(location = 2) out vec4 normalBuffer;
1299
+
1300
+ void main() {
1301
+ vec4 compositedColor = vec4(0.0);
1302
+ int winningSlot = -1;
1303
+ float isWater = 0.0;
1304
+ float isTexturized = 0.0;
1305
+
1306
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
1307
+ bool hasHillshadeNormal = false;
1308
+
1309
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
1310
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
1311
+ // computed lazily on first use and reused by the rest (see reproject block).
1312
+ float gReprojMLat = 0.0;
1313
+ bool gReprojMLatReady = false;
1314
+
1315
+
1316
+ // ---- compact slot 0 (absolute slot 0, raster) ----
1317
+ if (uShows[0] == 1) {
1318
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
1319
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1320
+ float inBounds0 = 1.0;
1321
+
1322
+ if (uReproject[0] == 1) {
1323
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1324
+ // fragment's latitude through Mercator so it samples the source correctly.
1325
+ //
1326
+ // The only per-fragment transcendental left is mLat: it depends solely on
1327
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1328
+ // identical for every reprojecting slot — computed once and reused. The
1329
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1330
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1331
+ if (!gReprojMLatReady) {
1332
+ float lat = uReprojectTerrainLat[0].x
1333
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
1334
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1335
+ gReprojMLatReady = true;
1336
+ }
1337
+ float mRs = uReprojectMerc[0].x;
1338
+ float mDen = uReprojectMerc[0].y;
1339
+ // Only reproject when the source's Mercator span is wide enough to be
1340
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1341
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1342
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1343
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1344
+ if (abs(mDen) > 1e-3) {
1345
+ texUv0.y = (gReprojMLat - mRs) / mDen;
1346
+ }
1347
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1348
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1349
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1350
+ // that edge texel so the last available source row stretches across the cap
1351
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1352
+ // the normal drop.
1353
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
1354
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
1355
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1356
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1357
+ // the sampler would smear the edge texel across the rest, so drop it there.
1358
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
1359
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
1360
+ }
1361
+
1362
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
1363
+
1364
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
1365
+ if (alpha0 > 0.01) {
1366
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
1367
+ winningSlot = 0;
1368
+ isTexturized = 0.0;
1369
+
1370
+ }
1371
+ }
1372
+
1373
+ // ---- compact slot 1 (absolute slot 1, raster) ----
1374
+ if (uShows[1] == 1) {
1375
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
1376
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1377
+ float inBounds1 = 1.0;
1378
+
1379
+ if (uReproject[1] == 1) {
1380
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1381
+ // fragment's latitude through Mercator so it samples the source correctly.
1382
+ //
1383
+ // The only per-fragment transcendental left is mLat: it depends solely on
1384
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1385
+ // identical for every reprojecting slot — computed once and reused. The
1386
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1387
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1388
+ if (!gReprojMLatReady) {
1389
+ float lat = uReprojectTerrainLat[1].x
1390
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
1391
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1392
+ gReprojMLatReady = true;
1393
+ }
1394
+ float mRs = uReprojectMerc[1].x;
1395
+ float mDen = uReprojectMerc[1].y;
1396
+ // Only reproject when the source's Mercator span is wide enough to be
1397
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1398
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1399
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1400
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1401
+ if (abs(mDen) > 1e-3) {
1402
+ texUv1.y = (gReprojMLat - mRs) / mDen;
1403
+ }
1404
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1405
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1406
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1407
+ // that edge texel so the last available source row stretches across the cap
1408
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1409
+ // the normal drop.
1410
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
1411
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
1412
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1413
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1414
+ // the sampler would smear the edge texel across the rest, so drop it there.
1415
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
1416
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
1417
+ }
1418
+
1419
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
1420
+
1421
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
1422
+ if (alpha1 > 0.01) {
1423
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
1424
+ winningSlot = 1;
1425
+ isTexturized = 0.0;
1426
+
1427
+ }
1428
+ }
1429
+
1430
+ // ---- compact slot 2 (absolute slot 24, vector) ----
1431
+ if (uShows[2] == 1) {
1432
+ vec2 texUv2 = vUv * uLayerUvScale[2] + uLayerUvOffset[2];
1433
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1434
+ float inBounds2 = 1.0;
1435
+
1436
+ if (uReproject[2] == 1) {
1437
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1438
+ // fragment's latitude through Mercator so it samples the source correctly.
1439
+ //
1440
+ // The only per-fragment transcendental left is mLat: it depends solely on
1441
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1442
+ // identical for every reprojecting slot — computed once and reused. The
1443
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1444
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1445
+ if (!gReprojMLatReady) {
1446
+ float lat = uReprojectTerrainLat[2].x
1447
+ + vUv.y * (uReprojectTerrainLat[2].y - uReprojectTerrainLat[2].x);
1448
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1449
+ gReprojMLatReady = true;
1450
+ }
1451
+ float mRs = uReprojectMerc[2].x;
1452
+ float mDen = uReprojectMerc[2].y;
1453
+ // Only reproject when the source's Mercator span is wide enough to be
1454
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1455
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1456
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1457
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1458
+ if (abs(mDen) > 1e-3) {
1459
+ texUv2.y = (gReprojMLat - mRs) / mDen;
1460
+ }
1461
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1462
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1463
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1464
+ // that edge texel so the last available source row stretches across the cap
1465
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1466
+ // the normal drop.
1467
+ if (uReprojectMerc[2].z > 0.5) texUv2.y = min(texUv2.y, 1.0);
1468
+ if (uReprojectMerc[2].w > 0.5) texUv2.y = max(texUv2.y, 0.0);
1469
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1470
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1471
+ // the sampler would smear the edge texel across the rest, so drop it there.
1472
+ inBounds2 = step(0.0, texUv2.x) * step(texUv2.x, 1.0)
1473
+ * step(0.0, texUv2.y) * step(texUv2.y, 1.0);
1474
+ }
1475
+
1476
+ vec4 texColor2 = texture2D(uTextures[2], texUv2) * vec4(uColors[2], 1.0);
1477
+
1478
+ float alpha2 = texColor2.a * uOpacities[2] * inBounds2;
1479
+ if (alpha2 > 0.01) {
1480
+ compositedColor = mix(compositedColor, vec4(texColor2.rgb, 1.0), alpha2);
1481
+ winningSlot = 24;
1482
+ isTexturized = 1.0;
1483
+
1484
+ }
1485
+ }
1486
+
1487
+
1488
+
1489
+ colorBuffer = compositedColor;
1490
+
1491
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
1492
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
1493
+ attrBuffer = vec4(
1494
+ isWater,
1495
+ isTexturized,
1496
+ 0.0,
1497
+ float(winningSlot + 1) / 255.0
1498
+ );
1499
+
1500
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
1501
+ }
1502
+ "
1503
+ `;
1504
+
1505
+ exports[`generateCompositeFragmentShader snapshots > raster+vector, all features off 1`] = `
1506
+ "
1507
+ precision highp float;
1508
+ precision highp int;
1509
+
1510
+ #include <common>
1511
+
1512
+ varying vec2 vUv;
1513
+
1514
+
1515
+
1516
+ uniform int uShows[4];
1517
+ uniform vec3 uColors[4];
1518
+ uniform float uOpacities[4];
1519
+ uniform sampler2D uTextures[4];
1520
+ uniform vec2 uLayerUvOffset[4];
1521
+ uniform vec2 uLayerUvScale[4];
1522
+ uniform int uReproject[4];
1523
+ uniform vec2 uReprojectTerrainLat[4];
1524
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
1525
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
1526
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
1527
+ uniform vec4 uReprojectMerc[4];
1528
+
1529
+
1530
+
1531
+
1532
+
1533
+
1534
+
1535
+ layout(location = 0) out vec4 colorBuffer;
1536
+ layout(location = 1) out vec4 attrBuffer;
1537
+ layout(location = 2) out vec4 normalBuffer;
1538
+
1539
+ void main() {
1540
+ vec4 compositedColor = vec4(0.0);
1541
+ int winningSlot = -1;
1542
+ float isWater = 0.0;
1543
+ float isTexturized = 0.0;
1544
+
1545
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
1546
+ bool hasHillshadeNormal = false;
1547
+
1548
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
1549
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
1550
+ // computed lazily on first use and reused by the rest (see reproject block).
1551
+ float gReprojMLat = 0.0;
1552
+ bool gReprojMLatReady = false;
1553
+
1554
+
1555
+ // ---- compact slot 0 (absolute slot 0, raster) ----
1556
+ if (uShows[0] == 1) {
1557
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
1558
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1559
+ float inBounds0 = 1.0;
1560
+
1561
+ if (uReproject[0] == 1) {
1562
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1563
+ // fragment's latitude through Mercator so it samples the source correctly.
1564
+ //
1565
+ // The only per-fragment transcendental left is mLat: it depends solely on
1566
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1567
+ // identical for every reprojecting slot — computed once and reused. The
1568
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1569
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1570
+ if (!gReprojMLatReady) {
1571
+ float lat = uReprojectTerrainLat[0].x
1572
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
1573
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1574
+ gReprojMLatReady = true;
1575
+ }
1576
+ float mRs = uReprojectMerc[0].x;
1577
+ float mDen = uReprojectMerc[0].y;
1578
+ // Only reproject when the source's Mercator span is wide enough to be
1579
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1580
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1581
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1582
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1583
+ if (abs(mDen) > 1e-3) {
1584
+ texUv0.y = (gReprojMLat - mRs) / mDen;
1585
+ }
1586
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1587
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1588
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1589
+ // that edge texel so the last available source row stretches across the cap
1590
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1591
+ // the normal drop.
1592
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
1593
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
1594
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1595
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1596
+ // the sampler would smear the edge texel across the rest, so drop it there.
1597
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
1598
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
1599
+ }
1600
+
1601
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
1602
+
1603
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
1604
+ if (alpha0 > 0.01) {
1605
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
1606
+ winningSlot = 0;
1607
+ isTexturized = 0.0;
1608
+
1609
+ }
1610
+ }
1611
+
1612
+ // ---- compact slot 1 (absolute slot 1, raster) ----
1613
+ if (uShows[1] == 1) {
1614
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
1615
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1616
+ float inBounds1 = 1.0;
1617
+
1618
+ if (uReproject[1] == 1) {
1619
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1620
+ // fragment's latitude through Mercator so it samples the source correctly.
1621
+ //
1622
+ // The only per-fragment transcendental left is mLat: it depends solely on
1623
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1624
+ // identical for every reprojecting slot — computed once and reused. The
1625
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1626
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1627
+ if (!gReprojMLatReady) {
1628
+ float lat = uReprojectTerrainLat[1].x
1629
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
1630
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1631
+ gReprojMLatReady = true;
1632
+ }
1633
+ float mRs = uReprojectMerc[1].x;
1634
+ float mDen = uReprojectMerc[1].y;
1635
+ // Only reproject when the source's Mercator span is wide enough to be
1636
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1637
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1638
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1639
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1640
+ if (abs(mDen) > 1e-3) {
1641
+ texUv1.y = (gReprojMLat - mRs) / mDen;
1642
+ }
1643
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1644
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1645
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1646
+ // that edge texel so the last available source row stretches across the cap
1647
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1648
+ // the normal drop.
1649
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
1650
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
1651
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1652
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1653
+ // the sampler would smear the edge texel across the rest, so drop it there.
1654
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
1655
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
1656
+ }
1657
+
1658
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
1659
+
1660
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
1661
+ if (alpha1 > 0.01) {
1662
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
1663
+ winningSlot = 1;
1664
+ isTexturized = 0.0;
1665
+
1666
+ }
1667
+ }
1668
+
1669
+ // ---- compact slot 2 (absolute slot 2, vector) ----
1670
+ if (uShows[2] == 1) {
1671
+ vec2 texUv2 = vUv * uLayerUvScale[2] + uLayerUvOffset[2];
1672
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1673
+ float inBounds2 = 1.0;
1674
+
1675
+ if (uReproject[2] == 1) {
1676
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1677
+ // fragment's latitude through Mercator so it samples the source correctly.
1678
+ //
1679
+ // The only per-fragment transcendental left is mLat: it depends solely on
1680
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1681
+ // identical for every reprojecting slot — computed once and reused. The
1682
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1683
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1684
+ if (!gReprojMLatReady) {
1685
+ float lat = uReprojectTerrainLat[2].x
1686
+ + vUv.y * (uReprojectTerrainLat[2].y - uReprojectTerrainLat[2].x);
1687
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1688
+ gReprojMLatReady = true;
1689
+ }
1690
+ float mRs = uReprojectMerc[2].x;
1691
+ float mDen = uReprojectMerc[2].y;
1692
+ // Only reproject when the source's Mercator span is wide enough to be
1693
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1694
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1695
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1696
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1697
+ if (abs(mDen) > 1e-3) {
1698
+ texUv2.y = (gReprojMLat - mRs) / mDen;
1699
+ }
1700
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1701
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1702
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1703
+ // that edge texel so the last available source row stretches across the cap
1704
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1705
+ // the normal drop.
1706
+ if (uReprojectMerc[2].z > 0.5) texUv2.y = min(texUv2.y, 1.0);
1707
+ if (uReprojectMerc[2].w > 0.5) texUv2.y = max(texUv2.y, 0.0);
1708
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1709
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1710
+ // the sampler would smear the edge texel across the rest, so drop it there.
1711
+ inBounds2 = step(0.0, texUv2.x) * step(texUv2.x, 1.0)
1712
+ * step(0.0, texUv2.y) * step(texUv2.y, 1.0);
1713
+ }
1714
+
1715
+ vec4 texColor2 = texture2D(uTextures[2], texUv2) * vec4(uColors[2], 1.0);
1716
+
1717
+ float alpha2 = texColor2.a * uOpacities[2] * inBounds2;
1718
+ if (alpha2 > 0.01) {
1719
+ compositedColor = mix(compositedColor, vec4(texColor2.rgb, 1.0), alpha2);
1720
+ winningSlot = 2;
1721
+ isTexturized = 1.0;
1722
+
1723
+ }
1724
+ }
1725
+
1726
+ // ---- compact slot 3 (absolute slot 3, vector) ----
1727
+ if (uShows[3] == 1) {
1728
+ vec2 texUv3 = vUv * uLayerUvScale[3] + uLayerUvOffset[3];
1729
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1730
+ float inBounds3 = 1.0;
1731
+
1732
+ if (uReproject[3] == 1) {
1733
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1734
+ // fragment's latitude through Mercator so it samples the source correctly.
1735
+ //
1736
+ // The only per-fragment transcendental left is mLat: it depends solely on
1737
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1738
+ // identical for every reprojecting slot — computed once and reused. The
1739
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1740
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1741
+ if (!gReprojMLatReady) {
1742
+ float lat = uReprojectTerrainLat[3].x
1743
+ + vUv.y * (uReprojectTerrainLat[3].y - uReprojectTerrainLat[3].x);
1744
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1745
+ gReprojMLatReady = true;
1746
+ }
1747
+ float mRs = uReprojectMerc[3].x;
1748
+ float mDen = uReprojectMerc[3].y;
1749
+ // Only reproject when the source's Mercator span is wide enough to be
1750
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1751
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1752
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1753
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1754
+ if (abs(mDen) > 1e-3) {
1755
+ texUv3.y = (gReprojMLat - mRs) / mDen;
1756
+ }
1757
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1758
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1759
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1760
+ // that edge texel so the last available source row stretches across the cap
1761
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1762
+ // the normal drop.
1763
+ if (uReprojectMerc[3].z > 0.5) texUv3.y = min(texUv3.y, 1.0);
1764
+ if (uReprojectMerc[3].w > 0.5) texUv3.y = max(texUv3.y, 0.0);
1765
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1766
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1767
+ // the sampler would smear the edge texel across the rest, so drop it there.
1768
+ inBounds3 = step(0.0, texUv3.x) * step(texUv3.x, 1.0)
1769
+ * step(0.0, texUv3.y) * step(texUv3.y, 1.0);
1770
+ }
1771
+
1772
+ vec4 texColor3 = texture2D(uTextures[3], texUv3) * vec4(uColors[3], 1.0);
1773
+
1774
+ float alpha3 = texColor3.a * uOpacities[3] * inBounds3;
1775
+ if (alpha3 > 0.01) {
1776
+ compositedColor = mix(compositedColor, vec4(texColor3.rgb, 1.0), alpha3);
1777
+ winningSlot = 3;
1778
+ isTexturized = 1.0;
1779
+
1780
+ }
1781
+ }
1782
+
1783
+
1784
+
1785
+ colorBuffer = compositedColor;
1786
+
1787
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
1788
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
1789
+ attrBuffer = vec4(
1790
+ isWater,
1791
+ isTexturized,
1792
+ 0.0,
1793
+ float(winningSlot + 1) / 255.0
1794
+ );
1795
+
1796
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
1797
+ }
1798
+ "
1799
+ `;
1800
+
1801
+ exports[`generateCompositeFragmentShader snapshots > single raster+2 vector, boundary 24 1`] = `
1802
+ "
1803
+ precision highp float;
1804
+ precision highp int;
1805
+
1806
+ #include <common>
1807
+
1808
+ varying vec2 vUv;
1809
+
1810
+
1811
+
1812
+ uniform int uShows[3];
1813
+ uniform vec3 uColors[3];
1814
+ uniform float uOpacities[3];
1815
+ uniform sampler2D uTextures[3];
1816
+ uniform vec2 uLayerUvOffset[3];
1817
+ uniform vec2 uLayerUvScale[3];
1818
+ uniform int uReproject[3];
1819
+ uniform vec2 uReprojectTerrainLat[3];
1820
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
1821
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
1822
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
1823
+ uniform vec4 uReprojectMerc[3];
1824
+
1825
+
1826
+
1827
+
1828
+
1829
+
1830
+
1831
+ layout(location = 0) out vec4 colorBuffer;
1832
+ layout(location = 1) out vec4 attrBuffer;
1833
+ layout(location = 2) out vec4 normalBuffer;
1834
+
1835
+ void main() {
1836
+ vec4 compositedColor = vec4(0.0);
1837
+ int winningSlot = -1;
1838
+ float isWater = 0.0;
1839
+ float isTexturized = 0.0;
1840
+
1841
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
1842
+ bool hasHillshadeNormal = false;
1843
+
1844
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
1845
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
1846
+ // computed lazily on first use and reused by the rest (see reproject block).
1847
+ float gReprojMLat = 0.0;
1848
+ bool gReprojMLatReady = false;
1849
+
1850
+
1851
+ // ---- compact slot 0 (absolute slot 0, raster) ----
1852
+ if (uShows[0] == 1) {
1853
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
1854
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1855
+ float inBounds0 = 1.0;
1856
+
1857
+ if (uReproject[0] == 1) {
1858
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1859
+ // fragment's latitude through Mercator so it samples the source correctly.
1860
+ //
1861
+ // The only per-fragment transcendental left is mLat: it depends solely on
1862
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1863
+ // identical for every reprojecting slot — computed once and reused. The
1864
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1865
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1866
+ if (!gReprojMLatReady) {
1867
+ float lat = uReprojectTerrainLat[0].x
1868
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
1869
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1870
+ gReprojMLatReady = true;
1871
+ }
1872
+ float mRs = uReprojectMerc[0].x;
1873
+ float mDen = uReprojectMerc[0].y;
1874
+ // Only reproject when the source's Mercator span is wide enough to be
1875
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1876
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1877
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1878
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1879
+ if (abs(mDen) > 1e-3) {
1880
+ texUv0.y = (gReprojMLat - mRs) / mDen;
1881
+ }
1882
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1883
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1884
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1885
+ // that edge texel so the last available source row stretches across the cap
1886
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1887
+ // the normal drop.
1888
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
1889
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
1890
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1891
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1892
+ // the sampler would smear the edge texel across the rest, so drop it there.
1893
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
1894
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
1895
+ }
1896
+
1897
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
1898
+
1899
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
1900
+ if (alpha0 > 0.01) {
1901
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
1902
+ winningSlot = 0;
1903
+ isTexturized = 0.0;
1904
+
1905
+ }
1906
+ }
1907
+
1908
+ // ---- compact slot 1 (absolute slot 24, vector) ----
1909
+ if (uShows[1] == 1) {
1910
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
1911
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1912
+ float inBounds1 = 1.0;
1913
+
1914
+ if (uReproject[1] == 1) {
1915
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1916
+ // fragment's latitude through Mercator so it samples the source correctly.
1917
+ //
1918
+ // The only per-fragment transcendental left is mLat: it depends solely on
1919
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1920
+ // identical for every reprojecting slot — computed once and reused. The
1921
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1922
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1923
+ if (!gReprojMLatReady) {
1924
+ float lat = uReprojectTerrainLat[1].x
1925
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
1926
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1927
+ gReprojMLatReady = true;
1928
+ }
1929
+ float mRs = uReprojectMerc[1].x;
1930
+ float mDen = uReprojectMerc[1].y;
1931
+ // Only reproject when the source's Mercator span is wide enough to be
1932
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1933
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1934
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1935
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1936
+ if (abs(mDen) > 1e-3) {
1937
+ texUv1.y = (gReprojMLat - mRs) / mDen;
1938
+ }
1939
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1940
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1941
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1942
+ // that edge texel so the last available source row stretches across the cap
1943
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
1944
+ // the normal drop.
1945
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
1946
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
1947
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
1948
+ // source tile covers only part of the terrain tile; outside its [0,1] range
1949
+ // the sampler would smear the edge texel across the rest, so drop it there.
1950
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
1951
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
1952
+ }
1953
+
1954
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
1955
+
1956
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
1957
+ if (alpha1 > 0.01) {
1958
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
1959
+ winningSlot = 24;
1960
+ isTexturized = 1.0;
1961
+
1962
+ }
1963
+ }
1964
+
1965
+ // ---- compact slot 2 (absolute slot 25, vector) ----
1966
+ if (uShows[2] == 1) {
1967
+ vec2 texUv2 = vUv * uLayerUvScale[2] + uLayerUvOffset[2];
1968
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
1969
+ float inBounds2 = 1.0;
1970
+
1971
+ if (uReproject[2] == 1) {
1972
+ // Affine UV is correct in longitude but stretches latitude. Remap this
1973
+ // fragment's latitude through Mercator so it samples the source correctly.
1974
+ //
1975
+ // The only per-fragment transcendental left is mLat: it depends solely on
1976
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
1977
+ // identical for every reprojecting slot — computed once and reused. The
1978
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
1979
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
1980
+ if (!gReprojMLatReady) {
1981
+ float lat = uReprojectTerrainLat[2].x
1982
+ + vUv.y * (uReprojectTerrainLat[2].y - uReprojectTerrainLat[2].x);
1983
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
1984
+ gReprojMLatReady = true;
1985
+ }
1986
+ float mRs = uReprojectMerc[2].x;
1987
+ float mDen = uReprojectMerc[2].y;
1988
+ // Only reproject when the source's Mercator span is wide enough to be
1989
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
1990
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
1991
+ // float32 and produces vertical-stretch noise. There the Mercator correction
1992
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
1993
+ if (abs(mDen) > 1e-3) {
1994
+ texUv2.y = (gReprojMLat - mRs) / mDen;
1995
+ }
1996
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
1997
+ // terrain reaches ±90°. When this slot is the band-edge source (its
1998
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
1999
+ // that edge texel so the last available source row stretches across the cap
2000
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2001
+ // the normal drop.
2002
+ if (uReprojectMerc[2].z > 0.5) texUv2.y = min(texUv2.y, 1.0);
2003
+ if (uReprojectMerc[2].w > 0.5) texUv2.y = max(texUv2.y, 0.0);
2004
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2005
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2006
+ // the sampler would smear the edge texel across the rest, so drop it there.
2007
+ inBounds2 = step(0.0, texUv2.x) * step(texUv2.x, 1.0)
2008
+ * step(0.0, texUv2.y) * step(texUv2.y, 1.0);
2009
+ }
2010
+
2011
+ vec4 texColor2 = texture2D(uTextures[2], texUv2) * vec4(uColors[2], 1.0);
2012
+
2013
+ float alpha2 = texColor2.a * uOpacities[2] * inBounds2;
2014
+ if (alpha2 > 0.01) {
2015
+ compositedColor = mix(compositedColor, vec4(texColor2.rgb, 1.0), alpha2);
2016
+ winningSlot = 25;
2017
+ isTexturized = 1.0;
2018
+
2019
+ }
2020
+ }
2021
+
2022
+
2023
+
2024
+ colorBuffer = compositedColor;
2025
+
2026
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
2027
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
2028
+ attrBuffer = vec4(
2029
+ isWater,
2030
+ isTexturized,
2031
+ 0.0,
2032
+ float(winningSlot + 1) / 255.0
2033
+ );
2034
+
2035
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
2036
+ }
2037
+ "
2038
+ `;
2039
+
2040
+ exports[`generateCompositeFragmentShader snapshots > vector-only (rasterCount 0) 1`] = `
2041
+ "
2042
+ precision highp float;
2043
+ precision highp int;
2044
+
2045
+ #include <common>
2046
+
2047
+ varying vec2 vUv;
2048
+
2049
+
2050
+
2051
+ uniform int uShows[2];
2052
+ uniform vec3 uColors[2];
2053
+ uniform float uOpacities[2];
2054
+ uniform sampler2D uTextures[2];
2055
+ uniform vec2 uLayerUvOffset[2];
2056
+ uniform vec2 uLayerUvScale[2];
2057
+ uniform int uReproject[2];
2058
+ uniform vec2 uReprojectTerrainLat[2];
2059
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
2060
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
2061
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
2062
+ uniform vec4 uReprojectMerc[2];
2063
+
2064
+
2065
+
2066
+
2067
+
2068
+
2069
+
2070
+ layout(location = 0) out vec4 colorBuffer;
2071
+ layout(location = 1) out vec4 attrBuffer;
2072
+ layout(location = 2) out vec4 normalBuffer;
2073
+
2074
+ void main() {
2075
+ vec4 compositedColor = vec4(0.0);
2076
+ int winningSlot = -1;
2077
+ float isWater = 0.0;
2078
+ float isTexturized = 0.0;
2079
+
2080
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
2081
+ bool hasHillshadeNormal = false;
2082
+
2083
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
2084
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
2085
+ // computed lazily on first use and reused by the rest (see reproject block).
2086
+ float gReprojMLat = 0.0;
2087
+ bool gReprojMLatReady = false;
2088
+
2089
+
2090
+ // ---- compact slot 0 (absolute slot 24, vector) ----
2091
+ if (uShows[0] == 1) {
2092
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
2093
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
2094
+ float inBounds0 = 1.0;
2095
+
2096
+ if (uReproject[0] == 1) {
2097
+ // Affine UV is correct in longitude but stretches latitude. Remap this
2098
+ // fragment's latitude through Mercator so it samples the source correctly.
2099
+ //
2100
+ // The only per-fragment transcendental left is mLat: it depends solely on
2101
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
2102
+ // identical for every reprojecting slot — computed once and reused. The
2103
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
2104
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
2105
+ if (!gReprojMLatReady) {
2106
+ float lat = uReprojectTerrainLat[0].x
2107
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
2108
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
2109
+ gReprojMLatReady = true;
2110
+ }
2111
+ float mRs = uReprojectMerc[0].x;
2112
+ float mDen = uReprojectMerc[0].y;
2113
+ // Only reproject when the source's Mercator span is wide enough to be
2114
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
2115
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
2116
+ // float32 and produces vertical-stretch noise. There the Mercator correction
2117
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
2118
+ if (abs(mDen) > 1e-3) {
2119
+ texUv0.y = (gReprojMLat - mRs) / mDen;
2120
+ }
2121
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
2122
+ // terrain reaches ±90°. When this slot is the band-edge source (its
2123
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
2124
+ // that edge texel so the last available source row stretches across the cap
2125
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2126
+ // the normal drop.
2127
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
2128
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
2129
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2130
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2131
+ // the sampler would smear the edge texel across the rest, so drop it there.
2132
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
2133
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
2134
+ }
2135
+
2136
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
2137
+
2138
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
2139
+ if (alpha0 > 0.01) {
2140
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
2141
+ winningSlot = 24;
2142
+ isTexturized = 1.0;
2143
+
2144
+ }
2145
+ }
2146
+
2147
+ // ---- compact slot 1 (absolute slot 25, vector) ----
2148
+ if (uShows[1] == 1) {
2149
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
2150
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
2151
+ float inBounds1 = 1.0;
2152
+
2153
+ if (uReproject[1] == 1) {
2154
+ // Affine UV is correct in longitude but stretches latitude. Remap this
2155
+ // fragment's latitude through Mercator so it samples the source correctly.
2156
+ //
2157
+ // The only per-fragment transcendental left is mLat: it depends solely on
2158
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
2159
+ // identical for every reprojecting slot — computed once and reused. The
2160
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
2161
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
2162
+ if (!gReprojMLatReady) {
2163
+ float lat = uReprojectTerrainLat[1].x
2164
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
2165
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
2166
+ gReprojMLatReady = true;
2167
+ }
2168
+ float mRs = uReprojectMerc[1].x;
2169
+ float mDen = uReprojectMerc[1].y;
2170
+ // Only reproject when the source's Mercator span is wide enough to be
2171
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
2172
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
2173
+ // float32 and produces vertical-stretch noise. There the Mercator correction
2174
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
2175
+ if (abs(mDen) > 1e-3) {
2176
+ texUv1.y = (gReprojMLat - mRs) / mDen;
2177
+ }
2178
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
2179
+ // terrain reaches ±90°. When this slot is the band-edge source (its
2180
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
2181
+ // that edge texel so the last available source row stretches across the cap
2182
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2183
+ // the normal drop.
2184
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
2185
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
2186
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2187
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2188
+ // the sampler would smear the edge texel across the rest, so drop it there.
2189
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
2190
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
2191
+ }
2192
+
2193
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
2194
+
2195
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
2196
+ if (alpha1 > 0.01) {
2197
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
2198
+ winningSlot = 25;
2199
+ isTexturized = 1.0;
2200
+
2201
+ }
2202
+ }
2203
+
2204
+
2205
+
2206
+ colorBuffer = compositedColor;
2207
+
2208
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
2209
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
2210
+ attrBuffer = vec4(
2211
+ isWater,
2212
+ isTexturized,
2213
+ 0.0,
2214
+ float(winningSlot + 1) / 255.0
2215
+ );
2216
+
2217
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
2218
+ }
2219
+ "
2220
+ `;
2221
+
2222
+ exports[`generateCompositeFragmentShader snapshots > water only 1`] = `
2223
+ "
2224
+ precision highp float;
2225
+ precision highp int;
2226
+
2227
+ #include <common>
2228
+
2229
+ varying vec2 vUv;
2230
+
2231
+
2232
+
2233
+ uniform int uShows[2];
2234
+ uniform vec3 uColors[2];
2235
+ uniform float uOpacities[2];
2236
+ uniform sampler2D uTextures[2];
2237
+ uniform vec2 uLayerUvOffset[2];
2238
+ uniform vec2 uLayerUvScale[2];
2239
+ uniform int uReproject[2];
2240
+ uniform vec2 uReprojectTerrainLat[2];
2241
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
2242
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
2243
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
2244
+ uniform vec4 uReprojectMerc[2];
2245
+ uniform bool uWaters[2];
2246
+
2247
+
2248
+
2249
+
2250
+
2251
+
2252
+ layout(location = 0) out vec4 colorBuffer;
2253
+ layout(location = 1) out vec4 attrBuffer;
2254
+ layout(location = 2) out vec4 normalBuffer;
2255
+
2256
+ void main() {
2257
+ vec4 compositedColor = vec4(0.0);
2258
+ int winningSlot = -1;
2259
+ float isWater = 0.0;
2260
+ float isTexturized = 0.0;
2261
+
2262
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
2263
+ bool hasHillshadeNormal = false;
2264
+
2265
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
2266
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
2267
+ // computed lazily on first use and reused by the rest (see reproject block).
2268
+ float gReprojMLat = 0.0;
2269
+ bool gReprojMLatReady = false;
2270
+
2271
+
2272
+ // ---- compact slot 0 (absolute slot 0, raster) ----
2273
+ if (uShows[0] == 1) {
2274
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
2275
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
2276
+ float inBounds0 = 1.0;
2277
+
2278
+ if (uReproject[0] == 1) {
2279
+ // Affine UV is correct in longitude but stretches latitude. Remap this
2280
+ // fragment's latitude through Mercator so it samples the source correctly.
2281
+ //
2282
+ // The only per-fragment transcendental left is mLat: it depends solely on
2283
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
2284
+ // identical for every reprojecting slot — computed once and reused. The
2285
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
2286
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
2287
+ if (!gReprojMLatReady) {
2288
+ float lat = uReprojectTerrainLat[0].x
2289
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
2290
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
2291
+ gReprojMLatReady = true;
2292
+ }
2293
+ float mRs = uReprojectMerc[0].x;
2294
+ float mDen = uReprojectMerc[0].y;
2295
+ // Only reproject when the source's Mercator span is wide enough to be
2296
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
2297
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
2298
+ // float32 and produces vertical-stretch noise. There the Mercator correction
2299
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
2300
+ if (abs(mDen) > 1e-3) {
2301
+ texUv0.y = (gReprojMLat - mRs) / mDen;
2302
+ }
2303
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
2304
+ // terrain reaches ±90°. When this slot is the band-edge source (its
2305
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
2306
+ // that edge texel so the last available source row stretches across the cap
2307
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2308
+ // the normal drop.
2309
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
2310
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
2311
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2312
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2313
+ // the sampler would smear the edge texel across the rest, so drop it there.
2314
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
2315
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
2316
+ }
2317
+
2318
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
2319
+
2320
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
2321
+ if (alpha0 > 0.01) {
2322
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
2323
+ winningSlot = 0;
2324
+ isTexturized = 0.0;
2325
+
2326
+ isWater = uWaters[0] ? 1.0 : 0.0;
2327
+ }
2328
+ }
2329
+
2330
+ // ---- compact slot 1 (absolute slot 1, raster) ----
2331
+ if (uShows[1] == 1) {
2332
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
2333
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
2334
+ float inBounds1 = 1.0;
2335
+
2336
+ if (uReproject[1] == 1) {
2337
+ // Affine UV is correct in longitude but stretches latitude. Remap this
2338
+ // fragment's latitude through Mercator so it samples the source correctly.
2339
+ //
2340
+ // The only per-fragment transcendental left is mLat: it depends solely on
2341
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
2342
+ // identical for every reprojecting slot — computed once and reused. The
2343
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
2344
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
2345
+ if (!gReprojMLatReady) {
2346
+ float lat = uReprojectTerrainLat[1].x
2347
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
2348
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
2349
+ gReprojMLatReady = true;
2350
+ }
2351
+ float mRs = uReprojectMerc[1].x;
2352
+ float mDen = uReprojectMerc[1].y;
2353
+ // Only reproject when the source's Mercator span is wide enough to be
2354
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
2355
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
2356
+ // float32 and produces vertical-stretch noise. There the Mercator correction
2357
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
2358
+ if (abs(mDen) > 1e-3) {
2359
+ texUv1.y = (gReprojMLat - mRs) / mDen;
2360
+ }
2361
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
2362
+ // terrain reaches ±90°. When this slot is the band-edge source (its
2363
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
2364
+ // that edge texel so the last available source row stretches across the cap
2365
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2366
+ // the normal drop.
2367
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
2368
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
2369
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2370
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2371
+ // the sampler would smear the edge texel across the rest, so drop it there.
2372
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
2373
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
2374
+ }
2375
+
2376
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
2377
+
2378
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
2379
+ if (alpha1 > 0.01) {
2380
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
2381
+ winningSlot = 1;
2382
+ isTexturized = 0.0;
2383
+
2384
+ isWater = uWaters[1] ? 1.0 : 0.0;
2385
+ }
2386
+ }
2387
+
2388
+
2389
+
2390
+ colorBuffer = compositedColor;
2391
+
2392
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
2393
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
2394
+ attrBuffer = vec4(
2395
+ isWater,
2396
+ isTexturized,
2397
+ 0.0,
2398
+ float(winningSlot + 1) / 255.0
2399
+ );
2400
+
2401
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
2402
+ }
2403
+ "
2404
+ `;
2405
+
2406
+ exports[`generateCompositeFragmentShader snapshots > watermask only feature, with slots 1`] = `
2407
+ "
2408
+ precision highp float;
2409
+ precision highp int;
2410
+
2411
+ #include <common>
2412
+
2413
+ varying vec2 vUv;
2414
+
2415
+
2416
+
2417
+ uniform int uShows[2];
2418
+ uniform vec3 uColors[2];
2419
+ uniform float uOpacities[2];
2420
+ uniform sampler2D uTextures[2];
2421
+ uniform vec2 uLayerUvOffset[2];
2422
+ uniform vec2 uLayerUvScale[2];
2423
+ uniform int uReproject[2];
2424
+ uniform vec2 uReprojectTerrainLat[2];
2425
+ // CPU-precomputed per-slot reprojection constants (see mutates.ts):
2426
+ // .xy = (mRs, mDen) — source tile's Mercator band start + span,
2427
+ // .z = clamp-to-top-edge flag, .w = clamp-to-bottom-edge flag (polar cap).
2428
+ uniform vec4 uReprojectMerc[2];
2429
+
2430
+
2431
+ uniform sampler2D uWatermask;
2432
+
2433
+
2434
+
2435
+
2436
+ layout(location = 0) out vec4 colorBuffer;
2437
+ layout(location = 1) out vec4 attrBuffer;
2438
+ layout(location = 2) out vec4 normalBuffer;
2439
+
2440
+ void main() {
2441
+ vec4 compositedColor = vec4(0.0);
2442
+ int winningSlot = -1;
2443
+ float isWater = 0.0;
2444
+ float isTexturized = 0.0;
2445
+
2446
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
2447
+ bool hasHillshadeNormal = false;
2448
+
2449
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
2450
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
2451
+ // computed lazily on first use and reused by the rest (see reproject block).
2452
+ float gReprojMLat = 0.0;
2453
+ bool gReprojMLatReady = false;
2454
+
2455
+
2456
+ // ---- compact slot 0 (absolute slot 0, raster) ----
2457
+ if (uShows[0] == 1) {
2458
+ vec2 texUv0 = vUv * uLayerUvScale[0] + uLayerUvOffset[0];
2459
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
2460
+ float inBounds0 = 1.0;
2461
+
2462
+ if (uReproject[0] == 1) {
2463
+ // Affine UV is correct in longitude but stretches latitude. Remap this
2464
+ // fragment's latitude through Mercator so it samples the source correctly.
2465
+ //
2466
+ // The only per-fragment transcendental left is mLat: it depends solely on
2467
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
2468
+ // identical for every reprojecting slot — computed once and reused. The
2469
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
2470
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
2471
+ if (!gReprojMLatReady) {
2472
+ float lat = uReprojectTerrainLat[0].x
2473
+ + vUv.y * (uReprojectTerrainLat[0].y - uReprojectTerrainLat[0].x);
2474
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
2475
+ gReprojMLatReady = true;
2476
+ }
2477
+ float mRs = uReprojectMerc[0].x;
2478
+ float mDen = uReprojectMerc[0].y;
2479
+ // Only reproject when the source's Mercator span is wide enough to be
2480
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
2481
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
2482
+ // float32 and produces vertical-stretch noise. There the Mercator correction
2483
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
2484
+ if (abs(mDen) > 1e-3) {
2485
+ texUv0.y = (gReprojMLat - mRs) / mDen;
2486
+ }
2487
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
2488
+ // terrain reaches ±90°. When this slot is the band-edge source (its
2489
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
2490
+ // that edge texel so the last available source row stretches across the cap
2491
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2492
+ // the normal drop.
2493
+ if (uReprojectMerc[0].z > 0.5) texUv0.y = min(texUv0.y, 1.0);
2494
+ if (uReprojectMerc[0].w > 0.5) texUv0.y = max(texUv0.y, 0.0);
2495
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2496
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2497
+ // the sampler would smear the edge texel across the rest, so drop it there.
2498
+ inBounds0 = step(0.0, texUv0.x) * step(texUv0.x, 1.0)
2499
+ * step(0.0, texUv0.y) * step(texUv0.y, 1.0);
2500
+ }
2501
+
2502
+ vec4 texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
2503
+
2504
+ float alpha0 = texColor0.a * uOpacities[0] * inBounds0;
2505
+ if (alpha0 > 0.01) {
2506
+ compositedColor = mix(compositedColor, vec4(texColor0.rgb, 1.0), alpha0);
2507
+ winningSlot = 0;
2508
+ isTexturized = 0.0;
2509
+
2510
+ }
2511
+ }
2512
+
2513
+ // ---- compact slot 1 (absolute slot 1, raster) ----
2514
+ if (uShows[1] == 1) {
2515
+ vec2 texUv1 = vUv * uLayerUvScale[1] + uLayerUvOffset[1];
2516
+ // 1.0 unless the reproject branch below confines this slot to a sub-rect.
2517
+ float inBounds1 = 1.0;
2518
+
2519
+ if (uReproject[1] == 1) {
2520
+ // Affine UV is correct in longitude but stretches latitude. Remap this
2521
+ // fragment's latitude through Mercator so it samples the source correctly.
2522
+ //
2523
+ // The only per-fragment transcendental left is mLat: it depends solely on
2524
+ // vUv.y and the (tile-wide, slot-invariant) terrain lat range, so it is
2525
+ // identical for every reprojecting slot — computed once and reused. The
2526
+ // per-slot source band (mRs, mDen) and polar-cap flags are constant per slot
2527
+ // and precomputed on the CPU (see mutates.ts), so no log/tan runs per slot.
2528
+ if (!gReprojMLatReady) {
2529
+ float lat = uReprojectTerrainLat[1].x
2530
+ + vUv.y * (uReprojectTerrainLat[1].y - uReprojectTerrainLat[1].x);
2531
+ gReprojMLat = log(tan(PI * 0.25 + lat * 0.5));
2532
+ gReprojMLatReady = true;
2533
+ }
2534
+ float mRs = uReprojectMerc[1].x;
2535
+ float mDen = uReprojectMerc[1].y;
2536
+ // Only reproject when the source's Mercator span is wide enough to be
2537
+ // numerically safe. For deep/upsampled tiles the span shrinks to ~1e-6 while
2538
+ // mLat/mRs are ~O(1), so (mLat-mRs)/(mRn-mRs) catastrophically cancels in
2539
+ // float32 and produces vertical-stretch noise. There the Mercator correction
2540
+ // is sub-pixel anyway, so the affine texUv.y is both correct and stable.
2541
+ if (abs(mDen) > 1e-3) {
2542
+ texUv1.y = (gReprojMLat - mRs) / mDen;
2543
+ }
2544
+ // Polar cap fill: WebMercator content stops at ±~85.05° but Geographic
2545
+ // terrain reaches ±90°. When this slot is the band-edge source (its
2546
+ // north/south edge sits on the WM limit), clamp the polar overshoot onto
2547
+ // that edge texel so the last available source row stretches across the cap
2548
+ // instead of being dropped by the in-bounds test below. Non-edge tiles keep
2549
+ // the normal drop.
2550
+ if (uReprojectMerc[1].z > 0.5) texUv1.y = min(texUv1.y, 1.0);
2551
+ if (uReprojectMerc[1].w > 0.5) texUv1.y = max(texUv1.y, 0.0);
2552
+ // Confine the slot to the sub-rect it actually covers. With N:M draping each
2553
+ // source tile covers only part of the terrain tile; outside its [0,1] range
2554
+ // the sampler would smear the edge texel across the rest, so drop it there.
2555
+ inBounds1 = step(0.0, texUv1.x) * step(texUv1.x, 1.0)
2556
+ * step(0.0, texUv1.y) * step(texUv1.y, 1.0);
2557
+ }
2558
+
2559
+ vec4 texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
2560
+
2561
+ float alpha1 = texColor1.a * uOpacities[1] * inBounds1;
2562
+ if (alpha1 > 0.01) {
2563
+ compositedColor = mix(compositedColor, vec4(texColor1.rgb, 1.0), alpha1);
2564
+ winningSlot = 1;
2565
+ isTexturized = 0.0;
2566
+
2567
+ }
2568
+ }
2569
+
2570
+
2571
+ // Quantized-mesh watermask: 0 = land, 255 = water (per spec). Takes priority
2572
+ // over per-slot uWaters — even if no winning slot flagged water, watermask
2573
+ // can still mark this pixel as water. Threshold at 0.5 (byte > 127).
2574
+ isWater = max(isWater, step(0.5, texture(uWatermask, vUv).r));
2575
+
2576
+ colorBuffer = compositedColor;
2577
+
2578
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
2579
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
2580
+ attrBuffer = vec4(
2581
+ isWater,
2582
+ isTexturized,
2583
+ 0.0,
2584
+ float(winningSlot + 1) / 255.0
2585
+ );
2586
+
2587
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
2588
+ }
2589
+ "
2590
+ `;
2591
+
2592
+ exports[`generateCompositeFragmentShader snapshots > watermask-only shader (no slots) 1`] = `
2593
+ "
2594
+ precision highp float;
2595
+ precision highp int;
2596
+
2597
+ #include <common>
2598
+
2599
+ varying vec2 vUv;
2600
+
2601
+
2602
+
2603
+ uniform sampler2D uWatermask;
2604
+
2605
+
2606
+
2607
+
2608
+ layout(location = 0) out vec4 colorBuffer;
2609
+ layout(location = 1) out vec4 attrBuffer;
2610
+ layout(location = 2) out vec4 normalBuffer;
2611
+
2612
+ void main() {
2613
+ vec4 compositedColor = vec4(0.0);
2614
+ int winningSlot = -1;
2615
+ float isWater = 0.0;
2616
+ float isTexturized = 0.0;
2617
+
2618
+ vec3 hillshadeNormal = vec3(0.5, 0.5, 1.0); // neutral upward
2619
+ bool hasHillshadeNormal = false;
2620
+
2621
+ // Shared WebMercator reprojection latitude for this fragment. The terrain lat
2622
+ // range is tile-wide, so mLat is identical across every reprojecting slot —
2623
+ // computed lazily on first use and reused by the rest (see reproject block).
2624
+ float gReprojMLat = 0.0;
2625
+ bool gReprojMLatReady = false;
2626
+
2627
+
2628
+
2629
+
2630
+ // Quantized-mesh watermask: 0 = land, 255 = water (per spec). Takes priority
2631
+ // over per-slot uWaters — even if no winning slot flagged water, watermask
2632
+ // can still mark this pixel as water. Threshold at 0.5 (byte > 127).
2633
+ isWater = max(isWater, step(0.5, texture(uWatermask, vUv).r));
2634
+
2635
+ colorBuffer = compositedColor;
2636
+
2637
+ // Attr.a packs the winning slot index as (idx+1)/255 — 0 means no winner.
2638
+ // Main shader: int winIdx = int(round(attr.a * 255.0)) - 1.
2639
+ attrBuffer = vec4(
2640
+ isWater,
2641
+ isTexturized,
2642
+ 0.0,
2643
+ float(winningSlot + 1) / 255.0
2644
+ );
2645
+
2646
+ normalBuffer = vec4(hillshadeNormal, hasHillshadeNormal ? 1.0 : 0.0);
2647
+ }
2648
+ "
2649
+ `;