feat(three.js layer): support rendering three.js meshes

This commit is contained in:
yuqi.pyq 2020-01-09 14:53:27 +08:00
parent bc22e02657
commit e982ef89ba
17 changed files with 4330 additions and 3904 deletions

View File

@ -104,6 +104,7 @@
"stylelint-config-standard": "^18.2.0",
"stylelint-config-styled-components": "^0.1.1",
"stylelint-processor-styled-components": "^1.3.2",
"three": "^0.111.0",
"ts-jest": "^24.0.2",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",

View File

@ -53,7 +53,6 @@ export default class LayerService implements ILayerService {
public renderLayers() {
// TODO脏检查只渲染发生改变的 Layer
//
this.clear();
this.layers
.filter((layer) => layer.isVisible())

View File

@ -23,8 +23,6 @@ const bytesPerElementMap = {
[gl.UNSIGNED_SHORT]: 2,
};
let counter = 0;
/**
* Layer
*/
@ -41,10 +39,6 @@ export default class StyleAttributeService implements IStyleAttributeService {
private attributes: IStyleAttribute[] = [];
private triangulation: Triangulation;
private c = counter++;
private featureLayout: {
sizePerElement: number;
elements: Array<{
@ -177,16 +171,13 @@ export default class StyleAttributeService implements IStyleAttributeService {
public createAttributesAndIndices(
features: IEncodeFeature[],
triangulation?: Triangulation,
triangulation: Triangulation,
): {
attributes: {
[attributeName: string]: IAttribute;
};
elements: IElements;
} {
if (triangulation) {
this.triangulation = triangulation;
}
const descriptors = this.attributes.map((attr) => {
attr.resetDescriptor();
return attr.descriptor;
@ -204,7 +195,7 @@ export default class StyleAttributeService implements IStyleAttributeService {
vertices: verticesForCurrentFeature,
normals: normalsForCurrentFeature,
size: vertexSize,
} = this.triangulation(feature);
} = triangulation(feature);
indices.push(...indicesForCurrentFeature.map((i) => i + verticesNum));
vertices.push(...verticesForCurrentFeature);
if (normalsForCurrentFeature) {

View File

@ -11,6 +11,12 @@ export interface IPoint {
y: number;
}
export interface IMercator {
x: number;
y: number;
z: number;
}
export interface IMapWrapper {
setContainer(container: Container, id: string): void;
}
@ -58,6 +64,14 @@ export interface IMapService<RawMap = {}> {
lngLatToPixel(lnglat: Point): IPoint;
containerToLngLat(pixel: Point): ILngLat;
lngLatToContainer(lnglat: Point): IPoint;
lngLatToMercator(lnglat: [number, number], altitude: number): IMercator;
getModelMatrix(
lnglat: [number, number],
altitude: number,
rotate: [number, number, number],
scale: [number, number, number],
origin: IMercator,
): number[];
}
export const MapServiceEvent = ['mapload'];

View File

@ -54,6 +54,8 @@ export interface IRendererService {
): void;
getViewportSize(): { width: number; height: number };
getContainer(): HTMLElement | null;
getCanvas(): HTMLCanvasElement | null;
getGLContext(): WebGLRenderingContext;
viewport(size: { x: number; y: number; width: number; height: number }): void;
readPixels(options: IReadPixelsOptions): Uint8Array;
destroy(): void;

View File

@ -40,7 +40,8 @@
"merge-json-schemas": "1.0.0",
"polyline-miter-util": "^1.0.1",
"reflect-metadata": "^0.1.13",
"tapable": "^2.0.0-beta.8"
"tapable": "^2.0.0-beta.8",
"three": "^0.111.0"
},
"devDependencies": {
"@types/d3-array": "^2.0.0",

View File

@ -2,6 +2,8 @@ import {
gl,
IActiveOption,
IAnimateOption,
ICameraService,
ICoordinateSystemService,
IDataState,
IEncodeFeature,
IFontService,
@ -108,6 +110,10 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
@lazyInject(TYPES.IShaderModuleService)
protected readonly shaderModuleService: IShaderModuleService;
protected cameraService: ICameraService;
protected coordinateService: ICoordinateSystemService;
protected iconService: IIconService;
protected fontService: IFontService;
@ -229,6 +235,12 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
TYPES.IInteractionService,
);
this.mapService = this.container.get<IMapService>(TYPES.IMapService);
this.cameraService = this.container.get<ICameraService>(
TYPES.ICameraService,
);
this.coordinateService = this.container.get<ICoordinateSystemService>(
TYPES.ICoordinateSystemService,
);
this.postProcessingPassFactory = this.container.get(
TYPES.IFactoryPostProcessingPass,
);

View File

@ -8,6 +8,7 @@ import PointLayer from './point';
import PolygonLayer from './polygon';
import ImageLayer from './raster/image';
import RasterLayer from './raster/raster';
import ThreeJSLayer from './three';
import ConfigSchemaValidationPlugin from './plugins/ConfigSchemaValidationPlugin';
import DataMappingPlugin from './plugins/DataMappingPlugin';
@ -111,4 +112,5 @@ export {
ImageLayer,
RasterLayer,
HeatmapLayer,
ThreeJSLayer,
};

View File

@ -64,7 +64,7 @@ export default class FeatureScalePlugin implements ILayerPlugin {
this.caculateScalesForAttributes(attributes || [], dataArray);
});
// 检测数据是否需要更新
// 检测数据是否需要更新
layer.hooks.beforeRenderData.tap('FeatureScalePlugin', (flag) => {
if (flag) {
this.scaleOptions = layer.getScaleOptions();

View File

@ -0,0 +1,114 @@
/**
* inspired by threebox & Mapbox examples
* @see https://github.com/peterqliu/threebox/blob/master/src/Threebox.js
* @see https://github.com/peterqliu/threebox/blob/master/examples/Object3D.html
*/
import { IMercator } from '@antv/l7-core';
import { Camera, Matrix4, Scene, WebGLRenderer } from 'three';
import BaseLayer from '../core/BaseLayer';
export default class ThreeJSLayer extends BaseLayer<{
onAddMeshes: (threeScene: Scene, layer: ThreeJSLayer) => void;
}> {
public name: string = 'ThreeJSLayer';
private scene: Scene;
private camera: Camera;
private renderer: WebGLRenderer;
// 地图中点墨卡托坐标
private center: IMercator;
// 初始状态相机变换矩阵
private cameraTransform: Matrix4;
/**
*
*/
public getModelMatrix(
lnglat: [number, number],
altitude: number = 0,
rotation: [number, number, number] = [0, 0, 0],
scale: [number, number, number] = [1, 1, 1],
): Matrix4 {
return new Matrix4().fromArray(
this.mapService.getModelMatrix(
lnglat,
altitude,
rotation,
scale,
this.center,
),
);
}
protected getConfigSchema() {
return {
properties: {
// opacity: {
// type: 'altitude',
// minimum: 0,
// maximum: 100,
// },
},
};
}
protected buildModels() {
const canvas = this.rendererService.getCanvas();
const gl = this.rendererService.getGLContext();
if (canvas && gl) {
const center = this.mapService.getCenter();
this.center = this.mapService.lngLatToMercator(
[center.lng, center.lat],
0,
);
const { x, y, z } = this.center;
this.cameraTransform = new Matrix4().makeTranslation(x, y, z);
this.renderer = new WebGLRenderer({
canvas,
context: gl,
antialias: true,
});
// L7 负责 clear
this.renderer.autoClear = false;
// 是否需要 gamma correction?
this.renderer.gammaOutput = true;
this.renderer.gammaFactor = 2.2;
this.scene = new Scene();
// 后续同步 L7 相机
this.camera = new Camera();
const config = this.getLayerConfig();
if (config && config.onAddMeshes) {
config.onAddMeshes(this.scene, this);
}
}
}
protected renderModels() {
const { width, height } = this.rendererService.getViewportSize();
this.renderer.setSize(width, height, false);
const gl = this.rendererService.getGLContext();
gl.frontFace(gl.CCW);
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT);
// 同步相机
const mercatorMatrix = new Matrix4().fromArray(
// @ts-ignore
this.mapService.map.transform.customLayerMatrix(),
);
this.camera.projectionMatrix = mercatorMatrix.multiply(
this.cameraTransform,
);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
}
}

View File

@ -8,12 +8,14 @@ import {
ILngLat,
IMapConfig,
IMapService,
IMercator,
IPoint,
IViewport,
MapServiceEvent,
TYPES,
} from '@antv/l7-core';
import { DOM } from '@antv/l7-utils';
import { mat4, vec2, vec3 } from 'gl-matrix';
import { inject, injectable } from 'inversify';
import { IAMapEvent, IAMapInstance } from '../../typings/index';
import { MapTheme } from './theme';
@ -208,6 +210,27 @@ export default class AMapService
};
}
public lngLatToMercator(
lnglat: [number, number],
altitude: number,
): IMercator {
return {
x: 0,
y: 0,
z: 0,
};
}
public getModelMatrix(
lnglat: [number, number],
altitude: number,
rotate: [number, number, number],
scale: [number, number, number],
origin: IMercator,
): number[] {
return (mat4.create() as unknown) as number[];
}
public async init(): Promise<void> {
const {
id,

View File

@ -8,23 +8,26 @@ import {
ILngLat,
IMapConfig,
IMapService,
IMercator,
IPoint,
IViewport,
MapServiceEvent,
TYPES,
} from '@antv/l7-core';
import { DOM } from '@antv/l7-utils';
import { mat4, vec2, vec3 } from 'gl-matrix';
import { inject, injectable } from 'inversify';
import mapboxgl, { IControl, Map } from 'mapbox-gl';
import { IMapboxInstance } from '../../typings/index';
import { MapTheme } from './theme';
import Viewport from './Viewport';
const EventMap: {
[key: string]: any;
} = {
mapmove: 'move',
camerachange: 'move',
};
import { MapTheme } from './theme';
const LNGLAT_OFFSET_ZOOM_THRESHOLD = 12;
@ -172,6 +175,55 @@ export default class MapboxService
return this.map.project(lnglat);
}
public lngLatToMercator(
lnglat: [number, number],
altitude: number,
): IMercator {
const { x = 0, y = 0, z = 0 } = mapboxgl.MercatorCoordinate.fromLngLat(
lnglat,
altitude,
);
return { x, y, z };
}
public getModelMatrix(
lnglat: [number, number],
altitude: number,
rotate: [number, number, number],
scale: [number, number, number] = [1, 1, 1],
origin: IMercator = { x: 0, y: 0, z: 0 },
): number[] {
const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
lnglat,
altitude,
);
// @ts-ignore
const meters = modelAsMercatorCoordinate.meterInMercatorCoordinateUnits();
const modelMatrix = mat4.create();
mat4.translate(
modelMatrix,
modelMatrix,
vec3.fromValues(
modelAsMercatorCoordinate.x - origin.x,
modelAsMercatorCoordinate.y - origin.y,
modelAsMercatorCoordinate.z || 0 - origin.z,
),
);
mat4.scale(
modelMatrix,
modelMatrix,
vec3.fromValues(meters * scale[0], -meters * scale[1], meters * scale[2]),
);
mat4.rotateX(modelMatrix, modelMatrix, rotate[0]);
mat4.rotateY(modelMatrix, modelMatrix, rotate[1]);
mat4.rotateZ(modelMatrix, modelMatrix, rotate[2]);
return (modelMatrix as unknown) as number[];
}
public async init(): Promise<void> {
const {
id = 'map',

View File

@ -167,6 +167,14 @@ export default class ReglRendererService implements IRendererService {
return this.$container;
};
public getCanvas = () => {
return this.$container?.getElementsByTagName('canvas')[0] || null;
};
public getGLContext = () => {
return this.gl._gl;
};
public destroy = () => {
// @see https://github.com/regl-project/regl/blob/gh-pages/API.md#clean-up
this.gl.destroy();

View File

@ -3,7 +3,9 @@ import * as React from 'react';
import Arc2DLineDemo from './components/Arc2DLine';
import ArcLineDemo from './components/Arcline';
import Column from './components/column';
import CustomThreeJSDemo from './components/CustomThreeJSLayer';
import DataUpdate from './components/data_update';
import GlTFThreeJSDemo from './components/GlTFThreeJSDemo';
import HeatMapDemo from './components/HeatMap';
import LineLayer from './components/Line';
import PointDemo from './components/Point';
@ -26,4 +28,6 @@ storiesOf('图层', module)
.add('2D弧线', () => <Arc2DLineDemo />)
.add('热力图', () => <HeatMapDemo />)
.add('栅格', () => <RasterLayerDemo />)
.add('图片', () => <ImageLayerDemo />);
.add('图片', () => <ImageLayerDemo />)
.add('Three.js 图层', () => <CustomThreeJSDemo />)
.add('glTF 图层', () => <GlTFThreeJSDemo />);

View File

@ -0,0 +1,97 @@
import { PointLayer, Scene, ThreeJSLayer } from '@antv/l7';
import { GaodeMap, Mapbox } from '@antv/l7-maps';
import * as React from 'react';
import {
BackSide,
BoxGeometry,
DirectionalLight,
Mesh,
MeshLambertMaterial,
Scene as ThreeScene,
} from 'three';
// @ts-ignore
import data from '../data/data.json';
export default class ThreeJSLayerComponent extends React.Component {
// @ts-ignore
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public async componentDidMount() {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json',
);
const pointsData = await response.json();
const scene = new Scene({
id: 'map',
map: new Mapbox({
center: [120.19382669582967, 30.258134],
pitch: 60,
rotation: 30,
zoom: 16,
}),
});
this.scene = scene;
// const pointLayer = new PointLayer({})
// .source(pointsData, {
// cluster: true,
// })
// .shape('circle')
// .scale('point_count', {
// type: 'quantile',
// })
// .size('point_count', [5, 10, 15, 20, 25])
// .color('red')
// .style({
// opacity: 0.3,
// strokeWidth: 1,
// });
// scene.addLayer(pointLayer);
const threeJSLayer = new ThreeJSLayer({
enableMultiPassRenderer: false,
onAddMeshes: (threeScene: ThreeScene, layer: ThreeJSLayer) => {
// 添加光源
const directionalLight1 = new DirectionalLight(0xffffff);
directionalLight1.position.set(0, -70, 100).normalize();
threeScene.add(directionalLight1);
const directionalLight2 = new DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
threeScene.add(directionalLight2);
const geometry = new BoxGeometry(20, 20, 20);
const redMaterial = new MeshLambertMaterial({
color: 0xffffff,
side: BackSide,
});
const cube = new Mesh(geometry, redMaterial);
cube.applyMatrix(
layer.getModelMatrix([120.19382669582967, 30.258134], 10, [0, 0, 0]),
);
cube.frustumCulled = false;
threeScene.add(cube);
},
}).source(pointsData);
scene.addLayer(threeJSLayer);
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}

View File

@ -0,0 +1,95 @@
import { Scene, ThreeJSLayer } from '@antv/l7';
import { Mapbox } from '@antv/l7-maps';
import * as React from 'react';
import { DirectionalLight, Scene as ThreeScene } from 'three';
// tslint:disable-next-line:no-submodule-imports
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
export default class GlTFThreeJSDemo extends React.Component {
// @ts-ignore
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public async componentDidMount() {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/893d1d5f-11d9-45f3-8322-ee9140d288ae.json',
);
const pointsData = await response.json();
const scene = new Scene({
id: 'map',
map: new Mapbox({
center: [121.434765, 31.256735],
pitch: 45,
rotation: 30,
zoom: 16,
}),
});
this.scene = scene;
const threeJSLayer = new ThreeJSLayer({
enableMultiPassRenderer: false,
onAddMeshes: (threeScene: ThreeScene, layer: ThreeJSLayer) => {
// 添加光源
const directionalLight1 = new DirectionalLight(0xffffff);
directionalLight1.position.set(0, -70, 100).normalize();
threeScene.add(directionalLight1);
const directionalLight2 = new DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
threeScene.add(directionalLight2);
// 使用 Three.js glTFLoader 加载模型
const loader = new GLTFLoader();
loader.load(
// 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf',
// 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/AnimatedCube/glTF/AnimatedCube.gltf',
'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
(gltf) => {
// 根据 GeoJSON 数据放置模型
layer.getSource().data.dataArray.forEach(({ coordinates }) => {
const gltfScene = gltf.scene.clone();
gltfScene.applyMatrix(
// 生成模型矩阵
layer.getModelMatrix(
[coordinates[0], coordinates[1]], // 经纬度坐标
0, // 高度,单位米
[Math.PI / 2, 0, 0], // 沿 XYZ 轴旋转角度
[5, 5, 5], // 沿 XYZ 轴缩放比例
),
);
// 向场景中添加模型
threeScene.add(gltfScene);
});
// 重绘图层
layer.render();
},
);
},
}).source(pointsData, {
parser: {
type: 'json',
x: 'longitude',
y: 'latitude',
},
});
scene.addLayer(threeJSLayer);
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}

7787
yarn.lock

File diff suppressed because it is too large Load Diff