@mapcomponents/three 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +12 -0
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +0 -0
- package/README.md +54 -0
- package/cypress.config.ts +13 -0
- package/eslint.config.mjs +12 -0
- package/package.json +24 -0
- package/project.json +15 -0
- package/public/assets/3D/godzilla_simple.glb +0 -0
- package/public/assets/splats/output.splat +0 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.cy.tsx +63 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.meta.json +21 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.stories.tsx +161 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.tsx +153 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.cy.tsx +62 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.meta.json +21 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.stories.tsx +151 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.tsx +158 -0
- package/src/components/MlTransformControls.tsx +112 -0
- package/src/components/ThreeContext.tsx +26 -0
- package/src/components/ThreeObjectControls.tsx +197 -0
- package/src/components/ThreeProvider.tsx +149 -0
- package/src/cypress/support/commands.ts +1 -0
- package/src/cypress/support/component-index.html +13 -0
- package/src/cypress/support/component.ts +13 -0
- package/src/decorators/ThreejsContextDecorator.tsx +42 -0
- package/src/decorators/style.css +33 -0
- package/src/index.ts +7 -0
- package/src/lib/ThreejsSceneHelper.ts +250 -0
- package/src/lib/ThreejsSceneRenderer.ts +73 -0
- package/src/lib/ThreejsUtils.ts +62 -0
- package/src/lib/splats/GaussianSplattingMesh.ts +848 -0
- package/src/lib/splats/GaussianSplattingShaders.ts +266 -0
- package/src/lib/splats/loaders/PlySplatLoader.ts +537 -0
- package/src/lib/splats/loaders/SplatLoader.ts +52 -0
- package/src/lib/utils/coroutine.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsconfig.lib.json +27 -0
- package/tsconfig.storybook.json +24 -0
- package/vite.config.ts +49 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { useMap } from '@mapcomponents/react-maplibre';
|
|
3
|
+
import { Scene, PerspectiveCamera, Group, Matrix4 } from 'three';
|
|
4
|
+
import { LngLatLike, CustomLayerInterface } from 'maplibre-gl';
|
|
5
|
+
import ThreejsSceneHelper from '../lib/ThreejsSceneHelper';
|
|
6
|
+
import ThreejsSceneRenderer from '../lib/ThreejsSceneRenderer';
|
|
7
|
+
import ThreejsUtils from '../lib/ThreejsUtils';
|
|
8
|
+
import { ThreeContext } from './ThreeContext';
|
|
9
|
+
|
|
10
|
+
export interface ThreeProviderProps {
|
|
11
|
+
mapId?: string;
|
|
12
|
+
id: string;
|
|
13
|
+
refCenter?: LngLatLike;
|
|
14
|
+
envTexture?: string;
|
|
15
|
+
envIntensity?: number;
|
|
16
|
+
createLight?: boolean;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Id of an existing layer in the MapLibre instance to help specify the layer order.
|
|
20
|
+
* The Three.js layer will be rendered visually beneath the layer with the specified id.
|
|
21
|
+
*/
|
|
22
|
+
beforeId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ThreeProvider: React.FC<ThreeProviderProps> = ({
|
|
26
|
+
mapId,
|
|
27
|
+
id,
|
|
28
|
+
refCenter,
|
|
29
|
+
envTexture,
|
|
30
|
+
envIntensity = 1,
|
|
31
|
+
createLight = true,
|
|
32
|
+
children,
|
|
33
|
+
beforeId,
|
|
34
|
+
}) => {
|
|
35
|
+
const { map } = useMap({ mapId, waitForLayer: beforeId });
|
|
36
|
+
const [scene, setScene] = useState<Scene>();
|
|
37
|
+
const [camera, setCamera] = useState<PerspectiveCamera>();
|
|
38
|
+
const [renderer, setRenderer] = useState<ThreejsSceneRenderer>();
|
|
39
|
+
const [sceneRoot, setSceneRoot] = useState<Group>();
|
|
40
|
+
const [worldMatrix, setWorldMatrix] = useState<Matrix4>();
|
|
41
|
+
const [worldMatrixInv, setWorldMatrixInv] = useState<Matrix4>();
|
|
42
|
+
|
|
43
|
+
const helperRef = useRef(new ThreejsSceneHelper());
|
|
44
|
+
const worldMatrixRef = useRef<Matrix4>(new Matrix4());
|
|
45
|
+
const worldMatrixInvRef = useRef<Matrix4>(new Matrix4());
|
|
46
|
+
const rendererRef = useRef<ThreejsSceneRenderer | undefined>(undefined);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!map) return;
|
|
50
|
+
|
|
51
|
+
const helper = helperRef.current;
|
|
52
|
+
const threeScene = helper.createScene(createLight);
|
|
53
|
+
const root = helper.createGroup(threeScene, 'scene-root');
|
|
54
|
+
const threeCamera = helper.createCamera(root, 'camera-for-render');
|
|
55
|
+
|
|
56
|
+
const customLayer: CustomLayerInterface = {
|
|
57
|
+
id: id,
|
|
58
|
+
type: 'custom',
|
|
59
|
+
renderingMode: '3d',
|
|
60
|
+
onAdd: (mapInstance, gl) => {
|
|
61
|
+
const threeRenderer = new ThreejsSceneRenderer(mapInstance, gl as WebGL2RenderingContext);
|
|
62
|
+
rendererRef.current = threeRenderer;
|
|
63
|
+
setRenderer(threeRenderer);
|
|
64
|
+
|
|
65
|
+
const center = refCenter || mapInstance.getCenter();
|
|
66
|
+
worldMatrixRef.current = ThreejsUtils.updateWorldMatrix(mapInstance, center);
|
|
67
|
+
worldMatrixInvRef.current = worldMatrixRef.current.clone().invert();
|
|
68
|
+
setWorldMatrix(worldMatrixRef.current);
|
|
69
|
+
setWorldMatrixInv(worldMatrixInvRef.current);
|
|
70
|
+
|
|
71
|
+
if (envTexture) {
|
|
72
|
+
helper.createEnvTexture(envTexture, threeScene);
|
|
73
|
+
}
|
|
74
|
+
threeScene.environmentIntensity = envIntensity;
|
|
75
|
+
|
|
76
|
+
mapInstance.triggerRepaint();
|
|
77
|
+
},
|
|
78
|
+
render: (gl, matrix) => {
|
|
79
|
+
if (!rendererRef.current || !threeScene || !threeCamera) return;
|
|
80
|
+
|
|
81
|
+
helper.updateCameraForRender(
|
|
82
|
+
threeCamera,
|
|
83
|
+
map.map,
|
|
84
|
+
matrix,
|
|
85
|
+
worldMatrixRef.current,
|
|
86
|
+
worldMatrixInvRef.current
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
rendererRef.current.render(threeScene, threeCamera);
|
|
90
|
+
map.triggerRepaint();
|
|
91
|
+
},
|
|
92
|
+
onRemove: () => {
|
|
93
|
+
if (rendererRef.current) {
|
|
94
|
+
rendererRef.current.dispose();
|
|
95
|
+
rendererRef.current = undefined;
|
|
96
|
+
}
|
|
97
|
+
setRenderer(undefined);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (!map.getLayer(id)) {
|
|
102
|
+
map.addLayer(customLayer, beforeId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setScene(threeScene);
|
|
106
|
+
setCamera(threeCamera);
|
|
107
|
+
setSceneRoot(root);
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
if (map.getLayer(id)) {
|
|
111
|
+
map.removeLayer(id);
|
|
112
|
+
}
|
|
113
|
+
// Cleanup is handled in onRemove
|
|
114
|
+
};
|
|
115
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
}, [map, id]); // Re-run if map or id changes.
|
|
117
|
+
|
|
118
|
+
// Handle dynamic prop changes
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (scene && envTexture) {
|
|
121
|
+
helperRef.current.createEnvTexture(envTexture, scene);
|
|
122
|
+
}
|
|
123
|
+
}, [scene, envTexture]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (scene) {
|
|
127
|
+
scene.environmentIntensity = envIntensity;
|
|
128
|
+
}
|
|
129
|
+
}, [scene, envIntensity]);
|
|
130
|
+
|
|
131
|
+
// Handle refCenter change
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (map && refCenter) {
|
|
134
|
+
worldMatrixRef.current = ThreejsUtils.updateWorldMatrix(map.map, refCenter);
|
|
135
|
+
worldMatrixInvRef.current = worldMatrixRef.current.clone().invert();
|
|
136
|
+
setWorldMatrix(worldMatrixRef.current);
|
|
137
|
+
setWorldMatrixInv(worldMatrixInvRef.current);
|
|
138
|
+
map.triggerRepaint();
|
|
139
|
+
}
|
|
140
|
+
}, [map, refCenter]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<ThreeContext.Provider
|
|
144
|
+
value={{ scene, camera, renderer, map: map?.map, sceneRoot, worldMatrix, worldMatrixInv }}
|
|
145
|
+
>
|
|
146
|
+
{children}
|
|
147
|
+
</ThreeContext.Provider>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
7
|
+
<title>@mapcomponents/three Components App</title>
|
|
8
|
+
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div data-cy-root></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mount } from 'cypress/react';
|
|
2
|
+
import './commands';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
+
namespace Cypress {
|
|
7
|
+
interface Chainable {
|
|
8
|
+
mount: typeof mount;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Cypress.Commands.add('mount', mount);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ThreeProvider } from '../components/ThreeProvider';
|
|
3
|
+
import {
|
|
4
|
+
MapComponentsProvider,
|
|
5
|
+
MapLibreMap,
|
|
6
|
+
MlNavigationTools,
|
|
7
|
+
getTheme,
|
|
8
|
+
} from '@mapcomponents/react-maplibre';
|
|
9
|
+
import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles';
|
|
10
|
+
import './style.css';
|
|
11
|
+
|
|
12
|
+
const decorators = [
|
|
13
|
+
(Story: any, context: any) => {
|
|
14
|
+
const theme = useMemo(() => getTheme(context?.globals?.theme), [context?.globals?.theme]);
|
|
15
|
+
return (
|
|
16
|
+
<div className="fullscreen_map">
|
|
17
|
+
<MapComponentsProvider>
|
|
18
|
+
<MUIThemeProvider theme={theme}>
|
|
19
|
+
<ThreeProvider mapId="map_1" id="three-scene-layer" beforeId="water_name_line">
|
|
20
|
+
<Story />
|
|
21
|
+
</ThreeProvider>
|
|
22
|
+
<MapLibreMap
|
|
23
|
+
options={{
|
|
24
|
+
zoom: 14.5,
|
|
25
|
+
style: 'https://wms.wheregroup.com/tileserver/style/osm-liberty.json',
|
|
26
|
+
center: [7.099771581806502, 50.73395746209983],
|
|
27
|
+
}}
|
|
28
|
+
mapId="map_1"
|
|
29
|
+
/>
|
|
30
|
+
<MlNavigationTools
|
|
31
|
+
sx={{ bottom: '25px', right: '5px' }}
|
|
32
|
+
showZoomButtons={false}
|
|
33
|
+
mapId="map_1"
|
|
34
|
+
/>
|
|
35
|
+
</MUIThemeProvider>
|
|
36
|
+
</MapComponentsProvider>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export default decorators;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#root {
|
|
2
|
+
background-color: #000;
|
|
3
|
+
position: absolute;
|
|
4
|
+
min-height: 400px;
|
|
5
|
+
top: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
left: 0;
|
|
8
|
+
right: 0;
|
|
9
|
+
}
|
|
10
|
+
.docs-story {
|
|
11
|
+
min-height: 400px;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: stretch;
|
|
14
|
+
}
|
|
15
|
+
.docs-story > div:first-child {
|
|
16
|
+
width: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.App {
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 0;
|
|
22
|
+
right: 0;
|
|
23
|
+
bottom: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
}
|
|
26
|
+
.fullscreen_map .mapContainer {
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 0;
|
|
29
|
+
right: 0;
|
|
30
|
+
left: 0;
|
|
31
|
+
bottom: 0;
|
|
32
|
+
z-index: 100;
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './lib/ThreejsUtils';
|
|
2
|
+
export * from './lib/ThreejsSceneHelper';
|
|
3
|
+
export * from './lib/ThreejsSceneRenderer';
|
|
4
|
+
export * from './components/ThreeContext';
|
|
5
|
+
export * from './components/ThreeProvider';
|
|
6
|
+
export { default as MlThreeModelLayer } from './components/MlThreeModelLayer/MlThreeModelLayer';
|
|
7
|
+
export { default as MlThreeSplatLayer } from './components/MlThreeSplatLayer/MlThreeSplatLayer';
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
|
|
3
|
+
* https://github.com/yangjs6/mapbox-3d-tiles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type Map as MaplibreMap } from 'maplibre-gl';
|
|
7
|
+
import {
|
|
8
|
+
Scene,
|
|
9
|
+
PerspectiveCamera,
|
|
10
|
+
Matrix4,
|
|
11
|
+
Group,
|
|
12
|
+
EquirectangularReflectionMapping,
|
|
13
|
+
DirectionalLight,
|
|
14
|
+
AmbientLight,
|
|
15
|
+
Vector3,
|
|
16
|
+
Quaternion,
|
|
17
|
+
Euler,
|
|
18
|
+
} from 'three';
|
|
19
|
+
import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
|
|
20
|
+
import ThreejsUtils from './ThreejsUtils';
|
|
21
|
+
|
|
22
|
+
export default class ThreejsSceneHelper {
|
|
23
|
+
createScene(createLight = true): Scene {
|
|
24
|
+
const scene = new Scene();
|
|
25
|
+
|
|
26
|
+
if (createLight) {
|
|
27
|
+
const dirLight = new DirectionalLight(0xffffff, 4);
|
|
28
|
+
dirLight.position.set(1, 2, 3);
|
|
29
|
+
scene.add(dirLight);
|
|
30
|
+
|
|
31
|
+
const ambLight = new AmbientLight(0xffffff, 0.2);
|
|
32
|
+
scene.add(ambLight);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return scene;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
createGroup(parent: Scene | Group, name: string): Group {
|
|
39
|
+
const group = new Group();
|
|
40
|
+
group.name = name;
|
|
41
|
+
parent.add(group);
|
|
42
|
+
return group;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
createCamera(sceneRoot: Group, name: string): PerspectiveCamera {
|
|
46
|
+
const camera = new PerspectiveCamera();
|
|
47
|
+
camera.name = name;
|
|
48
|
+
|
|
49
|
+
const group = new Group();
|
|
50
|
+
group.name = `${name}-parent`;
|
|
51
|
+
group.add(camera);
|
|
52
|
+
|
|
53
|
+
sceneRoot.add(group);
|
|
54
|
+
return camera;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private buildPerspectiveMatrix(
|
|
58
|
+
fov: number,
|
|
59
|
+
aspect: number,
|
|
60
|
+
near: number,
|
|
61
|
+
far: number
|
|
62
|
+
): Float64Array {
|
|
63
|
+
const f = 1.0 / Math.tan(fov / 2);
|
|
64
|
+
const nf = 1.0 / (near - far);
|
|
65
|
+
|
|
66
|
+
return new Float64Array([
|
|
67
|
+
f / aspect,
|
|
68
|
+
0,
|
|
69
|
+
0,
|
|
70
|
+
0,
|
|
71
|
+
0,
|
|
72
|
+
f,
|
|
73
|
+
0,
|
|
74
|
+
0,
|
|
75
|
+
0,
|
|
76
|
+
0,
|
|
77
|
+
(far + near) * nf,
|
|
78
|
+
-1,
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
2 * far * near * nf,
|
|
82
|
+
0,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private buildOrthographicMatrix(
|
|
87
|
+
left: number,
|
|
88
|
+
right: number,
|
|
89
|
+
bottom: number,
|
|
90
|
+
top: number,
|
|
91
|
+
near: number,
|
|
92
|
+
far: number
|
|
93
|
+
): Float64Array {
|
|
94
|
+
const lr = 1 / (left - right);
|
|
95
|
+
const bt = 1 / (bottom - top);
|
|
96
|
+
const nf = 1 / (near - far);
|
|
97
|
+
|
|
98
|
+
return new Float64Array([
|
|
99
|
+
-2 * lr,
|
|
100
|
+
0,
|
|
101
|
+
0,
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
104
|
+
-2 * bt,
|
|
105
|
+
0,
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
0,
|
|
109
|
+
2 * nf,
|
|
110
|
+
0,
|
|
111
|
+
(left + right) * lr,
|
|
112
|
+
(top + bottom) * bt,
|
|
113
|
+
(far + near) * nf,
|
|
114
|
+
1,
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private calcProjectionMatrix(transform: any, fov: number, nearZ: number, farZ: number): Matrix4 {
|
|
119
|
+
const offset = transform.centerOffset;
|
|
120
|
+
const aspect = transform.width / transform.height;
|
|
121
|
+
|
|
122
|
+
const perspective = this.buildPerspectiveMatrix(fov, aspect, nearZ, farZ);
|
|
123
|
+
perspective[8] = (-offset.x * 2) / transform.width;
|
|
124
|
+
perspective[9] = (offset.y * 2) / transform.height;
|
|
125
|
+
|
|
126
|
+
if (!transform.isOrthographic) {
|
|
127
|
+
return new Matrix4().fromArray(perspective);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cameraToCenterDistance = (0.5 * transform.height) / Math.tan(fov / 2.0);
|
|
131
|
+
const halfHeight = cameraToCenterDistance * Math.tan(fov * 0.5);
|
|
132
|
+
const halfWidth = halfHeight * aspect;
|
|
133
|
+
|
|
134
|
+
const ortho = this.buildOrthographicMatrix(
|
|
135
|
+
-halfWidth - offset.x,
|
|
136
|
+
halfWidth - offset.x,
|
|
137
|
+
-halfHeight + offset.y,
|
|
138
|
+
halfHeight + offset.y,
|
|
139
|
+
nearZ,
|
|
140
|
+
farZ
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const transitionPitch = 15;
|
|
144
|
+
const t = Math.min(transform.pitch / transitionPitch, 1.0);
|
|
145
|
+
const eased = t * t * t * t * t;
|
|
146
|
+
|
|
147
|
+
const blended = new Float64Array(16);
|
|
148
|
+
for (let i = 0; i < 16; i++) {
|
|
149
|
+
blended[i] = (1 - eased) * ortho[i] + eased * perspective[i];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return new Matrix4().fromArray(blended);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
updateCameraForRender(
|
|
156
|
+
camera: PerspectiveCamera,
|
|
157
|
+
map: MaplibreMap,
|
|
158
|
+
matrix: any,
|
|
159
|
+
worldMatrix: Matrix4,
|
|
160
|
+
worldMatrixInv: Matrix4
|
|
161
|
+
): void {
|
|
162
|
+
const transform = map.transform;
|
|
163
|
+
|
|
164
|
+
const { fov, nearZ, farZ, aspect } = this.extractCameraParams(matrix, transform);
|
|
165
|
+
|
|
166
|
+
camera.fov = ThreejsUtils.radToDeg(fov);
|
|
167
|
+
camera.aspect = aspect;
|
|
168
|
+
camera.near = nearZ;
|
|
169
|
+
camera.far = farZ;
|
|
170
|
+
|
|
171
|
+
const cleanProjection = this.buildPerspectiveMatrix(fov, aspect, nearZ, farZ);
|
|
172
|
+
(camera as any)._cleanProjectionMatrix = cleanProjection;
|
|
173
|
+
|
|
174
|
+
const mvpMatrix = this.extractMVPMatrix(matrix, worldMatrix);
|
|
175
|
+
const projectionMatrix = this.calcProjectionMatrix(transform, fov, nearZ, farZ);
|
|
176
|
+
|
|
177
|
+
camera.projectionMatrix.copy(projectionMatrix);
|
|
178
|
+
camera.projectionMatrixInverse.copy(projectionMatrix).invert();
|
|
179
|
+
|
|
180
|
+
const viewMatrix = new Matrix4().multiplyMatrices(camera.projectionMatrixInverse, mvpMatrix);
|
|
181
|
+
|
|
182
|
+
camera.matrixWorld.copy(viewMatrix).invert();
|
|
183
|
+
camera.matrixWorldInverse.copy(viewMatrix);
|
|
184
|
+
camera.matrixAutoUpdate = false;
|
|
185
|
+
camera.matrixWorldAutoUpdate = false;
|
|
186
|
+
|
|
187
|
+
this.updateCameraTransform(camera);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private extractCameraParams(matrix: any, transform: any) {
|
|
191
|
+
const aspect = transform.width / transform.height;
|
|
192
|
+
|
|
193
|
+
if (matrix.fov !== undefined) {
|
|
194
|
+
return {
|
|
195
|
+
fov: matrix.fov,
|
|
196
|
+
nearZ: matrix.nearZ,
|
|
197
|
+
farZ: matrix.farZ,
|
|
198
|
+
aspect,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const cameraToCenterDistance = transform.cameraToCenterDistance;
|
|
203
|
+
const fov =
|
|
204
|
+
cameraToCenterDistance && transform.height
|
|
205
|
+
? 2 * Math.atan(transform.height / 2 / cameraToCenterDistance)
|
|
206
|
+
: 0.6435;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
fov,
|
|
210
|
+
nearZ: transform.nearZ || 0.1,
|
|
211
|
+
farZ: transform.farZ || 10000,
|
|
212
|
+
aspect,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private extractMVPMatrix(matrix: any, worldMatrix: Matrix4): Matrix4 {
|
|
217
|
+
let baseMatrix: Matrix4;
|
|
218
|
+
|
|
219
|
+
if (matrix.defaultProjectionData?.mainMatrix) {
|
|
220
|
+
baseMatrix = new Matrix4().fromArray(Object.values(matrix.defaultProjectionData.mainMatrix));
|
|
221
|
+
} else if (matrix.modelViewProjectionMatrix) {
|
|
222
|
+
baseMatrix = new Matrix4().fromArray(matrix.modelViewProjectionMatrix);
|
|
223
|
+
} else {
|
|
224
|
+
const arr = Array.isArray(matrix) ? matrix : Array.from(matrix);
|
|
225
|
+
baseMatrix = new Matrix4().fromArray(arr);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return new Matrix4().multiplyMatrices(baseMatrix, worldMatrix);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private updateCameraTransform(camera: PerspectiveCamera): void {
|
|
232
|
+
const position = new Vector3();
|
|
233
|
+
const quaternion = new Quaternion();
|
|
234
|
+
const scale = new Vector3();
|
|
235
|
+
|
|
236
|
+
camera.matrixWorld.decompose(position, quaternion, scale);
|
|
237
|
+
camera.position.copy(position);
|
|
238
|
+
camera.rotation.copy(new Euler().setFromQuaternion(quaternion, 'YXZ'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
createEnvTexture(envTexture: string, scene: Scene): void {
|
|
242
|
+
if (!envTexture?.endsWith('.hdr')) return;
|
|
243
|
+
|
|
244
|
+
new HDRLoader().load(envTexture, (texture) => {
|
|
245
|
+
texture.mapping = EquirectangularReflectionMapping;
|
|
246
|
+
scene.environment = texture;
|
|
247
|
+
scene.environmentRotation.x = Math.PI / 2;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
|
|
3
|
+
* https://github.com/yangjs6/mapbox-3d-tiles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type Map as MaplibreMap } from 'maplibre-gl';
|
|
7
|
+
import { WebGLRenderer, Scene, Camera } from 'three';
|
|
8
|
+
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
|
|
9
|
+
|
|
10
|
+
export default class ThreejsSceneRenderer {
|
|
11
|
+
private renderer: WebGLRenderer;
|
|
12
|
+
private labelRenderer: CSS2DRenderer;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a ThreejsSceneRenderer instance.
|
|
16
|
+
*
|
|
17
|
+
* @param map - The MapLibre map instance
|
|
18
|
+
* @param gl - The WebGL2 rendering context from MapLibre
|
|
19
|
+
*/
|
|
20
|
+
constructor(map: MaplibreMap, gl: WebGL2RenderingContext) {
|
|
21
|
+
if (!gl || gl.isContextLost()) {
|
|
22
|
+
throw new Error('WebGL context is lost or invalid');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.renderer = new WebGLRenderer({
|
|
26
|
+
alpha: true,
|
|
27
|
+
antialias: true,
|
|
28
|
+
canvas: map.getCanvas(),
|
|
29
|
+
context: gl,
|
|
30
|
+
});
|
|
31
|
+
this.renderer.autoClear = false;
|
|
32
|
+
|
|
33
|
+
this.renderer.shadowMap.enabled = true;
|
|
34
|
+
this.labelRenderer = this.createLabelRenderer(map);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private createLabelRenderer(map: MaplibreMap): CSS2DRenderer {
|
|
38
|
+
const container = map.getContainer();
|
|
39
|
+
const labelRenderer = new CSS2DRenderer();
|
|
40
|
+
|
|
41
|
+
labelRenderer.setSize(container.clientWidth, container.clientHeight);
|
|
42
|
+
labelRenderer.domElement.style.position = 'absolute';
|
|
43
|
+
labelRenderer.domElement.style.top = '0px';
|
|
44
|
+
labelRenderer.domElement.style.pointerEvents = 'none';
|
|
45
|
+
|
|
46
|
+
map._container.appendChild(labelRenderer.domElement);
|
|
47
|
+
map.on('resize', () => {
|
|
48
|
+
const { clientWidth, clientHeight } = map.getContainer();
|
|
49
|
+
labelRenderer.setSize(clientWidth, clientHeight);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return labelRenderer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getRenderer(): WebGLRenderer {
|
|
56
|
+
return this.renderer;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render(scene: Scene, camera: Camera): void {
|
|
60
|
+
// Reset WebGL state to avoid conflicts with MapLibre
|
|
61
|
+
// but DO NOT clear the depth buffer - we want to preserve MapLibre's depth
|
|
62
|
+
// information so Three.js objects can be properly occluded by MapLibre 3D
|
|
63
|
+
// content (fill-extrusion buildings, terrain, etc.) and vice versa.
|
|
64
|
+
this.renderer.resetState();
|
|
65
|
+
this.renderer.render(scene, camera);
|
|
66
|
+
this.labelRenderer.render(scene, camera);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dispose(): void {
|
|
70
|
+
this.labelRenderer.domElement?.parentNode?.removeChild(this.labelRenderer.domElement);
|
|
71
|
+
this.renderer?.dispose();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
|
|
3
|
+
* https://github.com/yangjs6/mapbox-3d-tiles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type Map as MaplibreMap, MercatorCoordinate, LngLatLike } from 'maplibre-gl';
|
|
7
|
+
import { Vector3, Quaternion, Matrix4 } from 'three';
|
|
8
|
+
|
|
9
|
+
type Position = number[];
|
|
10
|
+
|
|
11
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
12
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
13
|
+
|
|
14
|
+
export default class ThreejsUtils {
|
|
15
|
+
static updateWorldMatrix(map: MaplibreMap | null, refCenter: LngLatLike | null = null): Matrix4 {
|
|
16
|
+
if (!map) return new Matrix4();
|
|
17
|
+
|
|
18
|
+
const center = refCenter ?? map.getCenter();
|
|
19
|
+
const origin = MercatorCoordinate.fromLngLat(center);
|
|
20
|
+
const scale = origin.meterInMercatorCoordinateUnits();
|
|
21
|
+
|
|
22
|
+
return new Matrix4().compose(
|
|
23
|
+
new Vector3(origin.x, origin.y, origin.z),
|
|
24
|
+
new Quaternion(),
|
|
25
|
+
new Vector3(scale, -scale, scale)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static toScenePositionMercator(worldMatrixInv: Matrix4, mercator: MercatorCoordinate): Vector3 {
|
|
30
|
+
return new Vector3(mercator.x, mercator.y, mercator.z).applyMatrix4(worldMatrixInv);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static toMapPositionMercator(worldMatrix: Matrix4, position: Vector3): MercatorCoordinate {
|
|
34
|
+
const transformed = position.clone().applyMatrix4(worldMatrix);
|
|
35
|
+
return new MercatorCoordinate(transformed.x, transformed.y, transformed.z);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static toScenePosition(
|
|
39
|
+
worldMatrixInv: Matrix4,
|
|
40
|
+
position: LngLatLike,
|
|
41
|
+
altitude?: number
|
|
42
|
+
): Vector3 {
|
|
43
|
+
return this.toScenePositionMercator(
|
|
44
|
+
worldMatrixInv,
|
|
45
|
+
MercatorCoordinate.fromLngLat(position, altitude)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static toMapPosition(worldMatrix: Matrix4, position: Vector3): Position {
|
|
50
|
+
const mercator = this.toMapPositionMercator(worldMatrix, position);
|
|
51
|
+
const lngLat = mercator.toLngLat();
|
|
52
|
+
return [lngLat.lng, lngLat.lat, mercator.toAltitude()];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static degToRad(degrees: number): number {
|
|
56
|
+
return degrees * DEG_TO_RAD;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static radToDeg(radians: number): number {
|
|
60
|
+
return radians * RAD_TO_DEG;
|
|
61
|
+
}
|
|
62
|
+
}
|