├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── gh-pages.yml
│ ├── publish.yml
│ └── run-tests-for-prs.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE.md
├── README.md
├── examples
├── .gitignore
├── README.md
├── assets
│ ├── big_buck_bunny_720p_1mb.mp4
│ ├── lowpoly-sedan.glb
│ ├── readme-header.gif
│ └── ubi-icon.png
├── jsm
│ ├── load-maps-api.js
│ └── old-browser-handler.js
├── package.json
├── scripts
│ └── write-index-html.js
└── src
│ ├── car.html
│ ├── car.js
│ ├── cube.html
│ ├── cube.js
│ ├── video.html
│ ├── video.js
│ ├── wireframe.html
│ └── wireframe.js
├── package-lock.json
├── package.json
├── src
├── geo-utils.ts
├── index.ts
├── threejs-overlay-view.ts
└── types.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy examples to gh-pages
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build-deploy:
10 | name: 'Build and Deploy Examples to gh-pages'
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-node@v2
15 | with:
16 | node-version: '15.x'
17 | - run: npm install
18 | - name: Build Examples
19 | run: npm run build
20 | working-directory: examples
21 | env:
22 | GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
23 | GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }}
24 | - name: Deploy to gh-pages
25 | uses: peaceiris/actions-gh-pages@v3
26 | with:
27 | github_token: ${{ secrets.GITHUB_TOKEN }}
28 | publish_dir: ./examples/dist
29 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish package
2 | on:
3 | workflow_dispatch:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | publish:
10 | name: Publish npm-package
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-node@v2
15 | with:
16 | node-version: '15.x'
17 | registry-url: 'https://registry.npmjs.org'
18 | - run: npm install
19 | - run: npm test
20 | - run: npm publish
21 | env:
22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_BOT_ACCESS_TOKEN }}
23 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests-for-prs.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: '15.x'
18 | registry-url: 'https://registry.npmjs.org'
19 | - run: npm install
20 | - run: npm test
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .parcel-cache
4 | /.env
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.*
2 | package-lock.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "none",
8 | "bracketSpacing": false,
9 | "jsxBracketSameLine": true,
10 | "arrowParens": "avoid"
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2021 Ubilabs GmbH
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a
4 | copy of this software and associated documentation files (the "Software"),
5 | to deal in the Software without restriction, including without limitation
6 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | and/or sell copies of the Software, and to permit persons to whom the
8 | Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > **Please note: this package is no longer maintained.**
2 | >
3 | > The official [`@googlemaps/three`](https://www.npmjs.com/package/@googlemaps/three)
4 | > package provides even more features, almost the same API and will be further maintained.
5 |
6 | # ThreejsOverlayView for Google Maps
7 |
8 | A wrapper for `google.maps.WebGLOverlayView` that takes care of the
9 | integration between three.js and the Google Maps JavaScript API. It lets
10 | you create Google Maps overlays directly with three.js.
11 |
12 |
13 |

14 |
15 |
16 | We recommend that you first make yourself familiar with
17 | [the official documentation][docs] for the WebGLOverlayView. The rest of the
18 | documentation will assume that you know how to load the Google Maps API and
19 | how to configure and create the map.
20 |
21 | To see what can be done with this new API, have a look at the demos we built:
22 |
23 | - [Feature Tour](https://goo.gle/maps-platform-webgl-tour)
24 | - [Travel App Demo](https://goo.gle/maps-platform-webgl-travel-demo)
25 |
26 | [docs]: https://developers.google.com/maps/documentation/javascript/webgl
27 |
28 | ## Installation
29 |
30 | You can install this module via npm:
31 |
32 | ```sh
33 | npm install @ubilabs/threejs-overlay-view
34 | ```
35 |
36 | ## Examples
37 |
38 | We provide a set of examples to quickly get you going. Those can be found in
39 | [`./examples`](./examples). They are also hosted on
40 | [GitHub Pages](https://ubilabs.github.io/threejs-overlay-view):
41 |
42 | - [Simple Cube](https://ubilabs.github.io/threejs-overlay-view/cube.html)
43 | - [Wireframe Building](https://ubilabs.github.io/threejs-overlay-view/wireframe.html)
44 | - [Animated Car](https://ubilabs.github.io/threejs-overlay-view/car.html)
45 | - [Video Texture](https://ubilabs.github.io/threejs-overlay-view/video.html)
46 |
47 | ## Usage
48 |
49 | Once you have loaded the Google Maps API, you first have to create a new
50 | map. Here's what this typically looks like for a full page map view:
51 |
52 | ```js
53 | const mapOptions = {
54 | mapId: YOUR_MAP_ID_HERE,
55 | disableDefaultUI: true,
56 | gestureHandling: 'greedy',
57 |
58 | center: {lat: 53.554486, lng: 10.007479},
59 | zoom: 19,
60 | heading: 324,
61 | tilt: 65
62 | };
63 |
64 | const map = new google.maps.Map(document.querySelector('#map'), mapOptions);
65 | ```
66 |
67 | After that (or before - the order doesn't matter), you can create the
68 | `ThreejsOverlayView`. The only parameter you need to specify here is the
69 | reference point, which will become the origin of the coordinate system used
70 | by your three.js scene.
71 |
72 | ```js
73 | import ThreejsOverlayView from '@ubilabs/threejs-overlay-view';
74 |
75 | // ...
76 |
77 | const overlay = new ThreejsOverlayView({
78 | lat: 53.554486,
79 | lng: 10.007479
80 | });
81 | ```
82 |
83 | With both of those initialized, you can add your three.js objects to the
84 | scene, and finally add the overlay to the map:
85 |
86 | ```js
87 | import {BoxGeometry, Mesh, MeshBasicMaterial} from 'three';
88 |
89 | // ...
90 |
91 | const scene = overlay.getScene();
92 | const box = new Mesh(
93 | new BoxGeometry(50, 50, 50),
94 | new MeshBasicMaterial({color: 0xff0000})
95 | );
96 | scene.add(box);
97 |
98 | overlay.setMap(map);
99 | ```
100 |
101 | And that's it. With this, you can already add any 3D object to the map.
102 |
103 | Of course, there is a bit more to know about. In the following sections,
104 | we'll talk about the world space coordinates and the relation to
105 | geographic coordinates, about lifecycle hooks and implementing animated
106 | content, and finally about raycasting and implementing interactive elements.
107 |
108 | ### Coordinate System and Georeferencing
109 |
110 | The coordinate system that you see as world coordinates is a right-handed
111 | coordinate system in z-up orientation. The y-axis is pointing true north,
112 | and the x-axis is pointing east. The units are meters. So the point
113 | `new Vector3(0, 50, 10)` would be 10 meters above ground and 50 meters to
114 | the east of your specified reference point.
115 |
116 | Now, we don't always want to use only meters in a map view, quite often we
117 | have to deal with multiple points specified in geographic coordinates. For
118 | this, the ThreejsOverlayView provides two utility functions to convert
119 | between latitude/longitude and world space coordinates:
120 |
121 | ```js
122 | const vector3 = overlay.latLngAltToVector3({lat, lng});
123 | const {lat, lng, altitude} = overlay.vector3ToLatLngAlt(vector3);
124 |
125 | // as usual in three.js, both methods also accept a target-object
126 | // as the second parameter to prevent unnecessary object creation and
127 | // garbage-collection overhead, for example:
128 | overlay.latLngAltToVector3({lat, lng}, object3d.position);
129 | overlay.vector3ToLatLngAlt(vec3, latLngAltObject);
130 | ```
131 |
132 | This only works well if points are within a reasonable distance of each
133 | other. At some point, the numeric precision of 32-bit float values in the
134 | shaders will become a problem, and the precision of the position
135 | computations will no longer work.
136 |
137 | If coordinates are thousands of kilometers apart, you could either use
138 | multiple overlay instances or update the reference point (and all objects
139 | in your scene) depending on the current map-viewport using
140 | `overlay.setReferencePoint({lat, lng})`.
141 |
142 | ### Lifecycle Hooks and Animations
143 |
144 | Similar to the `WebGLOverlayView`, the `ThreejsOverlayView` also provides a
145 | set of lifecycle hooks that you can define to react to certain events in
146 | the overlays' lifecycle. There are three of them - all optional: `onAdd`,
147 | `update`, and `onRemove`. They all are called without any parameters and
148 | are not expected to have a return value.
149 |
150 | - `onAdd()` can be used to do some final setup of your scene before it
151 | gets added to the map. Be aware that this happens in the rendering
152 | lifecycle of the map, so you shouldn't do expensive operations here.
153 | - `update()` will be called immediately before each render of the map.
154 | Here you can update the state of your scene. This is mostly useful in
155 | situations where you want to run animations or update the interactive
156 | state of your scene.
157 | - `onRemove()` is symmetrical to onAdd and will be called after the
158 | overlay has been removed from the map.
159 |
160 | By default, the map uses the energy-efficient approach and only renders a
161 | new frame when the camera parameters changed. If you want to run
162 | animations or interactive content, you can manually trigger a full redraw
163 | of the map including all overlays by calling `overlay.requestRedraw()`
164 | whenever there is anything changed in your scene – or just at the end of
165 | the `update`-callback if you have an animation running that has to update
166 | on every frame:
167 |
168 | ```js
169 | let animationRunning = true;
170 |
171 | overlay.update = () => {
172 | // ...update your scene...
173 |
174 | if (animationRunning) {
175 | overlay.requestRedraw();
176 | }
177 | };
178 | ```
179 |
180 | ### Raycasting and Interactions
181 |
182 | If you want to add interactivity to any three.js content, you typically
183 | have to implement raycasting. We took care of that for you, and the
184 | ThreejsOverlayView provides a method `overlay.raycast()` for this. To make
185 | use of this, you first have to keep track of mouse movements on the map:
186 |
187 | ```js
188 | import {Vector2} from 'three';
189 |
190 | // ...
191 |
192 | const mapDiv = map.getDiv();
193 | const mousePosition = new Vector2();
194 |
195 | map.addListener('mousemove', ev => {
196 | const {domEvent} = ev;
197 | const {left, top, width, height} = mapDiv.getBoundingClientRect();
198 |
199 | const x = domEvent.clientX - left;
200 | const y = domEvent.clientY - top;
201 |
202 | mousePosition.x = 2 * (x / width) - 1;
203 | mousePosition.y = 1 - 2 * (y / height);
204 |
205 | // since the actual raycasting is happening in the update function,
206 | // we have to make sure that it will be called for the next frame.
207 | overlay.requestRedraw();
208 | });
209 | ```
210 |
211 | With the mouse position being always up to date, you can then use the
212 | `raycast()` function in the update callback. In this example, we change the
213 | color of the object under the cursor:
214 |
215 | ```js
216 | const DEFAULT_COLOR = 0xffffff;
217 | const HIGHLIGHT_COLOR = 0xff0000;
218 |
219 | let highlightedObject = null;
220 |
221 | overlay.update = () => {
222 | const intersections = overlay.raycast(mousePosition);
223 | if (highlightedObject) {
224 | highlightedObject.material.color.setHex(DEFAULT_COLOR);
225 | }
226 |
227 | if (intersections.length === 0) return;
228 |
229 | highlightedObject = intersections[0].object;
230 | highlightedObject.material.color.setHex(HIGHLIGHT_COLOR);
231 | };
232 | ```
233 |
234 | ## API Reference
235 |
236 | Described above is still not the complete picture, and there are some
237 | details left out for readability reasons.
238 |
239 | We plan to publish full API documentation at some point. Until then, please
240 | refer to the TypeScript source code or declaration files as well as our
241 | demos that contain a lot of comments.
242 |
243 | ## Contributing
244 |
245 | We are always happy to accept contributions from outside collaborators. For
246 | bugs or problems you encounter as well as questions, ideas and
247 | feature requests, please head over to our [issue tracker][].
248 |
249 | When reporting issues, please try to be as concise as possible and provide
250 | a [reproducible example][] so we can quickly address the problem.
251 |
252 | [issue tracker]: https://github.com/ubilabs/threejs-overlay-view/issues
253 | [reproducible example]: https://stackoverflow.com/help/minimal-reproducible-example
254 |
255 | ### Setting up for Development and Running Examples Locally
256 |
257 | If you want to contribute code to the project, excellent.
258 |
259 | Setting up a local development environment is pretty straightforward and
260 | just needs [Node.js](https://nodejs.org/) to be installed on your system.
261 |
262 | Once you have cloned the repository, run
263 |
264 | ```sh
265 | npm install
266 | ```
267 |
268 | in the cloned directory to install all dependencies.
269 |
270 | To run the examples, you will need to have an API-Key and MapId for
271 | the Google Maps JavaScript API. It's easiest to store those in a file
272 | `.env` in the project directory:
273 |
274 | ```sh
275 | GOOGLE_MAPS_API_KEY='your API key here'
276 | GOOGLE_MAPS_MAP_ID='your map ID'
277 | ```
278 |
279 | Once you have that file in place, you can run
280 |
281 | ```sh
282 | npm start
283 | ```
284 |
285 | to start the dev server. This will start the parcel server on
286 | http://localhost:1234 where you can access the examples (if port 1234 is
287 | already in use, a different URL will be shown in the console).
288 |
289 | ### Running Tests
290 |
291 | Tests are currently limited to checking for formatting and typescript errors.
292 | They can be run with
293 |
294 | ```sh
295 | npm run test
296 | ```
297 |
298 | Please make sure that all tests are passing before opening a pull request.
299 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | /.parcel-cache
2 | /dist
3 | /node_modules
4 | /src/index.html
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # threejs-overlay-view Examples
2 |
3 | This is a mostly standalone repository with some usage-examples of the
4 | `ThreejsOverlayView`. Mostly here refers to the fact that it installs the
5 | `@ubilabs/threejs-overlay-view` module from the parent-directory instead of
6 | from npm.
7 |
8 | Similar to the `./examples/jsm` directory found in three.js this will also act
9 | as a place for extra functionality that could be useful in some use-cases but
10 | not often enough to warrant being included in the main module.
11 |
12 | ## [cube.html](https://ubilabs.github.io/threejs-overlay-view/cube.html)
13 |
14 | Basically our hello-world example, shows the basic functionality of adding
15 | 3d-objects to a map.
16 |
17 | ## [wireframe.html](https://ubilabs.github.io/threejs-overlay-view/wireframe.html)
18 |
19 | Shows how you can highlight your own buildings by rendering just outside the Google Maps buildings.
20 |
21 | ## [video.html](https://ubilabs.github.io/threejs-overlay-view/video.html)
22 |
23 | Import an HTML video into the 3D scene with the three.js VideoTexture class.
24 |
25 | ## [car.html](https://ubilabs.github.io/threejs-overlay-view/car.html)
26 |
27 | This example shows how you can animate a simple gltf-object along a path
28 | specified in lat/lng coordinates.
29 |
--------------------------------------------------------------------------------
/examples/assets/big_buck_bunny_720p_1mb.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubilabs/threejs-overlay-view/88309bc2c52d5ec9a3f9d85d56559fb8904cfeee/examples/assets/big_buck_bunny_720p_1mb.mp4
--------------------------------------------------------------------------------
/examples/assets/lowpoly-sedan.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubilabs/threejs-overlay-view/88309bc2c52d5ec9a3f9d85d56559fb8904cfeee/examples/assets/lowpoly-sedan.glb
--------------------------------------------------------------------------------
/examples/assets/readme-header.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubilabs/threejs-overlay-view/88309bc2c52d5ec9a3f9d85d56559fb8904cfeee/examples/assets/readme-header.gif
--------------------------------------------------------------------------------
/examples/assets/ubi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubilabs/threejs-overlay-view/88309bc2c52d5ec9a3f9d85d56559fb8904cfeee/examples/assets/ubi-icon.png
--------------------------------------------------------------------------------
/examples/jsm/load-maps-api.js:
--------------------------------------------------------------------------------
1 | import {Loader as MapsApiLoader} from '@googlemaps/js-api-loader';
2 |
3 | const LOCAL_STORAGE_API_KEY = 'threejs-overlay-view-api-key';
4 | const LOCAL_STORAGE_MAP_ID = 'threejs-overlay-view-map-id';
5 |
6 | // fetch order: env > url params > local storage
7 | export function getMapsApiOptions() {
8 | const storage = window.localStorage;
9 | const url = new URL(location.href);
10 |
11 | let apiKey = process.env.GOOGLE_MAPS_API_KEY;
12 | let mapId = process.env.GOOGLE_MAPS_MAP_ID;
13 |
14 | if (!apiKey || !mapId) {
15 | apiKey = url.searchParams.get('apiKey');
16 | mapId = url.searchParams.get('mapId');
17 |
18 | apiKey
19 | ? storage.setItem(LOCAL_STORAGE_API_KEY, apiKey)
20 | : (apiKey = storage.getItem(LOCAL_STORAGE_API_KEY));
21 | mapId
22 | ? storage.setItem(LOCAL_STORAGE_MAP_ID, mapId)
23 | : (mapId = storage.getItem(LOCAL_STORAGE_MAP_ID));
24 | }
25 | return apiKey && mapId ? {apiKey, mapId} : {};
26 | }
27 |
28 | export async function loadMapsApi(libraries = []) {
29 | const {apiKey, mapId} = getMapsApiOptions();
30 |
31 | // fixme:
32 | // - nice to have: The error and UI should also contain information about
33 | // requested libraries (e.g. places-api)
34 | // - nice to have: replace alert with something ui-friendly
35 |
36 | if (!apiKey || !mapId) {
37 | alert(`
38 | Could not find apikey or mapId as URL parameters.
39 | Add them like this to the URL: example.com?apiKey=XXXX&mapId=XXXX.
40 | If you dont have an API key or map ID, visit the Google Maps documentation to find out on how to obtain them:
41 | https://developers.google.com/maps/documentation/javascript/webgl
42 | `);
43 | }
44 |
45 | const loader = new MapsApiLoader({
46 | version: 'beta',
47 | apiKey,
48 | libraries
49 | });
50 |
51 | await loader.load();
52 | }
53 |
--------------------------------------------------------------------------------
/examples/jsm/old-browser-handler.js:
--------------------------------------------------------------------------------
1 | function webglIsSupported() {
2 | try {
3 | const canvas = document.createElement('canvas');
4 | return (
5 | !!window.WebGLRenderingContext &&
6 | (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
7 | );
8 | } catch (e) {
9 | return false;
10 | }
11 | }
12 | function supportsStaticImport() {
13 | const script = document.createElement('script');
14 | return 'noModule' in script;
15 | }
16 |
17 | if (!supportsStaticImport()) {
18 | alert(
19 | 'Your browser does not support modules. Try updating your browser or switch to a different one.'
20 | );
21 | }
22 |
23 | if (!webglIsSupported()) {
24 | alert(
25 | 'Your browser does not support WebGL. Try updating your browser or switch to a different one.'
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ubilabs/threejs-overlay-view-examples",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "npm run build:index && parcel ./src/*.html",
7 | "clean": "rm -rf ./dist ./.parcel-cache",
8 | "build": "npm run build:index && npm run clean && parcel build --public-url ./ ./src/*.html && npm run copy-assets",
9 | "copy-assets": "cp -r ./assets ./dist/",
10 | "test": "prettier -c './{src,jsm}/**/*.{ts,js,html}'",
11 | "build:index": "node ./scripts/write-index-html"
12 | },
13 | "author": "",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@parcel/transformer-image": "^2.0.0-nightly.2312",
17 | "parcel": "^2.0.0-nightly.688"
18 | },
19 | "dependencies": {
20 | "@googlemaps/js-api-loader": "^1.11.4",
21 | "@ubilabs/threejs-overlay-view": "..",
22 | "three": "^0.129.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/scripts/write-index-html.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | fs.readdir('./src', (err, entries) => {
4 | if (err) {
5 | console.error(err);
6 | process.exit(1);
7 | }
8 |
9 | const html = entries
10 | .filter(entry => entry.endsWith('.html') && entry !== 'index.html')
11 | .map(demo => `${demo}
`)
12 | .join('');
13 | fs.writeFileSync('./src/index.html', html);
14 | });
15 |
--------------------------------------------------------------------------------
/examples/src/car.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/src/car.js:
--------------------------------------------------------------------------------
1 | import ThreeJSOverlayView from '@ubilabs/threejs-overlay-view';
2 | import {CatmullRomCurve3, Vector3} from 'three';
3 | import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader';
4 | import {Line2} from 'three/examples/jsm/lines/Line2.js';
5 | import {LineMaterial} from 'three/examples/jsm/lines/LineMaterial.js';
6 | import {LineGeometry} from 'three/examples/jsm/lines/LineGeometry.js';
7 | import {getMapsApiOptions, loadMapsApi} from '../jsm/load-maps-api';
8 |
9 | import CAR_MODEL_URL from 'url:../assets/lowpoly-sedan.glb';
10 |
11 | const CAR_FRONT = new Vector3(0, 1, 0);
12 |
13 | const VIEW_PARAMS = {
14 | center: {lat: 53.554486, lng: 10.007479},
15 | zoom: 18,
16 | heading: 40,
17 | tilt: 65
18 | };
19 |
20 | const ANIMATION_DURATION = 12000;
21 | const ANIMATION_POINTS = [
22 | {lat: 53.554473, lng: 10.008226},
23 | {lat: 53.554913, lng: 10.008124},
24 | {lat: 53.554986, lng: 10.007928},
25 | {lat: 53.554775, lng: 10.006363},
26 | {lat: 53.554674, lng: 10.006383},
27 | {lat: 53.554473, lng: 10.006681},
28 | {lat: 53.554363, lng: 10.006971},
29 | {lat: 53.554453, lng: 10.008091},
30 | {lat: 53.554424, lng: 10.008201},
31 | {lat: 53.554473, lng: 10.008226}
32 | ];
33 |
34 | const mapContainer = document.querySelector('#map');
35 | const tmpVec3 = new Vector3();
36 |
37 | async function main() {
38 | const map = await initMap();
39 |
40 | const overlay = new ThreeJSOverlayView(VIEW_PARAMS.center);
41 | const scene = overlay.getScene();
42 |
43 | overlay.setMap(map);
44 |
45 | // create a Catmull-Rom spline from the points to smooth out the corners
46 | // for the animation
47 | const points = ANIMATION_POINTS.map(p => overlay.latLngAltToVector3(p));
48 | const curve = new CatmullRomCurve3(points, true, 'catmullrom', 0.2);
49 | curve.updateArcLengths();
50 |
51 | const trackLine = createTrackLine(curve);
52 | scene.add(trackLine);
53 |
54 | let carModel = null;
55 | loadCarModel().then(obj => {
56 | carModel = obj;
57 | scene.add(carModel);
58 |
59 | // since loading the car-model happened asynchronously, we need to
60 | // explicitly trigger a redraw.
61 | overlay.requestRedraw();
62 | });
63 |
64 | // the update-function will animate the car along the spline
65 | overlay.update = () => {
66 | trackLine.material.resolution.copy(overlay.getViewportSize());
67 |
68 | if (!carModel) return;
69 |
70 | const animationProgress =
71 | (performance.now() % ANIMATION_DURATION) / ANIMATION_DURATION;
72 |
73 | curve.getPointAt(animationProgress, carModel.position);
74 | curve.getTangentAt(animationProgress, tmpVec3);
75 | carModel.quaternion.setFromUnitVectors(CAR_FRONT, tmpVec3);
76 |
77 | overlay.requestRedraw();
78 | };
79 | }
80 |
81 | /**
82 | * Load the Google Maps API and create the fullscreen map.
83 | */
84 | async function initMap() {
85 | const {mapId} = getMapsApiOptions();
86 | await loadMapsApi();
87 |
88 | return new google.maps.Map(mapContainer, {
89 | mapId,
90 | disableDefaultUI: true,
91 | backgroundColor: 'transparent',
92 | gestureHandling: 'greedy',
93 | ...VIEW_PARAMS
94 | });
95 | }
96 |
97 | /**
98 | * Create a mesh-line from the spline to render the track the car is driving.
99 | */
100 | function createTrackLine(curve) {
101 | const numPoints = 10 * curve.points.length;
102 | const curvePoints = curve.getSpacedPoints(numPoints);
103 | const positions = new Float32Array(numPoints * 3);
104 |
105 | for (let i = 0; i < numPoints; i++) {
106 | curvePoints[i].toArray(positions, 3 * i);
107 | }
108 |
109 | const trackLine = new Line2(
110 | new LineGeometry(),
111 | new LineMaterial({
112 | color: 0x0f9d58,
113 | linewidth: 5
114 | })
115 | );
116 |
117 | trackLine.geometry.setPositions(positions);
118 |
119 | return trackLine;
120 | }
121 |
122 | /**
123 | * Load and prepare the car-model for animation.
124 | */
125 | async function loadCarModel() {
126 | const loader = new GLTFLoader();
127 |
128 | return new Promise(resolve => {
129 | loader.load(CAR_MODEL_URL, gltf => {
130 | const group = gltf.scene;
131 | const carModel = group.getObjectByName('sedan');
132 |
133 | carModel.scale.setScalar(3);
134 | carModel.rotation.set(Math.PI / 2, 0, Math.PI, 'ZXY');
135 |
136 | resolve(group);
137 | });
138 | });
139 | }
140 |
141 | main().catch(err => console.error(err));
142 |
--------------------------------------------------------------------------------
/examples/src/cube.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/src/cube.js:
--------------------------------------------------------------------------------
1 | import {Mesh, MeshStandardMaterial, BoxGeometry} from 'three';
2 | import ThreeJSOverlayView from '@ubilabs/threejs-overlay-view';
3 |
4 | import {getMapsApiOptions, loadMapsApi} from '../jsm/load-maps-api';
5 |
6 | const VIEW_PARAMS = {
7 | center: {
8 | lat: 53.554,
9 | lng: 10.007
10 | },
11 | tilt: 67.5,
12 | heading: 60,
13 | zoom: 18
14 | };
15 |
16 | async function main() {
17 | const map = await initMap();
18 |
19 | const overlay = new ThreeJSOverlayView({
20 | ...VIEW_PARAMS.center
21 | });
22 | overlay.setMap(map);
23 |
24 | const scene = overlay.getScene();
25 | const cube = new Mesh(
26 | new BoxGeometry(20, 20, 20),
27 | new MeshStandardMaterial({color: 0xff0000})
28 | );
29 |
30 | const cubeLocation = {...VIEW_PARAMS.center, altitude: 50};
31 | overlay.latLngAltToVector3(cubeLocation, cube.position);
32 |
33 | scene.add(cube);
34 | }
35 |
36 | async function initMap() {
37 | const {mapId} = getMapsApiOptions();
38 | await loadMapsApi();
39 |
40 | return new google.maps.Map(document.querySelector('#map'), {
41 | mapId,
42 | disableDefaultUI: true,
43 | backgroundColor: 'transparent',
44 | gestureHandling: 'greedy',
45 | ...VIEW_PARAMS
46 | });
47 | }
48 |
49 | main().catch(err => {
50 | console.error('uncaught error in main: ', err);
51 | });
52 |
--------------------------------------------------------------------------------
/examples/src/video.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/src/video.js:
--------------------------------------------------------------------------------
1 | import {
2 | Mesh,
3 | VideoTexture,
4 | MeshBasicMaterial,
5 | FrontSide,
6 | PlaneGeometry
7 | } from 'three';
8 | import ThreeJSOverlayView from '@ubilabs/threejs-overlay-view';
9 | import {getMapsApiOptions, loadMapsApi} from '../jsm/load-maps-api';
10 | import VIDEO_URL from 'url:../assets/big_buck_bunny_720p_1mb.mp4';
11 |
12 | const SCREEN_SIZE = [50, 25];
13 | const ALTITUDE_OFFSET = 3;
14 | const SCREEN_POSITION = {
15 | lat: 53.55450742,
16 | lng: 10.0074296368,
17 | altitude: SCREEN_SIZE[1] / 2 + ALTITUDE_OFFSET
18 | };
19 | const SCREEN_ROTATION = [Math.PI / 2, 0, Math.PI / 13];
20 |
21 | const VIEW_PARAMS = {
22 | center: {
23 | lat: SCREEN_POSITION.lat,
24 | lng: SCREEN_POSITION.lng
25 | },
26 | heading: 324,
27 | tilt: 65,
28 | zoom: 19
29 | };
30 |
31 | async function main() {
32 | const map = await initMap();
33 |
34 | const overlay = new ThreeJSOverlayView({
35 | ...VIEW_PARAMS.center
36 | });
37 | overlay.setMap(map);
38 |
39 | const scene = overlay.getScene();
40 | const video = document.createElement('video');
41 |
42 | video.src = VIDEO_URL;
43 | video.loop = true;
44 | video.muted = true;
45 | video.autoplay = true;
46 | video.load();
47 | video.play();
48 |
49 | const videoTexture = new VideoTexture(video);
50 | const videoMaterial = new MeshBasicMaterial({
51 | map: videoTexture,
52 | side: FrontSide
53 | });
54 |
55 | const screenGeometry = new PlaneGeometry(...SCREEN_SIZE);
56 | const screen = new Mesh(screenGeometry, videoMaterial);
57 |
58 | overlay.latLngAltToVector3(SCREEN_POSITION, screen.position);
59 | screen.rotation.order = 'ZYX';
60 | screen.rotation.set(...SCREEN_ROTATION);
61 |
62 | scene.add(screen);
63 | overlay.update = () => overlay.requestRedraw();
64 | }
65 |
66 | async function initMap() {
67 | const {mapId} = getMapsApiOptions();
68 | await loadMapsApi();
69 |
70 | return new google.maps.Map(document.querySelector('#map'), {
71 | mapId,
72 | disableDefaultUI: true,
73 | backgroundColor: 'transparent',
74 | gestureHandling: 'greedy',
75 | ...VIEW_PARAMS
76 | });
77 | }
78 |
79 | main().catch(err => {
80 | console.error('uncaught error in main: ', err);
81 | });
82 |
--------------------------------------------------------------------------------
/examples/src/wireframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/src/wireframe.js:
--------------------------------------------------------------------------------
1 | import {
2 | Mesh,
3 | MeshStandardMaterial,
4 | Vector3,
5 | LineMaterial,
6 | Shape,
7 | ExtrudeGeometry,
8 | TextureLoader,
9 | MeshBasicMaterial,
10 | PlaneGeometry
11 | } from 'three';
12 | import {LineSegmentsGeometry} from 'three/examples/jsm/lines/LineSegmentsGeometry';
13 | import {Line2} from 'three/examples/jsm/lines/Line2.js';
14 | import {LineMaterial} from 'three/examples/jsm/lines/LineMaterial.js';
15 | import UBI_ICON from 'url:../assets/ubi-icon.png';
16 | import ThreeJSOverlayView from '@ubilabs/threejs-overlay-view';
17 |
18 | import {getMapsApiOptions, loadMapsApi} from '../jsm/load-maps-api';
19 |
20 | const VIEW_PARAMS = {
21 | center: {
22 | lat: 53.55493986295417,
23 | lng: 10.007137126703523
24 | },
25 | heading: 324.66666666666674,
26 | tilt: 65.66666666666667,
27 | zoom: 19.43375
28 | };
29 |
30 | const BUILDING_HEIGHT = 31;
31 | const BUILDING_LINE_COLOR = 0xffffff;
32 | const BUILDING_FILL_COLOR = 0x000000;
33 | const Z_FIGHTING_OFFSET = 0.001;
34 | const LOGO_SIZE = [16, 16];
35 | const LOGO_POSITION = {
36 | lat: 53.55463986295417,
37 | lng: 10.007317126703523,
38 | altitude: BUILDING_HEIGHT + Z_FIGHTING_OFFSET
39 | };
40 | const LOGO_ROTATION_Z = Math.PI / 12;
41 | const COLOR_CHANGE_DURATION = 30; // completes one hue cycle in x seconds
42 |
43 | async function main() {
44 | const map = await initMap();
45 |
46 | const overlay = new ThreeJSOverlayView(VIEW_PARAMS.center);
47 | overlay.setMap(map);
48 |
49 | initScene(overlay).then(() => overlay.requestRedraw());
50 | }
51 |
52 | async function initMap() {
53 | const {mapId} = getMapsApiOptions();
54 | await loadMapsApi();
55 |
56 | return new google.maps.Map(document.querySelector('#map'), {
57 | mapId,
58 | disableDefaultUI: true,
59 | backgroundColor: 'transparent',
60 | gestureHandling: 'greedy',
61 | ...VIEW_PARAMS
62 | });
63 | }
64 |
65 | async function initScene(overlay) {
66 | const scene = overlay.getScene();
67 |
68 | const wireframePath = [
69 | [10.00787808793857, 53.554574397774715],
70 | [10.006971374559072, 53.55444226566294],
71 | [10.006565280581388, 53.55467172811441],
72 | [10.006569569754523, 53.55471724470295],
73 | [10.007768697370686, 53.554874083634],
74 | [10.007848668422987, 53.554846301309745],
75 | [10.007913475744536, 53.554604563663226]
76 | ];
77 | const points = wireframePath.map(([lng, lat]) =>
78 | overlay.latLngAltToVector3({lat, lng})
79 | );
80 |
81 | const line = getWireframe(points);
82 | scene.add(line);
83 | scene.add(getBuilding(points));
84 | scene.add(await getLogo(overlay));
85 |
86 | overlay.update = () => {
87 | const time = performance.now();
88 |
89 | line.material.resolution.copy(overlay.getViewportSize());
90 | line.material.color.setHSL(
91 | ((time * 0.001) / COLOR_CHANGE_DURATION) % 1,
92 | 0.69,
93 | 0.5
94 | );
95 |
96 | overlay.requestRedraw();
97 | };
98 | }
99 |
100 | function getWireframe(points) {
101 | const positions = new Float32Array(18 * points.length).fill(0);
102 |
103 | const offset = new Vector3(0, 0, BUILDING_HEIGHT);
104 | const pointsTop = points.map(p => p.clone().add(offset));
105 |
106 | for (let i = 0, n = points.length; i < n; i++) {
107 | points[i].toArray(positions, 6 * i);
108 | points[(i + 1) % n].toArray(positions, 6 * i + 3);
109 | }
110 |
111 | let topOffset = points.length * 6;
112 | for (let i = 0, n = pointsTop.length; i < n; i++) {
113 | pointsTop[i].toArray(positions, topOffset + 6 * i);
114 | pointsTop[(i + 1) % n].toArray(positions, topOffset + 6 * i + 3);
115 | }
116 |
117 | let vertEdgeOffset = points.length * 12;
118 | for (let i = 0; i < points.length; i++) {
119 | const p = points[i];
120 | const pTop = pointsTop[i];
121 |
122 | p.toArray(positions, vertEdgeOffset + 6 * i);
123 | pTop.toArray(positions, vertEdgeOffset + 6 * i + 3);
124 | }
125 |
126 | const lineGeometry = new LineSegmentsGeometry();
127 | lineGeometry.instanceCount = 3 * points.length;
128 | lineGeometry.setPositions(positions);
129 | const lineMaterial = new LineMaterial({
130 | color: BUILDING_LINE_COLOR,
131 | linewidth: 3
132 | });
133 |
134 | const line = new Line2(lineGeometry, lineMaterial);
135 | line.computeLineDistances();
136 | return line;
137 | }
138 |
139 | function getBuilding(points) {
140 | const buildingMaterial = new MeshStandardMaterial({
141 | transparent: true,
142 | opacity: 0.5,
143 | color: BUILDING_FILL_COLOR
144 | });
145 |
146 | const buildingShape = new Shape();
147 | points.forEach((p, i) => {
148 | i === 0 ? buildingShape.moveTo(p.x, p.y) : buildingShape.lineTo(p.x, p.y);
149 | });
150 |
151 | const extrudeSettings = {
152 | depth: BUILDING_HEIGHT,
153 | bevelEnabled: false
154 | };
155 | const buildingGeometry = new ExtrudeGeometry(buildingShape, extrudeSettings);
156 | return new Mesh(buildingGeometry, buildingMaterial);
157 | }
158 |
159 | function getLogo(overlay) {
160 | return new Promise(resolve => {
161 | const loader = new TextureLoader();
162 | loader.load(UBI_ICON, texture => {
163 | const logoGeometry = new PlaneGeometry(...LOGO_SIZE);
164 | const logoMaterial = new MeshBasicMaterial({
165 | map: texture,
166 | transparent: true
167 | });
168 | const logo = new Mesh(logoGeometry, logoMaterial);
169 | overlay.latLngAltToVector3(LOGO_POSITION, logo.position);
170 | logo.rotateZ(LOGO_ROTATION_Z);
171 | resolve(logo);
172 | });
173 | });
174 | }
175 |
176 | main().catch(err => {
177 | console.error('uncaught error in main: ', err);
178 | });
179 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ubilabs/threejs-overlay-view",
3 | "version": "0.7.1",
4 | "description": "A wrapper for the Google Maps WebGLOverlayView that takes care of the integration between three.js and the Google Maps JavaScript API. It lets you create a Google Maps overlays directly with three.js.",
5 | "keywords": [
6 | "google maps",
7 | "maps",
8 | "three",
9 | "threejs",
10 | "webgl"
11 | ],
12 | "author": "ubilabs GmbH ",
13 | "contributors": [
14 | "Martin Schuhfuss ",
15 | "Andreas Kofler ",
16 | "Martin Kleppe "
17 | ],
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/ubilabs/threejs-overlay-view.git"
21 | },
22 | "bugs": {
23 | "url": "https://github.com/ubilabs/threejs-overlay-view/issues"
24 | },
25 | "homepage": "https://github.com/ubilabs/threejs-overlay-view#readme",
26 | "license": "MIT",
27 | "type": "module",
28 | "source": "src/index.ts",
29 | "exports": "./dist/threejs-overlay-view.modern.js",
30 | "main": "./dist/threejs-overlay-view.js",
31 | "types": "./dist/threejs-overlay-view.d.ts",
32 | "module": "./dist/threejs-overlay-view.module.js",
33 | "unpkg": "./dist/threejs-overlay-view.umd.js",
34 | "workspaces": [
35 | "./examples"
36 | ],
37 | "scripts": {
38 | "test": "tsc -p tsconfig.json --noEmit && prettier -c './src/**/*.{ts,js}' && npm run test --workspaces",
39 | "prettier": "prettier --write './src/**/*.{ts,js}'",
40 | "start": "npm start --workspace examples",
41 | "build": "microbundle",
42 | "prepack": "npm install && npm run build"
43 | },
44 | "peerDependencies": {
45 | "three": ">=0.125.0"
46 | },
47 | "devDependencies": {
48 | "@types/google.maps": "^3.48.0",
49 | "@types/three": "^0.128.0",
50 | "microbundle": "^0.13.0",
51 | "prettier": "^2.3.0",
52 | "typescript": "^4.2.4"
53 | },
54 | "publishConfig": {
55 | "access": "public"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/geo-utils.ts:
--------------------------------------------------------------------------------
1 | import {MathUtils, Vector3} from 'three';
2 | import type {LatLngAltitudeLiteral} from './types';
3 |
4 | // shorthands for math-functions, makes equations more readable
5 | const {sin, cos, pow, sqrt, atan2, asin, sign} = Math;
6 | const {degToRad, radToDeg, euclideanModulo} = MathUtils;
7 |
8 | const EARTH_RADIUS_METERS = 6371008.8;
9 |
10 | /**
11 | * Returns the true bearing (=compass direction) of the point from the origin.
12 | * @param point
13 | */
14 | function getTrueBearing(point: Vector3): number {
15 | return euclideanModulo(90 - radToDeg(atan2(point.y, point.x)), 360);
16 | }
17 |
18 | /**
19 | * Computes the distance in meters between two coordinates using the
20 | * haversine formula.
21 | * @param from
22 | * @param to
23 | */
24 | function distance(
25 | from: google.maps.LatLngLiteral,
26 | to: google.maps.LatLngLiteral
27 | ): number {
28 | const {lat: latFrom, lng: lngFrom} = from;
29 | const {lat: latTo, lng: lngTo} = to;
30 |
31 | const dLat = degToRad(latTo - latFrom);
32 | const dLon = degToRad(lngTo - lngFrom);
33 | const lat1 = degToRad(latFrom);
34 | const lat2 = degToRad(latTo);
35 |
36 | const a =
37 | pow(sin(dLat / 2), 2) + pow(sin(dLon / 2), 2) * cos(lat1) * cos(lat2);
38 |
39 | return 2 * atan2(sqrt(a), sqrt(1 - a)) * EARTH_RADIUS_METERS;
40 | }
41 |
42 | /**
43 | * Computes a destination-point from a geographic origin, distance
44 | * and true bearing.
45 | * @param origin
46 | * @param distance
47 | * @param bearing
48 | * @param target optional target to write the result to
49 | */
50 | function destination(
51 | origin: google.maps.LatLngLiteral,
52 | distance: number,
53 | bearing: number,
54 | target: google.maps.LatLngLiteral = {lat: 0, lng: 0}
55 | ): google.maps.LatLngLiteral {
56 | const lngOrigin = degToRad(origin.lng);
57 | const latOrigin = degToRad(origin.lat);
58 |
59 | const bearingRad = degToRad(bearing);
60 | const radians = distance / EARTH_RADIUS_METERS;
61 |
62 | const latDestination = asin(
63 | sin(latOrigin) * cos(radians) +
64 | cos(latOrigin) * sin(radians) * cos(bearingRad)
65 | );
66 | const lngDestination =
67 | lngOrigin +
68 | atan2(
69 | sin(bearingRad) * sin(radians) * cos(latOrigin),
70 | cos(radians) - sin(latOrigin) * sin(latDestination)
71 | );
72 |
73 | target.lat = radToDeg(latDestination);
74 | target.lng = radToDeg(lngDestination);
75 |
76 | return target;
77 | }
78 |
79 | /**
80 | * Converts a point given in lat/lng or lat/lng/altitude-format to world-space coordinates.
81 | * @param point
82 | * @param reference
83 | * @param target optional target to write the result to
84 | */
85 | export function latLngAltToVector3(
86 | point: LatLngAltitudeLiteral | google.maps.LatLngLiteral,
87 | reference: LatLngAltitudeLiteral,
88 | target: Vector3 = new Vector3()
89 | ): Vector3 {
90 | const dx = distance(reference, {lng: point.lng, lat: reference.lat});
91 | const dy = distance(reference, {lng: reference.lng, lat: point.lat});
92 |
93 | const sx = sign(point.lng - reference.lng);
94 | const sy = sign(point.lat - reference.lat);
95 |
96 | const {altitude = 0} = point;
97 |
98 | return target.set(sx * dx, sy * dy, altitude);
99 | }
100 |
101 | /**
102 | * Converts a point given in world-space coordinates into geographic format.
103 | * @param point
104 | * @param sceneAnchor
105 | * @param target optional target to write the result to
106 | */
107 | export function vector3ToLatLngAlt(
108 | point: Vector3,
109 | sceneAnchor: LatLngAltitudeLiteral,
110 | target: LatLngAltitudeLiteral = {lat: 0, lng: 0, altitude: 0}
111 | ): LatLngAltitudeLiteral {
112 | const distance = point.length();
113 | const bearing = getTrueBearing(point);
114 |
115 | destination(sceneAnchor, distance, bearing, target);
116 | target.altitude = point.z;
117 |
118 | return target;
119 | }
120 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './threejs-overlay-view';
2 | export {default} from './threejs-overlay-view';
3 |
--------------------------------------------------------------------------------
/src/threejs-overlay-view.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DirectionalLight,
3 | HemisphereLight,
4 | Intersection,
5 | Matrix4,
6 | Object3D,
7 | PerspectiveCamera,
8 | Raycaster,
9 | Scene,
10 | Vector2,
11 | Vector3,
12 | WebGL1Renderer
13 | } from 'three';
14 |
15 | import {latLngAltToVector3, vector3ToLatLngAlt} from './geo-utils';
16 |
17 | import type {LatLngAltitudeLiteral, RaycastOptions} from './types';
18 |
19 | const projectionMatrixInverse = new Matrix4();
20 |
21 | /**
22 | * A wrapper for google.maps.WebGLOverlayView handling the details of the
23 | * integration with three.js.
24 | */
25 | export default class ThreeJSOverlayView {
26 | /**
27 | * The WebGLOverlayView instance being used. Aggregation is used instead
28 | * of extending the class to allow for this class to be parsed before the
29 | * google-maps API has been loaded.
30 | */
31 | protected readonly overlay: google.maps.WebGLOverlayView;
32 |
33 | /**
34 | * The three.js camera-instance. When interacting with this camera it is
35 | * important to know that the world-matrix doesn't contain any useful
36 | * information. Position and orientation of the camera are instead part
37 | * of the projectionMatrix.
38 | */
39 | protected readonly camera: PerspectiveCamera;
40 |
41 | /**
42 | * The three.js renderer instance. This is initialized in the
43 | * onContextRestored-callback.
44 | */
45 | protected renderer: WebGL1Renderer | null;
46 |
47 | /**
48 | * The three.js Scene instance.
49 | */
50 | protected scene: Scene;
51 |
52 | /**
53 | * The geographic reference-point in latitude/longitude/altitude above ground.
54 | */
55 | protected referencePoint: LatLngAltitudeLiteral;
56 |
57 | /**
58 | * The viewport size used by the renderer, this is always kept up-to-date.
59 | */
60 | protected viewportSize: Vector2 = new Vector2();
61 |
62 | /**
63 | * The raycaster used by the `raycast()` method.
64 | */
65 | protected raycaster: Raycaster = new Raycaster();
66 |
67 | /**
68 | * This callback is called when the overlay has been added to the map, but
69 | * before it is first rendered.
70 | */
71 | onAdd: (() => void) | null = null;
72 |
73 | /**
74 | * This callback is called after the overlay has been removed from the map.
75 | */
76 | onRemove: (() => void) | null = null;
77 |
78 | /**
79 | * This callback is called for every frame being rendered.
80 | */
81 | update: (() => void) | null = null;
82 |
83 | /**
84 | * Creates a new ThreejsOverlayView with the specified origin-point.
85 | * @param referencePoint
86 | */
87 | constructor(
88 | referencePoint: LatLngAltitudeLiteral | google.maps.LatLngLiteral
89 | ) {
90 | this.referencePoint = {altitude: 0, ...referencePoint};
91 | this.overlay = this.initWebGLOverlayView();
92 | this.renderer = null;
93 | this.scene = this.initScene();
94 | this.camera = new PerspectiveCamera();
95 | }
96 |
97 | /**
98 | * Sets the map-instance this overlay should be shown on.
99 | */
100 | setMap(map: google.maps.Map) {
101 | this.overlay.setMap(map);
102 | }
103 |
104 | /**
105 | * Sets the geographic coordinates of the reference point where the scene will
106 | * have it's origin.
107 | * @param referencePoint
108 | */
109 | setReferencePoint(
110 | referencePoint: LatLngAltitudeLiteral | google.maps.LatLngLiteral
111 | ) {
112 | this.referencePoint = {altitude: 0, ...referencePoint};
113 | }
114 |
115 | /**
116 | * Returns the scene-instance that is rendered on the map.
117 | */
118 | getScene(): Scene {
119 | return this.scene;
120 | }
121 |
122 | /**
123 | * Returns the viewport-size used by the map and the three.js renderer.
124 | */
125 | getViewportSize(): Vector2 {
126 | return this.viewportSize;
127 | }
128 |
129 | /**
130 | * Requests a full redraw of the map and all overlays for the next frame.
131 | *
132 | * This has to be called whenever changes to the scene were made to make
133 | * sure they are actually rendered.
134 | */
135 | requestRedraw() {
136 | this.overlay.requestRedraw();
137 | }
138 |
139 | /**
140 | * Runs raycasting for the specified screen-coordinate against the scene
141 | * or the optionally specified list of objects.
142 | * @param normalizedScreenPoint the screen-coordinates, x/y in range [-1, 1],
143 | * y pointing up.
144 | * @param objects optional list of objects to consider, raycasts against the
145 | * complete scene if none are specified
146 | * @param options.recursive set to true to also check children of the specified
147 | * objects for intersections. Only applies when a list of objects is
148 | * specified.
149 | * @param options.updateMatrix set this to false to skip updating the
150 | * inverse-projection-matrix (useful if you need to run multiple
151 | * raycasts for the same frame).
152 | * @param options.raycasterParameters parameters to pass on to the raycaster
153 | * @return returns the list of intersections
154 | */
155 | raycast(
156 | normalizedScreenPoint: Vector2,
157 | objects: Object3D | Object3D[] | null = null,
158 | options: RaycastOptions = {updateMatrix: true, recursive: false}
159 | ): Intersection[] {
160 | let {updateMatrix, recursive, raycasterParameters} = options;
161 |
162 | // the mvp-matrix used to render the previous frame is still stored in
163 | // this.camera.projectionMatrix so we don't need to recompute it. That
164 | // matrix would transform meters in our world-space (relative to
165 | // this.referencePoint) to clip-space/NDC coordinates. The inverse matrix
166 | // created here does the exact opposite and convert NDC-coordinates to
167 | // world-space
168 | if (updateMatrix) {
169 | projectionMatrixInverse.copy(this.camera.projectionMatrix).invert();
170 | }
171 |
172 | // create two points with different depth from the mouse-position and
173 | // convert to world-space to setup the ray.
174 | this.raycaster.ray.origin
175 | .set(normalizedScreenPoint.x, normalizedScreenPoint.y, 0)
176 | .applyMatrix4(projectionMatrixInverse);
177 |
178 | this.raycaster.ray.direction
179 | .set(normalizedScreenPoint.x, normalizedScreenPoint.y, 0.5)
180 | .applyMatrix4(projectionMatrixInverse)
181 | .sub(this.raycaster.ray.origin)
182 | .normalize();
183 |
184 | let oldRaycasterParams = this.raycaster.params;
185 | if (raycasterParameters) {
186 | this.raycaster.params = raycasterParameters;
187 | }
188 |
189 | if (objects === null) {
190 | objects = this.scene;
191 | recursive = true;
192 | }
193 |
194 | const results = Array.isArray(objects)
195 | ? this.raycaster.intersectObjects(objects, recursive)
196 | : this.raycaster.intersectObject(objects, recursive);
197 |
198 | // reset raycaster params to whatever they were before
199 | this.raycaster.params = oldRaycasterParams;
200 |
201 | return results;
202 | }
203 |
204 | /**
205 | * Converts geographic coordinates into world-space coordinates.
206 | * Optionally accepts a Vector3 instance as second parameter to write the value to.
207 | * @param point
208 | * @param target optional target the result will be written to
209 | */
210 | latLngAltToVector3(
211 | point: LatLngAltitudeLiteral | google.maps.LatLngLiteral,
212 | target: Vector3 = new Vector3()
213 | ): Vector3 {
214 | return latLngAltToVector3(point, this.referencePoint, target);
215 | }
216 |
217 | /**
218 | * Converts world-space coordinates to geographic coordinates.
219 | * Optionally accepts a LatLngAltitudeLiteral instance to write the value to.
220 | * @param point
221 | * @param target optional target the result will be written to
222 | */
223 | vector3ToLatLngAlt(
224 | point: Vector3,
225 | target: LatLngAltitudeLiteral = {lat: 0, lng: 0, altitude: 0}
226 | ): LatLngAltitudeLiteral {
227 | return vector3ToLatLngAlt(point, this.referencePoint, target);
228 | }
229 |
230 | /**
231 | * Initializes the threejs-renderer when the rendering-context becomes available.
232 | * @param gl
233 | */
234 | protected onContextRestored(stateOptions: google.maps.WebGLStateOptions) {
235 | const {gl} = stateOptions;
236 | const mapGlCanvas = gl.canvas as HTMLCanvasElement;
237 |
238 | let renderer = new WebGL1Renderer({
239 | canvas: mapGlCanvas,
240 | context: gl,
241 | ...gl.getContextAttributes()
242 | });
243 |
244 | renderer.autoClear = false;
245 | renderer.autoClearDepth = false;
246 |
247 | const {width, height} = gl.canvas;
248 | this.viewportSize.set(width, height);
249 | this.renderer = renderer;
250 | }
251 |
252 | /**
253 | * Cleans up and destroy the renderer when the context becomes invalid.
254 | */
255 | protected onContextLost() {
256 | if (!this.renderer) {
257 | return;
258 | }
259 |
260 | this.viewportSize.set(0, 0);
261 | this.renderer.dispose();
262 | this.renderer = null;
263 | }
264 |
265 | /**
266 | * Renders a new frame. Is called by the maps-api when the camera parameters
267 | * changed or a redraw was requested.
268 | * @param gl
269 | * @param transformer
270 | */
271 |
272 | protected onDraw(drawOptions: google.maps.WebGLDrawOptions) {
273 | const {gl, transformer} = drawOptions;
274 |
275 | if (!this.scene || !this.renderer) {
276 | return;
277 | }
278 |
279 | // fix: this appears to be a bug in the maps-API. onDraw will
280 | // continue to be called by the api after it has been removed
281 | // from the map. We should remove this once fixed upstream.
282 | if (this.overlay.getMap() === null) {
283 | return;
284 | }
285 |
286 | this.camera.projectionMatrix.fromArray(
287 | transformer.fromLatLngAltitude(this.referencePoint)
288 | );
289 |
290 | const {width, height} = gl.canvas;
291 | this.viewportSize.set(width, height);
292 | this.renderer.setViewport(0, 0, width, height);
293 |
294 | if (this.update) {
295 | this.update();
296 | }
297 |
298 | this.renderer.render(this.scene, this.camera);
299 | this.renderer.resetState();
300 | }
301 |
302 | /**
303 | * Initializes the scene with basic lighting that mimics the lighting used for
304 | * the buildings on the map.
305 | *
306 | * At some point it might be possible to retrieve information about the actual lighting
307 | * used for the buildings (which is dependent on time-of-day) from the api, which
308 | * is why the light-setup is handled by the ThreejsOverlayView.
309 | */
310 | protected initScene(): Scene {
311 | const scene = new Scene();
312 |
313 | // create two three.js lights to illuminate the model (roughly approximates
314 | // the lighting of buildings in maps)
315 | const hemiLight = new HemisphereLight(0xffffff, 0x444444, 1);
316 | hemiLight.position.set(0, -0.2, 1).normalize();
317 |
318 | const dirLight = new DirectionalLight(0xffffff);
319 | dirLight.position.set(0, 10, 100);
320 |
321 | scene.add(hemiLight, dirLight);
322 |
323 | return scene;
324 | }
325 |
326 | /**
327 | * Creates the google.maps.WebGLOverlayView instance
328 | */
329 | protected initWebGLOverlayView(): google.maps.WebGLOverlayView {
330 | if (!google || !google.maps) {
331 | throw new Error(
332 | 'Google Maps API not loaded. Please make sure to create the ' +
333 | 'overlay after the API has been loaded.'
334 | );
335 | }
336 |
337 | if (!google.maps.WebGLOverlayView) {
338 | throw new Error(
339 | 'WebGLOverlayView not found. Please make sure to load the ' +
340 | 'beta-channel of the Google Maps API.'
341 | );
342 | }
343 |
344 | const overlay = new google.maps.WebGLOverlayView();
345 |
346 | overlay.onAdd = wrapExceptionLogger(() => {
347 | if (this.onAdd === null) return;
348 | this.onAdd();
349 | });
350 |
351 | overlay.onRemove = wrapExceptionLogger(() => {
352 | if (this.onRemove === null) return;
353 | this.onRemove();
354 | });
355 |
356 | overlay.onDraw = wrapExceptionLogger(this.onDraw.bind(this));
357 |
358 | overlay.onContextRestored = wrapExceptionLogger(
359 | this.onContextRestored.bind(this)
360 | );
361 |
362 | overlay.onContextLost = wrapExceptionLogger(this.onContextLost.bind(this));
363 |
364 | return overlay;
365 | }
366 | }
367 |
368 | // (hopefully) temporary solution to make sure exceptions wont be silently ignored.
369 | function wrapExceptionLogger(fn: T): T {
370 | return ((...args: any[]) => {
371 | try {
372 | return fn(...args);
373 | } catch (err) {
374 | console.error(err);
375 | throw err;
376 | }
377 | }) as any;
378 | }
379 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type {RaycasterParameters} from 'three';
2 |
3 | export type LatLngAltitudeLiteral = {
4 | lat: number;
5 | lng: number;
6 | altitude: number;
7 | };
8 |
9 | export interface RaycastOptions {
10 | recursive?: boolean;
11 | raycasterParameters?: RaycasterParameters;
12 | updateMatrix?: boolean;
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["./src/**/*"],
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "jsx": "preserve",
8 | "baseUrl": "./",
9 | "paths": {},
10 | "typeRoots": ["./node_modules/@types"],
11 | "allowSyntheticDefaultImports": true,
12 | "importsNotUsedAsValues": "error",
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "strict": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "resolveJsonModule": true,
19 | "useDefineForClassFields": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------