├── .babelrc ├── .browserslistrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── README.md ├── dist ├── index.d.ts ├── index.js └── mtp.min.js ├── examples ├── 3d-gs-multi-splats.html ├── 3d-gs-multi-splats.js ├── 3d-gs-ply.html ├── 3d-gs-ply.js ├── 3d-gs-splat.html ├── 3d-gs-splat.js ├── 3d-gs-splat.png ├── 3d-tiles-3dgs.html ├── 3d-tiles-3dgs.js ├── 3d-tiles-3dgs.png ├── 3d-tiles-cesium-ion.html ├── 3d-tiles-cesium-ion.js ├── 3d-tiles-cesium-ion.png ├── 3d-tiles-osgb.html ├── 3d-tiles-osgb.js ├── 3d-tiles-osgb.png ├── 3d-tiles-shadow.html ├── 3d-tiles-shadow.js ├── 3d-tiles.html ├── 3d-tiles.js ├── 3d-tiles.png ├── assets │ ├── 34M_17 │ │ ├── 34M_17.bin │ │ ├── 34M_17.gltf │ │ ├── base_AO.png │ │ ├── frame_AO.png │ │ ├── stairs_plt_AO.png │ │ ├── truss_2_AO.png │ │ ├── truss_dish_AO.jpg │ │ └── wheels_AO.png │ └── icon │ │ └── camera.png ├── billboard.html ├── billboard.js ├── billboard.png ├── config.js ├── div-icon.html ├── div-icon.js ├── div-icon.png ├── heat-map.html ├── heat-map.js ├── heat-map.png ├── index.html ├── index.js ├── index.png ├── point-collection.html ├── point-collection.js ├── point-collection.png ├── point.html ├── point.js ├── point.png ├── shadow.html ├── shadow.js ├── src │ ├── index.js │ ├── modules │ │ ├── extensions │ │ │ ├── gaussian_splatting │ │ │ │ ├── GLTFGaussianSplattingExtension.js │ │ │ │ ├── GLTFSpzGaussianSplattingExtension.js │ │ │ │ ├── GaussianSplattingTilesetPlugin.js │ │ │ │ ├── SortScheduler.js │ │ │ │ ├── Splat.js │ │ │ │ └── SplatMesh.js │ │ │ ├── gltf │ │ │ │ └── GLTFKtx2TextureInspectorPlugin.js │ │ │ └── index.js │ │ ├── heat-map │ │ │ └── HeatMap.js │ │ ├── index.js │ │ ├── loaders │ │ │ ├── ModelLoader.js │ │ │ ├── PlyLoader.js │ │ │ ├── SplatLoader.js │ │ │ └── index.js │ │ ├── material │ │ │ ├── MaterialCache.js │ │ │ ├── index.js │ │ │ └── types │ │ │ │ ├── BillboardMaterial.js │ │ │ │ ├── HeatMapMaterial.js │ │ │ │ └── PointMaterial.js │ │ ├── overlay │ │ │ ├── Overlay.js │ │ │ ├── index.js │ │ │ └── types │ │ │ │ ├── Billboard.js │ │ │ │ ├── Circle.js │ │ │ │ ├── DivIcon.js │ │ │ │ ├── Model.js │ │ │ │ ├── Point.js │ │ │ │ ├── PointCollection.js │ │ │ │ ├── Polygon.js │ │ │ │ ├── Polyline.js │ │ │ │ ├── Splat.js │ │ │ │ └── Tileset.js │ │ ├── shaders │ │ │ ├── gaussian_splatting_fs_glsl.js │ │ │ ├── gaussian_splatting_vs_glsl.js │ │ │ ├── heat_map_fs_glsl.js │ │ │ ├── heat_map_vs_glsl.js │ │ │ ├── point_fs.glsl.js │ │ │ └── point_vs.glsl.js │ │ ├── tasks │ │ │ ├── WasmTaskProcessor.js │ │ │ └── WorkerTaskProcessor.js │ │ ├── utils │ │ │ ├── Util.js │ │ │ └── index.js │ │ └── workers │ │ │ ├── SplatSortWorker.js │ │ │ ├── WasmWorker.js │ │ │ └── WorkerPool.js │ └── wasm │ │ └── splats │ │ ├── wasm_splats.min.js │ │ └── wasm_splats_bg.wasm ├── sun-light.html ├── sun-light.js ├── sun-light.png ├── sun-shadow.html └── sun-shadow.js ├── gulpfile.js ├── package.json ├── src ├── index.ts └── modules │ ├── camera │ └── CameraSync.ts │ ├── constants │ └── index.ts │ ├── creator │ └── Creator.ts │ ├── index.ts │ ├── layer │ └── ThreeLayer.ts │ ├── scene │ └── MapScene.ts │ ├── sun │ └── Sun.ts │ ├── transform │ └── SceneTransform.ts │ └── utils │ ├── SunCalc.ts │ └── Util.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": ["> 1%", "last 2 versions", "ie >= 10"] 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": ["@babel/plugin-transform-runtime","@babel/plugin-proposal-class-properties"] 14 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /libs/ 2 | /web/ 3 | /pack/ 4 | dist/* 5 | /**/*.ts -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@babel/eslint-parser", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "es6": true, 9 | "node": true, 10 | "browser": true 11 | }, 12 | "plugins": ["prettier"], 13 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 14 | "globals": { 15 | }, 16 | "rules": { 17 | "global-require": 0, 18 | "indent": 0, 19 | "no-new": 0, 20 | "camelcase": 0, 21 | "padded-blocks": 0, 22 | "no-unused-vars": 0, 23 | "no-trailing-spaces": 0, 24 | "no-mixed-spaces-and-tabs": 0, 25 | "space-before-function-paren": [0, "always"], 26 | "no-multiple-empty-lines": 0, 27 | "no-prototype-builtins": 0, 28 | "no-loss-of-precision":0 29 | } 30 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | yarn.lock 6 | web/ 7 | pack/ 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | package-lock.json 15 | .DS_Store 16 | .history 17 | */config.js -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "singleQuote": true, 4 | "semi": false 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # maplibre-three-plugin 2 | 3 | `maplibre-three-plugin` is a bridge plugin that cleverly 4 | connects [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) with [Three.js](https://threejs.org/), enabling 5 | developers to implement 3D rendering and animation on maps. 6 | 7 | ## Install 8 | 9 | ```shell 10 | npm install @dvt3d/maplibre-three-plugin 11 | ---------------------------------------- 12 | yarn add @dvt3d/maplibre-three-plugin 13 | ``` 14 | 15 | ## Quickly Start 16 | 17 | `maplibre-three-plugin` depends on three, please make sure three is installed before using it. 18 | 19 | ```html 20 | 21 |
22 | ``` 23 | 24 | ```javascript 25 | 26 | import maplibregl from 'maplibre-gl' 27 | import * as THREE from 'three' 28 | import { GLTFLoader } from 'three/addons' 29 | import * as MTP from '@dvt3d/maplibre-three-plugin' 30 | 31 | const map = new maplibregl.Map({ 32 | container: 'map-container', // container id 33 | style: 'https://api.maptiler.com/maps/basic-v2/style.json?key=get_access_key', 34 | zoom: 18, 35 | center: [148.9819, -35.3981], 36 | pitch: 60, 37 | canvasContextAttributes: { antialias: true }, 38 | maxPitch: 85, 39 | }) 40 | 41 | //init three scene 42 | const mapScene = new MTP.MapScene(map) 43 | 44 | //add light 45 | mapScene.addLight(new THREE.AmbientLight()) 46 | 47 | // add model 48 | const glTFLoader = new GLTFLoader() 49 | 50 | glTFLoader.load('./assets/34M_17/34M_17.gltf', (gltf) => { 51 | let rtcGroup = MTP.Creator.createRTCGroup([148.9819, -35.39847]) 52 | rtcGroup.add(gltf.scene) 53 | mapScene.addObject(rtcGroup) 54 | }) 55 | ``` 56 | 57 | ## Examples 58 | 59 | | ![pic](./examples/index.png) | ![pic](./examples/sun-light.png) | ![pic](./examples/point.png) | ![pic](./examples/point-collection.png) | 60 | |:--------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------:| 61 | | [model](https://dvt3d.github.io/maplibre-three-plugin/examples/index.html) | [sun-light](https://dvt3d.github.io/maplibre-three-plugin/examples/sun-light.html) | [point](https://dvt3d.github.io/maplibre-three-plugin/examples/point.html) | [point-collection](https://dvt3d.github.io/maplibre-three-plugin/examples/point-collection.html) | 62 | | ![pic](./examples/billboard.png) | ![pic](./examples/div-icon.png) | ![pic](./examples/3d-tiles.png) | ![pic](./examples/3d-tiles-osgb.png) | 63 | | [billboard](https://dvt3d.github.io/maplibre-three-plugin/examples/billboard.html) | [div-icon](https://dvt3d.github.io/maplibre-three-plugin/examples/div-icon.html) | [3d-tiles](https://dvt3d.github.io/maplibre-three-plugin/examples/3d-tiles.html) | [3d-tiles-osgb](https://dvt3d.github.io/maplibre-three-plugin/examples/3d-tiles-osgb.html) | 64 | | ![pic](./examples/3d-tiles-cesium-ion.png) | ![pic](./examples/3d-tiles-3dgs.png) | ![pic](./examples/3d-gs-splat.png) | ![pic](./examples/heat-map.png) | 65 | | [3d-tiles-cesium](https://dvt3d.github.io/maplibre-three-plugin/examples/3d-tiles-cesium-ion.html) | [3d-tiles-3dgs](https://dvt3d.github.io/maplibre-three-plugin/examples/3d-tiles-3dgs.html) | [3d-gs-splat](https://dvt3d.github.io/maplibre-three-plugin/examples/3d-gs-splat.html) | [heat-map](https://dvt3d.github.io/maplibre-three-plugin/examples/heat-map.html) | 66 | 67 | ## Docs 68 | 69 | ### MapScene 70 | 71 | #### examples 72 | 73 | ```js 74 | const mapScene = new MapScene(map) 75 | ``` 76 | 77 | #### creation 78 | 79 | - constructor(map,[options]) 80 | - params 81 | - `{Map} map ` : map instance 82 | - `{Object} options ` : config 83 | 84 | ```js 85 | // config 86 | Object({ 87 | scene: null, //THREE.Scene,if not passed in, the default scene will be used 88 | camera: null, //THREE.Camera, if not passed in, the default camera will be used 89 | renderer: null, //THREE.WebGLRenderer if not passed in, the default renderer will be used 90 | preserveDrawingBuffer: false, 91 | renderLoop: (ins) => { 92 | } //Frame animation rendering function, if not passed in, the default function will be used,the params is an instance for MapScene 93 | }) 94 | ``` 95 | 96 | #### event hooks 97 | 98 | - `preReset` : A hook that calls `renderer.resetState` before each animation frame 99 | - `postReset`: A hook that calls `renderer.resetState` after each animation frame 100 | - `preRender`: A hook that calls `renderer.render` before each animation frame 101 | - `postRender`: A hook that calls `renderer.render` after each animation frame 102 | 103 | #### properties 104 | 105 | - `{maplibregl.Map} map ` : `readonly` 106 | - `{HTMLCanvasElement} canvas ` : `readonly` 107 | - `{THREE.Camera} camera `: `readonly` 108 | - `{THREE.Sence} scene` : `readonly` 109 | - `{THREE.Group} lights`: `readonly` 110 | - `{THREE.Group} world` : `readonly` 111 | - `{THREE.WebGLRenderer} renderer` : `readonly` 112 | 113 | #### methods 114 | 115 | - **_addLight(light)_** 116 | 117 | Add light to the scene, support custom light objects, but the custom light objects need to support the `delegate` 118 | property, and the `delegate` type is `THREE.Object3D` 119 | - params 120 | - `{THREE.Object3D | Sun | CustomLight } light ` 121 | - returns 122 | - `this` 123 | 124 | - **_removeLight(light)_** 125 | 126 | Remove light from the scene 127 | 128 | - params 129 | - `{THREE.Object3D | Sun | CustomLight } light ` 130 | - returns 131 | - `this` 132 | 133 | - **_addObject(object)_** 134 | 135 | Add an object to world,support custom object, but the custom object need to support the `delegate` property, and the 136 | `delegate` type is `THREE.Object3D` 137 | 138 | - params 139 | - `{THREE.Object3D | CustomObject} object ` 140 | - returns 141 | - `this` 142 | - **_removeObject(object)_** 143 | 144 | Remove an object from world 145 | 146 | - params 147 | - `{THREE.Object3D | CustomObject} object ` 148 | - returns 149 | - `this` 150 | 151 | - **_flyTo(target,[completed],[duration])_** 152 | 153 | Fly the map to the provided target over a period of time, the completion callback will be triggered when the flight is 154 | complete, the target needs to contain the `position` property 155 | 156 | - params 157 | - `{THREE.Object3D | CustomObject} target ` 158 | - `{Function} completed `: 159 | - `{Number} duration `: 160 | - returns 161 | - `this` 162 | 163 | - **_zoomTo(target,[completed])_** 164 | 165 | Zoom the map to the provided target 166 | 167 | - params 168 | - `{Ojbect} target ` 169 | - `{Function} completed `: 170 | - returns 171 | - `this` 172 | 173 | - **_on(type,callback)_** 174 | - params 175 | - `{String} type ` 176 | - `{Function} callback `: 177 | - returns 178 | - `this` 179 | 180 | - **_off(type,callback)_** 181 | - params 182 | - `{String} type ` 183 | - `{Function} callback `: 184 | - returns 185 | - `this` 186 | 187 | ### SceneTransform 188 | 189 | #### examples 190 | 191 | ```js 192 | const scale = new SceneTransform.projectedUnitsPerMeter(24) 193 | ``` 194 | 195 | #### static methods 196 | 197 | - **_projectedMercatorUnitsPerMeter()_** 198 | - params 199 | - returns 200 | - `{Number} value` 201 | 202 | - **_projectedUnitsPerMeter(lat)_** 203 | - params 204 | - `{Number} lat ` 205 | - returns 206 | - `{Number} value` 207 | 208 | - **_lngLatToVector3(lng, [lat], [alt] )_** 209 | - params 210 | - `{Array | Number} lng ` 211 | - `{ Number} lat ` 212 | - `{ Number} alt ` 213 | - returns 214 | - `{THREE.Vector3} v` 215 | 216 | - **_vector3ToLngLat(v)_** 217 | - params 218 | - `{THREE.Vector3} v` 219 | - returns 220 | - `{Array} value` 221 | 222 | ### Sun 223 | 224 | #### examples 225 | 226 | ```js 227 | const sun = new Sun() 228 | ``` 229 | 230 | #### creation 231 | 232 | - constructor() 233 | - params 234 | 235 | #### properties 236 | 237 | - `{THREE.Group} delegate ` : `readonly` 238 | - `{Boolean} castShadow ` 239 | - `{Date || String} currentTime ` 240 | - `{THREE.DirectionalLight} sunLight` : `readonly` 241 | - `{THREE.HemisphereLight} hemiLight`: `readonly` 242 | 243 | #### methods 244 | 245 | - **_update(frameState)_** 246 | - params 247 | - `{Object} frameState`: 248 | - returns 249 | - `this` 250 | 251 | ### Creator 252 | 253 | #### examples 254 | 255 | ```js 256 | const rtcGroup = Creator.createRTCGroup([-1000, 0, 0]) 257 | ``` 258 | 259 | #### static methods 260 | 261 | - **_createRTCGroup(center, [rotation], [scale])_** 262 | - params 263 | - `{Array} center` 264 | - `{Array} rotation`: default value is [0,0,0] 265 | - `{Array} scale`: scale corresponding to the current latitude 266 | - returns 267 | - `{THREE.Group} rtc` 268 | 269 | - **_createMercatorRTCGroup(center, [rotation], [scale])_** 270 | - params 271 | - `{Array} center` 272 | - `{Array} rotation`: default value is [0,0,0] 273 | - `{Array} scale`: scale corresponding to the current latitude 274 | - returns 275 | - `{THREE.Group} rtc` 276 | 277 | - **_createShadowGround(center, [width], [height])_** 278 | - params 279 | - `{THREE.Vector3} center` 280 | - `{Number} width`: default value is 100 281 | - `{Number} height` : default value is 100 282 | - returns 283 | - `{Object} rtc` 284 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | import { Scene, PerspectiveCamera, WebGLRenderer, Group, Light, Object3D, Vector3, DirectionalLight, HemisphereLight, Mesh } from 'three'; 3 | 4 | interface IMap { 5 | transform: any; 6 | on(type: string, listener: () => any): any; 7 | getCanvas(): HTMLCanvasElement; 8 | getLayer(id: string): any; 9 | addLayer(options: any): any; 10 | getCenter(): { 11 | lng: number; 12 | lat: number; 13 | }; 14 | once(type: string, completed: any): void; 15 | flyTo(param: { 16 | center: any[]; 17 | zoom: number; 18 | bearing: number; 19 | pitch: number; 20 | duration: number; 21 | }): void; 22 | } 23 | /** 24 | * Configuration options for initializing a MapScene 25 | */ 26 | interface IMapSceneOptions { 27 | /** Existing Three.js Scene instance (optional) */ 28 | scene: null | Scene; 29 | /** Existing Three.js PerspectiveCamera instance (optional) */ 30 | camera: null | PerspectiveCamera; 31 | /** Existing Three.js WebGLRenderer instance (optional) */ 32 | renderer: null | WebGLRenderer; 33 | /** Custom render loop function (optional) */ 34 | renderLoop: null | ((mapScene: MapScene) => void); 35 | /** Whether to preserve the drawing buffer (optional) */ 36 | preserveDrawingBuffer: boolean; 37 | } 38 | /** 39 | * Frame state information passed to event listeners 40 | */ 41 | interface IFrameState { 42 | /** Current map center coordinates */ 43 | center: { 44 | lng: number; 45 | lat: number; 46 | }; 47 | /** Three.js Scene instance */ 48 | scene: Scene; 49 | /** Three.js PerspectiveCamera instance */ 50 | camera: PerspectiveCamera; 51 | /** Three.js WebGLRenderer instance */ 52 | renderer: WebGLRenderer; 53 | } 54 | /** 55 | * Extended Three.js Light interface with optional delegate 56 | */ 57 | interface ILight extends Light { 58 | /** Optional delegate light source */ 59 | delegate?: Light; 60 | } 61 | /** 62 | * Extended Three.js Object3D interface with optional delegate and size 63 | */ 64 | interface IObject3D { 65 | /** Optional delegate object */ 66 | delegate: Object3D; 67 | /** Optional size vector */ 68 | size?: Vector3; 69 | } 70 | declare class MapScene { 71 | private readonly _map; 72 | private _options; 73 | private readonly _canvas; 74 | private readonly _scene; 75 | private readonly _camera; 76 | private readonly _renderer; 77 | private readonly _lights; 78 | private readonly _world; 79 | private _event; 80 | constructor(map: IMap, options?: Partial); 81 | get map(): IMap; 82 | get canvas(): HTMLCanvasElement; 83 | get camera(): PerspectiveCamera; 84 | get scene(): Scene; 85 | get lights(): Group; 86 | get world(): Group; 87 | get renderer(): WebGLRenderer; 88 | /** 89 | * 90 | * @private 91 | */ 92 | _onMapRender(): void; 93 | /** 94 | * 95 | * @returns {MapScene} 96 | */ 97 | render(): MapScene; 98 | /** 99 | * 100 | * @param light 101 | * @returns {MapScene} 102 | */ 103 | addLight(light: ILight): MapScene; 104 | /** 105 | * 106 | * @param light 107 | */ 108 | removeLight(light: ILight): this; 109 | /** 110 | * 111 | * @param object 112 | * @returns {MapScene} 113 | */ 114 | addObject(object: IObject3D | Object3D): MapScene; 115 | /** 116 | * 117 | * @param object 118 | * @returns {MapScene} 119 | */ 120 | removeObject(object: IObject3D | Object3D): MapScene; 121 | /** 122 | * 123 | * @returns {{position: *[], heading: *, pitch}} 124 | */ 125 | getViewPosition(): { 126 | position: number[]; 127 | heading: number; 128 | pitch: number; 129 | }; 130 | /** 131 | * 132 | * @param target 133 | * @param completed 134 | * @param duration 135 | * @returns {MapScene} 136 | */ 137 | flyTo(target: { 138 | position: { 139 | x: number; 140 | y: number; 141 | z: number; 142 | }; 143 | size?: any; 144 | delegate?: any; 145 | }, duration?: number, completed?: () => void): MapScene; 146 | /** 147 | * 148 | * @param target 149 | * @param completed 150 | * @returns {MapScene} 151 | */ 152 | zoomTo(target: { 153 | position: { 154 | x: number; 155 | y: number; 156 | z: number; 157 | }; 158 | size?: any; 159 | delegate?: any; 160 | }, completed?: () => void): MapScene; 161 | /** 162 | * 163 | * @returns {MapScene} 164 | */ 165 | flyToPosition(position: number[], hpr?: number[], completed?: () => void, duration?: number): MapScene; 166 | /** 167 | * 168 | * @returns {MapScene} 169 | */ 170 | zoomToPosition(position: any, hpr?: number[], completed?: () => void): MapScene; 171 | /** 172 | * 173 | * @param type 174 | * @param callback 175 | * @returns {MapScene} 176 | */ 177 | on(type: string, callback: (event: { 178 | frameState: IFrameState; 179 | }) => void): MapScene; 180 | /** 181 | * 182 | * @param type 183 | * @param callback 184 | * @returns {MapScene} 185 | */ 186 | off(type: string, callback: () => void): MapScene; 187 | } 188 | 189 | declare class SceneTransform { 190 | /** 191 | * 192 | * @returns {number} 193 | */ 194 | static projectedMercatorUnitsPerMeter(): number; 195 | /** 196 | * 197 | * @param lat 198 | * @returns {number} 199 | */ 200 | static projectedUnitsPerMeter(lat: number): number; 201 | /** 202 | * 203 | * @param lng 204 | * @param lat 205 | * @param alt 206 | * @returns {Vector3} 207 | */ 208 | static lngLatToVector3(lng: number | number[], lat?: number, alt?: number): Vector3; 209 | /** 210 | * 211 | * @param v 212 | * @returns {number[]} 213 | */ 214 | static vector3ToLngLat(v: { 215 | x: number; 216 | y: number; 217 | z: number; 218 | }): number[]; 219 | } 220 | 221 | interface ShadowOptions { 222 | /** Blur radius for shadow edges */ 223 | radius: number; 224 | /** Width and height of the shadow map */ 225 | mapSize: [number, number]; 226 | /** Top and right boundaries of the shadow camera frustum */ 227 | topRight: number; 228 | /** Bottom and left boundaries of the shadow camera frustum */ 229 | bottomLeft: number; 230 | /** Near clipping plane of the shadow camera */ 231 | near: number; 232 | /** Far clipping plane of the shadow camera */ 233 | far: number; 234 | } 235 | /** 236 | * 237 | */ 238 | declare class Sun { 239 | private readonly _delegate; 240 | private readonly _sunLight; 241 | private readonly _hemiLight; 242 | private _currentTime; 243 | constructor(); 244 | get delegate(): Group; 245 | set castShadow(castShadow: boolean); 246 | get castShadow(): boolean; 247 | set currentTime(currentTime: string | number | Date); 248 | get currentTime(): string | number | Date; 249 | get sunLight(): DirectionalLight; 250 | get hemiLight(): HemisphereLight; 251 | /** 252 | * 253 | * @param shadow 254 | * @returns {Sun} 255 | */ 256 | setShadow(shadow?: Partial): Sun; 257 | /** 258 | * 259 | * @param frameState 260 | */ 261 | update(frameState: IFrameState): void; 262 | } 263 | 264 | /** 265 | * @Author: Caven Chen 266 | */ 267 | 268 | declare class Creator { 269 | /** 270 | * 271 | * @param center 272 | * @param rotation 273 | * @param scale 274 | */ 275 | static createRTCGroup(center: number | number[], rotation: number[], scale: number[]): Group; 276 | /** 277 | * 278 | * @param center 279 | * @param rotation 280 | * @param scale 281 | */ 282 | static createMercatorRTCGroup(center: number | number[], rotation: number[], scale: number[]): Group; 283 | /** 284 | * 285 | * @param center 286 | * @param width 287 | * @param height 288 | * @returns {Mesh} 289 | */ 290 | static createShadowGround(center: number | number[], width?: number, height?: number): Mesh; 291 | } 292 | 293 | export { Creator, MapScene, SceneTransform, Sun }; 294 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import{Group as re,PerspectiveCamera as Ie,Scene as De,WebGLRenderer as Te,EventDispatcher as Re,Box3 as xe,Vector3 as Ce}from"three";var R=63710088e-1,C=2*Math.PI*R,l=Math.PI/180,Ve=180/Math.PI,P=1024e3/C,K=512;var Z=class{static clamp(e,t,r){return Math.min(r,Math.max(t,e))}static makePerspectiveMatrix(e,t,r,n){let s=1/Math.tan(e/2),i=1/(r-n);return[s/t,0,0,0,0,s,0,0,0,0,(n+r)*i,-1,0,0,2*n*r*i,0]}static mercatorXFromLng(e){return(180+e)/360}static mercatorYFromLat(e){return(180-180/Math.PI*Math.log(Math.tan(Math.PI/4+e*Math.PI/360)))/360}static getViewInfo(e,t,r){let n=e.fov*l,s=e.pitch*l,i=null;if(Array.isArray(t)&&(i={lng:t[0],lat:t[1],alt:t[2]||0}),typeof t=="string"){let p=t.split(",");i={lng:+p[0],lat:+p[1],alt:+p[2]||0}}let c=Math.max(r.x,r.y,r.z)/(2*Math.tan(n/2))*Math.cos(s)+i.alt,h=Math.abs(Math.cos(s)*e.cameraToCenterDistance),b=C*Math.abs(Math.cos(i.lat*l)),g=h/c*b,f=Math.round(Math.log2(g/e.tileSize));return{center:[i.lng,i.lat],cameraHeight:c,zoom:f}}static getHeightByZoom(e,t,r,n){let s=Math.abs(Math.cos(n*l)*e.cameraToCenterDistance),i=C*Math.abs(Math.cos(r*l)),o=Math.pow(2,t)*e.tileSize;return s*i/o}static getZoomByHeight(e,t,r,n){let s=Math.abs(Math.cos(n*l)*e.cameraToCenterDistance),i=C*Math.abs(Math.cos(r*l)),o=s/t*i;return Math.round(Math.log2(o/e.tileSize))}},I=Z;import{Matrix4 as y,Vector3 as ve}from"three";var Q=new y,$=new y,ee=85.051129,F=class{_map;_world;_camera;_translateCenter;_worldSizeRatio;constructor(e,t,r){this._map=e,this._world=t,this._camera=r,this._translateCenter=new y().makeTranslation(1024e3/2,-1024e3/2,0),this._worldSizeRatio=K/1024e3,this._map.on("move",()=>{this.syncCamera(!1)}),this._map.on("resize",()=>{this.syncCamera(!0)})}syncCamera(e){let t=this._map.transform,r=t.pitch*l,n=t.bearing*l;if(e){let f=t.fov*l,p=t.centerOffset||new ve;this._camera.aspect=t.width/t.height,Q.elements=I.makePerspectiveMatrix(f,this._camera.aspect,t.height/50,t.farZ),this._camera.projectionMatrix=Q,this._camera.projectionMatrix.elements[8]=-p.x*2/t.width,this._camera.projectionMatrix.elements[9]=p.y*2/t.height}$.makeTranslation(0,0,t.cameraToCenterDistance);let s=new y().premultiply($).premultiply(new y().makeRotationX(r)).premultiply(new y().makeRotationZ(-n));t.elevation&&(s.elements[14]=t.cameraToCenterDistance*Math.cos(r)),this._camera.matrixWorld.copy(s);let i=t.scale*this._worldSizeRatio,o=new y().makeScale(i,i,i),c=t.x,h=t.y;if(!c||!h){let f=t.center,p=I.clamp(f.lat,-ee,ee);c=I.mercatorXFromLng(f.lng)*t.worldSize,h=I.mercatorYFromLat(p)*t.worldSize}let b=new y().makeTranslation(-c,h,0),g=new y().makeRotationZ(Math.PI);this._world.matrix=new y().premultiply(g).premultiply(this._translateCenter).premultiply(o).premultiply(b)}},te=F;var k=class{_id;_mapScene;_cameraSync;constructor(e,t){this._id=e,this._mapScene=t,this._cameraSync=new te(this._mapScene.map,this._mapScene.world,this._mapScene.camera)}get id(){return this._id}get type(){return"custom"}get renderingMode(){return"3d"}onAdd(){this._cameraSync.syncCamera(!0)}render(){this._mapScene.render()}onRemove(){this._cameraSync=null,this._mapScene=null}},ne=k;import{Vector3 as Le}from"three";var J=class{static projectedMercatorUnitsPerMeter(){return this.projectedUnitsPerMeter(0)}static projectedUnitsPerMeter(e){return Math.abs(1024e3/Math.cos(l*e)/C)}static lngLatToVector3(e,t,r){let n=[0,0,0];return Array.isArray(e)?(n=[-R*l*e[0]*P,-R*Math.log(Math.tan(Math.PI*.25+.5*l*e[1]))*P],e[2]?n.push(e[2]*this.projectedUnitsPerMeter(e[1])):n.push(0)):(n=[-R*l*e*P,-R*Math.log(Math.tan(Math.PI*.25+.5*l*(t||0)))*P],r?n.push(r*this.projectedUnitsPerMeter(t||0)):n.push(0)),new Le(n[0],n[1],n[2])}static vector3ToLngLat(e){let t=[0,0,0];return e&&(t[0]=-e.x/(R*l*P),t[1]=2*(Math.atan(Math.exp(e.y/(P*-R)))-Math.PI/4)/l,t[2]=e.z/this.projectedUnitsPerMeter(t[1])),t}},w=J;var Pe={scene:null,camera:null,renderer:null,renderLoop:null,preserveDrawingBuffer:!1},O=class{_map;_options;_canvas;_scene;_camera;_renderer;_lights;_world;_event;constructor(e,t={}){if(!e)throw"missing map";this._map=e,this._options={...Pe,...t},this._canvas=e.getCanvas(),this._scene=this._options.scene||new De,this._camera=this._options.camera||new Ie(this._map.transform.fov,this._map.transform.width/this._map.transform.height,.001,1e21),this._camera.matrixAutoUpdate=!1,this._renderer=this._options.renderer||new Te({alpha:!0,antialias:!0,preserveDrawingBuffer:this._options.preserveDrawingBuffer,canvas:this._canvas,context:this._canvas.getContext("webgl2")}),this._renderer.setPixelRatio(window.devicePixelRatio),this._renderer.setSize(this._canvas.clientWidth,this._canvas.clientHeight),this._renderer.autoClear=!1,this._lights=new re,this._lights.name="lights",this._scene.add(this._lights),this._world=new re,this._world.name="world",this._world.userData={isWorld:!0,name:"world"},this._world.position.set(1024e3/2,1024e3/2,0),this._world.matrixAutoUpdate=!1,this._scene.add(this._world),this._map.on("render",this._onMapRender.bind(this)),this._event=new Re}get map(){return this._map}get canvas(){return this._canvas}get camera(){return this._camera}get scene(){return this._scene}get lights(){return this._lights}get world(){return this._world}get renderer(){return this._renderer}_onMapRender(){this._map.getLayer("map_scene_layer")||this._map.addLayer(new ne("map_scene_layer",this))}render(){if(this._options.renderLoop)this._options.renderLoop(this);else{let e={center:this._map.getCenter(),scene:this._scene,camera:this._camera,renderer:this._renderer};this._event.dispatchEvent({type:"preReset",frameState:e}),this.renderer.resetState(),this._event.dispatchEvent({type:"postReset",frameState:e}),this._event.dispatchEvent({type:"preRender",frameState:e}),this.renderer.render(this._scene,this._camera),this._event.dispatchEvent({type:"postRender",frameState:e})}return this}addLight(e){return this._lights.add(e.delegate||e),this}removeLight(e){return this._lights.remove(e.delegate||e),this}addObject(e){let t="delegate"in e?e.delegate:e;return this._world.add(t),this}removeObject(e){let t="delegate"in e?e.delegate:e;return this._world.remove(t),t.traverse(r=>{r.geometry&&r.geometry.dispose(),r.material&&(Array.isArray(r.material)?r.material.forEach(n=>n.dispose()):r.material.dispose()),r.texture&&r.texture.dispose()}),this}getViewPosition(){let e=this._map.transform,t=e.center;return{position:[t.lng,t.lat,I.getHeightByZoom(e,e.zoom,t.lat,e.pitch)],heading:e.bearing,pitch:e.pitch}}flyTo(e,t,r){if(e&&e.position){r&&this._map.once("moveend",r);let n=e.size;n||(n=new Ce,new xe().setFromObject(e.delegate||e,!0).getSize(n));let s=I.getViewInfo(this._map.transform,w.vector3ToLngLat(e.position),n);this._map.flyTo({center:s.center,zoom:s.zoom,duration:(t||3)*1e3})}return this}zoomTo(e,t){return this.flyTo(e,0,t)}flyToPosition(e,t=[0,0,0],r,n=3){return r&&this._map.once("moveend",r),this._map.flyTo({center:[e[0],e[1]],zoom:I.getZoomByHeight(this._map.transform,e[2],e[1],t[1]||0),bearing:t[0],pitch:t[1],duration:n*1e3}),this}zoomToPosition(e,t=[0,0,0],r){return this.flyToPosition(e,t,r,0)}on(e,t){return this._event.addEventListener(e,t),this}off(e,t){return this._event.removeEventListener(e,t),this}};import{Group as je,DirectionalLight as Ue,HemisphereLight as We,Color as Se}from"three";var U=Math.PI,m=Math.sin,u=Math.cos,B=Math.tan,ae=Math.asin,A=Math.atan2,ie=Math.acos,d=U/180,N=1e3*60*60*24,se=2440588,oe=2451545;function ze(a){return a.valueOf()/N-.5+se}function G(a){return new Date((a+.5-se)*N)}function W(a){return ze(a)-oe}var j=d*23.4397;function me(a,e){return A(m(a)*u(j)-B(e)*m(j),u(a))}function X(a,e){return ae(m(e)*u(j)+u(e)*m(j)*m(a))}function ce(a,e,t){return A(m(a),u(a)*m(e)-B(t)*u(e))}function ue(a,e,t){return ae(m(e)*m(t)+u(e)*u(t)*u(a))}function he(a,e){return d*(280.16+360.9856235*a)-e}function Ee(a){return a<0&&(a=0),2967e-7/Math.tan(a+.00312536/(a+.08901179))}function le(a){return d*(357.5291+.98560028*a)}function pe(a){let e=d*(1.9148*m(a)+.02*m(2*a)+3e-4*m(3*a)),t=d*102.9372;return a+e+t+U}function de(a){let e=le(a),t=pe(e);return{dec:X(t,0),ra:me(t,0)}}var M={};M.getPosition=function(a,e,t){let r=d*-t,n=d*e,s=W(a),i=de(s),o=he(s,r)-i.ra;return{azimuth:ce(o,n,i.dec),altitude:ue(o,n,i.dec)}};var V=M.times=[[-.833,"sunrise","sunset"],[-.3,"sunriseEnd","sunsetStart"],[-6,"dawn","dusk"],[-12,"nauticalDawn","nauticalDusk"],[-18,"nightEnd","night"],[6,"goldenHourEnd","goldenHour"]];M.addTime=function(a,e,t){V.push([a,e,t])};var be=9e-4;function Ae(a,e){return Math.round(a-be-e/(2*U))}function fe(a,e,t){return be+(a+e)/(2*U)+t}function ge(a,e,t){return oe+a+.0053*m(e)-.0069*m(2*t)}function Oe(a,e,t){return ie((m(a)-m(e)*m(t))/(u(e)*u(t)))}function Ge(a){return-2.076*Math.sqrt(a)/60}function He(a,e,t,r,n,s,i){let o=Oe(a,t,r),c=fe(o,e,n);return ge(c,s,i)}M.getTimes=function(a,e,t,r=0){let n=d*-t,s=d*e,i=Ge(r),o=W(a),c=Ae(o,n),h=fe(0,n,c),b=le(h),g=pe(b),f=X(g,0),p=ge(h,b,g),v,z,S,_,L,E,D={solarNoon:G(p),nadir:G(p-.5)};for(v=0,z=V.length;v=0&&(E=Math.sqrt(z)/(Math.abs(g)*2),_=p-E,L=p+E,Math.abs(_)<=1&&S++,Math.abs(L)<=1&&S++,_<-1&&(_=L)),S===1?i<0?h=T+_:b=T+_:S===2&&(h=T+(v<0?L:_),b=T+(v<0?_:L)),!(h&&b));T+=2)i=c;let D={};return h&&(D.rise=H(n,h)),b&&(D.set=H(n,b)),!h&&!b&&(D[v>0?"alwaysUp":"alwaysDown"]=!0),D};var Me=M;var Y=class{_delegate;_sunLight;_hemiLight;_currentTime;constructor(){this._delegate=new je,this._delegate.name="Sun",this._sunLight=new Ue(16777215,1),this._hemiLight=new We(new Se(16777215),new Se(16777215),.6),this._hemiLight.color.setHSL(.661,.96,.12),this._hemiLight.groundColor.setHSL(.11,.96,.14),this._hemiLight.position.set(0,0,50),this._delegate.add(this._sunLight),this._delegate.add(this._hemiLight),this._currentTime=new Date().getTime()}get delegate(){return this._delegate}set castShadow(e){this._sunLight.castShadow=e}get castShadow(){return this._sunLight.castShadow}set currentTime(e){this._currentTime=e}get currentTime(){return this._currentTime}get sunLight(){return this._sunLight}get hemiLight(){return this._hemiLight}setShadow(e={}){return this._sunLight.shadow.radius=e.radius||2,this._sunLight.shadow.mapSize.width=e.mapSize?e.mapSize[0]:8192,this._sunLight.shadow.mapSize.height=e.mapSize?e.mapSize[1]:8192,this._sunLight.shadow.camera.top=this._sunLight.shadow.camera.right=e.topRight||1e3,this._sunLight.shadow.camera.bottom=this._sunLight.shadow.camera.left=e.bottomLeft||-1e3,this._sunLight.shadow.camera.near=e.near||1,this._sunLight.shadow.camera.far=e.far||1e8,this._sunLight.shadow.camera.visible=!0,this}update(e){let r=new Date(this._currentTime||new Date().getTime()),n=e.center,s=Me.getPosition(r,n.lat,n.lng),i=s.altitude,o=Math.PI+s.azimuth,c=1024e3/2,h=Math.sin(i),b=Math.cos(i),g=Math.cos(o)*b,f=Math.sin(o)*b;this._sunLight.position.set(f,g,h),this._sunLight.position.multiplyScalar(c),this._sunLight.intensity=Math.max(h,0),this._hemiLight.intensity=Math.max(h*1,.1),this._sunLight.updateMatrixWorld()}},ye=Y;import{Group as Ze,Mesh as Fe,PlaneGeometry as ke,ShadowMaterial as Je}from"three";var q=class{static createRTCGroup(e,t,r){let n=new Ze;if(n.name="rtc",n.position.copy(w.lngLatToVector3(e)),t?(n.rotateX(t[0]||0),n.rotateY(t[1]||0),n.rotateZ(t[2]||0)):(n.rotateX(Math.PI/2),n.rotateY(Math.PI)),r)n.scale.set(r[0]||1,r[1]||1,r[2]||1);else{let s=1;Array.isArray(e)&&(s=w.projectedUnitsPerMeter(e[1])),n.scale.set(s,s,s)}return n}static createMercatorRTCGroup(e,t,r){let n=this.createRTCGroup(e,t,r);if(!r){let s=1,i=w.projectedMercatorUnitsPerMeter();Array.isArray(e)&&(s=w.projectedUnitsPerMeter(e[1])),n.scale.set(i,i,s)}return n}static createShadowGround(e,t,r){let n=new ke(t||100,r||100),s=new Je({opacity:.5,transparent:!0}),i=new Fe(n,s);return i.position.copy(w.lngLatToVector3(e)),i.receiveShadow=!0,i.name="shadow-ground",i}},we=q;export{we as Creator,O as MapScene,w as SceneTransform,ye as Sun}; 2 | -------------------------------------------------------------------------------- /examples/3d-gs-multi-splats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d gs splat 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-gs-multi-splats.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { SplatLoader } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | maxZoom: 30, 16 | center: [120.56114970334647, 31.236247342246173], 17 | zoom: 18, 18 | }) 19 | 20 | const mapScene = new MTP.MapScene(map) 21 | 22 | mapScene.addLight(new THREE.AmbientLight()) 23 | 24 | let rtc = new THREE.Group() 25 | rtc.position.copy( 26 | MTP.SceneTransform.lngLatToVector3( 27 | 120.56114970334647, 28 | 31.236247342246173, 29 | 200 30 | ) 31 | ) 32 | 33 | rtc.rotateX(-Math.PI / 2) 34 | rtc.rotateY(Math.PI / 2) 35 | 36 | mapScene.addObject(rtc) 37 | 38 | const splatLoader = new SplatLoader() 39 | 40 | splatLoader.loadStream('./assets/1.splat', (mesh) => { 41 | mesh.threshold = -0.000001 42 | rtc.add(mesh) 43 | }) 44 | 45 | splatLoader.loadStream('./assets/2.splat', (mesh) => { 46 | rtc.add(mesh) 47 | }) 48 | 49 | const center = new THREE.Vector3() 50 | 51 | mapScene 52 | .on('preRender', (e) => { 53 | const scene = e.frameState.scene 54 | const cameraMatrix = e.frameState.camera.matrixWorldInverse 55 | scene.traverse((child) => { 56 | if (child.isSplatMesh) { 57 | child.computeBounds() 58 | child.bounds.getCenter(center) 59 | center.applyMatrix4(child.matrixWorld) 60 | center.applyMatrix4(cameraMatrix) 61 | let depth = -center.z 62 | child.renderOrder = 1e5 - depth 63 | } 64 | }) 65 | }) 66 | .on('postRender', () => { 67 | map.triggerRepaint() 68 | }) 69 | -------------------------------------------------------------------------------- /examples/3d-gs-ply.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3dgs ply 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-gs-ply.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | 6 | const map = new maplibregl.Map({ 7 | container: 'map', 8 | style: 9 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 10 | config.maptiler_key, 11 | maxPitch: 85, 12 | pitch: 60, 13 | canvasContextAttributes: { antialias: true }, 14 | center: [148.9819, -35.39847], 15 | zoom: 16, 16 | }) 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | // add light 20 | mapScene.addLight(new THREE.AmbientLight()) 21 | -------------------------------------------------------------------------------- /examples/3d-gs-splat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d gs splat 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-gs-splat.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { SplatLoader } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | maxZoom: 30, 16 | center: [120.71508193750839, 31.270782107613073], 17 | zoom: 18, 18 | }) 19 | 20 | const mapScene = new MTP.MapScene(map) 21 | 22 | mapScene.addLight(new THREE.AmbientLight()) 23 | 24 | let rtc = new THREE.Group() 25 | rtc.position.copy( 26 | MTP.SceneTransform.lngLatToVector3(120.71508193750839, 31.270782107613073, 10) 27 | ) 28 | 29 | rtc.rotateX(Math.PI / 2) 30 | rtc.rotateY(Math.PI / 2) 31 | 32 | mapScene.addObject(rtc) 33 | 34 | const splatLoader = new SplatLoader() 35 | 36 | splatLoader.loadStream('//resource.dvgis.cn/data/models/yqjt.splat', (mesh) => { 37 | mesh.threshold = -0.0000001 38 | rtc.add(mesh) 39 | }) 40 | 41 | mapScene.on('postRender', () => { 42 | map.triggerRepaint() 43 | }) 44 | -------------------------------------------------------------------------------- /examples/3d-gs-splat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/3d-gs-splat.png -------------------------------------------------------------------------------- /examples/3d-tiles-3dgs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d tiles 3dgs 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-tiles-3dgs.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { Tileset } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | maxZoom: 30, 16 | }) 17 | 18 | const mapScene = new MTP.MapScene(map) 19 | 20 | mapScene.addLight(new THREE.AmbientLight()) 21 | 22 | let tileset = new Tileset(3667783, { 23 | lruCache: { 24 | minSize: 60, 25 | maxSize: 80, 26 | }, 27 | cesiumIon: { 28 | apiToken: config.cesium_key, 29 | }, 30 | }) 31 | 32 | tileset.autoDisableRendererCulling = true 33 | 34 | tileset.on('loaded', () => { 35 | mapScene.addObject(tileset) 36 | mapScene.flyTo(tileset) 37 | }) 38 | 39 | mapScene 40 | .on('preRender', (e) => { 41 | tileset.update(e.frameState) 42 | }) 43 | .on('postRender', () => { 44 | map.triggerRepaint() 45 | }) 46 | -------------------------------------------------------------------------------- /examples/3d-tiles-3dgs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/3d-tiles-3dgs.png -------------------------------------------------------------------------------- /examples/3d-tiles-cesium-ion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d tiles osgb 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-tiles-cesium-ion.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { ModelLoader, Tileset } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | maxZoom: 30, 16 | }) 17 | 18 | const mapScene = new MTP.MapScene(map) 19 | 20 | mapScene.addLight(new THREE.AmbientLight()) 21 | 22 | ModelLoader.setDracoLoader({ 23 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/draco/', 24 | }) 25 | 26 | ModelLoader.setKtx2loader({ 27 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/basis/', 28 | renderer: mapScene.renderer, 29 | }) 30 | 31 | let tileset = new Tileset(40866, { 32 | dracoLoader: ModelLoader.getDracoLoader(), 33 | ktxLoader: ModelLoader.getKtx2loader(), 34 | cesiumIon: { 35 | apiToken: config.cesium_key, 36 | }, 37 | }) 38 | 39 | tileset.autoDisableRendererCulling = true 40 | tileset.on('loaded', () => { 41 | mapScene.addObject(tileset) 42 | tileset.setHeight(-70) 43 | mapScene.flyTo(tileset) 44 | }) 45 | mapScene 46 | .on('preRender', (e) => { 47 | tileset.update(e.frameState) 48 | }) 49 | .on('postRender', () => { 50 | map.triggerRepaint() 51 | }) 52 | -------------------------------------------------------------------------------- /examples/3d-tiles-cesium-ion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/3d-tiles-cesium-ion.png -------------------------------------------------------------------------------- /examples/3d-tiles-osgb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d tiles osgb 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-tiles-osgb.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { ModelLoader, Tileset } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | }) 16 | 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | mapScene.addLight(new THREE.AmbientLight()) 20 | 21 | ModelLoader.setDracoLoader({ 22 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/draco/', 23 | }) 24 | 25 | ModelLoader.setKtx2loader({ 26 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/basis/', 27 | renderer: mapScene.renderer, 28 | }) 29 | 30 | let url = '//resource.dvgis.cn/data/3dtiles/dayanta/tileset.json' 31 | 32 | let tileset = new Tileset(url, { 33 | dracoLoader: ModelLoader.getDracoLoader(), 34 | ktxLoader: ModelLoader.getKtx2loader(), 35 | }) 36 | 37 | tileset.autoDisableRendererCulling = true 38 | tileset.errorTarget = 6 39 | 40 | tileset.on('loaded', () => { 41 | mapScene.addObject(tileset) 42 | tileset.setHeight(-420) 43 | mapScene.flyTo(tileset) 44 | }) 45 | 46 | mapScene 47 | .on('preRender', (e) => { 48 | tileset.update(e.frameState) 49 | }) 50 | .on('postRender', () => { 51 | map.triggerRepaint() 52 | }) 53 | -------------------------------------------------------------------------------- /examples/3d-tiles-osgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/3d-tiles-osgb.png -------------------------------------------------------------------------------- /examples/3d-tiles-shadow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d tiles shadow 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-tiles-shadow.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as MTP from '@dvt3d/maplibre-three-plugin' 3 | import config from './config.js' 4 | import { ModelLoader, Tileset } from './src/index.js' 5 | 6 | let map = new maplibregl.Map({ 7 | container: 'map', 8 | style: 9 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 10 | config.maptiler_key, // style URL 11 | maxPitch: 85, 12 | pitch: 60, 13 | canvasContextAttributes: { antialias: true }, 14 | }) 15 | 16 | let mapScene = new MTP.MapScene(map) 17 | 18 | mapScene.renderer.shadowMap.enabled = true 19 | 20 | const sun = new MTP.Sun() 21 | sun.currentTime = '2025/7/12 8:00:00' 22 | sun.castShadow = true 23 | sun.setShadow() 24 | mapScene.addLight(sun) 25 | 26 | ModelLoader.setDracoLoader({ 27 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/draco/', 28 | }) 29 | ModelLoader.setKtx2loader({ 30 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/basis/', 31 | renderer: mapScene.renderer, 32 | }) 33 | 34 | const ljz_url = '//resource.dvgis.cn/data/3dtiles/ljz/tileset.json' 35 | const tileset = new Tileset(ljz_url, { 36 | dracoLoader: ModelLoader.getDracoLoader(), 37 | ktxLoader: ModelLoader.getKtx2loader(), 38 | }) 39 | 40 | tileset.autoDisableRendererCulling = true 41 | tileset.errorTarget = 6 42 | 43 | tileset.on('loaded', () => { 44 | const shadowGround = MTP.Creator.createShadowGround( 45 | [tileset.positionDegrees[0], tileset.positionDegrees[1]], 46 | 1000, 47 | 1000 48 | ) 49 | mapScene.world.add(shadowGround) 50 | mapScene.addObject(tileset) 51 | mapScene.flyTo(tileset) 52 | }) 53 | 54 | tileset.on('load-model', (e) => { 55 | let model = e.scene 56 | model.traverse(function (obj) { 57 | if (obj.isMesh) { 58 | obj.castShadow = true 59 | } 60 | }) 61 | }) 62 | 63 | mapScene 64 | .on('preRender', (e) => { 65 | sun.update(e.frameState) 66 | tileset.update(e.frameState) 67 | }) 68 | .on('postRender', () => { 69 | map.triggerRepaint() 70 | }) 71 | -------------------------------------------------------------------------------- /examples/3d-tiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 3d - tiles 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/3d-tiles.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { Tileset, ModelLoader } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | }) 16 | 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | mapScene.addLight(new THREE.AmbientLight()) 20 | 21 | ModelLoader.setDracoLoader({ 22 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/draco/', 23 | }) 24 | 25 | ModelLoader.setKtx2loader({ 26 | path: 'https://cdn.jsdelivr.net/npm/three/examples/jsm/libs/basis/', 27 | renderer: mapScene.renderer, 28 | }) 29 | 30 | let ljz_url = '//resource.dvgis.cn/data/3dtiles/ljz/tileset.json' 31 | 32 | let tileset = new Tileset(ljz_url, { 33 | dracoLoader: ModelLoader.getDracoLoader(), 34 | ktxLoader: ModelLoader.getKtx2loader(), 35 | }) 36 | 37 | tileset.autoDisableRendererCulling = true 38 | tileset.errorTarget = 6 39 | 40 | tileset.on('loaded', () => { 41 | mapScene.addObject(tileset) 42 | mapScene.flyTo(tileset) 43 | }) 44 | 45 | mapScene 46 | .on('preRender', (e) => { 47 | tileset.update(e.frameState) 48 | }) 49 | .on('postRender', () => { 50 | map.triggerRepaint() 51 | }) 52 | -------------------------------------------------------------------------------- /examples/3d-tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/3d-tiles.png -------------------------------------------------------------------------------- /examples/assets/34M_17/34M_17.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/34M_17.bin -------------------------------------------------------------------------------- /examples/assets/34M_17/base_AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/base_AO.png -------------------------------------------------------------------------------- /examples/assets/34M_17/frame_AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/frame_AO.png -------------------------------------------------------------------------------- /examples/assets/34M_17/stairs_plt_AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/stairs_plt_AO.png -------------------------------------------------------------------------------- /examples/assets/34M_17/truss_2_AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/truss_2_AO.png -------------------------------------------------------------------------------- /examples/assets/34M_17/truss_dish_AO.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/truss_dish_AO.jpg -------------------------------------------------------------------------------- /examples/assets/34M_17/wheels_AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/34M_17/wheels_AO.png -------------------------------------------------------------------------------- /examples/assets/icon/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/assets/icon/camera.png -------------------------------------------------------------------------------- /examples/billboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | billboard 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/billboard.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { Billboard } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | }) 16 | 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | mapScene.addLight(new THREE.AmbientLight()) 20 | 21 | function generatePosition(num) { 22 | let list = [] 23 | for (let i = 0; i < num; i++) { 24 | let lng = 120.38105869 + Math.random() * 0.1 25 | let lat = 31.10115627 + Math.random() * 0.1 26 | list.push([lng, lat]) 27 | } 28 | return list 29 | } 30 | 31 | const positions = generatePosition(1000) 32 | 33 | let billboard = undefined 34 | positions.forEach((position) => { 35 | billboard = new Billboard( 36 | MTP.SceneTransform.lngLatToVector3(position[0], position[1]), 37 | './assets/icon/camera.png' 38 | ) 39 | mapScene.addObject(billboard) 40 | }) 41 | 42 | mapScene.flyTo(billboard) 43 | -------------------------------------------------------------------------------- /examples/billboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/billboard.png -------------------------------------------------------------------------------- /examples/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | // maptiler_key: 'get_your_own_OpIi9ZULNHzrESv6T2vL', 3 | maptiler_key: 'GhNJDabtJW2KhnZWhdq0', 4 | cesium_key: 5 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NTEwZTU2Yi0wOGEyLTQyZjgtOTJjNi04Mzc2NGRlNzA4NTkiLCJpZCI6MjU5LCJpYXQiOjE3NTY4NDExOTJ9._Y3MIsYgGKTVTpkEpKPNT0cQSa_hUocY0DdH7h0U-xM', 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /examples/div-icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | div icon 11 | 15 | 28 | 29 | 30 |
31 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/div-icon.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import { CSS3DRenderer } from 'three/addons' 4 | import * as MTP from '@dvt3d/maplibre-three-plugin' 5 | import config from './config.js' 6 | import { DivIcon } from './src/index.js' 7 | 8 | const map = new maplibregl.Map({ 9 | container: 'map', 10 | style: 11 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 12 | config.maptiler_key, 13 | maxPitch: 85, 14 | canvasContextAttributes: { antialias: true }, 15 | }) 16 | 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | mapScene.addLight(new THREE.AmbientLight()) 20 | 21 | const element = document.createElement('div') 22 | element.className = 'div-icon-container' 23 | document.getElementById('map').appendChild(element) 24 | 25 | const domRenderer = new CSS3DRenderer({ 26 | element: element, 27 | }) 28 | domRenderer.setSize(mapScene.canvas.clientWidth, mapScene.canvas.clientHeight) 29 | window.addEventListener('resize', () => { 30 | domRenderer.setSize(mapScene.canvas.clientWidth, mapScene.canvas.clientHeight) 31 | }) 32 | 33 | mapScene.on('preRender', (e) => { 34 | domRenderer.render(e.frameState.scene, e.frameState.camera) 35 | }) 36 | 37 | function generatePosition(num) { 38 | let list = [] 39 | for (let i = 0; i < num; i++) { 40 | let lng = 120.38105869 + Math.random() * 0.5 41 | let lat = 31.10115627 + Math.random() * 0.5 42 | list.push([lng, lat]) 43 | } 44 | return list 45 | } 46 | 47 | const positions = generatePosition(20) 48 | 49 | let divIcon = undefined 50 | positions.forEach((position) => { 51 | divIcon = new DivIcon( 52 | MTP.SceneTransform.lngLatToVector3(position[0], position[1]), 53 | '数字视界科技' 54 | ) 55 | mapScene.addObject(divIcon) 56 | }) 57 | 58 | map.on('style.load', () => { 59 | mapScene.flyToPosition( 60 | [120.6465605955243, 31.228473719008534, 15208.762327849023], 61 | [0, 75, 0] 62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /examples/div-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/div-icon.png -------------------------------------------------------------------------------- /examples/heat-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | heat map 11 | 15 | 28 | 29 | 30 |
31 | 32 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/heat-map.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as MTP from '@dvt3d/maplibre-three-plugin' 3 | import config from './config.js' 4 | import { HeatMap } from './src/index.js' 5 | 6 | const map = new maplibregl.Map({ 7 | container: 'map', 8 | style: 9 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 10 | config.maptiler_key, 11 | maxPitch: 85, 12 | pitch: 60, 13 | canvasContextAttributes: { antialias: true }, 14 | }) 15 | 16 | const mapScene = new MTP.MapScene(map) 17 | 18 | function generatePoints(num) { 19 | let list = [] 20 | for (let i = 0; i < num; i++) { 21 | let lng = 120.38105869 + Math.random() * 0.05 22 | let lat = 31.10115627 + Math.random() * 0.05 23 | list.push({ 24 | lng: lng, 25 | lat: lat, 26 | value: Math.random() * 1000, 27 | }) 28 | } 29 | return list 30 | } 31 | 32 | let heatMapContainer = document.createElement('div') 33 | 34 | map.getContainer().appendChild(heatMapContainer) 35 | 36 | let heatMap = new HeatMap(heatMapContainer, { 37 | h337: window.h337, 38 | }) 39 | 40 | heatMap.setPoints(generatePoints(1000)) 41 | 42 | mapScene.addObject(heatMap) 43 | 44 | mapScene.flyTo(heatMap) 45 | -------------------------------------------------------------------------------- /examples/heat-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/heat-map.png -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | maplibre three 11 | 15 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import { GLTFLoader } from 'three/addons' 5 | import config from './config.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map-container', // container id 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, // style URL 12 | zoom: 18, 13 | center: [148.9819, -35.3981], 14 | pitch: 60, 15 | canvasContextAttributes: { antialias: true }, 16 | maxPitch: 85, 17 | }) 18 | 19 | //init three scene 20 | const mapScene = new MTP.MapScene(map) 21 | 22 | //add light 23 | mapScene.addLight(new THREE.AmbientLight()) 24 | 25 | // add model 26 | const loader = new GLTFLoader() 27 | loader.load('./assets/34M_17/34M_17.gltf', (gltf) => { 28 | let rtcGroup = MTP.Creator.createRTCGroup([148.9819, -35.39847]) 29 | rtcGroup.add(gltf.scene) 30 | mapScene.addObject(rtcGroup) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/index.png -------------------------------------------------------------------------------- /examples/point-collection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | point collection 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/point-collection.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { PointCollection } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | }) 16 | 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | function generatePosition(num) { 20 | let list = [] 21 | for (let i = 0; i < num; i++) { 22 | let lng = 120.38105869 + Math.random() * 0.5 23 | let lat = 31.10115627 + Math.random() * 0.5 24 | list.push([lng, lat]) 25 | } 26 | return list 27 | } 28 | mapScene.addLight(new THREE.AmbientLight()) 29 | 30 | const positions = generatePosition(10000) 31 | let pointCollection = new PointCollection( 32 | positions.map((position) => 33 | MTP.SceneTransform.lngLatToVector3(position[0], position[1]) 34 | ) 35 | ) 36 | 37 | mapScene.addObject(pointCollection) 38 | mapScene.flyTo(pointCollection) 39 | -------------------------------------------------------------------------------- /examples/point-collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/point-collection.png -------------------------------------------------------------------------------- /examples/point.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | point 11 | 15 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/point.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { Point } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map', 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, 12 | maxPitch: 85, 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | }) 16 | 17 | const mapScene = new MTP.MapScene(map) 18 | 19 | function generatePosition(num) { 20 | let list = [] 21 | for (let i = 0; i < num; i++) { 22 | let lng = 120.38105869 + Math.random() * 0.5 23 | let lat = 31.10115627 + Math.random() * 0.5 24 | list.push([lng, lat]) 25 | } 26 | return list 27 | } 28 | mapScene.addLight(new THREE.AmbientLight()) 29 | 30 | const positions = generatePosition(30) 31 | 32 | let point = undefined 33 | positions.forEach((position) => { 34 | point = new Point( 35 | MTP.SceneTransform.lngLatToVector3(position[0], position[1]) 36 | ) 37 | mapScene.addObject(point) 38 | }) 39 | mapScene.flyTo(point) 40 | -------------------------------------------------------------------------------- /examples/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/point.png -------------------------------------------------------------------------------- /examples/shadow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | shadow 11 | 15 | 33 | 34 | 35 |
36 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/shadow.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as THREE from 'three' 3 | import * as MTP from '@dvt3d/maplibre-three-plugin' 4 | import config from './config.js' 5 | import { Model } from './src/index.js' 6 | 7 | const map = new maplibregl.Map({ 8 | container: 'map-container', // container id 9 | style: 10 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 11 | config.maptiler_key, // style URL 12 | zoom: 18, 13 | center: [148.9819, -35.3981], 14 | pitch: 60, 15 | canvasContextAttributes: { antialias: true }, 16 | maxPitch: 85, 17 | }) 18 | 19 | //init three scene 20 | const mapScene = new MTP.MapScene(map) 21 | 22 | mapScene.renderer.shadowMap.enabled = true 23 | 24 | mapScene.addLight(new THREE.AmbientLight()) 25 | 26 | const dirLight = new THREE.DirectionalLight(0xffffff, 1) 27 | dirLight.castShadow = true 28 | dirLight.shadow.radius = 2 29 | dirLight.shadow.mapSize.width = 8192 30 | dirLight.shadow.mapSize.height = 8192 31 | dirLight.shadow.camera.top = dirLight.shadow.camera.right = 1000 32 | dirLight.shadow.camera.bottom = dirLight.shadow.camera.left = -1000 33 | dirLight.shadow.camera.near = 1 34 | dirLight.shadow.camera.far = 1e8 35 | dirLight.shadow.camera.visible = true 36 | dirLight.position.set(30, 100, 100) 37 | dirLight.updateMatrixWorld() 38 | 39 | mapScene.addLight(dirLight) 40 | 41 | const shadowGround = MTP.Creator.createShadowGround([148.9819, -35.39847]) 42 | mapScene.addObject(shadowGround) 43 | 44 | Model.fromGltfAsync({ 45 | url: './assets/34M_17/34M_17.gltf', 46 | position: MTP.SceneTransform.lngLatToVector3(148.9819, -35.39847), 47 | castShadow: true, 48 | }).then((model) => { 49 | mapScene.addObject(model) 50 | }) 51 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | export * from './modules/index.js' 6 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/gaussian_splatting/GLTFGaussianSplattingExtension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | import { Group } from 'three' 5 | import SplatMesh from './SplatMesh.js' 6 | 7 | class GLTFGaussianSplattingExtension { 8 | constructor(parser) { 9 | this.parser = parser 10 | this.name = 'KHR_gaussian_splatting' 11 | } 12 | 13 | /** 14 | * 15 | * @param meshIndex 16 | * @returns {Promise[]>} 17 | */ 18 | loadMesh(meshIndex) { 19 | const parser = this.parser 20 | const json = parser.json 21 | const extensionsUsed = json.extensionsUsed 22 | if ( 23 | !extensionsUsed || 24 | !extensionsUsed.includes(this.name) || 25 | extensionsUsed.includes('KHR_gaussian_splatting_compression_spz_2') 26 | ) { 27 | return null 28 | } 29 | const meshDef = json.meshes[meshIndex] 30 | const primitives = meshDef.primitives 31 | const pending = [] 32 | pending.push(parser.loadGeometries(primitives)) 33 | return Promise.all(pending).then((results) => { 34 | const group = new Group() 35 | const geometries = results[0] 36 | const geometry = geometries[0] 37 | const mesh = new SplatMesh() 38 | mesh.vertexCount = geometry.attributes.position.count 39 | mesh.setDataFromGeometry(geometry) 40 | group.add(mesh) 41 | return group 42 | }) 43 | } 44 | } 45 | 46 | export default GLTFGaussianSplattingExtension 47 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/gaussian_splatting/GLTFSpzGaussianSplattingExtension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | import { Group } from 'three' 5 | import { loadSpz } from '@spz-loader/core' 6 | import SplatMesh from './SplatMesh.js' 7 | 8 | class GLTFSpzGaussianSplattingExtension { 9 | constructor(parser) { 10 | this.parser = parser 11 | this.name = 'KHR_gaussian_splatting_compression_spz_2' 12 | } 13 | 14 | /** 15 | * 16 | * @param meshIndex 17 | * @returns {Promise[]>} 18 | */ 19 | loadMesh(meshIndex) { 20 | const parser = this.parser 21 | const json = parser.json 22 | const extensionsUsed = json.extensionsUsed 23 | if (!extensionsUsed || !extensionsUsed.includes(this.name)) { 24 | return null 25 | } 26 | 27 | const meshDef = json.meshes[meshIndex] 28 | const primitives = meshDef.primitives 29 | const pending = [] 30 | pending.push(this.loadBufferViews(primitives)) 31 | return Promise.all(pending).then((results) => { 32 | const group = new Group() 33 | const bufferViews = results[0] 34 | const attribute = bufferViews[0] 35 | const mesh = new SplatMesh() 36 | mesh.vertexCount = attribute.numPoints 37 | mesh.setDataFromSpz(attribute) 38 | group.add(mesh) 39 | return group 40 | }) 41 | } 42 | /** 43 | * 44 | * @param primitives 45 | * @returns {*[]} 46 | */ 47 | loadBufferViews(primitives) { 48 | const parser = this.parser 49 | const pendingBufferViews = [] 50 | for (let i = 0; i < primitives.length; i++) { 51 | const primitive = primitives[i] 52 | const extensions = primitive.extensions 53 | if ( 54 | extensions['KHR_gaussian_splatting'] && 55 | extensions['KHR_gaussian_splatting'].extensions && 56 | extensions['KHR_gaussian_splatting'].extensions[this.name] 57 | ) { 58 | pendingBufferViews.push( 59 | parser 60 | .getDependency( 61 | 'bufferView', 62 | extensions['KHR_gaussian_splatting'].extensions[this.name] 63 | .bufferView 64 | ) 65 | .then((bufferView) => loadSpz(bufferView)) 66 | ) 67 | } else { 68 | if (extensions[this.name]) { 69 | pendingBufferViews.push( 70 | parser 71 | .getDependency('bufferView', extensions[this.name].bufferView) 72 | .then((bufferView) => loadSpz(bufferView)) 73 | ) 74 | } 75 | } 76 | } 77 | return Promise.all(pendingBufferViews).then((bufferViews) => { 78 | return bufferViews 79 | }) 80 | } 81 | } 82 | 83 | export default GLTFSpzGaussianSplattingExtension 84 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/gaussian_splatting/GaussianSplattingTilesetPlugin.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three' 2 | 3 | const _center = new Vector3() 4 | 5 | class GaussianSplattingTilesetPlugin { 6 | constructor(threshold) { 7 | this._threshold = threshold || -0.00001 8 | this.name = 'GAUSSIAN_SPLATTING_TILESET_PLUGIN' 9 | this.tiles = null 10 | } 11 | 12 | init(tiles) { 13 | this.tiles = tiles 14 | tiles.addEventListener('update-before', this._onUpdateBefore.bind(this)) 15 | tiles.addEventListener('update-after', this._onUpdateAfter.bind(this)) 16 | tiles.addEventListener('dispose-model', this._onDisposeModel.bind(this)) 17 | } 18 | 19 | _onUpdateBefore() {} 20 | 21 | _onUpdateAfter() { 22 | const tiles = this.tiles 23 | let camera = tiles.cameras[0] 24 | if (camera) { 25 | const viewMatrix = camera.matrixWorldInverse 26 | tiles.forEachLoadedModel((scene) => { 27 | scene.traverse((child) => { 28 | if (child.isSplatMesh) { 29 | child.threshold = this._threshold 30 | child.computeBounds() 31 | _center.set(0, 0, 0) 32 | child.bounds.getCenter(_center) 33 | _center.applyMatrix4(child.matrixWorld) 34 | _center.applyMatrix4(viewMatrix) 35 | let depth = -_center.z 36 | child.renderOrder = 1e5 - depth 37 | } 38 | }) 39 | }) 40 | } 41 | } 42 | 43 | _onDisposeModel({ scene }) { 44 | scene.traverse((child) => { 45 | if (child.isSplatMesh) { 46 | child.dispose() 47 | } 48 | }) 49 | } 50 | 51 | dispose() { 52 | const tiles = this.tiles 53 | tiles.removeEventListener('update-before', this._onUpdateBefore.bind(this)) 54 | tiles.removeEventListener('update-after', this._onUpdateAfter.bind(this)) 55 | tiles.removeEventListener('dispose-model', this._onDisposeModel.bind(this)) 56 | } 57 | } 58 | 59 | export default GaussianSplattingTilesetPlugin 60 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/gaussian_splatting/SortScheduler.js: -------------------------------------------------------------------------------- 1 | class SortScheduler { 2 | constructor(intervalTime = 1000, stableStopTime = 3000) { 3 | this._intervalTime = intervalTime 4 | this._stableStopTime = stableStopTime 5 | this._isSorting = false 6 | this._dirty = true 7 | this._lastMvMatrix = null 8 | this._stableSince = 0 9 | this._lastSortTime = 0 10 | } 11 | 12 | set isSorting(isSorting) { 13 | this._isSorting = isSorting 14 | } 15 | 16 | get isSorting() { 17 | return this._isSorting 18 | } 19 | 20 | set dirty(dirty) { 21 | this._dirty = dirty 22 | } 23 | 24 | get dirty() { 25 | return this.dirty 26 | } 27 | 28 | _isMatrixChanged(prev, curr) { 29 | const now = performance.now() 30 | if (!prev) return true 31 | return !prev.equals(curr) 32 | } 33 | 34 | tick(mvMatrix, fn) { 35 | const now = performance.now() 36 | const changed = this._isMatrixChanged(this._lastMvMatrix, mvMatrix) 37 | if (changed) { 38 | this._stableSince = now 39 | } 40 | const canTrigger = 41 | !this._isSorting && 42 | now - this._lastSortTime >= this._intervalTime && 43 | (this._dirty || 44 | changed || 45 | (this._stableSince > 0 && 46 | now - this._stableSince < this._stableStopTime)) 47 | 48 | if (canTrigger) { 49 | this._lastSortTime = now 50 | this._isSorting = true 51 | this._dirty = false 52 | fn() 53 | } 54 | this._lastMvMatrix = mvMatrix 55 | if (this._stableSince === 0) this._stableSince = now 56 | return this 57 | } 58 | } 59 | 60 | export default SortScheduler 61 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/gaussian_splatting/Splat.js: -------------------------------------------------------------------------------- 1 | import { Box3, Object3D, Vector3 } from 'three' 2 | 3 | class Splat extends Object3D { 4 | constructor() { 5 | super() 6 | } 7 | } 8 | 9 | export default Splat 10 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/gltf/GLTFKtx2TextureInspectorPlugin.js: -------------------------------------------------------------------------------- 1 | class GLTFKtx2TextureInspectorPlugin { 2 | constructor(parse) { 3 | this.parse = parse 4 | this.name = 'ktx2_texture_inspector' 5 | } 6 | 7 | beforeRoot() { 8 | let textures = this.parse.json.textures 9 | let images = this.parse.json.images 10 | if (!textures || !images) { 11 | return 12 | } 13 | for (let i = 0; i < textures.length; i++) { 14 | let texture = textures[i] 15 | if ( 16 | !texture.extensions && 17 | images[texture.source] && 18 | images[texture.source].mimeType.indexOf('ktx2') >= 0 19 | ) { 20 | texture.extensions = { 21 | KHR_texture_basisu: { 22 | source: texture.source, 23 | }, 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | export default GLTFKtx2TextureInspectorPlugin 31 | -------------------------------------------------------------------------------- /examples/src/modules/extensions/index.js: -------------------------------------------------------------------------------- 1 | export { default as GLTFGaussianSplattingExtension } from './gaussian_splatting/GLTFGaussianSplattingExtension.js' 2 | export { default as GLTFSpzGaussianSplattingExtension } from './gaussian_splatting/GLTFSpzGaussianSplattingExtension.js' 3 | export { default as GaussianSplattingTilesetPlugin } from './gaussian_splatting/GaussianSplattingTilesetPlugin.js' 4 | export { default as GLTFKtx2TextureInspectorPlugin } from './gltf/GLTFKtx2TextureInspectorPlugin.js' 5 | export { default as SplatMesh } from './gaussian_splatting/SplatMesh.js' 6 | -------------------------------------------------------------------------------- /examples/src/modules/heat-map/HeatMap.js: -------------------------------------------------------------------------------- 1 | import { CanvasTexture, Group, Mesh, PlaneGeometry, Vector3 } from 'three' 2 | import { HeatMapMaterial } from '../material/index.js' 3 | import { SceneTransform } from '@dvt3d/maplibre-three-plugin' 4 | import { Util } from '../utils/index.js' 5 | 6 | const DEF_OPTS = { 7 | h337: null, 8 | width: 256, 9 | height: 256, 10 | radius: 10, 11 | heightFactor: 10, 12 | pad: 0.05, 13 | clamp: false, 14 | gradient: { 15 | 0.2: '#24d560', 16 | 0.4: '#9cd522', 17 | 0.6: '#f1e12a', 18 | 0.8: '#ffbf3a', 19 | 1.0: '#ff0000', 20 | }, 21 | } 22 | 23 | class HeatMap { 24 | constructor(container, options = {}) { 25 | if (!container) { 26 | throw 'container is required' 27 | } 28 | 29 | if (!options.h337) { 30 | throw 'heatmap h337 is required' 31 | } 32 | 33 | this._options = { 34 | ...DEF_OPTS, 35 | ...options, 36 | } 37 | 38 | container.setAttribute('id', Util.uuid()) 39 | container.style.cssText = `width:${this._options.width}px;height:${this._options.height}px;visibility:hidden;` 40 | 41 | this._colorMap = this._options.h337.create({ 42 | container: container, 43 | backgroundColor: 'rgba(0,0,0,0)', 44 | radius: this._options.radius, 45 | gradient: this._options.gradient, 46 | }) 47 | 48 | this._grayMap = this._options.h337.create({ 49 | container: container, 50 | backgroundColor: 'rgba(0,0,0,0)', 51 | radius: this._options.radius, 52 | gradient: { 53 | 0: 'black', 54 | 1: 'white', 55 | }, 56 | }) 57 | 58 | this._colorTexture = new CanvasTexture(this._colorMap._renderer.canvas) 59 | this._grayTexture = new CanvasTexture(this._grayMap._renderer.canvas) 60 | 61 | this._delegate = new Mesh( 62 | new PlaneGeometry(1, 1), 63 | new HeatMapMaterial({ 64 | ...this._options, 65 | colorTexture: this._colorTexture, 66 | grayTexture: this._grayTexture, 67 | }) 68 | ) 69 | 70 | this._position = new Vector3() 71 | this._size = new Vector3() 72 | } 73 | 74 | get delegate() { 75 | return this._delegate 76 | } 77 | 78 | get position() { 79 | return this._position 80 | } 81 | 82 | get size() { 83 | return this._size 84 | } 85 | 86 | /** 87 | * 88 | * @param points 89 | * @returns {{bounds: (number|number)[], positions: *[], minValue: number, maxValue: number, values: *[]}} 90 | * @private 91 | */ 92 | _parse(points) { 93 | let xMin = Infinity 94 | let xMax = -Infinity 95 | let yMin = Infinity 96 | let yMax = -Infinity 97 | let maxValue = -Infinity 98 | let minValue = Infinity 99 | let positions = [] 100 | let values = [] 101 | for (let i = 0; i < points.length; i++) { 102 | let point = points[i] 103 | let v = SceneTransform.lngLatToVector3(point.lng, point.lat) 104 | 105 | if (v.x < xMin) { 106 | xMin = v.x 107 | } 108 | if (v.x > xMax) { 109 | xMax = v.x 110 | } 111 | if (v.y < yMin) { 112 | yMin = v.y 113 | } 114 | if (v.y > yMax) { 115 | yMax = v.y 116 | } 117 | positions.push(v) 118 | if (point.value < minValue) { 119 | minValue = point.value 120 | } 121 | 122 | if (point.value > maxValue) { 123 | maxValue = point.value 124 | } 125 | 126 | values.push(point.value) 127 | } 128 | return { 129 | bounds: [xMin, yMin, xMax, yMax], 130 | positions, 131 | minValue, 132 | maxValue, 133 | values, 134 | } 135 | } 136 | 137 | /** 138 | * 139 | * @param bounds 140 | * @returns {function(*): [number,number]} 141 | * @private 142 | */ 143 | _generateCanvasProjector(bounds) { 144 | const pad = this._options.pad 145 | const clamp = this._options.clamp 146 | const [xMin, yMin, xMax, yMax] = bounds 147 | const px = Math.abs(xMax - xMin) * pad 148 | const py = Math.abs(yMax - yMin) * pad 149 | 150 | const x_min = xMin - px 151 | const x_max = xMax + px 152 | const y_min = yMin - py 153 | const y_max = yMax + py 154 | 155 | const invW = 1.0 / Math.abs(x_max - x_min) 156 | const invH = 1.0 / Math.abs(y_max - y_min) 157 | 158 | return (v) => { 159 | const u = (v.x - x_min) * invW 160 | const vNorm = 1.0 - (v.y - y_min) * invH 161 | let x = u * this._options.width 162 | let y = vNorm * this._options.height 163 | if (clamp) { 164 | x = Math.min(this._options.width, Math.max(0, x)) 165 | y = Math.min(this._options.height, Math.max(0, y)) 166 | } 167 | return [x, y] 168 | } 169 | } 170 | 171 | /** 172 | * 173 | * @param points 174 | * @returns {HeatMap} 175 | */ 176 | setPoints(points) { 177 | let { bounds, positions, values, minValue, maxValue } = this._parse(points) 178 | 179 | let toCanvas = this._generateCanvasProjector(bounds) 180 | let heatMapData = positions.map((v, index) => { 181 | const [cx, cy] = toCanvas(v) 182 | return { x: Math.floor(cx), y: Math.floor(cy), value: values[index] } 183 | }) 184 | this._colorMap.setData({ 185 | min: minValue, 186 | max: maxValue, 187 | data: heatMapData, 188 | }) 189 | this._grayMap.setData({ 190 | min: minValue, 191 | max: maxValue, 192 | data: heatMapData, 193 | }) 194 | 195 | this._colorTexture.needsUpdate = true 196 | this._grayTexture.needsUpdate = true 197 | this._delegate.geometry.dispose() 198 | const width = Math.abs(bounds[2] - bounds[0]) 199 | const height = Math.abs(bounds[3] - bounds[1]) 200 | this._delegate.geometry = new PlaneGeometry(width, height, 300, 300) 201 | this._position.set( 202 | (bounds[0] + bounds[2]) / 2, 203 | (bounds[1] + bounds[3]) / 2, 204 | 0 205 | ) 206 | this._size.set(width, height, this._options.heightFactor * maxValue) 207 | this._delegate.position.copy(this._position) 208 | 209 | return this 210 | } 211 | } 212 | 213 | export default HeatMap 214 | -------------------------------------------------------------------------------- /examples/src/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | export * from './utils/index.js' 5 | export * from './overlay/index.js' 6 | export * from './loaders/index.js' 7 | export { default as HeatMap } from './heat-map/HeatMap.js' 8 | -------------------------------------------------------------------------------- /examples/src/modules/loaders/ModelLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DRACOLoader, 3 | KTX2Loader, 4 | GLTFLoader, 5 | FBXLoader, 6 | OBJLoader, 7 | } from 'three/addons' 8 | import { LoadingManager } from 'three' 9 | 10 | const loadingManager = new LoadingManager() 11 | 12 | const dracoLoader = new DRACOLoader(loadingManager) 13 | const ktx2loader = new KTX2Loader(loadingManager) 14 | const gltfLoader = new GLTFLoader(loadingManager) 15 | const fbxLoader = new FBXLoader(loadingManager) 16 | const objLoader = new OBJLoader(loadingManager) 17 | 18 | gltfLoader.setDRACOLoader(dracoLoader) 19 | gltfLoader.setKTX2Loader(ktx2loader) 20 | 21 | class ModelLoader { 22 | /** 23 | * 24 | * @returns {LoadingManager} 25 | */ 26 | static getLoadingManager() { 27 | return loadingManager 28 | } 29 | 30 | /** 31 | * 32 | * @param options 33 | */ 34 | static setDracoLoader(options = {}) { 35 | options.path && dracoLoader.setDecoderPath(options.path) 36 | return this 37 | } 38 | 39 | /** 40 | * 41 | * @returns {DRACOLoader} 42 | */ 43 | static getDracoLoader() { 44 | return dracoLoader 45 | } 46 | 47 | static setKtx2loader(options = {}) { 48 | options.path && ktx2loader.setTranscoderPath(options.path) 49 | options.renderer && ktx2loader.detectSupport(options.renderer) 50 | return this 51 | } 52 | 53 | /** 54 | * 55 | * @param options 56 | * @returns {KTX2Loader} 57 | */ 58 | static getKtx2loader() { 59 | return ktx2loader 60 | } 61 | 62 | /** 63 | * 64 | * @param url 65 | * @returns {Promise} 66 | */ 67 | static loadGLTF(url) { 68 | return new Promise((resolve, reject) => { 69 | gltfLoader.load( 70 | url, 71 | (gltf) => { 72 | resolve(gltf) 73 | }, 74 | () => {}, 75 | () => { 76 | reject() 77 | } 78 | ) 79 | }) 80 | } 81 | 82 | /** 83 | * 84 | * @param data 85 | * @param path 86 | * @returns {Promise} 87 | */ 88 | static parseGLTF(data, path) { 89 | return new Promise((resolve, reject) => { 90 | gltfLoader.parse( 91 | data, 92 | path, 93 | (gltf) => { 94 | resolve(gltf) 95 | }, 96 | () => { 97 | reject() 98 | } 99 | ) 100 | }) 101 | } 102 | 103 | /** 104 | * 105 | * @param url 106 | * @returns {Promise} 107 | */ 108 | static loadFbx(url) { 109 | return new Promise((resolve, reject) => { 110 | fbxLoader.load( 111 | url, 112 | (fbx) => { 113 | resolve(fbx) 114 | }, 115 | () => {}, 116 | () => { 117 | reject() 118 | } 119 | ) 120 | }) 121 | } 122 | 123 | /** 124 | * 125 | * @param data 126 | * @param path 127 | * @returns {Promise} 128 | */ 129 | static parseFbx(data, path) { 130 | return new Promise((resolve) => { 131 | resolve(fbxLoader.parse(data, path)) 132 | }) 133 | } 134 | 135 | /** 136 | * 137 | * @param url 138 | * @returns {Promise} 139 | */ 140 | static loadObj(url) { 141 | return new Promise((resolve, reject) => { 142 | objLoader.load( 143 | url, 144 | (obj) => { 145 | resolve(obj) 146 | }, 147 | () => {}, 148 | () => { 149 | reject() 150 | } 151 | ) 152 | }) 153 | } 154 | 155 | /** 156 | * 157 | * @param data 158 | * @returns {Promise} 159 | */ 160 | static parseObj(data) { 161 | return new Promise((resolve) => { 162 | resolve(objLoader.parse(data)) 163 | }) 164 | } 165 | } 166 | 167 | export default ModelLoader 168 | -------------------------------------------------------------------------------- /examples/src/modules/loaders/PlyLoader.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/src/modules/loaders/PlyLoader.js -------------------------------------------------------------------------------- /examples/src/modules/loaders/SplatLoader.js: -------------------------------------------------------------------------------- 1 | import { SplatMesh } from '../extensions/index.js' 2 | 3 | const rowLength = 3 * 4 + 3 * 4 + 4 + 4 4 | 5 | class SplatLoader { 6 | constructor() {} 7 | 8 | /** 9 | * 10 | * @param url 11 | * @param onDone 12 | */ 13 | loadData(url, onDone) { 14 | fetch(url).then(async (res) => { 15 | const reader = res.body.getReader() 16 | const chunks = [] 17 | let receivedLength = 0 18 | // eslint-disable-next-line no-constant-condition 19 | while (true) { 20 | const { done, value } = await reader.read() 21 | if (done) break 22 | chunks.push(value) 23 | receivedLength += value.length 24 | } 25 | const buffer = new Uint8Array(receivedLength) 26 | let offset = 0 27 | for (const chunk of chunks) { 28 | buffer.set(chunk, offset) 29 | offset += chunk.length 30 | } 31 | const vertexCount = Math.floor(receivedLength / rowLength) 32 | onDone(buffer.buffer, vertexCount) 33 | }) 34 | } 35 | 36 | /** 37 | * 38 | * @param url 39 | * @param onDone 40 | * @returns {SplatLoader} 41 | */ 42 | loadDataStream(url, onDone, onPrecess) { 43 | fetch(url).then(async (res) => { 44 | const reader = res.body.getReader() 45 | const totalBytes = parseInt(res.headers.get('Content-Length') || 0) 46 | const vertexCount = Math.floor(totalBytes / rowLength) 47 | onDone && onDone(vertexCount) 48 | let leftover = new Uint8Array(0) // 存残余字节 49 | // eslint-disable-next-line no-constant-condition 50 | while (true) { 51 | try { 52 | const { value, done } = await reader.read() 53 | if (done) break 54 | // 存储上一次多余的字节和这一次读取到字节 55 | const buffer = new Uint8Array(leftover.length + value.length) 56 | buffer.set(leftover, 0) 57 | buffer.set(value, leftover.length) 58 | // 计算出合并的高斯数量 59 | const vertexCount = Math.floor(buffer.length / rowLength) 60 | if (vertexCount) { 61 | const vertexBytes = vertexCount * rowLength 62 | const vertexData = buffer.subarray(0, vertexBytes) // 保证处理的数据为 N * rowLength 63 | onPrecess && onPrecess(vertexData.buffer, vertexCount) 64 | } 65 | // 更新leftover,存储多出来的数字节,字节长度可能不足 rowLength,需要存储下来,用于下一次计算 66 | leftover = buffer.subarray( 67 | buffer.length - (buffer.length % rowLength) 68 | ) 69 | } catch (error) { 70 | console.error(error) 71 | break 72 | } 73 | } 74 | if (leftover.length) { 75 | const vertexCount = Math.floor(leftover.length / rowLength) 76 | if (vertexCount) { 77 | onPrecess && onPrecess(leftover.buffer, vertexCount) 78 | } 79 | } 80 | }) 81 | return this 82 | } 83 | 84 | /** 85 | * 86 | * @param url 87 | * @param onDone 88 | * @returns {SplatLoader} 89 | */ 90 | load(url, onDone) { 91 | this.loadData(url, async (buffer, vertexCount) => { 92 | const mesh = new SplatMesh() 93 | mesh.vertexCount = vertexCount 94 | await mesh.setDataFromBuffer(buffer) 95 | onDone && onDone(mesh) 96 | }) 97 | return this 98 | } 99 | 100 | /** 101 | * 102 | * @param url 103 | * @param onDone 104 | * @returns {SplatLoader} 105 | */ 106 | loadStream(url, onDone) { 107 | let mesh = null 108 | this.loadDataStream( 109 | url, 110 | (vertexCount) => { 111 | mesh = new SplatMesh() 112 | mesh.vertexCount = vertexCount 113 | onDone && onDone(mesh) 114 | }, 115 | async (buffer, vertexCount) => { 116 | if (mesh) { 117 | await mesh.appendDataFromBuffer(buffer, vertexCount) 118 | } 119 | } 120 | ) 121 | return this 122 | } 123 | } 124 | 125 | export default SplatLoader 126 | -------------------------------------------------------------------------------- /examples/src/modules/loaders/index.js: -------------------------------------------------------------------------------- 1 | export { default as ModelLoader } from './ModelLoader.js' 2 | export { default as SplatLoader } from './SplatLoader.js' 3 | -------------------------------------------------------------------------------- /examples/src/modules/material/MaterialCache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | import BillboardMaterial from './types/BillboardMaterial.js' 6 | 7 | const cache = {} 8 | 9 | class MaterialCache { 10 | /** 11 | * 12 | * @param options 13 | * @returns {*} 14 | */ 15 | static createMaterial(options) { 16 | let key = '' 17 | if (options.type === 'billboard') { 18 | key = options.type + '-' + options.image 19 | if (!cache[key]) { 20 | cache[key] = new BillboardMaterial(options) 21 | } 22 | } 23 | return cache[key] 24 | } 25 | } 26 | 27 | export default MaterialCache 28 | -------------------------------------------------------------------------------- /examples/src/modules/material/index.js: -------------------------------------------------------------------------------- 1 | export { default as PointMaterial } from './types/PointMaterial.js' 2 | export { default as HeatMapMaterial } from './types/HeatMapMaterial.js' 3 | export { default as BillboardMaterial } from './types/BillboardMaterial.js' 4 | export { default as MaterialCache } from './MaterialCache.js' 5 | -------------------------------------------------------------------------------- /examples/src/modules/material/types/BillboardMaterial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | import { SpriteMaterial, TextureLoader } from 'three' 5 | 6 | const _textureLoader = new TextureLoader() 7 | 8 | class BillboardMaterial extends SpriteMaterial { 9 | constructor(options = {}) { 10 | super({ 11 | depthWrite: !!options.depthWrite, 12 | depthTest: !!options.depthTest, 13 | transparent: true, 14 | map: _textureLoader.load(options.image), 15 | }) 16 | this._image = options.image 17 | } 18 | 19 | set image(image) { 20 | this._image = image 21 | this.map = _textureLoader.load(image) 22 | } 23 | 24 | get image() { 25 | return this._image 26 | } 27 | } 28 | export default BillboardMaterial 29 | -------------------------------------------------------------------------------- /examples/src/modules/material/types/HeatMapMaterial.js: -------------------------------------------------------------------------------- 1 | import { Color, ShaderMaterial } from 'three' 2 | import heat_map_vs from '../../shaders/heat_map_vs_glsl.js' 3 | import heat_map_fs from '../../shaders/heat_map_fs_glsl.js' 4 | 5 | class HeatMapMaterial extends ShaderMaterial { 6 | constructor(options = {}) { 7 | super({ 8 | depthWrite: true, 9 | depthTest: true, 10 | transparent: true, 11 | vertexShader: heat_map_vs, 12 | fragmentShader: heat_map_fs, 13 | uniforms: { 14 | heatMap: { value: options.colorTexture }, 15 | greyMap: { value: options.grayTexture }, 16 | heightFactor: { value: options.heightFactor }, 17 | u_color: { value: options.color || new Color().setStyle('#ffffff') }, 18 | u_opacity: { value: options.opacity ?? 1.0 }, 19 | }, 20 | }) 21 | } 22 | } 23 | 24 | export default HeatMapMaterial 25 | -------------------------------------------------------------------------------- /examples/src/modules/material/types/PointMaterial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | import { ShaderMaterial, Color } from 'three' 5 | import point_vs from '../../shaders/point_vs.glsl.js' 6 | import point_fs from '../../shaders/point_fs.glsl.js' 7 | 8 | class PointMaterial extends ShaderMaterial { 9 | constructor(options = {}) { 10 | super({ 11 | vertexShader: point_vs, 12 | fragmentShader: point_fs, 13 | depthWrite: !!options.depthWrite, 14 | depthTest: !!options.depthTest, 15 | transparent: !!options.transparent, 16 | uniforms: { 17 | pixelSize: { 18 | value: 30, 19 | }, 20 | color: { value: options.color || new Color().setStyle('#ffffff') }, 21 | outlineWidth: { 22 | value: 3, 23 | }, 24 | outlineColor: { 25 | value: options.color || new Color().setStyle('#0000ff'), 26 | }, 27 | }, 28 | }) 29 | } 30 | 31 | set color(color) { 32 | this.uniforms.color.value.copy(color) 33 | } 34 | 35 | get color() { 36 | return this.uniforms.color.value 37 | } 38 | 39 | set pixelSize(pixelSize) { 40 | this.uniforms.pixelSize.value = pixelSize * 30 41 | } 42 | 43 | get pixelSize() { 44 | return this.uniforms.pixelSize.value 45 | } 46 | 47 | set outlineWidth(outlineWidth) { 48 | this.uniforms.outlineWidth.value = outlineWidth 49 | } 50 | 51 | get outlineWidth() { 52 | return this.uniforms.outlineWidth.value 53 | } 54 | 55 | set outlineColor(outlineColor) { 56 | this.uniforms.outlineColo.copy(outlineColor) 57 | } 58 | 59 | get outlineColor() { 60 | return this.uniforms.outlineColor.value 61 | } 62 | } 63 | 64 | export default PointMaterial 65 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/Overlay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | import { EventDispatcher, Vector3 } from 'three' 5 | import { SceneTransform } from '@dvt3d/maplibre-three-plugin' 6 | import { Util } from '../utils/index.js' 7 | 8 | class Overlay { 9 | constructor() { 10 | this._id = Util.uuid() 11 | this._delegate = undefined 12 | this._style = {} 13 | this._show = true 14 | this._position = new Vector3() 15 | this._event = new EventDispatcher() 16 | this._type = 'overlay' 17 | } 18 | 19 | get id() { 20 | return this._id 21 | } 22 | 23 | get type() { 24 | return this._type 25 | } 26 | 27 | get delegate() { 28 | return this._delegate 29 | } 30 | 31 | set show(show) { 32 | if (this._show === show) { 33 | return 34 | } 35 | this._show = show 36 | this._delegate.visible = show 37 | } 38 | 39 | get show() { 40 | return this._show 41 | } 42 | 43 | set position(position) { 44 | this._position = position 45 | this._delegate.position.copy(this._position) 46 | } 47 | 48 | get position() { 49 | return this._position 50 | } 51 | 52 | get positionDegrees() { 53 | return SceneTransform.vector3ToLngLat(this._position) 54 | } 55 | 56 | /** 57 | * 58 | * @returns {Overlay} 59 | */ 60 | updateMatrixWorld() { 61 | this._delegate.updateMatrixWorld() 62 | return this 63 | } 64 | 65 | /** 66 | * 67 | * @param style 68 | * @returns {Overlay} 69 | */ 70 | setStyle(style) { 71 | this._style = style 72 | return this 73 | } 74 | 75 | /** 76 | * 77 | * @param type 78 | * @param callback 79 | * @returns {Overlay} 80 | */ 81 | on(type, callback) { 82 | this._event.addEventListener(type, callback) 83 | return this 84 | } 85 | 86 | /** 87 | * 88 | * @param type 89 | * @param callback 90 | * @returns {Overlay} 91 | */ 92 | off(type, callback) { 93 | this._event.removeEventListener(type, callback) 94 | return this 95 | } 96 | 97 | /** 98 | * 99 | * @param type 100 | * @param params 101 | * @returns {Overlay} 102 | */ 103 | fire(type, params = {}) { 104 | this._event.dispatchEvent({ 105 | type: type, 106 | params: params, 107 | }) 108 | return this 109 | } 110 | } 111 | 112 | export default Overlay 113 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | export { default as Overlay } from './Overlay.js' 6 | export { default as Tileset } from './types/Tileset.js' 7 | export { default as Model } from './types/Model.js' 8 | export { default as Point } from './types/Point.js' 9 | export { default as PointCollection } from './types/PointCollection.js' 10 | export { default as Billboard } from './types/Billboard.js' 11 | export { default as DivIcon } from './types/DivIcon.js' 12 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Billboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | import { Group, Sprite } from 'three' 6 | import Overlay from '../Overlay.js' 7 | import { Util } from '../../utils/index.js' 8 | import { MaterialCache } from '../../material/index.js' 9 | 10 | class Billboard extends Overlay { 11 | constructor(position, image) { 12 | super() 13 | if (!position) { 14 | throw 'position is required' 15 | } 16 | if (!image) { 17 | throw 'image is required' 18 | } 19 | this._position = position 20 | this._image = image 21 | this._delegate = new Group() 22 | this._delegate.name = 'billboard-root' 23 | this._object3d = new Sprite( 24 | MaterialCache.createMaterial({ 25 | type: 'billboard', 26 | image: this._image, 27 | }) 28 | ) 29 | this._object3d.position.copy(this._position) 30 | this._delegate.add(this._object3d) 31 | this._type = 'Billboard' 32 | } 33 | 34 | /** 35 | * 36 | * @param style 37 | * @returns {Billboard} 38 | */ 39 | setStyle(style) { 40 | if (!style || Object.keys(style).length === 0) { 41 | return this 42 | } 43 | Util.merge(this._style, style) 44 | if (this._object3d.material) { 45 | Util.merge(this._object3d.material, this._style) 46 | this._object3d.material.needsUpdate = true 47 | } 48 | return this 49 | } 50 | } 51 | 52 | export default Billboard 53 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Circle.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/src/modules/overlay/types/Circle.js -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/DivIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | import { CSS3DSprite } from 'three/addons' 6 | import Overlay from '../Overlay.js' 7 | import { Util } from '../../utils/index.js' 8 | 9 | class DivIcon extends Overlay { 10 | constructor(position, content) { 11 | if (!position) { 12 | throw 'position is required' 13 | } 14 | if (!content) { 15 | throw 'content is required' 16 | } 17 | super() 18 | this._position = position 19 | this._content = content 20 | this._wrapper = document.createElement('div') 21 | this._wrapper.className = 'div-icon' 22 | 23 | if (typeof content === 'string') { 24 | this._wrapper.innerHTML = content 25 | } else if (content instanceof Element) { 26 | while (this._wrapper.hasChildNodes()) { 27 | this._wrapper.removeChild(this._wrapper.firstChild) 28 | } 29 | this._wrapper.appendChild(content) 30 | } 31 | this._delegate = new CSS3DSprite(this._wrapper) 32 | this._delegate.position.copy(position) 33 | this._type = 'DivIcon' 34 | } 35 | 36 | /** 37 | * 38 | * @param style 39 | * @returns {DivIcon} 40 | */ 41 | setStyle(style) { 42 | if (!style || Object.keys(style).length === 0) { 43 | return this 44 | } 45 | Util.merge(this._style, style) 46 | if (style.className) { 47 | this._wrapper.className = 'div-icon' 48 | this._wrapper.classList.add(style.className) 49 | } 50 | return this 51 | } 52 | } 53 | 54 | export default DivIcon 55 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | import { Box3, Vector3 } from 'three' 5 | import { ModelLoader } from '../../loaders/index.js' 6 | import { Creator, SceneTransform } from '@dvt3d/maplibre-three-plugin' 7 | import Overlay from '../Overlay.js' 8 | 9 | const _box = new Box3() 10 | class Model extends Overlay { 11 | constructor(content, options) { 12 | super() 13 | this._content = content 14 | this._position = options.position 15 | this._delegate = Creator.createRTCGroup( 16 | SceneTransform.vector3ToLngLat(this._position) 17 | ) 18 | this._delegate.name = 'model-root' 19 | this._delegate.add(this._content) 20 | this._size = new Vector3() 21 | _box.setFromObject(this._content, true).getSize(this._size) 22 | this._castShadow = false 23 | this._type = 'Model' 24 | } 25 | 26 | get size() { 27 | return this._size 28 | } 29 | 30 | set castShadow(castShadow) { 31 | if (this._castShadow === castShadow) { 32 | return 33 | } 34 | this._castShadow = castShadow 35 | this._content.traverse((obj) => { 36 | if (obj.isMesh) obj.castShadow = this._castShadow 37 | }) 38 | } 39 | 40 | get castShadow() { 41 | return this._castShadow 42 | } 43 | 44 | /** 45 | * 46 | * @param options 47 | * @returns {Promise} 48 | */ 49 | static async fromGltfAsync(options = {}) { 50 | if (!options.url) { 51 | throw 'url is required' 52 | } 53 | if (!options.position) { 54 | throw 'position is required' 55 | } 56 | let gltf = await ModelLoader.loadGLTF(options.url) 57 | let model = new Model(gltf.scene, options) 58 | model.castShadow = options.castShadow 59 | return model 60 | } 61 | 62 | /** 63 | * 64 | * @param options 65 | * @returns {Promise} 66 | */ 67 | static async fromB3dmAsync(options) { 68 | return 69 | } 70 | } 71 | 72 | export default Model 73 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Point.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | import { Group, Points, Float32BufferAttribute } from 'three' 6 | import Overlay from '../Overlay.js' 7 | import { Util } from '../../utils/index.js' 8 | import { PointMaterial } from '../../material/index.js' 9 | 10 | class Point extends Overlay { 11 | constructor(position) { 12 | super() 13 | this._position = position 14 | this._delegate = new Group() 15 | this._delegate.name = 'point-root' 16 | this._delegate.position.copy(position) 17 | 18 | this._object3d = new Points() 19 | this._object3d.geometry.setAttribute( 20 | 'position', 21 | new Float32BufferAttribute([0, 0, 0], 3) 22 | ) 23 | this._object3d.geometry.needsUpdate = true 24 | this._object3d.material = new PointMaterial() 25 | 26 | this._delegate.add(this._object3d) 27 | this._type = 'Point' 28 | } 29 | 30 | /** 31 | * 32 | * @param style 33 | * @returns {Point} 34 | */ 35 | setStyle(style) { 36 | Util.merge(this._style, style) 37 | if (this._object3d.material) { 38 | Util.merge(this._points.material, this._style) 39 | this._object3d.material.needsUpdate = true 40 | } 41 | return this 42 | } 43 | } 44 | 45 | export default Point 46 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/PointCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | import { Group, Points, Float32BufferAttribute } from 'three' 6 | import Overlay from '../Overlay.js' 7 | import { SceneTransform } from '@dvt3d/maplibre-three-plugin' 8 | import { Util } from '../../utils/index.js' 9 | import PointMaterial from '../../material/types/PointMaterial.js' 10 | 11 | class PointCollection extends Overlay { 12 | constructor(positions) { 13 | if (!positions || !positions.length) { 14 | throw 'positions length must be greater than 0' 15 | } 16 | super() 17 | this._positions = positions 18 | this._delegate = new Group() 19 | 20 | this._delegate.name = 'point-collection-root' 21 | this._delegate.position.copy(this._positions[0]) 22 | 23 | this._object3d = new Points() 24 | this._object3d.geometry.setAttribute( 25 | 'position', 26 | new Float32BufferAttribute( 27 | this._positions 28 | .map((position) => position.clone().sub(this._positions[0]).toArray()) 29 | .flat(), 30 | 3 31 | ) 32 | ) 33 | this._object3d.geometry.needsUpdate = true 34 | this._object3d.material = new PointMaterial() 35 | this._delegate.add(this._object3d) 36 | this._type = 'PointCollection' 37 | } 38 | 39 | /** 40 | * 41 | * @param positions 42 | */ 43 | set positions(positions) { 44 | if (!positions || !positions.length) { 45 | throw 'positions length must be greater than 0' 46 | } 47 | this._positions = positions 48 | this._delegate.position.copy(this._positions[0]) 49 | this._object3d.geometry.setAttribute( 50 | 'position', 51 | new Float32BufferAttribute( 52 | this._positions 53 | .map((position) => position.clone().sub(this._positions[0]).toArray()) 54 | .flat(), 55 | 3 56 | ) 57 | ) 58 | this._object3d.geometry.needsUpdate = true 59 | } 60 | 61 | get positions() { 62 | return this._positions 63 | } 64 | 65 | get position() { 66 | return this._positions[0] 67 | } 68 | 69 | get positionDegrees() { 70 | return SceneTransform.vector3ToLngLat(this._positions[0]) 71 | } 72 | 73 | /** 74 | * 75 | * @param style 76 | * @returns {PointCollection} 77 | */ 78 | setStyle(style) { 79 | Util.merge(this._style, style) 80 | if (this._object3d.material) { 81 | Util.merge(this._object3d.material, this._style) 82 | this._object3d.material.needsUpdate = true 83 | } 84 | return this 85 | } 86 | } 87 | 88 | export default PointCollection 89 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Polygon.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/src/modules/overlay/types/Polygon.js -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Polyline.js: -------------------------------------------------------------------------------- 1 | import Overlay from '../Overlay.js' 2 | 3 | class Polyline extends Overlay { 4 | constructor(positions) { 5 | if (!positions || !positions.length) { 6 | throw 'positions length must be greater than 1' 7 | } 8 | super() 9 | this._positions = positions 10 | } 11 | } 12 | 13 | export default Polyline 14 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Splat.js: -------------------------------------------------------------------------------- 1 | import Overlay from '../Overlay.js' 2 | import { SplatMesh } from '../../extensions/index.js' 3 | 4 | class Splat extends Overlay { 5 | constructor(options = {}) { 6 | super() 7 | this._delegate = new SplatMesh(options.data, options.numVertexes) 8 | } 9 | 10 | static async fromGltfAsync() {} 11 | 12 | static async fromSpzAsync() {} 13 | 14 | static async fromSplatAsync() {} 15 | } 16 | 17 | export default Splat 18 | -------------------------------------------------------------------------------- /examples/src/modules/overlay/types/Tileset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Caven Chen 3 | */ 4 | 5 | import { Group, Vector3, Matrix4, Box3, Sphere } from 'three' 6 | import { TilesRenderer } from '3d-tiles-renderer' 7 | import { 8 | GLTFExtensionsPlugin, 9 | UnloadTilesPlugin, 10 | DebugTilesPlugin, 11 | TilesFadePlugin, 12 | CesiumIonAuthPlugin, 13 | UpdateOnChangePlugin, 14 | } from '3d-tiles-renderer/plugins' 15 | import { SceneTransform } from '@dvt3d/maplibre-three-plugin' 16 | import Overlay from '../Overlay.js' 17 | import { Util } from '../../utils/index.js' 18 | import { 19 | GaussianSplattingTilesetPlugin, 20 | GLTFGaussianSplattingExtension, 21 | GLTFKtx2TextureInspectorPlugin, 22 | GLTFSpzGaussianSplattingExtension, 23 | } from '../../extensions/index.js' 24 | 25 | const _box = new Box3() 26 | const _sphere = new Sphere() 27 | 28 | const DEF_OPTS = { 29 | fetchOptions: { 30 | mode: 'cors', 31 | }, 32 | lruCache: { 33 | maxBytesSize: Infinity, 34 | minSize: 6000, 35 | maxSize: 8000, 36 | }, 37 | cesiumIon: { 38 | assetId: '', 39 | apiToken: '', 40 | autoRefreshToken: true, 41 | }, 42 | useDebug: false, 43 | useUnload: false, 44 | useFade: false, 45 | useUpdate: false, 46 | splatThreshold: -0.0001, 47 | } 48 | 49 | class Tileset extends Overlay { 50 | constructor(url, options = {}) { 51 | if (!url) { 52 | throw 'url is required' 53 | } 54 | super() 55 | this._url = url 56 | this._options = options 57 | this._renderer = new TilesRenderer(this._url) 58 | this._renderer.registerPlugin( 59 | new GLTFExtensionsPlugin({ 60 | dracoLoader: options.dracoLoader, 61 | ktxLoader: options.ktxLoader, 62 | plugins: [ 63 | (parser) => new GLTFGaussianSplattingExtension(parser), 64 | (parser) => new GLTFSpzGaussianSplattingExtension(parser), 65 | (parser) => new GLTFKtx2TextureInspectorPlugin(parser), 66 | ], 67 | }) 68 | ) 69 | 70 | if (options.ktxLoader) { 71 | this._renderer.manager.addHandler(/.ktx2/, options.ktxLoader) 72 | } 73 | 74 | this._renderer.registerPlugin( 75 | new GaussianSplattingTilesetPlugin(options.splatThreshold) 76 | ) 77 | 78 | if (options.cesiumIon && options.cesiumIon.apiToken) { 79 | this._renderer.registerPlugin( 80 | new CesiumIonAuthPlugin( 81 | Util.merge({}, DEF_OPTS.cesiumIon, { 82 | apiToken: options.cesiumIon.apiToken, 83 | assetId: this._url, 84 | }) 85 | ) 86 | ) 87 | } 88 | 89 | options.useDebug && this._renderer.registerPlugin(new DebugTilesPlugin()) 90 | 91 | options.useUnload && this._renderer.registerPlugin(new UnloadTilesPlugin()) 92 | 93 | options.useUpdate && 94 | this._renderer.registerPlugin(new UpdateOnChangePlugin()) 95 | 96 | options.useFade && this._renderer.registerPlugin(new TilesFadePlugin()) 97 | 98 | Util.merge( 99 | this._renderer.fetchOptions, 100 | DEF_OPTS.fetchOptions, 101 | this._options.fetchOptions || {} 102 | ) 103 | 104 | Util.merge( 105 | this._renderer.lruCache, 106 | DEF_OPTS.lruCache, 107 | this._options.lruCache || {} 108 | ) 109 | 110 | this._isLoaded = false 111 | 112 | this._delegate = new Group() 113 | this._delegate.name = 'tileset-root' 114 | 115 | this._size = new Vector3() 116 | this._event = this._renderer 117 | 118 | this._type = 'Tileset' 119 | this.on('load-tile-set', this._onTilesLoaded.bind(this)) 120 | } 121 | 122 | get fetchOptions() { 123 | return this._renderer.fetchOptions 124 | } 125 | 126 | set lruCache(lruCache) { 127 | this._renderer.lruCache = lruCache 128 | } 129 | 130 | get lruCache() { 131 | return this._renderer.lruCache 132 | } 133 | 134 | set autoDisableRendererCulling(autoDisableRendererCulling) { 135 | this._renderer.autoDisableRendererCulling = autoDisableRendererCulling 136 | } 137 | 138 | get autoDisableRendererCulling() { 139 | return this._renderer.autoDisableRendererCulling 140 | } 141 | 142 | set errorTarget(errorTarget) { 143 | this._renderer.errorTarget = errorTarget 144 | } 145 | 146 | get errorTarget() { 147 | return this._renderer.errorTarget 148 | } 149 | 150 | get size() { 151 | return this._size 152 | } 153 | 154 | /** 155 | * 156 | * @param e 157 | * @private 158 | */ 159 | _onTilesLoaded(e) { 160 | if (!this._isLoaded) { 161 | this._isLoaded = true 162 | const center = new Vector3() 163 | if (this._renderer.getBoundingBox(_box)) { 164 | _box.getCenter(center) 165 | _box.getSize(this._size) 166 | } else if (this._renderer.getBoundingSphere(_sphere)) { 167 | center.copy(_sphere.center) 168 | this._size.set(_sphere.radius, _sphere.radius, _sphere.radius) 169 | } else { 170 | return 171 | } 172 | 173 | const cartographic = { lon: 0, lat: 0, height: 0 } 174 | 175 | this._renderer.ellipsoid.getPositionToCartographic(center, cartographic) 176 | 177 | const positionDegrees = { 178 | lng: (cartographic.lon * 180) / Math.PI, 179 | lat: (cartographic.lat * 180) / Math.PI, 180 | height: cartographic.height, 181 | } 182 | 183 | this._position = SceneTransform.lngLatToVector3( 184 | positionDegrees.lng, 185 | positionDegrees.lat, 186 | positionDegrees.height 187 | ) 188 | this._delegate.position.copy(this._position) 189 | 190 | const scale = SceneTransform.projectedUnitsPerMeter(positionDegrees.lat) 191 | 192 | this._delegate.scale.set(scale, scale, scale) 193 | 194 | if ( 195 | !this._renderer.rootTileSet.asset.gltfUpAxis || 196 | this._renderer.rootTileSet.asset.gltfUpAxis === 'Z' 197 | ) { 198 | this._delegate.rotateX(Math.PI) 199 | } 200 | 201 | this._delegate.rotateY(Math.PI) 202 | this._delegate.updateMatrixWorld() 203 | 204 | const enuMatrix = this._renderer.ellipsoid.getEastNorthUpFrame( 205 | cartographic.lat, 206 | cartographic.lon, 207 | cartographic.height, 208 | new Matrix4() 209 | ) 210 | 211 | const modelMatrix = enuMatrix.clone().invert() 212 | this._renderer.group.applyMatrix4(modelMatrix) 213 | this._renderer.group.updateMatrixWorld() 214 | this._delegate.add(this._renderer.group) 215 | 216 | this.fire('loaded') 217 | } 218 | this._renderer.removeEventListener( 219 | 'load-tile-set', 220 | this._onTilesLoaded.bind(this) 221 | ) 222 | } 223 | 224 | /** 225 | * 226 | * @param scene 227 | */ 228 | update(frameState) { 229 | this._renderer.setCamera(frameState.camera) 230 | this._renderer.setResolutionFromRenderer( 231 | frameState.camera, 232 | frameState.renderer 233 | ) 234 | this._renderer.update() 235 | } 236 | 237 | /** 238 | * 239 | * @param height 240 | * @returns {Tileset} 241 | */ 242 | setHeight(height) { 243 | const positionDegrees = this.positionDegrees 244 | this._position = SceneTransform.lngLatToVector3( 245 | positionDegrees[0], 246 | positionDegrees[1], 247 | positionDegrees[2] + height 248 | ) 249 | this._delegate.position.copy(this._position) 250 | return this 251 | } 252 | 253 | /** 254 | * 255 | * @param rotation 256 | * @returns {Tileset} 257 | */ 258 | setRotation(rotation) { 259 | if (rotation[0]) { 260 | this._delegate.rotateX(rotation[0]) 261 | } 262 | 263 | if (rotation[1]) { 264 | this._delegate.rotateY(rotation[1]) 265 | } 266 | 267 | if (rotation[2]) { 268 | this._delegate.rotateZ(rotation[2]) 269 | } 270 | 271 | return this 272 | } 273 | 274 | /** 275 | * 276 | * @returns {Tileset} 277 | */ 278 | destroy() { 279 | this._renderer.dispose() 280 | this._renderer = null 281 | return this 282 | } 283 | } 284 | 285 | export default Tileset 286 | -------------------------------------------------------------------------------- /examples/src/modules/shaders/gaussian_splatting_fs_glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | in vec4 vColor; 3 | in vec2 vPosition; 4 | 5 | void main () { 6 | float A = -dot(vPosition, vPosition); 7 | if (A < -4.0) discard; 8 | float B = exp(A) * vColor.a; 9 | gl_FragColor = vec4(vColor.rgb, B); 10 | } 11 | ` 12 | -------------------------------------------------------------------------------- /examples/src/modules/shaders/gaussian_splatting_vs_glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | precision highp sampler2D; 3 | precision highp usampler2D; 4 | 5 | out vec4 vColor; 6 | out vec2 vPosition; 7 | uniform vec2 viewport; 8 | uniform mat4 gsModelViewMatrix; 9 | 10 | attribute uint splatIndex; 11 | uniform sampler2D centerAndScaleTexture; 12 | uniform usampler2D covAndColorTexture; 13 | 14 | vec2 unpackInt16(in uint value) { 15 | int v = int(value); 16 | int v0 = v >> 16; 17 | int v1 = (v & 0xFFFF); 18 | if((v & 0x8000) != 0) 19 | v1 |= 0xFFFF0000; 20 | return vec2(float(v1), float(v0)); 21 | } 22 | 23 | vec4 calcCovVectors(vec3 viewPos, mat3 Vrk) { 24 | float focal = (viewport.y / 2.0) * abs(projectionMatrix[1][1]); 25 | mat3 J = mat3( 26 | focal / viewPos.z, 0., -(focal * viewPos.x) / (viewPos.z * viewPos.z), 27 | 0., focal / viewPos.z, -(focal * viewPos.y) / (viewPos.z * viewPos.z), 28 | 0., 0., 0. 29 | ); 30 | mat3 W = transpose(mat3(gsModelViewMatrix)); 31 | mat3 T = W * J; 32 | mat3 cov = transpose(T) * Vrk * T; 33 | float diagonal1 = cov[0][0] + 0.3; 34 | float offDiagonal = cov[0][1]; 35 | float diagonal2 = cov[1][1] + 0.3; 36 | float mid = 0.5 * (diagonal1 + diagonal2); 37 | float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal)); 38 | float lambda1 = mid + radius; 39 | float lambda2 = max(mid - radius, 0.1); 40 | vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1)); 41 | vec2 v1 = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector; 42 | vec2 v2 = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x); 43 | return vec4(v1,v2); 44 | } 45 | 46 | 47 | void main () { 48 | 49 | ivec2 texSize = textureSize(centerAndScaleTexture, 0); 50 | ivec2 texPos = ivec2(splatIndex % uint(texSize.x), splatIndex / uint(texSize.x)); 51 | 52 | vec4 centerAndScaleData = texelFetch(centerAndScaleTexture, texPos, 0); 53 | vec4 viewPos = gsModelViewMatrix * vec4(centerAndScaleData.xyz, 1); 54 | 55 | vec4 pos2d = projectionMatrix * viewPos; 56 | float clip = 1.2 * pos2d.w; 57 | 58 | if (pos2d.z < -pos2d.w || pos2d.x < -clip || pos2d.x > clip 59 | || pos2d.y < -clip || pos2d.y > clip) { 60 | gl_Position = vec4(0.0, 0.0, 2.0, 1.0); 61 | return; 62 | } 63 | 64 | uvec4 covAndColorData = texelFetch(covAndColorTexture, texPos, 0); 65 | vec2 cov3D_M11_M12 = unpackInt16(covAndColorData.x) * centerAndScaleData.w; 66 | vec2 cov3D_M13_M22 = unpackInt16(covAndColorData.y) * centerAndScaleData.w; 67 | vec2 cov3D_M23_M33 = unpackInt16(covAndColorData.z) * centerAndScaleData.w; 68 | mat3 Vrk = mat3( 69 | cov3D_M11_M12.x, cov3D_M11_M12.y, cov3D_M13_M22.x, 70 | cov3D_M11_M12.y, cov3D_M13_M22.y, cov3D_M23_M33.x, 71 | cov3D_M13_M22.x, cov3D_M23_M33.x, cov3D_M23_M33.y 72 | ); 73 | 74 | vec4 covVectors = calcCovVectors(viewPos.xyz, Vrk); 75 | vPosition = position.xy; 76 | uint colorUint = covAndColorData.w; 77 | vColor = vec4( 78 | float(colorUint & uint(0xFF)) / 255.0, 79 | float((colorUint >> uint(8)) & uint(0xFF)) / 255.0, 80 | float((colorUint >> uint(16)) & uint(0xFF)) / 255.0, 81 | float(colorUint >> uint(24)) / 255.0 82 | ); 83 | vec2 vCenter = vec2(pos2d) / pos2d.w; 84 | gl_Position = vec4( vCenter + (position.x * covVectors.zw + position.y * covVectors.xy) / viewport * 2.0, pos2d.z / pos2d.w, 1.0); 85 | } 86 | ` 87 | -------------------------------------------------------------------------------- /examples/src/modules/shaders/heat_map_fs_glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | precision highp float; 3 | varying vec2 vUv; 4 | uniform sampler2D heatMap; 5 | uniform vec3 u_color; 6 | uniform float u_opacity; 7 | void main() { 8 | gl_FragColor = vec4(u_color, u_opacity) * texture2D(heatMap, vUv); 9 | } 10 | ` 11 | -------------------------------------------------------------------------------- /examples/src/modules/shaders/heat_map_vs_glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | varying vec2 vUv; 3 | uniform float heightFactor; 4 | uniform sampler2D greyMap; 5 | void main() { 6 | vUv = uv; 7 | vec4 frgColor = texture2D(greyMap, uv); 8 | float height = heightFactor * frgColor.a; 9 | vec3 transformed = vec3( position.x, position.y, height); 10 | gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0); 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /examples/src/modules/shaders/point_fs.glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | precision highp float; 3 | varying vec2 vUv; 4 | uniform vec3 color; 5 | uniform vec3 outlineColor; 6 | uniform float outlineWidth; 7 | void main() { 8 | vec2 coord = gl_PointCoord * 2.0 - 1.0; 9 | float r = length(coord); 10 | if (r > 1.0) { 11 | discard; 12 | }else if (r > ( 1.0 - outlineWidth / 10.0) ) { 13 | gl_FragColor = vec4(outlineColor, 1.0); 14 | } else { 15 | gl_FragColor = vec4(color, 1.0); 16 | } 17 | } 18 | ` 19 | -------------------------------------------------------------------------------- /examples/src/modules/shaders/point_vs.glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | precision highp float; 3 | varying vec2 vUv; 4 | uniform float pixelSize; 5 | void main() { 6 | vUv = uv; 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | gl_PointSize = pixelSize; 9 | } 10 | ` 11 | -------------------------------------------------------------------------------- /examples/src/modules/tasks/WasmTaskProcessor.js: -------------------------------------------------------------------------------- 1 | class WasmTaskProcessor { 2 | /** 3 | * @param {string} options.moduleUrl Glue JS URL (e.g., wasm_spalts.js ESM glue) 4 | * @param {string} [wasmUrl] Optional: wasm file URL (if not provided, the glue resolves automatically) 5 | * @param {"auto"|"path"|"bytes"} [mode="auto"] 6 | * - auto: Prefer string path; if cross-origin/MIME fails, automatically fall back to bytes 7 | * - path: Always pass wasmUrl as a "path string" into init 8 | * - bytes: Always fetch wasmUrl and pass ArrayBuffer into init 9 | */ 10 | constructor(moduleUrl, wasmUrl, mode = 'auto') { 11 | if (!moduleUrl) throw new Error('moduleUrl is required') 12 | this._moduleUrl = moduleUrl 13 | this._wasmUrl = wasmUrl || null 14 | this._mode = mode 15 | this._glue = null // result of import(moduleUrl) 16 | this._init = null // default export of glue (init) 17 | this._ready = false 18 | this._initPromise = null 19 | } 20 | 21 | // get glue() { 22 | // return this.glue 23 | // } 24 | 25 | async init() { 26 | if (this._ready) return 27 | if (this._initPromise) return this._initPromise 28 | this._initPromise = (async () => { 29 | const glue = await import(this._moduleUrl) 30 | const initFn = glue?.default || glue?.init || glue 31 | if (typeof initFn !== 'function') { 32 | throw new Error('Invalid wasm glue: default export init() not found') 33 | } 34 | this._glue = glue 35 | this._init = initFn 36 | if (!this._wasmUrl) { 37 | await this._init() 38 | } else if (this._mode === 'path') { 39 | await this._init(this._wasmUrl) 40 | } else if (this._mode === 'bytes') { 41 | const resp = await fetch(this._wasmUrl) 42 | if (!resp.ok) throw new Error(`Failed to fetch wasm: ${resp.status}`) 43 | const bytes = await resp.arrayBuffer() 44 | await this._init(bytes) 45 | } else { 46 | try { 47 | await this._init(this._wasmUrl) 48 | } catch (e) { 49 | const resp = await fetch(this._wasmUrl) 50 | if (!resp.ok) throw new Error(`Failed to fetch wasm: ${resp.status}`) 51 | const bytes = await resp.arrayBuffer() 52 | await this._init(bytes) 53 | } 54 | } 55 | this._ready = true 56 | })() 57 | return this._initPromise 58 | } 59 | 60 | /** 61 | * Call wasm exported function (direct call on main thread) 62 | * Note: most of your exports write directly into output TypedArray, return value is usually undefined 63 | */ 64 | async call(fnName, ...args) { 65 | if (!fnName || typeof fnName !== 'string') { 66 | throw new Error('fnName must be a string') 67 | } 68 | if (!this._ready) { 69 | await this.init 70 | } 71 | const ns = this._glue 72 | if (!ns || typeof ns[fnName] !== 'function') { 73 | throw new Error(`Exported function "${fnName}" not found on wasm module`) 74 | } 75 | return ns[fnName](...args) ?? null 76 | } 77 | 78 | async getMemory() { 79 | if (!this._ready) { 80 | await this.init 81 | } 82 | return this._glue.memory 83 | } 84 | 85 | /** 86 | * Release references (optional) 87 | */ 88 | dispose() { 89 | this._glue = null 90 | this._init = null 91 | this._ready = false 92 | this._initPromise = null 93 | } 94 | } 95 | 96 | export default WasmTaskProcessor 97 | -------------------------------------------------------------------------------- /examples/src/modules/tasks/WorkerTaskProcessor.js: -------------------------------------------------------------------------------- 1 | function defined(v) { 2 | return v !== undefined && v !== null 3 | } 4 | function urlFromScript(script) { 5 | const blob = new Blob([script], { type: 'application/javascript' }) 6 | const URL_ = self.URL || self.webkitURL 7 | return URL_.createObjectURL(blob) 8 | } 9 | function isCrossOriginUrl(url) { 10 | try { 11 | const u = new URL(url, self.location?.href ?? 'http://localhost/') 12 | return self.location && u.origin !== self.location.origin 13 | } catch { 14 | return false 15 | } 16 | } 17 | function createWorkerUrl(workerUrl) { 18 | if (isCrossOriginUrl(workerUrl)) { 19 | const shim = `import "${workerUrl}";` 20 | return urlFromScript(shim) 21 | } 22 | return workerUrl 23 | } 24 | 25 | let _canTransferArrayBuffer 26 | 27 | function canTransferArrayBuffer() { 28 | if (defined(_canTransferArrayBuffer)) { 29 | return Promise.resolve(_canTransferArrayBuffer) 30 | } 31 | return new Promise((resolve) => { 32 | const code = ` 33 | self.onmessage = ({data}) => { 34 | self.postMessage({ array: data.array }, [data.array.buffer]); 35 | }; 36 | ` 37 | const w = new Worker(urlFromScript(code)) 38 | const value = 99 39 | const arr = new Int8Array([value]) 40 | try { 41 | w.onmessage = (e) => { 42 | const ok = defined(e.data?.array) && e.data.array[0] === value 43 | _canTransferArrayBuffer = ok 44 | resolve(ok) 45 | w.terminate() 46 | } 47 | w.postMessage({ array: arr }, [arr.buffer]) 48 | } catch { 49 | _canTransferArrayBuffer = false 50 | resolve(false) 51 | w.terminate() 52 | } 53 | }) 54 | } 55 | 56 | class WorkerTaskProcessor { 57 | /** 58 | * 59 | * @param workerUrl 60 | * @param maximumActiveTasks 61 | */ 62 | constructor(workerUrl, maximumActiveTasks = Number.POSITIVE_INFINITY) { 63 | this._workerUrl = workerUrl 64 | this._maximumActiveTasks = maximumActiveTasks 65 | this._activeTasks = 0 66 | this._nextID = 0 67 | this._worker = undefined 68 | this._webAssemblyPromise = undefined 69 | } 70 | 71 | _ensureWorker() { 72 | if (!defined(this._worker)) { 73 | const url = createWorkerUrl(this._workerUrl) 74 | this._worker = new Worker(url, { type: 'module' }) 75 | } 76 | return this._worker 77 | } 78 | 79 | /** 80 | * 81 | * @param parameters 82 | * @param transferableObjects 83 | * @returns {Promise} 84 | * @private 85 | */ 86 | async _runTask(parameters, transferableObjects = []) { 87 | const canTransfer = await canTransferArrayBuffer() 88 | if (!canTransfer) transferableObjects = [] 89 | 90 | const worker = this._ensureWorker() 91 | const id = this._nextID++ 92 | 93 | const promise = new Promise((resolve, reject) => { 94 | const listener = (ev) => { 95 | const data = ev.data 96 | if (!data || data.id !== id) return 97 | worker.removeEventListener('message', listener) 98 | if (defined(data.error)) { 99 | const err = new Error(data.error.message || String(data.error)) 100 | err.name = data.error.name || 'Error' 101 | err.stack = data.error.stack 102 | reject(err) 103 | } else { 104 | resolve(data.result) 105 | } 106 | } 107 | worker.addEventListener('message', listener) 108 | }) 109 | worker.postMessage( 110 | { 111 | id, 112 | parameters, 113 | canTransferArrayBuffer: canTransfer, 114 | }, 115 | transferableObjects 116 | ) 117 | return promise 118 | } 119 | 120 | /** 121 | * 122 | * @param parameters 123 | * @param transferableObjects 124 | * @returns {Promise<*>} 125 | */ 126 | async scheduleTask(parameters, transferableObjects) { 127 | if (this._activeTasks >= this._maximumActiveTasks) { 128 | return undefined 129 | } 130 | this._activeTasks++ 131 | try { 132 | const result = await this._runTask(parameters, transferableObjects) 133 | this._activeTasks-- 134 | return result 135 | } catch (err) { 136 | this._activeTasks-- 137 | throw err 138 | } 139 | } 140 | 141 | /** 142 | * 143 | * @param webAssemblyOptions 144 | * @returns {Promise<*>} 145 | */ 146 | async initWasm(webAssemblyOptions) { 147 | if (defined(this._webAssemblyPromise)) return this._webAssemblyPromise 148 | const init = async () => { 149 | this._ensureWorker() 150 | const canTransfer = await canTransferArrayBuffer() 151 | const transferable = [] 152 | if (webAssemblyOptions.wasmBinary && canTransfer) { 153 | transferable.push(webAssemblyOptions.wasmBinary) 154 | } 155 | const result = await this._runTask( 156 | { webAssemblyConfig: webAssemblyOptions }, 157 | transferable 158 | ) 159 | return result 160 | } 161 | this._webAssemblyPromise = init() 162 | return this._webAssemblyPromise 163 | } 164 | 165 | /** 166 | * 167 | */ 168 | destroy() { 169 | if (defined(this._worker)) { 170 | this._worker.terminate() 171 | this._worker = undefined 172 | } 173 | } 174 | } 175 | 176 | export default WorkerTaskProcessor 177 | -------------------------------------------------------------------------------- /examples/src/modules/utils/Util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Caven 3 | * @Date: 2019-12-31 17:58:01 4 | */ 5 | 6 | const CHARS = 7 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('') 8 | 9 | /** 10 | * Some of the code borrows from leaflet 11 | * https://github.com/Leaflet/Leaflet/tree/master/src/core 12 | */ 13 | class Util { 14 | /** 15 | * Generates uuid 16 | * @param prefix 17 | * @returns {string} 18 | */ 19 | static uuid(prefix = 'D') { 20 | let uuid = [] 21 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-' 22 | uuid[14] = '4' 23 | let r 24 | for (let i = 0; i < 36; i++) { 25 | if (!uuid[i]) { 26 | r = 0 | (Math.random() * 16) 27 | uuid[i] = CHARS[i === 19 ? (r & 0x3) | 0x8 : r] 28 | } 29 | } 30 | return prefix + '-' + uuid.join('') 31 | } 32 | 33 | /** 34 | 35 | * Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. 36 | * @param dest 37 | * @param sources 38 | * @returns {*} 39 | */ 40 | static merge(dest, ...sources) { 41 | let i, j, len, src 42 | for (j = 0, len = sources.length; j < len; j++) { 43 | src = sources[j] 44 | for (i in src) { 45 | dest[i] = src[i] 46 | } 47 | } 48 | return dest 49 | } 50 | 51 | /** 52 | * @function splitWords(str: String): String[] 53 | * Trims and splits the string on whitespace and returns the array of parts. 54 | * @param {*} str 55 | */ 56 | static splitWords(str) { 57 | return this.trim(str).split(/\s+/) 58 | } 59 | 60 | /** 61 | * @function setOptions(obj: Object, options: Object): Object 62 | * Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. 63 | * @param {*} obj 64 | * @param {*} options 65 | */ 66 | static setOptions(obj, options) { 67 | if (!obj.hasOwnProperty('options')) { 68 | obj.options = obj.options ? Object.create(obj.options) : {} 69 | } 70 | for (let i in options) { 71 | obj.options[i] = options[i] 72 | } 73 | return obj.options 74 | } 75 | 76 | /** 77 | * @function formatNum(num: Number, digits?: Number): Number 78 | * Returns the number `num` rounded to `digits` decimals, or to 6 decimals by default. 79 | * @param num 80 | * @param digits 81 | * @returns {number} 82 | */ 83 | static formatNum(num, digits) { 84 | let pow = Math.pow(10, digits === undefined ? 6 : digits) 85 | return Math.round(num * pow) / pow 86 | } 87 | 88 | /** 89 | * @function trim(str: String): String 90 | * Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) 91 | * @param {*} str 92 | */ 93 | static trim(str) { 94 | return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '') 95 | } 96 | 97 | /** 98 | * Data URI string containing a base64-encoded empty GIF image. 99 | * Used as a hack to free memory from unused images on WebKit-powered 100 | * mobile devices (by setting image `src` to this string). 101 | * @returns {string} 102 | */ 103 | static emptyImageUrl() { 104 | return (function () { 105 | return 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' 106 | })() 107 | } 108 | 109 | /** 110 | * @function checkPosition(position: Object): Boolean 111 | * Check position for validity 112 | * @param {*} position 113 | */ 114 | static checkPosition(position) { 115 | return ( 116 | position && 117 | position.hasOwnProperty('_lng') && 118 | position.hasOwnProperty('_lat') && 119 | position.hasOwnProperty('_alt') 120 | ) 121 | } 122 | 123 | /** 124 | * Creates a debounced function that delays invoking `fn` until after `delay` 125 | * @param fn 126 | * @param delay 127 | * @returns {function(): void} 128 | */ 129 | static debounce(fn, delay) { 130 | let timer = null 131 | return function () { 132 | timer && clearTimeout(timer) 133 | timer = setTimeout(fn, delay) 134 | } 135 | } 136 | 137 | /** 138 | * Creates a throttled function that only invokes `fn` at most once per 139 | * @param fn 140 | * @param delay 141 | * @returns {function(): void} 142 | */ 143 | static throttle(fn, delay) { 144 | let valid = true 145 | return function () { 146 | if (!valid) { 147 | return false 148 | } 149 | valid = false 150 | setTimeout(() => { 151 | fn() 152 | valid = true 153 | }, delay) 154 | } 155 | } 156 | 157 | /** 158 | * 159 | * @param dataUrl 160 | * @returns {Blob} 161 | */ 162 | static dataURLtoBlob(dataUrl) { 163 | let arr = dataUrl.split(',') 164 | let mime = arr[0].match(/:(.*?);/)[1] 165 | let bStr = atob(arr[1]) 166 | let len = bStr.length 167 | let u8Arr = new Uint8Array(len) 168 | while (len--) { 169 | u8Arr[len] = bStr.charCodeAt(len) 170 | } 171 | return new Blob([u8Arr], { type: mime }) 172 | } 173 | 174 | /** 175 | * 176 | * @param {*} obj 177 | * @returns 178 | */ 179 | static isPromise(obj) { 180 | return Promise.resolve(obj) == obj 181 | } 182 | 183 | /** 184 | * 185 | * @returns {any} 186 | */ 187 | static getMaxTextureSize() {} 188 | 189 | /** 190 | * 191 | * @param n 192 | * @param min 193 | * @param max 194 | * @returns {number} 195 | */ 196 | static clamp(n, min, max) { 197 | return Math.min(max, Math.max(min, n)) 198 | } 199 | } 200 | 201 | export default Util 202 | -------------------------------------------------------------------------------- /examples/src/modules/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as Util } from './Util.js' 2 | -------------------------------------------------------------------------------- /examples/src/modules/workers/SplatSortWorker.js: -------------------------------------------------------------------------------- 1 | import WorkerPool from './WorkerPool.js' 2 | const workerPool = new WorkerPool(1) 3 | 4 | function splatSort(positionsBuffer, viewBuffer, threshold) { 5 | const positions = new Float32Array(positionsBuffer) 6 | const view = new Float32Array(viewBuffer) 7 | const vertexCount = positions.length / 4 8 | let maxDepth = -Infinity 9 | let minDepth = Infinity 10 | let depthList = new Float32Array(vertexCount) 11 | let sizeList = new Int32Array(depthList.buffer) 12 | let validIndexList = new Int32Array(vertexCount) 13 | let validCount = 0 14 | for (let i = 0; i < vertexCount; i++) { 15 | // Sign of depth is reversed 16 | let depth = 17 | view[0] * positions[i * 4 + 0] + 18 | view[1] * positions[i * 4 + 1] + 19 | view[2] * positions[i * 4 + 2] + 20 | view[3] 21 | 22 | // Skip behind of camera and small, transparent splat 23 | if (depth < 0 && positions[i * 4 + 3] > threshold * depth) { 24 | depthList[validCount] = depth 25 | validIndexList[validCount] = i 26 | validCount++ 27 | if (depth > maxDepth) maxDepth = depth 28 | if (depth < minDepth) minDepth = depth 29 | } 30 | } 31 | 32 | let depthInv = (256 * 256 - 1) / (maxDepth - minDepth) 33 | let counts = new Uint32Array(256 * 256) 34 | for (let i = 0; i < validCount; i++) { 35 | sizeList[i] = ((depthList[i] - minDepth) * depthInv) | 0 36 | counts[sizeList[i]]++ 37 | } 38 | let starts = new Uint32Array(256 * 256) 39 | for (let i = 1; i < 256 * 256; i++) starts[i] = starts[i - 1] + counts[i - 1] 40 | let depthIndex = new Uint32Array(validCount) 41 | for (let i = 0; i < validCount; i++) 42 | depthIndex[starts[sizeList[i]]++] = validIndexList[i] 43 | return depthIndex 44 | } 45 | 46 | export function doSplatSort(positionsBuffer, viewBuffer, threshold = -0.0001) { 47 | return workerPool.run(splatSort, [positionsBuffer, viewBuffer, threshold], { 48 | transfer: [viewBuffer], 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /examples/src/modules/workers/WasmWorker.js: -------------------------------------------------------------------------------- 1 | let wasmNS = null 2 | let wasmReady = false 3 | 4 | /** 重建参数:把 { buffer, length, type } 转回 TypedArray */ 5 | function reviveArgs(args) { 6 | return args.map((arg) => { 7 | if ( 8 | arg && 9 | arg.buffer instanceof ArrayBuffer && 10 | typeof arg.length === 'number' && 11 | typeof arg.type === 'string' 12 | ) { 13 | switch (arg.type) { 14 | case 'Float32Array': 15 | return new Float32Array(arg.buffer, 0, arg.length) 16 | case 'Uint32Array': 17 | return new Uint32Array(arg.buffer, 0, arg.length) 18 | case 'Int32Array': 19 | return new Int32Array(arg.buffer, 0, arg.length) 20 | case 'Uint8Array': 21 | return new Uint8Array(arg.buffer, 0, arg.length) 22 | case 'Int8Array': 23 | return new Int8Array(arg.buffer, 0, arg.length) 24 | case 'Uint16Array': 25 | return new Uint16Array(arg.buffer, 0, arg.length) 26 | case 'Int16Array': 27 | return new Int16Array(arg.buffer, 0, arg.length) 28 | case 'Float64Array': 29 | return new Float64Array(arg.buffer, 0, arg.length) 30 | default: 31 | throw new Error(`Unsupported TypedArray type: ${arg.type}`) 32 | } 33 | } 34 | return arg 35 | }) 36 | } 37 | 38 | /** 规范化返回值:把 TypedArray 转成 { buffer, length, type } */ 39 | function normalizeResult(result) { 40 | if (ArrayBuffer.isView(result)) { 41 | return { 42 | buffer: result.buffer, 43 | length: result.length, 44 | type: result.constructor.name, // "Float32Array" / "Uint32Array" ... 45 | } 46 | } 47 | return result 48 | } 49 | 50 | self.onmessage = async (ev) => { 51 | const { id, parameters } = ev.data || {} 52 | try { 53 | if (parameters && parameters.webAssemblyConfig) { 54 | const cfg = parameters.webAssemblyConfig 55 | const mod = await import(cfg.modulePath) 56 | 57 | if (cfg.wasmBinary) { 58 | await mod.default(cfg.wasmBinary) 59 | } else if (cfg.wasmBinaryFile) { 60 | await mod.default(cfg.wasmBinaryFile) 61 | } else { 62 | await mod.default() 63 | } 64 | 65 | wasmNS = mod 66 | wasmReady = true 67 | postMessage({ id, result: { ok: true } }) 68 | return 69 | } 70 | 71 | if (!wasmReady) throw new Error('WASM module is not initialized yet.') 72 | 73 | const { call, args = [] } = parameters || {} 74 | if (!call || typeof call !== 'string') { 75 | throw new Error('parameters.call (function name) is required.') 76 | } 77 | if (!wasmNS || typeof wasmNS[call] !== 'function') { 78 | throw new Error(`Exported function "${call}" not found on wasm module.`) 79 | } 80 | 81 | // 重建参数 82 | const revived = reviveArgs(args) 83 | 84 | // 调用 wasm 导出函数 85 | const result = await wasmNS[call](...revived) 86 | 87 | // 规范化返回值 88 | const normalized = normalizeResult(result) 89 | 90 | // 如果是 TypedArray,则 transfer buffer 91 | if (normalized?.buffer instanceof ArrayBuffer) { 92 | postMessage({ id, result: normalized }, [normalized.buffer]) 93 | } else { 94 | postMessage({ id, result: normalized }) 95 | } 96 | } catch (error) { 97 | postMessage({ 98 | id, 99 | error: { 100 | name: error?.name || 'Error', 101 | message: error?.message || String(error), 102 | stack: error?.stack, 103 | }, 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/src/modules/workers/WorkerPool.js: -------------------------------------------------------------------------------- 1 | class WorkerPool { 2 | constructor(size = 4, workerType = 'module') { 3 | this.size = Math.max(1, size | 0) 4 | this.workerType = workerType 5 | this._taskId = 0 6 | this._queue = [] 7 | this._idle = [] 8 | this._pending = new Map() 9 | this._blobURL = this._createWorkerBlobURL() 10 | this._workers = Array.from({ length: this.size }, () => this._spawn()) 11 | } 12 | 13 | /** 14 | * 15 | * @param payload 16 | * @param transfer 17 | * @param timeout 18 | * @returns {Promise} 19 | * @private 20 | */ 21 | _enqueue(payload, { transfer = [], timeout } = {}) { 22 | const id = ++this._taskId 23 | return new Promise((resolve, reject) => { 24 | const task = { 25 | id, 26 | payload, 27 | transfer, 28 | resolve, 29 | reject, 30 | timeout, 31 | timer: null, 32 | } 33 | this._queue.push(task) 34 | this._pump() 35 | }) 36 | } 37 | 38 | /** 39 | * 40 | * @private 41 | */ 42 | _pump() { 43 | while (this._idle.length && this._queue.length) { 44 | const worker = this._idle.pop() 45 | const task = this._queue.shift() 46 | this._send(worker, task) 47 | } 48 | } 49 | 50 | /** 51 | * 52 | * @param worker 53 | * @param task 54 | * @private 55 | */ 56 | _send(worker, task) { 57 | const { id, payload, transfer, timeout } = task 58 | 59 | const onMessage = (e) => { 60 | const msg = e.data 61 | if (!msg || msg.id !== id) return 62 | this._clearPending(id) 63 | worker.removeEventListener('message', onMessage) 64 | worker.removeEventListener('error', onError) 65 | 66 | if (task.timer) clearTimeout(task.timer) 67 | 68 | if (msg.ok) { 69 | task.resolve(msg.result) 70 | } else { 71 | const err = new Error(msg.error?.message || 'Worker tasks failed') 72 | err.name = msg.error?.name || 'WorkerError' 73 | err.stack = msg.error?.stack || err.stack 74 | task.reject(err) 75 | } 76 | this._idle.push(worker) 77 | this._pump() 78 | } 79 | 80 | const onError = (err) => { 81 | this._clearPending(id) 82 | worker.removeEventListener('message', onMessage) 83 | worker.removeEventListener('error', onError) 84 | if (task.timer) clearTimeout(task.timer) 85 | this._respawn(worker) 86 | task.reject(err instanceof Error ? err : new Error(String(err))) 87 | this._pump() 88 | } 89 | 90 | this._pending.set(id, { worker, onMessage, onError }) 91 | 92 | worker.addEventListener('message', onMessage) 93 | worker.addEventListener('error', onError) 94 | 95 | if (timeout && timeout > 0) { 96 | task.timer = setTimeout(() => { 97 | this._clearPending(id) 98 | worker.removeEventListener('message', onMessage) 99 | worker.removeEventListener('error', onError) 100 | this._respawn(worker) 101 | task.reject(new Error(`Worker task timeout after ${timeout}ms`)) 102 | this._pump() 103 | }, timeout) 104 | } 105 | 106 | try { 107 | worker.postMessage({ id, ...payload }, transfer) 108 | } catch (e) { 109 | this._clearPending(id) 110 | worker.removeEventListener('message', onMessage) 111 | worker.removeEventListener('error', onError) 112 | this._idle.push(worker) 113 | task.reject(e) 114 | this._pump() 115 | } 116 | } 117 | 118 | /** 119 | * 120 | * @param id 121 | * @private 122 | */ 123 | _clearPending(id) { 124 | this._pending.delete(id) 125 | } 126 | 127 | /** 128 | * 129 | * @returns {Worker} 130 | * @private 131 | */ 132 | _spawn() { 133 | const worker = new Worker(this._blobURL, { type: this.workerType }) 134 | this._idle.push(worker) 135 | return worker 136 | } 137 | 138 | /** 139 | * 140 | * @param badWorker 141 | * @private 142 | */ 143 | _respawn(badWorker) { 144 | try { 145 | badWorker.terminate() 146 | } catch (e) { 147 | console.error(e) 148 | } 149 | const index = this._workers.indexOf(badWorker) 150 | if (index !== -1) this._workers.splice(index, 1) 151 | const new_worker = this._spawn() 152 | this._workers.push(new_worker) 153 | } 154 | 155 | /** 156 | * 157 | * @returns {string} 158 | * @private 159 | */ 160 | _createWorkerBlobURL() { 161 | const workerScript = (self) => { 162 | const isArrayBuffer = (v) => v instanceof ArrayBuffer 163 | const isTyped = (v) => 164 | v && 165 | v.buffer instanceof ArrayBuffer && 166 | typeof v.BYTES_PER_ELEMENT === 'number' 167 | const isMsgPort = (v) => 168 | typeof MessagePort !== 'undefined' && v instanceof MessagePort 169 | const isOffscreen = (v) => 170 | typeof OffscreenCanvas !== 'undefined' && v instanceof OffscreenCanvas 171 | const getTransferableObjects = (val, acc = new Set(), depth = 0) => { 172 | if (val == null || depth > 2) return acc 173 | if (isArrayBuffer(val)) { 174 | acc.add(val) 175 | return acc 176 | } 177 | if (isTyped(val)) { 178 | acc.add(val.buffer) 179 | return acc 180 | } 181 | if (isMsgPort(val) || isOffscreen(val)) { 182 | acc.add(val) 183 | return acc 184 | } 185 | if (Array.isArray(val)) { 186 | for (const x of val) getTransferableObjects(x, acc, depth + 1) 187 | return acc 188 | } 189 | if (typeof val === 'object') { 190 | for (const k in val) getTransferableObjects(val[k], acc, depth + 1) 191 | } 192 | return acc 193 | } 194 | 195 | self.onmessage = async (e) => { 196 | const { id, mode } = e.data || {} 197 | try { 198 | let args = e.data.args || [] 199 | let fn 200 | if (mode === 'fn') { 201 | const { code } = e.data 202 | fn = (0, eval)('(' + code + ')') 203 | if (typeof fn !== 'function') { 204 | throw 'Provided code is not a function.' 205 | } 206 | } else if (mode === 'module') { 207 | const { url, exportName = 'default' } = e.data 208 | const mod = await import(url) 209 | fn = mod?.[exportName] 210 | if (typeof fn !== 'function') { 211 | throw `Export ${exportName} is not a function in module: ${url}` 212 | } 213 | } else { 214 | throw 'Unknown tasks mode: ' + mode 215 | } 216 | const result = await fn(...args) 217 | const transfers = Array.from(getTransferableObjects(result)) 218 | self.postMessage({ id, ok: true, result }, transfers) 219 | } catch (error) { 220 | self.postMessage({ 221 | id, 222 | ok: false, 223 | error: { 224 | name: error?.name || 'Error', 225 | message: error?.message || String(error), 226 | stack: error?.stack || '', 227 | }, 228 | }) 229 | } 230 | } 231 | } 232 | const blob = new Blob(['(', workerScript.toString(), ')(self)'], { 233 | type: 'application/javascript', 234 | }) 235 | return URL.createObjectURL(blob) 236 | } 237 | 238 | /** 239 | * 240 | * @param fn 241 | * @param args 242 | * @param opts 243 | * @returns {Promise} 244 | */ 245 | run(fn, args = [], opts = {}) { 246 | const code = typeof fn === 'function' ? fn.toString() : String(fn) 247 | return this._enqueue({ mode: 'fn', code, args }, opts) 248 | } 249 | 250 | /** 251 | * 252 | * @param moduleURL 253 | * @param exportName 254 | * @param args 255 | * @param opts 256 | * @returns {Promise} 257 | */ 258 | runModule(moduleURL, exportName = 'default', args = [], opts = {}) { 259 | return this._enqueue( 260 | { mode: 'module', url: moduleURL, exportName, args }, 261 | opts 262 | ) 263 | } 264 | 265 | /** 266 | * 267 | * @returns {Promise} 268 | */ 269 | async terminate() { 270 | this._queue.length = 0 271 | for (const w of this._workers) { 272 | try { 273 | w.terminate() 274 | } catch (e) { 275 | console.error(e) 276 | } 277 | } 278 | this._workers.length = 0 279 | if (this._blobURL) { 280 | URL.revokeObjectURL(this._blobURL) 281 | this._blobURL = null 282 | } 283 | } 284 | } 285 | 286 | export default WorkerPool 287 | -------------------------------------------------------------------------------- /examples/src/wasm/splats/wasm_splats.min.js: -------------------------------------------------------------------------------- 1 | let e,n=null;function t(){return null!==n&&0!==n.byteLength||(n=new Uint8Array(e.memory.buffer)),n}const r=new Array(128).fill(void 0);function o(e){return r[e]}r.push(void 0,null,!0,!1);let _=r.length;function s(e){const n=o(e);return function(e){e<132||(r[e]=_,_=e)}(e),n}let i=0;function a(e,n){const r=n(1*e.length,1)>>>0;return t().set(e,r/1),i=e.length,r}let u=null;function l(n,t){const r=t(4*n.length,4)>>>0;return(null!==u&&0!==u.byteLength||(u=new Float32Array(e.memory.buffer)),u).set(n,r/4),i=n.length,r}function c(e){_===r.length&&r.push(r.length+1);const n=_;return _=r[n],r[n]=e,n}let f=null;function p(){return null!==f&&0!==f.byteLength||(f=new Uint32Array(e.memory.buffer)),f}function b(e,n){const t=n(4*e.length,4)>>>0;return p().set(e,t/4),i=e.length,t}export function process_splats_from_buffer(n,t,r,o,_,s){const u=a(n,e.__wbindgen_export_0),f=i,p=l(t,e.__wbindgen_export_0),d=i;var g=l(o,e.__wbindgen_export_0),w=i,y=b(_,e.__wbindgen_export_0),m=i,x=l(s,e.__wbindgen_export_0),h=i;e.process_splats_from_buffer(u,f,p,d,r,g,w,c(o),y,m,c(_),x,h,c(s))}export function process_splats_from_geometry(n,t,r,o,_,s,u,f){const p=l(n,e.__wbindgen_export_0),d=i,g=l(t,e.__wbindgen_export_0),w=i,y=l(r,e.__wbindgen_export_0),m=i,x=a(o,e.__wbindgen_export_0),h=i;var v=l(s,e.__wbindgen_export_0),A=i,W=b(u,e.__wbindgen_export_0),O=i,j=l(f,e.__wbindgen_export_0),L=i;e.process_splats_from_geometry(p,d,g,w,y,m,x,h,_,v,A,c(s),W,O,c(u),j,L,c(f))}export function process_splats_from_spz(n,t,r,o,_,s,a,u,f){const p=l(n,e.__wbindgen_export_0),d=i,g=l(t,e.__wbindgen_export_0),w=i,y=l(r,e.__wbindgen_export_0),m=i,x=l(o,e.__wbindgen_export_0),h=i,v=l(_,e.__wbindgen_export_0),A=i;var W=l(a,e.__wbindgen_export_0),O=i,j=b(u,e.__wbindgen_export_0),L=i,R=l(f,e.__wbindgen_export_0),U=i;e.process_splats_from_spz(p,d,g,w,y,m,x,h,v,A,s,W,O,c(a),j,L,c(u),R,U,c(f))}let d=null;function g(){return(null===d||!0===d.buffer.detached||void 0===d.buffer.detached&&d.buffer!==e.memory.buffer)&&(d=new DataView(e.memory.buffer)),d}export function sort_splats(n,t,r){try{const c=e.__wbindgen_add_to_stack_pointer(-16),f=l(n,e.__wbindgen_export_0),b=i,d=l(t,e.__wbindgen_export_0),w=i;e.sort_splats(c,f,b,d,w,r);var o=g().getInt32(c+0,!0),_=g().getInt32(c+4,!0),s=(a=o,u=_,a>>>=0,p().subarray(a/4,a/4+u)).slice();return e.__wbindgen_export_1(o,4*_,4),s}finally{e.__wbindgen_add_to_stack_pointer(16)}var a,u}function w(){const e={wbg:{}};return e.wbg.__wbindgen_copy_to_typed_array=function(e,n,r){var _,s;new Uint8Array(o(r).buffer,o(r).byteOffset,o(r).byteLength).set((_=e,s=n,_>>>=0,t().subarray(_/1,_/1+s)))},e.wbg.__wbindgen_object_drop_ref=function(e){s(e)},e}function y(t,r){return e=t.exports,x.__wbindgen_wasm_module=r,d=null,u=null,f=null,n=null,e}function m(n){if(void 0!==e)return e;void 0!==n&&(Object.getPrototypeOf(n)===Object.prototype?({module:n}=n):console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const t=w();n instanceof WebAssembly.Module||(n=new WebAssembly.Module(n));return y(new WebAssembly.Instance(n,t),n)}async function x(n){if(void 0!==e)return e;void 0!==n&&(Object.getPrototypeOf(n)===Object.prototype?({module_or_path:n}=n):console.warn("using deprecated parameters for the initialization function; pass a single object instead")),void 0===n&&(n=new URL("wasm_splats_bg.wasm",import.meta.url));const t=w();("string"==typeof n||"function"==typeof Request&&n instanceof Request||"function"==typeof URL&&n instanceof URL)&&(n=fetch(n));const{instance:r,module:o}=await async function(e,n){if("function"==typeof Response&&e instanceof Response){if("function"==typeof WebAssembly.instantiateStreaming)try{return await WebAssembly.instantiateStreaming(e,n)}catch(n){if("application/wasm"==e.headers.get("Content-Type"))throw n;console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",n)}const t=await e.arrayBuffer();return await WebAssembly.instantiate(t,n)}{const t=await WebAssembly.instantiate(e,n);return t instanceof WebAssembly.Instance?{instance:t,module:e}:t}}(await n,t);return y(r,o)}export{m as initSync};export default x; -------------------------------------------------------------------------------- /examples/src/wasm/splats/wasm_splats_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/src/wasm/splats/wasm_splats_bg.wasm -------------------------------------------------------------------------------- /examples/sun-light.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | sun light 11 | 15 | 34 | 35 | 36 |
37 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/sun-light.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as MTP from '@dvt3d/maplibre-three-plugin' 3 | import config from './config.js' 4 | import { Model } from './src/index.js' 5 | 6 | const map = new maplibregl.Map({ 7 | container: 'map-container', // container id 8 | style: 9 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 10 | config.maptiler_key, // style URL 11 | zoom: 18, 12 | center: [148.9819, -35.3981], 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | maxPitch: 85, 16 | }) 17 | 18 | //init three scene 19 | const mapScene = new MTP.MapScene(map) 20 | 21 | const sun = new MTP.Sun() 22 | mapScene.addLight(sun) 23 | 24 | mapScene 25 | .on('preRender', (e) => { 26 | sun.update(e.frameState) 27 | }) 28 | .on('postRender', () => { 29 | map.triggerRepaint() 30 | }) 31 | 32 | Model.fromGltfAsync({ 33 | url: './assets/34M_17/34M_17.gltf', 34 | position: MTP.SceneTransform.lngLatToVector3(148.9819, -35.39847), 35 | }).then((model) => { 36 | mapScene.addObject(model) 37 | }) 38 | -------------------------------------------------------------------------------- /examples/sun-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvt3d/maplibre-three-plugin/969dcf1f2e82361f7e59397497e5a0f37c6cffbb/examples/sun-light.png -------------------------------------------------------------------------------- /examples/sun-shadow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | sun shadow 11 | 15 | 34 | 35 | 36 |
37 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/sun-shadow.js: -------------------------------------------------------------------------------- 1 | import maplibregl from 'maplibre-gl' 2 | import * as MTP from '@dvt3d/maplibre-three-plugin' 3 | import config from './config.js' 4 | import { Model } from './src/index.js' 5 | 6 | const map = new maplibregl.Map({ 7 | container: 'map-container', // container id 8 | style: 9 | 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + 10 | config.maptiler_key, // style URL 11 | zoom: 18, 12 | center: [148.9819, -35.3981], 13 | pitch: 60, 14 | canvasContextAttributes: { antialias: true }, 15 | maxPitch: 85, 16 | }) 17 | 18 | //init three scene 19 | const mapScene = new MTP.MapScene(map) 20 | mapScene.renderer.shadowMap.enabled = true 21 | 22 | //init sun 23 | const sun = new MTP.Sun() 24 | sun.currentTime = '2025/7/12 12:00:00' 25 | sun.castShadow = true 26 | sun.setShadow() 27 | mapScene.addLight(sun) 28 | 29 | mapScene 30 | .on('preRender', (e) => { 31 | sun.update(e.frameState) 32 | }) 33 | .on('postRender', () => { 34 | map.triggerRepaint() 35 | }) 36 | 37 | const shadowGround = MTP.Creator.createShadowGround([148.9819, -35.39847]) 38 | mapScene.addObject(shadowGround) 39 | 40 | Model.fromGltfAsync({ 41 | url: './assets/34M_17/34M_17.gltf', 42 | position: MTP.SceneTransform.lngLatToVector3(148.9819, -35.39847), 43 | castShadow: true, 44 | }).then((model) => { 45 | mapScene.addObject(model) 46 | }) 47 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Caven Chen 3 | * @Date: 2024-04-26 4 | */ 5 | 6 | 'use strict' 7 | 8 | import fse from 'fs-extra' 9 | import path from 'path' 10 | import gulp from 'gulp' 11 | import tsup from 'tsup' 12 | import GlobalsPlugin from 'esbuild-plugin-globals' 13 | import shell from 'shelljs' 14 | import chalk from 'chalk' 15 | 16 | const buildConfig = { 17 | entryPoints: ['src/index.ts'], 18 | dts: true, 19 | target: `es2022`, 20 | minify: false, 21 | sourcemap: false, 22 | external: ['three'], 23 | splitting: false, 24 | } 25 | 26 | async function buildModules(options) { 27 | // Build IIFE 28 | if (options.iife) { 29 | await tsup.build({ 30 | ...buildConfig, 31 | format: 'iife', 32 | globalName: 'MTP', 33 | minify: options.minify, 34 | esbuildPlugins: [ 35 | GlobalsPlugin({ 36 | three: 'THREE', 37 | }), 38 | ], 39 | esbuildOptions: (options, context) => { 40 | delete options.outdir 41 | options.outfile = path.join('dist', 'mtp.min.js') 42 | }, 43 | }) 44 | } 45 | // Build Node 46 | if (options.node) { 47 | await tsup.build({ 48 | ...buildConfig, 49 | format: 'esm', 50 | minify: options.minify, 51 | esbuildOptions: (options, context) => { 52 | delete options.outdir 53 | options.outfile = path.join('dist', 'index.js') 54 | }, 55 | }) 56 | } 57 | } 58 | 59 | async function regenerate(option, content) { 60 | await fse.remove('dist/index.js') 61 | await buildModules(option) 62 | } 63 | 64 | export const dev = gulp.series(() => { 65 | shell.echo(chalk.yellow('============= start dev ==============')) 66 | const watcher = gulp.watch('src', { 67 | persistent: true, 68 | awaitWriteFinish: { 69 | stabilityThreshold: 1000, 70 | pollInterval: 100, 71 | }, 72 | }) 73 | watcher 74 | .on('ready', async () => { 75 | await regenerate({ node: true }) 76 | }) 77 | .on('change', async () => { 78 | let now = new Date().getTime() 79 | await regenerate({ node: true }) 80 | shell.echo( 81 | chalk.green(`regenerate lib takes ${new Date().getTime() - now} ms`) 82 | ) 83 | }) 84 | return watcher 85 | }) 86 | 87 | export const buildIIFE = gulp.series(() => buildModules({ iife: true })) 88 | 89 | export const buildNode = gulp.series(() => buildModules({ node: true })) 90 | 91 | export const build = gulp.series( 92 | () => buildModules({ iife: true }), 93 | () => buildModules({ node: true }) 94 | ) 95 | 96 | export const buildRelease = gulp.series( 97 | () => buildModules({ iife: true, minify: true }), 98 | () => buildModules({ node: true, minify: true }) 99 | ) 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dvt3d/maplibre-three-plugin", 3 | "version": "1.3.0", 4 | "repository": "https://github.com/dvt3d/maplibre-three-plugin.git", 5 | "author": "cavencj ", 6 | "license": "MIT", 7 | "type": "module", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | } 13 | }, 14 | "main": "dist/index.js", 15 | "types": "dist/index.d.ts", 16 | "scripts": { 17 | "dev": "rimraf dist && gulp dev", 18 | "build": "rimraf dist && gulp build", 19 | "build:node": "rimraf dist && gulp buildNode", 20 | "build:iife": "rimraf dist && gulp buildIIFE", 21 | "build:release": "rimraf dist && gulp buildRelease", 22 | "prepublishOnly": "yarn run build:release", 23 | "lint": "eslint --fix src" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.21.4", 27 | "@babel/eslint-parser": "^7.21.8", 28 | "@babel/plugin-proposal-class-properties": "^7.18.6", 29 | "@babel/plugin-transform-runtime": "^7.21.4", 30 | "@babel/preset-env": "^7.21.5", 31 | "@types/three": "^0.179.0", 32 | "chalk": "^5.2.0", 33 | "esbuild-plugin-globals": "^0.2.0", 34 | "eslint": "^8.40.0", 35 | "eslint-config-prettier": "^8.8.0", 36 | "eslint-plugin-import": "^2.27.5", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "eslint-plugin-promise": "^6.1.1", 40 | "express": "^4.18.2", 41 | "fs-extra": "^11.1.1", 42 | "gulp": "^4.0.2", 43 | "prettier": "^2.8.8", 44 | "rimraf": "^5.0.0", 45 | "shelljs": "^0.8.5", 46 | "tsup": "^8.5.0", 47 | "typescript": "^5.9.2" 48 | }, 49 | "peerDependencies": { 50 | "three": "^0.178.0" 51 | }, 52 | "files": [ 53 | "dist" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { MapScene, SceneTransform, Creator, Sun } from './modules' 2 | export { MapScene, SceneTransform, Creator, Sun } 3 | -------------------------------------------------------------------------------- /src/modules/camera/CameraSync.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Caven Chen 3 | */ 4 | import { DEG2RAD, TILE_SIZE, WORLD_SIZE } from '../constants' 5 | import Util from '../utils/Util' 6 | import { type Group, Matrix4, type PerspectiveCamera, Vector3 } from 'three' 7 | import type { IMap } from '../scene/MapScene' 8 | 9 | const projectionMatrix = new Matrix4() 10 | const cameraTranslateZ = new Matrix4() 11 | const MAX_VALID_LATITUDE = 85.051129 12 | 13 | class CameraSync { 14 | private _map: IMap 15 | private _world: Group 16 | private _camera: PerspectiveCamera 17 | private _translateCenter: Matrix4 18 | private readonly _worldSizeRatio: number 19 | 20 | constructor(map: IMap, world: Group, camera: PerspectiveCamera) { 21 | this._map = map 22 | this._world = world 23 | this._camera = camera 24 | this._translateCenter = new Matrix4().makeTranslation( 25 | WORLD_SIZE / 2, 26 | -WORLD_SIZE / 2, 27 | 0 28 | ) 29 | this._worldSizeRatio = TILE_SIZE / WORLD_SIZE 30 | this._map.on('move', () => { 31 | this.syncCamera(false) 32 | }) 33 | this._map.on('resize', () => { 34 | this.syncCamera(true) 35 | }) 36 | } 37 | 38 | /** 39 | * 40 | */ 41 | syncCamera(updateProjectionMatrix: boolean) { 42 | const transform = this._map.transform 43 | 44 | const pitchInRadians = transform.pitch * DEG2RAD 45 | const bearingInRadians = transform.bearing * DEG2RAD 46 | 47 | if (updateProjectionMatrix) { 48 | const fovInRadians = transform.fov * DEG2RAD 49 | const centerOffset = transform.centerOffset || new Vector3() 50 | this._camera.aspect = transform.width / transform.height 51 | 52 | // set camera projection matrix 53 | // @ts-ignore 54 | projectionMatrix.elements = Util.makePerspectiveMatrix( 55 | fovInRadians, 56 | this._camera.aspect, 57 | transform.height / 50, 58 | transform.farZ 59 | ) 60 | this._camera.projectionMatrix = projectionMatrix 61 | 62 | this._camera.projectionMatrix.elements[8] = 63 | (-centerOffset.x * 2) / transform.width 64 | this._camera.projectionMatrix.elements[9] = 65 | (centerOffset.y * 2) / transform.height 66 | } 67 | 68 | //set camera world Matrix 69 | cameraTranslateZ.makeTranslation(0, 0, transform.cameraToCenterDistance) 70 | 71 | const cameraWorldMatrix = new Matrix4() 72 | .premultiply(cameraTranslateZ) 73 | .premultiply(new Matrix4().makeRotationX(pitchInRadians)) 74 | .premultiply(new Matrix4().makeRotationZ(-bearingInRadians)) 75 | 76 | if (transform.elevation) { 77 | cameraWorldMatrix.elements[14] = 78 | transform.cameraToCenterDistance * Math.cos(pitchInRadians) 79 | } 80 | 81 | this._camera.matrixWorld.copy(cameraWorldMatrix) 82 | 83 | // Handle scaling and translation of objects in the map in the world's matrix transform, not the camera 84 | const zoomPow = transform.scale * this._worldSizeRatio 85 | const scale = new Matrix4().makeScale(zoomPow, zoomPow, zoomPow) 86 | let x = transform.x 87 | let y = transform.y 88 | if (!x || !y) { 89 | const center = transform.center 90 | const lat = Util.clamp( 91 | center.lat, 92 | -MAX_VALID_LATITUDE, 93 | MAX_VALID_LATITUDE 94 | ) 95 | x = Util.mercatorXFromLng(center.lng) * transform.worldSize 96 | y = Util.mercatorYFromLat(lat) * transform.worldSize 97 | } 98 | 99 | const translateMap = new Matrix4().makeTranslation(-x, y, 0) 100 | const rotateMap = new Matrix4().makeRotationZ(Math.PI) 101 | this._world.matrix = new Matrix4() 102 | .premultiply(rotateMap) 103 | .premultiply(this._translateCenter) 104 | .premultiply(scale) 105 | .premultiply(translateMap) 106 | } 107 | } 108 | 109 | export default CameraSync 110 | -------------------------------------------------------------------------------- /src/modules/constants/index.ts: -------------------------------------------------------------------------------- 1 | const WORLD_SIZE = 512 * 2000 2 | const EARTH_RADIUS = 6371008.8 3 | const EARTH_CIRCUMFERENCE = 2 * Math.PI * EARTH_RADIUS 4 | const DEG2RAD = Math.PI / 180 5 | const RAD2DEG = 180 / Math.PI 6 | const PROJECTION_WORLD_SIZE = WORLD_SIZE / EARTH_CIRCUMFERENCE 7 | const TILE_SIZE = 512 8 | export { 9 | TILE_SIZE, 10 | WORLD_SIZE, 11 | EARTH_RADIUS, 12 | EARTH_CIRCUMFERENCE, 13 | DEG2RAD, 14 | RAD2DEG, 15 | PROJECTION_WORLD_SIZE, 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/creator/Creator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Caven Chen 3 | */ 4 | import { Group, Mesh, PlaneGeometry, ShadowMaterial } from 'three' 5 | import SceneTransform from '../transform/SceneTransform' 6 | 7 | class Creator { 8 | /** 9 | * 10 | * @param center 11 | * @param rotation 12 | * @param scale 13 | */ 14 | static createRTCGroup( 15 | center: number | number[], 16 | rotation: number[], 17 | scale: number[] 18 | ): Group { 19 | const group = new Group() 20 | group.name = 'rtc' 21 | group.position.copy(SceneTransform.lngLatToVector3(center)) 22 | 23 | if (rotation) { 24 | group.rotateX(rotation[0] || 0) 25 | group.rotateY(rotation[1] || 0) 26 | group.rotateZ(rotation[2] || 0) 27 | } else { 28 | group.rotateX(Math.PI / 2) 29 | group.rotateY(Math.PI) 30 | } 31 | 32 | if (scale) { 33 | group.scale.set(scale[0] || 1, scale[1] || 1, scale[2] || 1) 34 | } else { 35 | let lat_scale = 1 36 | if (Array.isArray(center)) { 37 | lat_scale = SceneTransform.projectedUnitsPerMeter(center[1]) 38 | } 39 | group.scale.set(lat_scale, lat_scale, lat_scale) 40 | } 41 | return group 42 | } 43 | 44 | /** 45 | * 46 | * @param center 47 | * @param rotation 48 | * @param scale 49 | */ 50 | static createMercatorRTCGroup( 51 | center: number | number[], 52 | rotation: number[], 53 | scale: number[] 54 | ): Group { 55 | const group = this.createRTCGroup(center, rotation, scale) 56 | if (!scale) { 57 | let lat_scale = 1 58 | let mercator_scale = SceneTransform.projectedMercatorUnitsPerMeter() 59 | if (Array.isArray(center)) { 60 | lat_scale = SceneTransform.projectedUnitsPerMeter(center[1]) 61 | } 62 | group.scale.set(mercator_scale, mercator_scale, lat_scale) 63 | } 64 | return group 65 | } 66 | 67 | /** 68 | * 69 | * @param center 70 | * @param width 71 | * @param height 72 | * @returns {Mesh} 73 | */ 74 | static createShadowGround( 75 | center: number | number[], 76 | width?: number, 77 | height?: number 78 | ): Mesh { 79 | const geo = new PlaneGeometry(width || 100, height || 100) 80 | const mat = new ShadowMaterial({ 81 | opacity: 0.5, 82 | transparent: true, 83 | }) 84 | let mesh = new Mesh(geo, mat) 85 | mesh.position.copy(SceneTransform.lngLatToVector3(center)) 86 | mesh.receiveShadow = true 87 | mesh.name = 'shadow-ground' 88 | return mesh 89 | } 90 | } 91 | 92 | export default Creator 93 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { MapScene } from './scene/MapScene' 2 | export { default as SceneTransform } from './transform/SceneTransform' 3 | export { default as Sun } from './sun/Sun' 4 | export { default as Creator } from './creator/Creator' 5 | -------------------------------------------------------------------------------- /src/modules/layer/ThreeLayer.ts: -------------------------------------------------------------------------------- 1 | import CameraSync from '../camera/CameraSync' 2 | import type {MapScene} from '../scene/MapScene' 3 | 4 | class ThreeLayer { 5 | private readonly _id: string 6 | private _mapScene: MapScene | null 7 | private _cameraSync: CameraSync | null 8 | 9 | constructor(id:string, mapScene:MapScene) { 10 | this._id = id 11 | this._mapScene = mapScene 12 | this._cameraSync = new CameraSync( 13 | this._mapScene.map, 14 | this._mapScene.world, 15 | this._mapScene.camera 16 | ) 17 | } 18 | 19 | get id() { 20 | return this._id 21 | } 22 | 23 | get type() { 24 | return 'custom' 25 | } 26 | 27 | get renderingMode() { 28 | return '3d' 29 | } 30 | 31 | onAdd() { 32 | this._cameraSync!.syncCamera(true) 33 | } 34 | 35 | render() { 36 | this._mapScene!.render() 37 | } 38 | 39 | onRemove() { 40 | this._cameraSync = null 41 | this._mapScene = null 42 | } 43 | } 44 | 45 | export default ThreeLayer 46 | -------------------------------------------------------------------------------- /src/modules/scene/MapScene.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Group, 3 | PerspectiveCamera, 4 | Scene, 5 | WebGLRenderer, 6 | EventDispatcher, 7 | Box3, 8 | Vector3, 9 | } from 'three' 10 | import type { Light, Object3D } from 'three' 11 | import ThreeLayer from '../layer/ThreeLayer' 12 | import { WORLD_SIZE } from '../constants' 13 | import Util from '../utils/Util' 14 | import SceneTransform from '../transform/SceneTransform' 15 | 16 | const DEF_OPTS = { 17 | scene: null, 18 | camera: null, 19 | renderer: null, 20 | renderLoop: null, 21 | preserveDrawingBuffer: false, 22 | } 23 | 24 | export interface IMap { 25 | transform: any 26 | on(type: string, listener: () => any): any 27 | getCanvas(): HTMLCanvasElement 28 | getLayer(id: string): any 29 | addLayer(options: any): any 30 | getCenter(): { lng: number; lat: number } 31 | once(type: string, completed: any): void 32 | flyTo(param: { 33 | center: any[] 34 | zoom: number 35 | bearing: number 36 | pitch: number 37 | duration: number 38 | }): void 39 | } 40 | 41 | /** 42 | * Configuration options for initializing a MapScene 43 | */ 44 | interface IMapSceneOptions { 45 | /** Existing Three.js Scene instance (optional) */ 46 | scene: null | Scene 47 | /** Existing Three.js PerspectiveCamera instance (optional) */ 48 | camera: null | PerspectiveCamera 49 | /** Existing Three.js WebGLRenderer instance (optional) */ 50 | renderer: null | WebGLRenderer 51 | /** Custom render loop function (optional) */ 52 | renderLoop: null | ((mapScene: MapScene) => void) 53 | /** Whether to preserve the drawing buffer (optional) */ 54 | preserveDrawingBuffer: boolean 55 | } 56 | 57 | /** 58 | * Event types and their payloads for MapScene events 59 | */ 60 | interface IMapSceneEvent { 61 | /** Dispatched after resetting the renderer state */ 62 | postReset: { frameState: IFrameState } 63 | /** Dispatched before rendering the scene */ 64 | preRender: { frameState: IFrameState } 65 | /** Dispatched before resetting the renderer state */ 66 | preReset: { frameState: IFrameState } 67 | /** Dispatched after rendering the scene */ 68 | postRender: { frameState: IFrameState } 69 | } 70 | 71 | /** 72 | * Frame state information passed to event listeners 73 | */ 74 | export interface IFrameState { 75 | /** Current map center coordinates */ 76 | center: { lng: number; lat: number } 77 | /** Three.js Scene instance */ 78 | scene: Scene 79 | /** Three.js PerspectiveCamera instance */ 80 | camera: PerspectiveCamera 81 | /** Three.js WebGLRenderer instance */ 82 | renderer: WebGLRenderer 83 | } 84 | 85 | /** 86 | * Extended Three.js Light interface with optional delegate 87 | */ 88 | interface ILight extends Light { 89 | /** Optional delegate light source */ 90 | delegate?: Light 91 | } 92 | 93 | /** 94 | * Extended Three.js Object3D interface with optional delegate and size 95 | */ 96 | interface IObject3D { 97 | /** Optional delegate object */ 98 | delegate: Object3D 99 | /** Optional size vector */ 100 | size?: Vector3 101 | } 102 | 103 | export class MapScene { 104 | private readonly _map: IMap 105 | private _options: IMapSceneOptions 106 | private readonly _canvas: HTMLCanvasElement 107 | private readonly _scene: Scene 108 | private readonly _camera: PerspectiveCamera 109 | private readonly _renderer: WebGLRenderer 110 | private readonly _lights: Group 111 | private readonly _world: Group 112 | private _event: EventDispatcher 113 | constructor(map: IMap, options: Partial = {}) { 114 | if (!map) { 115 | throw 'missing map' 116 | } 117 | this._map = map 118 | this._options = { 119 | ...DEF_OPTS, 120 | ...options, 121 | } 122 | this._canvas = map.getCanvas() 123 | this._scene = this._options.scene || new Scene() 124 | this._camera = 125 | this._options.camera || 126 | new PerspectiveCamera( 127 | this._map.transform.fov, 128 | this._map.transform.width / this._map.transform.height, 129 | 0.001, 130 | 1e21 131 | ) 132 | this._camera.matrixAutoUpdate = false 133 | this._renderer = 134 | this._options.renderer || 135 | new WebGLRenderer({ 136 | alpha: true, 137 | antialias: true, 138 | preserveDrawingBuffer: this._options.preserveDrawingBuffer, 139 | canvas: this._canvas, 140 | context: this._canvas.getContext('webgl2')!, 141 | }) 142 | this._renderer.setPixelRatio(window.devicePixelRatio) 143 | this._renderer.setSize(this._canvas.clientWidth, this._canvas.clientHeight) 144 | this._renderer.autoClear = false 145 | 146 | // init the lights container 147 | this._lights = new Group() 148 | this._lights.name = 'lights' 149 | this._scene.add(this._lights) 150 | 151 | // init the world container 152 | this._world = new Group() 153 | this._world.name = 'world' 154 | this._world.userData = { 155 | isWorld: true, 156 | name: 'world', 157 | } 158 | this._world.position.set(WORLD_SIZE / 2, WORLD_SIZE / 2, 0) 159 | this._world.matrixAutoUpdate = false 160 | this._scene.add(this._world) 161 | this._map.on('render', this._onMapRender.bind(this)) 162 | this._event = new EventDispatcher() 163 | } 164 | 165 | get map() { 166 | return this._map 167 | } 168 | 169 | get canvas() { 170 | return this._canvas 171 | } 172 | 173 | get camera() { 174 | return this._camera 175 | } 176 | 177 | get scene() { 178 | return this._scene 179 | } 180 | 181 | get lights() { 182 | return this._lights 183 | } 184 | 185 | get world() { 186 | return this._world 187 | } 188 | 189 | get renderer() { 190 | return this._renderer 191 | } 192 | 193 | /** 194 | * 195 | * @private 196 | */ 197 | _onMapRender() { 198 | if (!this._map.getLayer('map_scene_layer')) { 199 | this._map.addLayer(new ThreeLayer('map_scene_layer', this)) 200 | } 201 | } 202 | 203 | /** 204 | * 205 | * @returns {MapScene} 206 | */ 207 | render(): MapScene { 208 | if (this._options.renderLoop) { 209 | this._options.renderLoop(this) 210 | } else { 211 | const frameState = { 212 | center: this._map.getCenter(), 213 | scene: this._scene, 214 | camera: this._camera, 215 | renderer: this._renderer, 216 | } 217 | this._event.dispatchEvent({ 218 | type: 'preReset', 219 | frameState, 220 | }) 221 | this.renderer.resetState() 222 | this._event.dispatchEvent({ 223 | type: 'postReset', 224 | frameState, 225 | }) 226 | this._event.dispatchEvent({ 227 | type: 'preRender', 228 | frameState, 229 | }) 230 | this.renderer.render(this._scene, this._camera) 231 | this._event.dispatchEvent({ 232 | type: 'postRender', 233 | frameState, 234 | }) 235 | } 236 | return this 237 | } 238 | 239 | /** 240 | * 241 | * @param light 242 | * @returns {MapScene} 243 | */ 244 | addLight(light: ILight): MapScene { 245 | this._lights.add(light.delegate || light) 246 | return this 247 | } 248 | 249 | /** 250 | * 251 | * @param light 252 | */ 253 | removeLight(light: ILight) { 254 | this._lights.remove(light.delegate || light) 255 | return this 256 | } 257 | 258 | /** 259 | * 260 | * @param object 261 | * @returns {MapScene} 262 | */ 263 | addObject(object: IObject3D | Object3D): MapScene { 264 | let obj = 'delegate' in object ? object.delegate : object 265 | this._world.add(obj) 266 | return this 267 | } 268 | 269 | /** 270 | * 271 | * @param object 272 | * @returns {MapScene} 273 | */ 274 | removeObject(object: IObject3D | Object3D): MapScene { 275 | let obj = 'delegate' in object ? object.delegate : object 276 | this._world.remove(obj) 277 | obj.traverse((child:any) => { 278 | // @ts-ignore 279 | if (child.geometry) child.geometry.dispose() 280 | // @ts-ignore 281 | if (child.material) { 282 | // @ts-ignore 283 | if (Array.isArray(child.material)) { 284 | // @ts-ignore 285 | child.material.forEach((m) => m.dispose()) 286 | } else { 287 | // @ts-ignore 288 | child.material.dispose() 289 | } 290 | } 291 | // @ts-ignore 292 | if (child.texture) child.texture.dispose() 293 | }) 294 | return this 295 | } 296 | 297 | /** 298 | * 299 | * @returns {{position: *[], heading: *, pitch}} 300 | */ 301 | getViewPosition(): { position: number[]; heading: number; pitch: number } { 302 | const transform = this._map.transform 303 | const center = transform.center 304 | return { 305 | position: [ 306 | center.lng, 307 | center.lat, 308 | Util.getHeightByZoom( 309 | transform, 310 | transform.zoom, 311 | center.lat, 312 | transform.pitch 313 | ), 314 | ], 315 | heading: transform.bearing, 316 | pitch: transform.pitch, 317 | } 318 | } 319 | 320 | /** 321 | * 322 | * @param target 323 | * @param completed 324 | * @param duration 325 | * @returns {MapScene} 326 | */ 327 | flyTo( 328 | target: { 329 | position: { x: number; y: number; z: number } 330 | size?: any 331 | delegate?: any 332 | }, 333 | duration?: number, 334 | completed?: () => void 335 | ): MapScene { 336 | if (target && target.position) { 337 | if (completed) { 338 | this._map.once('moveend', completed) 339 | } 340 | let size = target.size 341 | if (!size) { 342 | size = new Vector3() 343 | new Box3().setFromObject(target.delegate || target, true).getSize(size) 344 | } 345 | const viewInfo = Util.getViewInfo( 346 | this._map.transform, 347 | SceneTransform.vector3ToLngLat(target.position), 348 | size 349 | ) 350 | // @ts-ignore 351 | this._map.flyTo({ 352 | center: viewInfo.center, 353 | zoom: viewInfo.zoom, 354 | duration: (duration || 3) * 1000, 355 | }) 356 | } 357 | return this 358 | } 359 | 360 | /** 361 | * 362 | * @param target 363 | * @param completed 364 | * @returns {MapScene} 365 | */ 366 | zoomTo( 367 | target: { 368 | position: { x: number; y: number; z: number } 369 | size?: any 370 | delegate?: any 371 | }, 372 | completed?: () => void 373 | ): MapScene { 374 | return this.flyTo(target, 0, completed) 375 | } 376 | 377 | /** 378 | * 379 | * @returns {MapScene} 380 | */ 381 | flyToPosition( 382 | position: number[], 383 | hpr: number[] = [0, 0, 0], 384 | completed?: () => void, 385 | duration: number = 3 386 | ): MapScene { 387 | if (completed) { 388 | this._map.once('moveend', completed) 389 | } 390 | this._map.flyTo({ 391 | center: [position[0], position[1]], 392 | zoom: Util.getZoomByHeight( 393 | this._map.transform, 394 | position[2], 395 | position[1], 396 | hpr[1] || 0 397 | ), 398 | bearing: hpr[0], 399 | pitch: hpr[1], 400 | duration: duration * 1000, 401 | }) 402 | return this 403 | } 404 | 405 | /** 406 | * 407 | * @returns {MapScene} 408 | */ 409 | zoomToPosition( 410 | position: any, 411 | hpr = [0, 0, 0], 412 | completed?: () => void 413 | ): MapScene { 414 | return this.flyToPosition(position, hpr, completed, 0) 415 | } 416 | 417 | /** 418 | * 419 | * @param type 420 | * @param callback 421 | * @returns {MapScene} 422 | */ 423 | on( 424 | type: string, 425 | callback: (event: { frameState: IFrameState }) => void 426 | ): MapScene { 427 | // @ts-ignore 428 | this._event.addEventListener(type, callback) 429 | return this 430 | } 431 | 432 | /** 433 | * 434 | * @param type 435 | * @param callback 436 | * @returns {MapScene} 437 | */ 438 | off(type: string, callback: () => void): MapScene { 439 | // @ts-ignore 440 | this._event.removeEventListener(type, callback) 441 | return this 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /src/modules/sun/Sun.ts: -------------------------------------------------------------------------------- 1 | import { Group, DirectionalLight, HemisphereLight, Color } from 'three' 2 | import SunCalc from '../utils/SunCalc' 3 | import type { IFrameState } from '../scene/MapScene' 4 | 5 | interface ShadowOptions { 6 | /** Blur radius for shadow edges */ 7 | radius: number 8 | /** Width and height of the shadow map */ 9 | mapSize: [number, number] 10 | /** Top and right boundaries of the shadow camera frustum */ 11 | topRight: number 12 | /** Bottom and left boundaries of the shadow camera frustum */ 13 | bottomLeft: number 14 | /** Near clipping plane of the shadow camera */ 15 | near: number 16 | /** Far clipping plane of the shadow camera */ 17 | far: number 18 | } 19 | 20 | /** 21 | * 22 | */ 23 | class Sun { 24 | private readonly _delegate: Group 25 | private readonly _sunLight: DirectionalLight 26 | private readonly _hemiLight: HemisphereLight 27 | private _currentTime: string | number | Date 28 | 29 | constructor() { 30 | this._delegate = new Group() 31 | this._delegate.name = 'Sun' 32 | this._sunLight = new DirectionalLight(0xffffff, 1) 33 | this._hemiLight = new HemisphereLight( 34 | new Color(0xffffff), 35 | new Color(0xffffff), 36 | 0.6 37 | ) 38 | this._hemiLight.color.setHSL(0.661, 0.96, 0.12) 39 | this._hemiLight.groundColor.setHSL(0.11, 0.96, 0.14) 40 | this._hemiLight.position.set(0, 0, 50) 41 | this._delegate.add(this._sunLight) 42 | this._delegate.add(this._hemiLight) 43 | this._currentTime = new Date().getTime() 44 | } 45 | 46 | get delegate() { 47 | return this._delegate 48 | } 49 | 50 | set castShadow(castShadow) { 51 | this._sunLight.castShadow = castShadow 52 | } 53 | 54 | get castShadow() { 55 | return this._sunLight.castShadow 56 | } 57 | 58 | set currentTime(currentTime) { 59 | this._currentTime = currentTime 60 | } 61 | 62 | get currentTime() { 63 | return this._currentTime 64 | } 65 | 66 | get sunLight() { 67 | return this._sunLight 68 | } 69 | 70 | get hemiLight() { 71 | return this._hemiLight 72 | } 73 | 74 | /** 75 | * 76 | * @param shadow 77 | * @returns {Sun} 78 | */ 79 | setShadow(shadow: Partial = {}): Sun { 80 | this._sunLight.shadow.radius = shadow.radius || 2 81 | this._sunLight.shadow.mapSize.width = shadow.mapSize 82 | ? shadow.mapSize[0] 83 | : 8192 84 | this._sunLight.shadow.mapSize.height = shadow.mapSize 85 | ? shadow.mapSize[1] 86 | : 8192 87 | this._sunLight.shadow.camera.top = this._sunLight.shadow.camera.right = 88 | shadow.topRight || 1000 89 | this._sunLight.shadow.camera.bottom = this._sunLight.shadow.camera.left = 90 | shadow.bottomLeft || -1000 91 | this._sunLight.shadow.camera.near = shadow.near || 1 92 | this._sunLight.shadow.camera.far = shadow.far || 1e8 93 | this._sunLight.shadow.camera.visible = true 94 | return this 95 | } 96 | 97 | /** 98 | * 99 | * @param frameState 100 | */ 101 | update(frameState: IFrameState): void { 102 | const WORLD_SIZE = 512 * 2000 103 | const date = new Date(this._currentTime || new Date().getTime()) 104 | const center = frameState.center 105 | const sunPosition = SunCalc.getPosition(date, center.lat, center.lng) 106 | const altitude = sunPosition.altitude 107 | const azimuth = Math.PI + sunPosition.azimuth 108 | const radius = WORLD_SIZE / 2 109 | const alt = Math.sin(altitude) 110 | const altRadius = Math.cos(altitude) 111 | const azCos = Math.cos(azimuth) * altRadius 112 | const azSin = Math.sin(azimuth) * altRadius 113 | this._sunLight.position.set(azSin, azCos, alt) 114 | this._sunLight.position.multiplyScalar(radius) 115 | this._sunLight.intensity = Math.max(alt, 0) 116 | this._hemiLight.intensity = Math.max(alt * 1, 0.1) 117 | this._sunLight.updateMatrixWorld() 118 | } 119 | } 120 | 121 | export default Sun 122 | -------------------------------------------------------------------------------- /src/modules/transform/SceneTransform.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three' 2 | import { 3 | DEG2RAD, 4 | EARTH_CIRCUMFERENCE, 5 | EARTH_RADIUS, 6 | PROJECTION_WORLD_SIZE, 7 | WORLD_SIZE, 8 | } from '../constants' 9 | 10 | class SceneTransform { 11 | /** 12 | * 13 | * @returns {number} 14 | */ 15 | static projectedMercatorUnitsPerMeter(): number { 16 | return this.projectedUnitsPerMeter(0) 17 | } 18 | 19 | /** 20 | * 21 | * @param lat 22 | * @returns {number} 23 | */ 24 | static projectedUnitsPerMeter(lat: number): number { 25 | return Math.abs(WORLD_SIZE / Math.cos(DEG2RAD * lat) / EARTH_CIRCUMFERENCE) 26 | } 27 | 28 | /** 29 | * 30 | * @param lng 31 | * @param lat 32 | * @param alt 33 | * @returns {Vector3} 34 | */ 35 | static lngLatToVector3( 36 | lng: number | number[], 37 | lat?: number, 38 | alt?: number 39 | ): Vector3 { 40 | let v: number[] = [0, 0, 0] 41 | if (Array.isArray(lng)) { 42 | v = [ 43 | -EARTH_RADIUS * DEG2RAD * lng[0] * PROJECTION_WORLD_SIZE, 44 | -EARTH_RADIUS * 45 | Math.log(Math.tan(Math.PI * 0.25 + 0.5 * DEG2RAD * lng[1])) * 46 | PROJECTION_WORLD_SIZE, 47 | ] 48 | if (!lng[2]) { 49 | v.push(0) 50 | } else { 51 | v.push(lng[2] * this.projectedUnitsPerMeter(lng[1])) 52 | } 53 | } else { 54 | v = [ 55 | -EARTH_RADIUS * DEG2RAD * lng * PROJECTION_WORLD_SIZE, 56 | -EARTH_RADIUS * 57 | Math.log(Math.tan(Math.PI * 0.25 + 0.5 * DEG2RAD * (lat || 0))) * 58 | PROJECTION_WORLD_SIZE, 59 | ] 60 | if (!alt) { 61 | v.push(0) 62 | } else { 63 | v.push(alt * this.projectedUnitsPerMeter(lat || 0)) 64 | } 65 | } 66 | return new Vector3(v[0], v[1], v[2]) 67 | } 68 | 69 | /** 70 | * 71 | * @param v 72 | * @returns {number[]} 73 | */ 74 | static vector3ToLngLat(v: { x: number; y: number; z: number }): number[] { 75 | let result = [0, 0, 0] 76 | if (v) { 77 | result[0] = -v.x / (EARTH_RADIUS * DEG2RAD * PROJECTION_WORLD_SIZE) 78 | result[1] = 79 | (2 * 80 | (Math.atan(Math.exp(v.y / (PROJECTION_WORLD_SIZE * -EARTH_RADIUS))) - 81 | Math.PI / 4)) / 82 | DEG2RAD 83 | result[2] = v.z / this.projectedUnitsPerMeter(result[1]) 84 | } 85 | return result 86 | } 87 | } 88 | 89 | export default SceneTransform 90 | -------------------------------------------------------------------------------- /src/modules/utils/Util.ts: -------------------------------------------------------------------------------- 1 | import { DEG2RAD, EARTH_CIRCUMFERENCE } from '../constants' 2 | 3 | class Util { 4 | /** 5 | * 6 | * @param n 7 | * @param min 8 | * @param max 9 | * @returns {number} 10 | */ 11 | static clamp(n: number, min: number, max: number): number { 12 | return Math.min(max, Math.max(min, n)) 13 | } 14 | 15 | /** 16 | * 17 | * @param fovy 18 | * @param aspect 19 | * @param near 20 | * @param far 21 | * @returns {number[]} 22 | */ 23 | static makePerspectiveMatrix( 24 | fovy: number, 25 | aspect: number, 26 | near: number, 27 | far: number 28 | ): number[] { 29 | let f = 1.0 / Math.tan(fovy / 2) 30 | let nf = 1 / (near - far) 31 | return [ 32 | f / aspect, 33 | 0, 34 | 0, 35 | 0, 36 | 0, 37 | f, 38 | 0, 39 | 0, 40 | 0, 41 | 0, 42 | (far + near) * nf, 43 | -1, 44 | 0, 45 | 0, 46 | 2 * far * near * nf, 47 | 0, 48 | ] 49 | } 50 | 51 | /** 52 | * 53 | * @param lng 54 | * @returns {number} 55 | */ 56 | static mercatorXFromLng(lng: number): number { 57 | return (180 + lng) / 360 58 | } 59 | 60 | /** 61 | * 62 | * @param lat 63 | * @returns {number} 64 | */ 65 | static mercatorYFromLat(lat: number): number { 66 | return ( 67 | (180 - 68 | (180 / Math.PI) * 69 | Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360))) / 70 | 360 71 | ) 72 | } 73 | 74 | /** 75 | * 76 | * @param transform 77 | * @param center 78 | * @param boundingSize 79 | * @returns {{center: (number|*)[], cameraHeight: number, zoom: number}} 80 | */ 81 | static getViewInfo( 82 | transform: { 83 | fov: number 84 | pitch: number 85 | cameraToCenterDistance: number 86 | tileSize: number 87 | }, 88 | center: string | number[], 89 | boundingSize: { x: number; y: number; z: number } 90 | ): { center: (number | any)[]; cameraHeight: number; zoom: number } { 91 | const fovInRadians = transform.fov * DEG2RAD 92 | const pitchInRadians = transform.pitch * DEG2RAD 93 | let _center: { lng: number; lat: number; alt: number } = null! 94 | if (Array.isArray(center)) { 95 | _center = { lng: center[0], lat: center[1], alt: center[2] || 0 } 96 | } 97 | 98 | if (typeof center === 'string') { 99 | let arr = center.split(',') 100 | _center = { lng: +arr[0], lat: +arr[1], alt: +arr[2] || 0 } 101 | } 102 | const distance = 103 | Math.max(boundingSize.x, boundingSize.y, boundingSize.z) / 104 | (2 * Math.tan(fovInRadians / 2)) 105 | 106 | const cameraHeight = distance * Math.cos(pitchInRadians) + _center.alt 107 | const pixelAltitude = Math.abs( 108 | Math.cos(pitchInRadians) * transform.cameraToCenterDistance 109 | ) 110 | const metersInWorldAtLat = 111 | EARTH_CIRCUMFERENCE * Math.abs(Math.cos(_center.lat * DEG2RAD)) 112 | const worldSize = (pixelAltitude / cameraHeight) * metersInWorldAtLat 113 | const zoom = Math.round(Math.log2(worldSize / transform.tileSize)) 114 | return { 115 | center: [_center.lng, _center.lat], 116 | cameraHeight, 117 | zoom, 118 | } 119 | } 120 | 121 | /** 122 | * 123 | * @param transform 124 | * @param zoom 125 | * @param lat 126 | * @param pitch 127 | * @returns {number} 128 | */ 129 | static getHeightByZoom( 130 | transform: { cameraToCenterDistance: number; tileSize: number }, 131 | zoom: number, 132 | lat: number, 133 | pitch: number 134 | ): number { 135 | const pixelAltitude = Math.abs( 136 | Math.cos(pitch * DEG2RAD) * transform.cameraToCenterDistance 137 | ) 138 | const metersInWorldAtLat = 139 | EARTH_CIRCUMFERENCE * Math.abs(Math.cos(lat * DEG2RAD)) 140 | const worldSize = Math.pow(2, zoom) * transform.tileSize 141 | return (pixelAltitude * metersInWorldAtLat) / worldSize 142 | } 143 | 144 | /** 145 | * 146 | * @param transform 147 | * @param height 148 | * @param lat 149 | * @param pitch 150 | * @returns {number} 151 | */ 152 | static getZoomByHeight( 153 | transform: { cameraToCenterDistance: number; tileSize: number }, 154 | height: number, 155 | lat: number, 156 | pitch: number 157 | ): number { 158 | const pixelAltitude = Math.abs( 159 | Math.cos(pitch * DEG2RAD) * transform.cameraToCenterDistance 160 | ) 161 | const metersInWorldAtLat = 162 | EARTH_CIRCUMFERENCE * Math.abs(Math.cos(lat * DEG2RAD)) 163 | const worldSize = (pixelAltitude / height) * metersInWorldAtLat 164 | return Math.round(Math.log2(worldSize / transform.tileSize)) 165 | } 166 | } 167 | 168 | export default Util 169 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["src"] 25 | } 26 | --------------------------------------------------------------------------------