fix(layer): fix merge conflict

This commit is contained in:
thinkinggis 2019-10-14 19:22:51 +08:00
commit 453adb09d6
32 changed files with 886 additions and 142 deletions

BIN
.DS_Store vendored

Binary file not shown.

2
.gitignore vendored
View File

@ -65,3 +65,5 @@ jspm_packages/
# End of https://www.gitignore.io/api/node
lib/
.DS_Store

BIN
dev-docs/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,150 @@
# PixelPickingEngine 设计
在地图交互中除了地图底图本身提供的平移、旋转、缩放、flyTo 等相机动作,最常用的就是信息要素的拾取以及后续的高亮了。
3D 引擎常用的拾取技术通常有两种RayPicking 和 PixelPicking。前者从鼠标点击处沿着投影方向发射一根射线通过包围盒碰撞检测获取到接触到的第一个对象后续就可以进行选中对象的高亮甚至是跟随移动了以上运算均在 CPU 侧完成。
但是在 L7 的场景中,海量数据在同一个 Geometry 中,无法计算每个要素的包围盒,因此在 GPU 侧完成的 PixelPicking 更加适合。
作为拾取引擎 PixelPickingEngine除了实现内置基本的拾取 Pass最重要的是提供友好易用的 API覆盖以下常见场景
* 基本的拾取场景,用户只需要开启 Layer 拾取功能并设置高亮颜色即可。
* 拾取后展示特定 UI 组件的场景,用户需要监听事件,在回调中使用上述拾取对象完成组件展示。
* 更灵活的联动场景,用户可以不依赖 L7 内置的事件监听机制,直接拾取并高亮指定点/区域包含的要素。
本文会依次介绍:
* PixelPicking 原理
* 使用方法
* 拾取对象结构
* 拾取 API 的使用方法
* 开启/关闭拾取
* 设置高亮颜色
* 展示自定义 UI 组件
* 在自定义 Layer 中使用
## PixelPicking 原理
在执行时机方面,基于 [MultiPassRenderer](./MultiPassRenderer.md) 的设计,拾取发生在实际渲染之前:
```
ClearPass -> PixelPickingPass -> RenderPass -> [ ...其他后处理 Pass ] -> CopyPass
```
PixelPickingPass 分解步骤如下:
1. 逐要素编码idx -> color传入 attributes 渲染 Layer 到纹理。
2. 获取鼠标在视口中的位置。由于目前 L7 与地图结合的方案为双 Canvas 而非共享 WebGL Context事件监听注册在地图底图上。
3. 读取纹理在指定位置的颜色进行解码color -> idx),查找对应要素,作为 Layer `onHover/onClick` 回调参数传入。
4. (可选)将待高亮要素对应的颜色传入 Vertex Shader 用于每个 Vertex 判断自身是否被选中,如果被选中,在 Fragment Shader 中将高亮颜色与计算颜色混合。
## 使用方法
### 拾取对象结构定义
拾取对象结构定义如下:
| 参数名 | 类型 | 说明 |
| -------- | --- | ------------- |
| x | `number` | 鼠标位置在视口空间 x 坐标,取值范围 `[0, viewportWidth]` |
| y | `number` | 鼠标位置在视口空间 y 坐标,取值范围 `[0, viewportHeight]` |
| lnglat | `{ lng: number; lat: number; }` | 鼠标位置经纬度坐标 |
| feature | `object` | GeoJSON feature 属性 |
### API
对于基本的拾取场景,用户只需要开启 Layer 拾取功能并设置高亮颜色即可。
而对于拾取后展示特定 UI 组件的场景,用户需要监听事件,在回调中使用上述拾取对象完成组件展示。
最后,对于更灵活的联动场景,用户可以不依赖 L7 内置的事件监听机制,直接拾取并高亮指定点/区域包含的要素。
#### 禁用/开启拾取
并不是所有 Layer 都需要拾取(例如文本渲染 Layer通过 `enablePicking` 关闭可以跳过该阶段,减少不必要的渲染开销:
```typescript
const layer = new PolygonLayer({
enablePicking: false, // 关闭拾取
});
```
L7 默认开启拾取。
#### 设置高亮颜色
如果一个 Layer 开启了拾取,我们可以通过 `highlightColor` 设置高亮颜色:
```typescript
const layer = new PolygonLayer({
enablePicking: true, // 开启拾取
highlightColor: 'red', // 设置高亮颜色
});
```
#### 展示自定义 UI 组件
监听 Layer 上的 `hover/mousemove` 事件就可以得到拾取对象,然后通过对象中包含的位置以及原始数据信息,就可以使用 L7 内置或者自定义 UI 组件展示:
```typescript
layer.on('hover', ({ x, y, lnglat, feature }) => {
// 展示 UI 组件
});
layer.on('mousemove', ({ x, y, lnglat, feature }) => {
// 同上
});
```
除了基于事件监听,还可以通过 Layer 的构造函数传入 `onHover` 回调,在后续 Layer 对应的 react 组件中也可以以这种方式使用:
```typescript
const layer = new PolygonLayer({
enablePicking: true,
onHover: ({ x, y, lnglat, feature }) => {
// 展示 UI 组件
},
});
```
#### 直接调用拾取引擎方法
除了默认在地图上交互完成拾取,在与其他系统进行联动时,脱离了地图交互,仍需要具备拾取指定点/区域内包含要素的能力。
```typescript
anotherSystem.on('hover', ({ x, y }) => {
layer.pick({
x,
y,
});
});
```
⚠️目前只支持拾取视口中一个点所在的要素,未来可以实现拾取指定区域内的全部要素。
### 自定义 Layer 中的拾取
用户实现自定义 Layer 时,必然需要实现 Vertex/Fragment Shader。如果也想使用拾取功能就需要在 Shader 中引入拾取模块,方法如下。
在 Vertex Shader 中引入 `picking` 模块。关于 L7 Shader 的模块化设计,[详见]()。
```glsl
// mylayer.vert.glsl
#pragma include "picking"
void main() {
setPickingColor(customPickingColors);
}
```
在 Fragment Shader 中
```glsl
// mylayer.frag.glsl
#pragma include "picking"
void main() {
// 必须在末尾,保证后续不会再对 gl_FragColor 进行修改
gl_FragColor = highlightPickingColor(gl_FragColor);
}
```
其中涉及 `picking` 模块方法说明如下:
| 方法名 | 应用 shader | 说明 |
| -------- | --- | ------------- |
| `setPickingColor` | `vertex` | 比较自身颜色编码与高亮颜色,判断是否被选中,传递结果给 fragment |
| `highlightPickingColor` | `fragment` | 当前 fragment 被选中则使用高亮颜色混合,否则直接输出原始计算结果 |
## 参考资料
* [Deck.gl 交互文档](https://deck.gl/#/documentation/developer-guide/adding-interactivity)
* [Deck.gl Picking 实现](https://deck.gl/#/documentation/developer-guide/writing-custom-layers/picking)
* 「Interactive.Computer.Graphics.Top.Down.Approach - 3.9 Picking」

View File

@ -22,6 +22,7 @@
"@l7/source": "0.0.1",
"eventemitter3": "^3.1.0",
"gl-matrix": "^3.1.0",
"hammerjs": "^2.0.8",
"inversify": "^5.0.1",
"inversify-inject-decorators": "^3.1.0",
"lodash": "^4.17.15",
@ -32,6 +33,7 @@
},
"devDependencies": {
"@types/gl-matrix": "^2.4.5",
"@types/hammerjs": "^2.0.36",
"@types/lodash": "^4.14.138",
"@types/viewport-mercator-project": "^6.1.0"
}

View File

@ -1,5 +1,7 @@
import container, { lazyInject } from './inversify.config';
import ClearPass from './services/renderer/passes/ClearPass';
import MultiPassRenderer from './services/renderer/passes/MultiPassRenderer';
import PixelPickingPass from './services/renderer/passes/PixelPickingPass';
import BlurHPass from './services/renderer/passes/post-processing/BlurHPass';
import BlurVPass from './services/renderer/passes/post-processing/BlurVPass';
import CopyPass from './services/renderer/passes/post-processing/CopyPass';
@ -27,8 +29,10 @@ export {
SceneService,
packCircleVertex,
/** pass */
MultiPassRenderer,
ClearPass,
RenderPass,
PixelPickingPass,
BlurHPass,
BlurVPass,
CopyPass,

View File

@ -10,6 +10,7 @@ import { IIconService} from './services/asset/IIconService';
import { ICameraService } from './services/camera/ICameraService';
import { IGlobalConfigService } from './services/config/IConfigService';
import { ICoordinateSystemService } from './services/coordinate/ICoordinateSystemService';
import { IInteractionService } from './services/interaction/IInteractionService';
import { ILayerService } from './services/layer/ILayerService';
import { ILogService } from './services/log/ILogService';
import { IShaderModuleService } from './services/shader/IShaderModuleService';
@ -19,6 +20,7 @@ import IconService from './services/asset/IconService';
import CameraService from './services/camera/CameraService';
import GlobalConfigService from './services/config/ConfigService';
import CoordinateSystemService from './services/coordinate/CoordinateSystemService';
import InteractionService from './services/interaction/InteractionService';
import LayerService from './services/layer/LayerService';
import LayerStyleService from './services/layer/LayerStyleService';
import LogService from './services/log/LogService';
@ -58,6 +60,10 @@ container
.bind<ILogService>(TYPES.ILogService)
.to(LogService)
.inSingletonScope();
container
.bind<IInteractionService>(TYPES.IInteractionService)
.to(InteractionService)
.inSingletonScope();
// @see https://github.com/inversify/InversifyJS/blob/master/wiki/inheritance.md#what-can-i-do-when-my-base-class-is-provided-by-a-third-party-module
decorate(injectable(), EventEmitter);

View File

@ -0,0 +1,4 @@
export interface IInteractionService {
init(): void;
destroy(): void;
}

View File

@ -0,0 +1,60 @@
import Hammer from 'hammerjs';
import { inject, injectable } from 'inversify';
import { TYPES } from '../../types';
import { ILogService } from '../log/ILogService';
import { IRendererService } from '../renderer/IRendererService';
import { IInteractionService } from './IInteractionService';
@injectable()
export default class InteractionService implements IInteractionService {
@inject(TYPES.IRendererService)
private readonly rendererService: IRendererService;
@inject(TYPES.ILogService)
private readonly logger: ILogService;
private hammertime: HammerManager;
public init() {
const $containter = this.rendererService.getContainer();
if ($containter) {
const hammertime = new Hammer($containter);
hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL });
hammertime.get('pinch').set({ enable: true });
// hammertime.on('panstart', this.onPanstart);
hammertime.on('panmove', this.onPanmove);
// hammertime.on('panend', this.onPanend);
// hammertime.on('pinch', this.onPinch);
// $containter.addEventListener('wheel', this.onMousewheel);
this.hammertime = hammertime;
}
}
public destroy() {
if (this.hammertime) {
this.hammertime.destroy();
}
const $containter = this.rendererService.getContainer();
if ($containter) {
// $containter.removeEventListener('wheel', this.onMousewheel);
}
}
private onPanmove = (e: HammerInput) => {
// @ts-ignore
// this.logger.info(e);
// if (this.isMoving) {
// this.deltaX = e.center.x - this.lastX;
// this.deltaY = e.center.y - this.lastY;
// this.lastX = e.center.x;
// this.lastY = e.center.y;
// this.emit(Mouse.MOVE_EVENT, {
// deltaX: this.deltaX,
// deltaY: this.deltaY,
// deltaZ: this.deltaZ
// });
// }
};
}

View File

@ -109,6 +109,7 @@ export interface ILayerPlugin {
*/
export interface ILayerInitializationOptions {
enableMultiPassRenderer: boolean;
enablePicking: boolean;
passes: Array<string | [string, { [key: string]: unknown }]>;
}

View File

@ -31,9 +31,6 @@ export interface IPostProcessingPass extends IPass {
export interface IPostProcessor {
getReadFBO(): IFramebuffer;
getWriteFBO(): IFramebuffer;
useScreenRenderTarget(renderCommand: () => void): void;
useOffscreenRenderTarget(renderCommand: () => void): void;
renderToPostProcessor(renderCommand: () => void): void;
resize(viewportWidth: number, viewportHeight: number): void;
add(pass: IPostProcessingPass, layer: ILayer): void;
render(layer: ILayer): Promise<unknown>;

View File

@ -2,9 +2,13 @@ import { ILayer } from '../layer/ILayerService';
import { IAttribute, IAttributeInitializationOptions } from './IAttribute';
import { IBuffer, IBufferInitializationOptions } from './IBuffer';
import { IElements, IElementsInitializationOptions } from './IElements';
import { IFramebuffer } from './IFramebuffer';
import {
IFramebuffer,
IFramebufferInitializationOptions,
} from './IFramebuffer';
import { IModel, IModelInitializationOptions } from './IModel';
import { IMultiPassRenderer, IPass } from './IMultiPassRenderer';
import { ITexture2D, ITexture2DInitializationOptions } from './ITexture2D';
export interface IRenderConfig {
/**
@ -32,7 +36,13 @@ export interface IRendererService {
createAttribute(options: IAttributeInitializationOptions): IAttribute;
createBuffer(options: IBufferInitializationOptions): IBuffer;
createElements(options: IElementsInitializationOptions): IElements;
createMultiPassRenderer(layer: ILayer): IMultiPassRenderer;
createTexture2D(options: ITexture2DInitializationOptions): ITexture2D;
createFramebuffer(options: IFramebufferInitializationOptions): IFramebuffer;
renderToFramebuffer(
framebuffer: IFramebuffer | null,
drawCommands: () => void,
): void;
getViewportSize(): { width: number; height: number };
getContainer(): HTMLElement | null;
viewport(size: { x: number; y: number; width: number; height: number }): void;
}

View File

@ -23,7 +23,7 @@ export default class BasePostProcessingPass<InitializationOptions = {}>
protected readonly shaderModule: IShaderModuleService;
@lazyInject(TYPES.IRendererService)
protected readonly renderer: IRendererService;
protected readonly rendererService: IRendererService;
protected config: Partial<InitializationOptions> | undefined;
@ -51,7 +51,7 @@ export default class BasePostProcessingPass<InitializationOptions = {}>
}
public init() {
const { createAttribute, createBuffer, createModel } = this.renderer;
const { createAttribute, createBuffer, createModel } = this.rendererService;
const { vs, fs, uniforms } = this.setupShaders();
this.model = createModel({
@ -81,19 +81,31 @@ export default class BasePostProcessingPass<InitializationOptions = {}>
public render(layer: ILayer) {
const postProcessor = layer.multiPassRenderer.getPostProcessor();
const { renderToFramebuffer } = this.rendererService;
const useRenderTarget = (this.renderToScreen
? postProcessor.useScreenRenderTarget
: postProcessor.useOffscreenRenderTarget
).bind(postProcessor);
useRenderTarget(async () => {
renderToFramebuffer(
this.renderToScreen ? null : postProcessor.getWriteFBO(),
() => {
this.model.draw({
uniforms: {
u_Texture: postProcessor.getReadFBO(),
},
});
});
},
);
// const useRenderTarget = (this.renderToScreen
// ? postProcessor.useScreenRenderTarget
// : postProcessor.useOffscreenRenderTarget
// ).bind(postProcessor);
// useRenderTarget(async () => {
// this.model.draw({
// uniforms: {
// u_Texture: postProcessor.getReadFBO(),
// },
// });
// });
}
public isEnabled() {

View File

@ -1,45 +1,44 @@
import { injectable } from 'inversify';
import { ILayer } from '../../layer/ILayerService';
import {
ILayer,
IMultiPassRenderer,
IPass,
IPostProcessingPass,
IPostProcessor,
PassType,
} from '@l7/core';
import regl from 'regl';
import ReglPostProcessor from './ReglPostProcessor';
} from '../IMultiPassRenderer';
import PostProcessor from './PostProcessor';
/**
* ported from Three.js EffectComposer
* @example
* const renderer = new MultiPassRenderer(gl, [
* new ClearPass(gl),
* new RenderPass(gl, {
* const renderer = new MultiPassRenderer([
* new ClearPass(),
* new RenderPass({
* models: [
* new Model(),
* new Model(),
* ],
* }),
* new CopyPass(gl, {
* new CopyPass({
* renderToScreen: true,
* }),
* new TAAPass(gl),
* new TAAPass(),
* ]);
* renderer.render();
* @see https://yuque.antfin-inc.com/yuqi.pyq/fgetpa/apuvbf#dRM8W
*/
export default class ReglMultiPassRenderer implements IMultiPassRenderer {
@injectable()
export default class MultiPassRenderer implements IMultiPassRenderer {
private passes: IPass[] = [];
private postProcessor: IPostProcessor;
private reGl: regl.Regl;
private layer: ILayer;
private renderFlag: boolean;
constructor(reGl: regl.Regl, layer: ILayer) {
this.reGl = reGl;
constructor(layer: ILayer) {
this.layer = layer;
this.postProcessor = new ReglPostProcessor(reGl);
this.postProcessor = new PostProcessor();
}
public setRenderFlag(renderFlag: boolean) {

View File

@ -0,0 +1,46 @@
import { inject, injectable } from 'inversify';
import { lazyInject } from '../../../index';
import { TYPES } from '../../../types';
import { ILayer, ILayerService } from '../../layer/ILayerService';
import { gl } from '../gl';
import { IFramebuffer } from '../IFramebuffer';
import { IPass, PassType } from '../IMultiPassRenderer';
import { IRendererService } from '../IRendererService';
/**
* PixelPickingPass based on
*/
@injectable()
export default class PixelPickingPass implements IPass {
@lazyInject(TYPES.IRendererService)
protected readonly rendererService: IRendererService;
private pickingFBO: IFramebuffer;
public getType() {
return PassType.Normal;
}
public init(layer: ILayer) {
const { createTexture2D, createFramebuffer } = this.rendererService;
this.pickingFBO = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
}),
});
}
public render(layer: ILayer) {
const { getViewportSize, renderToFramebuffer } = this.rendererService;
this.pickingFBO.resize(getViewportSize());
renderToFramebuffer(this.pickingFBO, () => {
layer.multiPassRenderer.setRenderFlag(false);
layer.render();
layer.multiPassRenderer.setRenderFlag(true);
});
}
}

View File

@ -1,32 +1,29 @@
import {
gl,
IFramebuffer,
ILayer,
IPostProcessingPass,
IPostProcessor,
} from '@l7/core';
import regl from 'regl';
import ReglFramebuffer from './ReglFramebuffer';
import ReglTexture2D from './ReglTexture2D';
import { injectable } from 'inversify';
import { lazyInject } from '../../../index';
import { TYPES } from '../../../types';
import { ILayer } from '../../layer/ILayerService';
import { gl } from '../gl';
import { IFramebuffer } from '../IFramebuffer';
import { IPostProcessingPass, IPostProcessor } from '../IMultiPassRenderer';
import { IRendererService } from '../IRendererService';
/**
* ported from Three.js EffectComposer
* pingpong read/write framebuffer pass
*/
export default class ReglPostProcessor implements IPostProcessor {
@injectable()
export default class PostProcessor implements IPostProcessor {
@lazyInject(TYPES.IRendererService)
protected readonly rendererService: IRendererService;
private passes: IPostProcessingPass[] = [];
private readFBO: IFramebuffer;
private writeFBO: IFramebuffer;
private screenRenderTarget: regl.DrawCommand;
private offscreenRenderTarget: regl.DrawCommand;
private inputRenderTarget: regl.DrawCommand;
private reGl: regl.Regl;
constructor(reGl: regl.Regl) {
this.reGl = reGl;
this.readFBO = new ReglFramebuffer(reGl, {
color: new ReglTexture2D(reGl, {
constructor() {
const { createFramebuffer, createTexture2D } = this.rendererService;
this.readFBO = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
@ -34,27 +31,14 @@ export default class ReglPostProcessor implements IPostProcessor {
}),
});
this.writeFBO = new ReglFramebuffer(reGl, {
color: new ReglTexture2D(reGl, {
this.writeFBO = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
}),
});
this.screenRenderTarget = reGl({
framebuffer: null,
});
this.offscreenRenderTarget = reGl({
// since post-processor will swap read/write fbos, we must retrieve it dynamically
framebuffer: () => (this.writeFBO as ReglFramebuffer).get(),
});
this.inputRenderTarget = reGl({
framebuffer: () => (this.readFBO as ReglFramebuffer).get(),
});
}
public getReadFBO() {
@ -65,26 +49,6 @@ export default class ReglPostProcessor implements IPostProcessor {
return this.writeFBO;
}
public renderToPostProcessor(renderCommand: () => void) {
this.inputRenderTarget(() => {
this.reGl.clear({
color: [0, 0, 0, 0],
depth: 1,
stencil: 0,
framebuffer: (this.getReadFBO() as ReglFramebuffer).get(),
});
renderCommand();
});
}
public useScreenRenderTarget(callback: () => void) {
this.screenRenderTarget({}, callback);
}
public useOffscreenRenderTarget(callback: () => void) {
this.offscreenRenderTarget({}, callback);
}
public async render(layer: ILayer) {
for (let i = 0; i < this.passes.length; i++) {
const pass = this.passes[i];

View File

@ -1,12 +1,18 @@
import { inject, injectable } from 'inversify';
import { ILayer, ILayerService } from '../../layer/ILayerService';
import { injectable } from 'inversify';
import { lazyInject } from '../../../index';
import { TYPES } from '../../../types';
import { ILayer } from '../../layer/ILayerService';
import { IPass, PassType } from '../IMultiPassRenderer';
import { IRendererService } from '../IRendererService';
/**
* RenderPass PostProcessor readFBO
*/
@injectable()
export default class RenderPass implements IPass {
@lazyInject(TYPES.IRendererService)
protected readonly rendererService: IRendererService;
public getType() {
return PassType.Normal;
}
@ -16,7 +22,16 @@ export default class RenderPass implements IPass {
}
public render(layer: ILayer) {
layer.multiPassRenderer.getPostProcessor().renderToPostProcessor(() => {
const { renderToFramebuffer, clear } = this.rendererService;
const readFBO = layer.multiPassRenderer.getPostProcessor().getReadFBO();
renderToFramebuffer(readFBO, () => {
clear({
color: [0, 0, 0, 0],
depth: 1,
stencil: 0,
framebuffer: readFBO,
});
// render to post processor
layer.multiPassRenderer.setRenderFlag(false);
layer.render();

View File

@ -1,6 +1,9 @@
import { injectable } from 'inversify';
import { lazyInject } from '../../../../index';
import blur from '../../../../shaders/post-processing/blur.glsl';
import quad from '../../../../shaders/post-processing/quad.glsl';
import { TYPES } from '../../../../types';
import { IRendererService } from '../../IRendererService';
import BasePostProcessingPass from '../BasePostProcessingPass';
export interface IBlurHPassConfig {
@ -15,6 +18,9 @@ const defaultConfig: IBlurHPassConfig = {
export default class BlurHPass extends BasePostProcessingPass<
IBlurHPassConfig
> {
@lazyInject(TYPES.IRendererService)
protected readonly rendererService: IRendererService;
public setupShaders() {
this.shaderModule.registerModule('blur-pass', {
vs: quad,
@ -22,7 +28,7 @@ export default class BlurHPass extends BasePostProcessingPass<
});
const { vs, fs, uniforms } = this.shaderModule.getModule('blur-pass');
const { width, height } = this.renderer.getViewportSize();
const { width, height } = this.rendererService.getViewportSize();
const { blurRadius } = {
...defaultConfig,

View File

@ -1,6 +1,9 @@
import { injectable } from 'inversify';
import { lazyInject } from '../../../../index';
import blur from '../../../../shaders/post-processing/blur.glsl';
import quad from '../../../../shaders/post-processing/quad.glsl';
import { TYPES } from '../../../../types';
import { IRendererService } from '../../IRendererService';
import BasePostProcessingPass from '../BasePostProcessingPass';
export interface IBlurVPassConfig {
@ -15,6 +18,9 @@ const defaultConfig: IBlurVPassConfig = {
export default class BlurVPass extends BasePostProcessingPass<
IBlurVPassConfig
> {
@lazyInject(TYPES.IRendererService)
protected readonly rendererService: IRendererService;
public setupShaders() {
this.shaderModule.registerModule('blur-pass', {
vs: quad,
@ -22,7 +28,7 @@ export default class BlurVPass extends BasePostProcessingPass<
});
const { vs, fs, uniforms } = this.shaderModule.getModule('blur-pass');
const { width, height } = this.renderer.getViewportSize();
const { width, height } = this.rendererService.getViewportSize();
const { blurRadius } = {
...defaultConfig,

View File

@ -5,6 +5,7 @@ import { TYPES } from '../../types';
import { createRendererContainer } from '../../utils/dom';
import { ICameraService, IViewport } from '../camera/ICameraService';
import { IGlobalConfig, IGlobalConfigService } from '../config/IConfigService';
import { IInteractionService } from '../interaction/IInteractionService';
import { ILayer, ILayerService } from '../layer/ILayerService';
import { ILogService } from '../log/ILogService';
import { IMapCamera, IMapService } from '../map/IMapService';
@ -38,6 +39,9 @@ export default class Scene extends EventEmitter implements ISceneService {
@inject(TYPES.ICameraService)
private readonly cameraService: ICameraService;
@inject(TYPES.IInteractionService)
private readonly interactionService: IInteractionService;
@inject(TYPES.IShaderModuleService)
private readonly shaderModule: IShaderModuleService;
@ -110,6 +114,9 @@ export default class Scene extends EventEmitter implements ISceneService {
// 初始化 ShaderModule
this.shaderModule.registerBuiltinModules();
// 初始化 container 上的交互
this.interactionService.init();
// TODOinit renderer
this.logger.info('renderer loaded');
});
@ -141,6 +148,7 @@ export default class Scene extends EventEmitter implements ISceneService {
this.inited = false;
this.layerService.clean();
this.configService.reset();
this.interactionService.destroy();
window.removeEventListener('resize', this.handleWindowResized, false);
}

View File

@ -10,6 +10,7 @@ const TYPES = {
IRendererService: Symbol.for('IRendererService'),
IShaderModuleService: Symbol.for('IShaderModuleService'),
IIconService: Symbol.for('IIconService'),
IInteractionService: Symbol.for('IInteractionService'),
/** multi-pass */
ClearPass: Symbol.for('ClearPass'),

View File

@ -0,0 +1,103 @@
import {
gl,
IRendererService,
IShaderModuleService,
lazyInject,
TYPES,
} from '@l7/core';
import BaseLayer from '../core/BaseLayer';
import LineBuffer from './buffers/line';
import line_frag from './shaders/line_frag.glsl';
import line_vert from './shaders/line_vert.glsl';
export default class LineLayer extends BaseLayer {
public name: string = 'LineLayer';
@lazyInject(TYPES.IShaderModuleService)
private readonly shaderModule: IShaderModuleService;
@lazyInject(TYPES.IRendererService)
private readonly renderer: IRendererService;
protected renderModels() {
this.models.forEach((model) =>
model.draw({
uniforms: {
u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
},
}),
);
return this;
}
protected buildModels(): void {
this.shaderModule.registerModule('line', {
vs: line_vert,
fs: line_frag,
});
this.models = [];
const { vs, fs, uniforms } = this.shaderModule.getModule('line');
const buffer = new LineBuffer({
data: this.getEncodedData(),
style: this.styleOption,
});
console.log(buffer);
const {
createAttribute,
createBuffer,
createElements,
createModel,
} = this.renderer;
this.models.push(
createModel({
attributes: {
a_Position: createAttribute({
buffer: createBuffer({
data: buffer.attributes.positions,
type: gl.FLOAT,
}),
size: 3,
}),
a_normal: createAttribute({
buffer: createBuffer({
data: buffer.attributes.normals,
type: gl.FLOAT,
}),
size: 3,
}),
a_color: createAttribute({
buffer: createBuffer({
data: buffer.attributes.colors,
type: gl.FLOAT,
}),
size: 4,
}),
a_size: createAttribute({
buffer: createBuffer({
data: buffer.attributes.sizes,
type: gl.FLOAT,
}),
size: 1,
}),
a_miter: createAttribute({
buffer: createBuffer({
data: buffer.attributes.miters,
type: gl.FLOAT,
}),
size: 1,
}),
},
uniforms: {
...uniforms,
u_opacity: this.styleOption.opacity as number,
},
fs,
vs,
count: buffer.indexArray.length,
elements: createElements({
data: buffer.indexArray,
type: gl.UNSIGNED_INT,
}),
}),
);
}
}

View File

@ -0,0 +1,9 @@
uniform float u_blur : 0.9;
varying vec4 v_color;
varying vec3 v_normal;
void main() {
gl_FragColor = v_color;
// anti-alias
// float blur = 1. - smoothstep(u_blur, 1., length(v_normal));
// gl_FragColor.a *= blur;
}

View File

@ -0,0 +1,23 @@
attribute float a_miter;
attribute vec4 a_color;
attribute float a_size;
attribute float a_distance;
attribute float a_dash_array;
attribute float a_total_distance;
attribute vec3 a_normal;
attribute vec3 a_Position;
uniform mat4 u_ModelMatrix;
varying vec4 v_color;
varying float v_dash_array;
varying vec3 v_normal;
#pragma include "projection"
void main() {
v_normal = a_normal;
v_color = a_color;
vec3 size = a_miter * a_size * v_normal;
vec2 offset = project_pixel(size.xy);
vec4 project_pos = project_position(vec4(a_Position.xy, 0, 1.0));
gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy + offset, 0, 1.0));
}

View File

@ -9,6 +9,8 @@ import {
IPostProcessingPass,
IRendererService,
lazyInject,
MultiPassRenderer,
PixelPickingPass,
RenderPass,
TYPES,
} from '@l7/core';
@ -20,6 +22,20 @@ const builtinPostProcessingPassMap: {
blurV: BlurVPass,
};
/**
* 'blurH' -> ['blurH', {}]
*/
function normalizePasses(
passes: Array<string | [string, { [key: string]: unknown }]>,
) {
return passes.map((pass: string | [string, { [key: string]: unknown }]) => {
if (typeof pass === 'string') {
pass = [pass, {}];
}
return pass;
});
}
/**
* Layer passes MultiPassRenderer
* @example
@ -83,28 +99,23 @@ export default class MultiPassRendererPlugin implements ILayerPlugin {
layer: ILayer,
passes: Array<string | [string, { [key: string]: unknown }]>,
) {
const multiPassRenderer = this.rendererService.createMultiPassRenderer(
layer,
);
// TODO: PickingPass
multiPassRenderer.add(new ClearPass());
multiPassRenderer.add(new RenderPass());
const multiPassRenderer = new MultiPassRenderer(layer);
const normalizedPasses: Array<
[string, { [key: string]: unknown }]
> = passes.map((pass: string | [string, { [key: string]: unknown }]) => {
if (typeof pass === 'string') {
pass = [pass, {}];
multiPassRenderer.add(new ClearPass());
if (layer.getInitializationOptions().enablePicking) {
multiPassRenderer.add(new PixelPickingPass());
}
return pass;
});
multiPassRenderer.add(new RenderPass());
// post processing
// TODO: pass initialization params
normalizedPasses.forEach((pass: [string, { [key: string]: unknown }]) => {
normalizePasses(passes).forEach(
(pass: [string, { [key: string]: unknown }]) => {
const PostProcessingPassClazz = builtinPostProcessingPassMap[pass[0]];
multiPassRenderer.add(new PostProcessingPassClazz(pass[1]));
});
},
);
// 末尾为固定的 CopyPass
multiPassRenderer.add(new CopyPass());

View File

@ -0,0 +1,195 @@
/**
* polyline-normal
* miter bevel
* Three.js THREE.FrontFaceDirectionCCW
* @see https://zhuanlan.zhihu.com/p/59541559
*/
// @ts-ignore
import { copy, create, dot } from 'gl-vec2';
// @ts-ignore
import { computeMiter, direction, normal } from 'polyline-miter-util';
// @ts-ignore
function extrusions(positions, out, miters, point, normal1, scale) {
addNext(out, miters, normal1, -scale);
addNext(out, miters, normal1, scale);
positions.push(...point, 0);
positions.push(...point, 0);
}
// @ts-ignore
// tslint:disable-next-line:no-shadowed-variable
function addNext(out, miters, normal, length) {
out.push(normal[0], normal[1], 0);
miters.push(length);
}
// @ts-ignore
function lineSegmentDistance(end, start) {
const dx = start[0] - end[0];
const dy = start[1] - end[1];
const dz = start[2] - end[2];
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
// @ts-ignore
function isPointEqual(a, b) {
return a[0] === b[0] && a[1] === b[1];
}
// @ts-ignore
export default function(points, closed, indexOffset) {
const lineA = [0, 0];
const lineB = [0, 0];
const tangent = [0, 0];
const miter = [0, 0];
// tslint:disable-next-line:variable-name
let _started = false;
// tslint:disable-next-line:variable-name
let _normal = null;
const tmp = create();
let count = indexOffset || 0;
const miterLimit = 3;
// @ts-ignore
const out = [];
const attrPos = [];
const attrIndex = [];
// @ts-ignore
const miters = [];
const attrDistance = [0, 0];
if (closed) {
points = points.slice();
points.push(points[0]);
}
const total = points.length;
for (let i = 1; i < total; i++) {
const index = count;
const last = points[i - 1];
const cur = points[i];
let next = i < points.length - 1 ? points[i + 1] : null;
// 如果当前点和前一点相同,跳过
if (isPointEqual(last, cur)) {
continue;
}
if (next) {
let nextIndex = i + 1;
// 找到不相同的下一点
while (next && isPointEqual(cur, next)) {
next = nextIndex < points.length - 1 ? points[++nextIndex] : null;
}
}
const lineDistance = lineSegmentDistance(cur, last);
const d = lineDistance + attrDistance[attrDistance.length - 1];
direction(lineA, cur, last);
if (!_normal) {
_normal = [0, 0];
normal(_normal, lineA);
}
if (!_started) {
_started = true;
// @ts-ignore
extrusions(attrPos, out, miters, last, _normal, 1);
}
attrIndex.push(index + 0, index + 2, index + 1);
// no miter, simple segment
if (!next) {
// reset normal
normal(_normal, lineA);
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrDistance.push(d, d);
attrIndex.push(index + 1, index + 2, index + 3);
count += 2;
} else {
// get unit dir of next line
direction(lineB, next, cur);
// stores tangent & miter
let miterLen = computeMiter(tangent, miter, lineA, lineB, 1);
// get orientation
const flip = dot(tangent, _normal) < 0 ? -1 : 1;
const bevel = Math.abs(miterLen) > miterLimit;
// 处理前后两条线段重合的情况这种情况不需要使用任何接头miter/bevel
// 理论上这种情况下 miterLen = Infinity本应通过 isFinite(miterLen) 判断,
// 但是 AMap 投影变换后丢失精度只能通过一个阈值1000判断。
if (Math.abs(miterLen) > 1000) {
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrIndex.push(index + 1, index + 2, index + 3);
attrIndex.push(index + 2, index + 4, index + 3);
normal(tmp, lineB);
copy(_normal, tmp); // store normal for next round
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrDistance.push(d, d, d, d);
// the miter is now the normal for our next join
count += 4;
continue;
}
if (bevel) {
miterLen = miterLimit;
// next two points in our first segment
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrIndex.push(index + 1, index + 2, index + 3);
// now add the bevel triangle
attrIndex.push(
...(flip === 1
? [index + 2, index + 4, index + 5]
: [index + 4, index + 5, index + 3]),
);
normal(tmp, lineB);
copy(_normal, tmp); // store normal for next round
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrDistance.push(d, d, d, d);
// the miter is now the normal for our next join
count += 4;
} else {
// next two points in our first segment
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrIndex.push(index + 1, index + 2, index + 3);
// now add the miter triangles
// @ts-ignore
addNext(out, miters, miter, miterLen * -flip);
attrPos.push(...cur, 0);
attrIndex.push(index + 2, index + 4, index + 3);
attrIndex.push(index + 4, index + 5, index + 6);
normal(tmp, lineB);
copy(_normal, tmp); // store normal for next round
// @ts-ignore
extrusions(attrPos, out, miters, cur, _normal, 1);
attrDistance.push(d, d, d, d, d);
// the miter is now the normal for our next join
count += 5;
}
}
}
// @ts-ignore
return {
// @ts-ignore
normals: out,
attrIndex,
attrPos,
attrDistance,
// @ts-ignore
miters,
};
}

View File

@ -1,7 +1,4 @@
import {
IRenderbuffer,
IRenderbufferInitializationOptions,
} from '@l7/core';
import { IRenderbuffer, IRenderbufferInitializationOptions } from '@l7/core';
import regl from 'regl';
import { formatMap } from './constants';

View File

@ -3,7 +3,6 @@
* @see https://github.com/regl-project/regl/blob/gh-pages/API.md
*/
import {
gl,
IAttribute,
IAttributeInitializationOptions,
IBuffer,
@ -11,20 +10,22 @@ import {
IClearOptions,
IElements,
IElementsInitializationOptions,
ILayer,
IFramebuffer,
IFramebufferInitializationOptions,
IModel,
IModelInitializationOptions,
IMultiPassRenderer,
IRendererService,
ITexture2D,
ITexture2DInitializationOptions,
} from '@l7/core';
import { inject, injectable } from 'inversify';
import { injectable } from 'inversify';
import regl from 'regl';
import ReglAttribute from './ReglAttribute';
import ReglBuffer from './ReglBuffer';
import ReglElements from './ReglElements';
import ReglFramebuffer from './ReglFramebuffer';
import ReglModel from './ReglModel';
import ReglMultiPassRenderer from './ReglMultiPassRenderer';
import ReglTexture2D from './ReglTexture2D';
/**
* regl renderer
@ -32,8 +33,10 @@ import ReglMultiPassRenderer from './ReglMultiPassRenderer';
@injectable()
export default class ReglRendererService implements IRendererService {
private gl: regl.Regl;
private $container: HTMLDivElement | null;
public async init($container: HTMLDivElement): Promise<void> {
this.$container = $container;
// tslint:disable-next-line:typedef
this.gl = await new Promise((resolve, reject) => {
regl({
@ -67,31 +70,43 @@ export default class ReglRendererService implements IRendererService {
});
}
public createModel = (options: IModelInitializationOptions): IModel => {
return new ReglModel(this.gl, options);
};
public createModel = (options: IModelInitializationOptions): IModel =>
new ReglModel(this.gl, options);
public createAttribute = (
options: IAttributeInitializationOptions,
): IAttribute => {
return new ReglAttribute(this.gl, options);
};
): IAttribute => new ReglAttribute(this.gl, options);
public createBuffer = (options: IBufferInitializationOptions): IBuffer => {
return new ReglBuffer(this.gl, options);
};
public createBuffer = (options: IBufferInitializationOptions): IBuffer =>
new ReglBuffer(this.gl, options);
public createElements = (
options: IElementsInitializationOptions,
): IElements => {
return new ReglElements(this.gl, options);
): IElements => new ReglElements(this.gl, options);
public createTexture2D = (
options: ITexture2DInitializationOptions,
): ITexture2D => new ReglTexture2D(this.gl, options);
public createFramebuffer = (options: IFramebufferInitializationOptions) =>
new ReglFramebuffer(this.gl, options);
public renderToFramebuffer = (
framebuffer: IFramebuffer | null,
drawCommands: () => void,
) => {
const useFramebuffer = this.gl({
// since post-processor will swap read/write fbos, we must retrieve it dynamically
framebuffer: framebuffer
? () => (framebuffer as ReglFramebuffer).get()
: null,
});
// TODO: pass other options
useFramebuffer({}, drawCommands);
};
public createMultiPassRenderer = (layer: ILayer): IMultiPassRenderer => {
return new ReglMultiPassRenderer(this.gl, layer);
};
public clear(options: IClearOptions) {
public clear = (options: IClearOptions) => {
// @see https://github.com/regl-project/regl/blob/gh-pages/API.md#clear-the-draw-buffer
const { color, depth, stencil, framebuffer = null } = options;
const reglClearOptions: regl.ClearOptions = {
@ -106,9 +121,9 @@ export default class ReglRendererService implements IRendererService {
: (framebuffer as ReglFramebuffer).get();
this.gl.clear(reglClearOptions);
}
};
public viewport({
public viewport = ({
x,
y,
width,
@ -118,17 +133,21 @@ export default class ReglRendererService implements IRendererService {
y: number;
width: number;
height: number;
}) {
}) => {
// use WebGL context directly
// @see https://github.com/regl-project/regl/blob/gh-pages/API.md#unsafe-escape-hatch
this.gl._gl.viewport(x, y, width, height);
this.gl._refresh();
}
};
public getViewportSize() {
public getViewportSize = () => {
return {
width: this.gl._gl.drawingBufferWidth,
height: this.gl._gl.drawingBufferHeight,
};
}
};
public getContainer = () => {
return this.$container;
};
}

View File

@ -0,0 +1,73 @@
import { Line } from '@l7/layers';
import { Scene } from '@l7/scene';
import * as React from 'react';
export default class Point3D extends React.Component {
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public async componentDidMount() {
const response = await fetch(
'https://gw.alipayobjects.com/os/rmsportal/ZVfOvhVCzwBkISNsuKCc.json',
);
const testdata = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [
[91.58203125, 34.95799531086792],
[96.767578125, 34.379712580462204],
[99.228515625, 33.7243396617476],
],
},
},
],
};
const scene = new Scene({
center: [102.602992, 23.107329],
id: 'map',
pitch: 0,
type: 'mapbox',
style: 'mapbox://styles/mapbox/dark-v9',
zoom: 2,
});
const LineLayer = new Line({});
LineLayer.source(testdata)
.size(5)
.color('red')
.shape('line')
.size(10);
scene.addLayer(LineLayer);
// function run() {
// scene.render();
// requestAnimationFrame(run);
// }
// requestAnimationFrame(run);
scene.render();
this.scene = scene;
console.log(LineLayer);
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}

View File

@ -83,6 +83,17 @@ export default class Mapbox extends React.Component {
this.scene = scene;
console.log(layer);
/*** 运行时修改样式属性 ***/
// const gui = new dat.GUI();
// this.gui = gui;
// const pointFolder = gui.addFolder('Polygon 样式属性');
// pointFolder
// .add(layer.styleOptions, 'opacity')
// .onChange((opacity: number) => {
// layer.style({
// opacity,
// });
// scene.render();
// });
}
public render() {

View File

@ -2825,6 +2825,11 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/hammerjs@^2.0.36":
version "2.0.36"
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c"
integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@ -7507,6 +7512,11 @@ gzip-size@5.1.1:
duplexer "^0.1.1"
pify "^4.0.1"
hammerjs@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
handle-thing@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"