├── .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 | |  |  |  |  |
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 | |  |  |  |  |
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 | |  |  |  |  |
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 |
--------------------------------------------------------------------------------