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