feat(multi-pass): support TAA(Temporal Anti-Aliasing)

we can enable TAA in layer's options now, but there're something wrong when picking enabled at same
time.
This commit is contained in:
yuqi.pyq 2019-10-29 14:42:41 +08:00
parent 896b02c1e7
commit 39aba9d9ec
35 changed files with 766 additions and 180 deletions

101
dev-docs/TAA.md Normal file
View File

@ -0,0 +1,101 @@
# 在地理场景中应用 TAA
## 问题背景
关于走样产生的原因以及常用的反走样手段,可以参考「知乎 - 反走样技术(一):几何反走样」[🔗](https://zhuanlan.zhihu.com/p/28800047)。
我之前也简单总结了下 SSAA、MLAA/SMAA、FXAA 等反走样技术的实现细节。
其中 MSAA 作为浏览器内置实现,开发者使用起来很简单:
> 相对于着色走样人眼对几何走样更敏感。MSAA 的原理很简单,它仍然把一个像素划分为若干个子采样点,但是相较于 SSAA每个子采样点的颜色值完全依赖于对应像素的颜色值进行简单的复制该子采样点位于当前像素光栅化结果的覆盖范围内不进行单独计算。此外它的做法和 SSAA 相同。由于 MSAA 拥有硬件支持,相对开销比较小,又能很好地解决几何走样问题,在游戏中应用非常广泛(我们在游戏画质选项中常看到的 4x/8x/16x 抗锯齿一般说的就是 MSAA 的子采样点数量分别为4/8/16个
下图为 4x MSAA 采样点示意:
![](./screenshots/MSAA.png)
在 Mapbox 中左图未开启 MSAA 而右图选择开启,观察立方体边缘可以发现明显的几何走样:相关 [ISSUE](https://github.com/mapbox/mapbox-gl-js/pull/8474)。
![](./screenshots/mapbox-MSAA.png)
但是 MSAA 存在一些限制:
* WebGL1 不支持对 FBO 进行,因此开启 post-processing 后处理时 MSAA 就失效了。当然 WebGL2 支持 🔗。
* 即使开启,浏览器在某些情况下也不保证应用 🔗。
因此在需要后处理的场景中(例如 L7 的热力图需要 blur pass、PBR 中的 SSAO 环境光遮蔽),只能采用其他反走样手段。
## TAA(Temporal Anti-Aliasing) 原理
来自「知乎 - Experimentalize TAA with no code」🔗
> 严格来说 TAA 并不能算一个具体的算法,而是更像一个统一的算法框架。和 SSAA 一样TAA 也能够同时减轻几何走样和着色走样的问题。
关于 TAA 的原理「GDC - Temporal Reprojection
Anti-Aliasing in INSIDE」[🔗](http://twvideo01.ubm-us.net/o1/vault/gdc2016/Presentations/Pedersen_LasseJonFuglsang_TemporalReprojectionAntiAliasing.pdf) 讲的十分清晰。如果相机和物体的相对位置在当前帧之前发生过变化,那么当前帧就可以以若干前序帧进行修正。
![](./screenshots/taa-1.png)
但如果在前序帧中相机和物体都没有发生过变化,那对于当前帧就无从修正了。因此可以对视锥进行抖动,在渲染每一帧之前,使用抖动矩阵对投影矩阵进行偏移,最终实现视锥的偏移:
![](./screenshots/taa-step1.png)
然后在 FS 中,最关键的就是 reproject 这一步:
![](./screenshots/taa-step2.png)
对于静止场景「Three.js - TAA example」[🔗](https://threejs.org/examples/#webgl_postprocessing_taa)、「ECharts.GL - temporalSuperSampling」[🔗](https://echarts.apache.org/zh/option-gl.html#globe.temporalSuperSampling) 都采用了这种方法。
## 实现方法
由于需要对投影矩阵进行抖动,我们需要选取低差异序列。
来自「知乎 - 低差异序列(一)- 常见序列的定义及性质」🔗,右图明显比左图纯随机生成覆盖面广:
![](./screenshots/halton.png)
参考 Echarts.GL我们选择 `Halton(2,3)` 低差异序列:
```typescript
const offset = this.haltonSequence[this.frame % this.haltonSequence.length];
this.cameraService.jitterProjectionMatrix(
((offset[0] * 2.0 - 1.0) / width) * jitterScale,
((offset[1] * 2.0 - 1.0) / height) * jitterScale,
);
```
在每一帧都会尝试进行累加。如果在连续运动过程中TAA 的累加过程必然来不及完成,此时只需要输出当前帧原始结果即可,随后尝试继续轮询累加是否完成。因此在累加完成之前,都会输出当前帧未经 TAA 的结果。
最后我们需要进行加权平均,历史帧的权重应当越来越小:
![](./screenshots/taa-step3.png)
这里我们选择当前帧权重为 0.9,历史帧为 0.1,最终的混合结果供后续后处理模块继续处理:
```typescript
useFramebuffer(this.outputRenderTarget, () => {
this.blendModel.draw({
uniforms: {
u_Opacity: layerStyleOptions.opacity || 1,
u_MixRatio: this.frame === 0 ? 1 : 0.9,
u_Diffuse1: this.sampleRenderTarget,
u_Diffuse2:
this.frame === 0
? layer.multiPassRenderer.getPostProcessor().getReadFBO()
: this.prevRenderTarget,
},
});
});
```
## 最终效果
为了更直观地看到效果,在 DEMO 中我们可以调节相机抖动范围:
![](./screenshots/taa-result.gif)
## 参考资料
* 「知乎 - 反走样技术(一):几何反走样」[🔗](https://zhuanlan.zhihu.com/p/28800047)
* 「知乎 - Experimentalize TAA with no code」[🔗](https://zhuanlan.zhihu.com/p/41642855)
* 「ECharts.GL - temporalSuperSampling」[🔗](https://echarts.apache.org/zh/option-gl.html#globe.temporalSuperSampling)
* 「Mapbox - set custom layers and extrusion examples to use antialias: true」[🔗](https://github.com/mapbox/mapbox-gl-js/pull/8474)
* 「Three.js - TAA example」[🔗](https://threejs.org/examples/#webgl_postprocessing_taa)
* 「Paper - Amortized Supersampling」[🔗](http://hhoppe.com/supersample.pdf)
* 「GDC - Temporal Reprojection Anti-Aliasing in INSIDE」[🔗](http://twvideo01.ubm-us.net/o1/vault/gdc2016/Presentations/Pedersen_LasseJonFuglsang_TemporalReprojectionAntiAliasing.pdf)
* 「知乎 - 低差异序列(一)- 常见序列的定义及性质」[🔗](https://zhuanlan.zhihu.com/p/20197323)

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@ -1,4 +1,4 @@
import container, { lazyInject } from './inversify.config';
import container, { lazyInject, lazyMultiInject } from './inversify.config';
import ClearPass from './services/renderer/passes/ClearPass';
import MultiPassRenderer from './services/renderer/passes/MultiPassRenderer';
import PixelPickingPass from './services/renderer/passes/PixelPickingPass';
@ -6,7 +6,7 @@ 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';
import RenderPass from './services/renderer/passes/RenderPass';
import SceneService from './services/scene/SceneService';
import TAAPass from './services/renderer/passes/TAAPass';
import { TYPES } from './types';
import { packCircleVertex } from './utils/vertex-compression';
@ -19,14 +19,11 @@ export {
* lazy inject Layer 使
*/
lazyInject,
lazyMultiInject,
/**
* Service
*/
TYPES,
/**
* Service
*/
SceneService,
packCircleVertex,
/** pass */
MultiPassRenderer,
@ -36,6 +33,7 @@ export {
BlurHPass,
BlurVPass,
CopyPass,
TAAPass,
};
/** 暴露服务接口供其他 packages 实现 */

View File

@ -16,6 +16,7 @@ import { IInteractionService } from './services/interaction/IInteractionService'
import { ILayerService } from './services/layer/ILayerService';
import { IStyleAttributeService } from './services/layer/IStyleAttributeService';
import { ILogService } from './services/log/ILogService';
import { ISceneService } from './services/scene/ISceneService';
import { IShaderModuleService } from './services/shader/IShaderModuleService';
/** Service implements */
@ -29,6 +30,7 @@ import InteractionService from './services/interaction/InteractionService';
import LayerService from './services/layer/LayerService';
import StyleAttributeService from './services/layer/StyleAttributeService';
import LogService from './services/log/LogService';
import SceneService from './services/scene/SceneService';
import ShaderModuleService from './services/shader/ShaderModuleService';
// @see https://github.com/inversify/InversifyJS/blob/master/wiki/container_api.md#defaultscope
const container = new Container();
@ -36,6 +38,10 @@ const container = new Container();
/**
* bind services
*/
container
.bind<ISceneService>(TYPES.ISceneService)
.to(SceneService)
.inSingletonScope();
container
.bind<IGlobalConfigService>(TYPES.IGlobalConfigService)
.to(GlobalConfigService)
@ -113,5 +119,25 @@ export const lazyInject = (
};
};
};
export const lazyMultiInject = (
serviceIdentifier: interfaces.ServiceIdentifier<any>,
) => {
const original = DECORATORS.lazyMultiInject(serviceIdentifier);
// the 'descriptor' parameter is actually always defined for class fields for Babel, but is considered undefined for TSC
// so we just hack it with ?/! combination to avoid "TS1240: Unable to resolve signature of property decorator when called as an expression"
return function(
this: any,
proto: any,
key: string,
descriptor?: IBabelPropertyDescriptor,
): void {
// make it work as usual
original.call(this, proto, key);
// return link to proto, so own value wont be 'undefined' after component's creation
descriptor!.initializer = () => {
return proto[key];
};
};
};
export default container;

View File

@ -1,3 +1,4 @@
import { mat4 } from 'gl-matrix';
import { inject, injectable } from 'inversify';
import { ICameraService, IViewport } from './ICameraService';
@ -10,6 +11,16 @@ export default class CameraService implements ICameraService {
*/
private overridedViewProjectionMatrix: number[] | undefined;
/**
* VP
*/
private jitteredViewProjectionMatrix: number[] | undefined;
/**
* Projection
*/
private jitteredProjectionMatrix: number[] | undefined;
public init() {
//
}
@ -22,7 +33,8 @@ export default class CameraService implements ICameraService {
}
public getProjectionMatrix(): number[] {
return this.viewport.getProjectionMatrix();
// 优先返回抖动后的 ProjectionMatrix
return this.jitteredProjectionMatrix || this.viewport.getProjectionMatrix();
}
public getViewMatrix(): number[] {
@ -36,6 +48,7 @@ export default class CameraService implements ICameraService {
public getViewProjectionMatrix(): number[] {
return (
this.overridedViewProjectionMatrix ||
this.jitteredViewProjectionMatrix ||
this.viewport.getViewProjectionMatrix()
);
}
@ -70,4 +83,25 @@ export default class CameraService implements ICameraService {
public setViewProjectionMatrix(viewProjectionMatrix: number[] | undefined) {
this.overridedViewProjectionMatrix = viewProjectionMatrix;
}
public jitterProjectionMatrix(x: number, y: number) {
const translation = mat4.fromTranslation(mat4.create(), [x, y, 0]);
this.jitteredProjectionMatrix = (mat4.multiply(
mat4.create(),
translation,
(this.viewport.getProjectionMatrix() as unknown) as mat4,
) as unknown) as number[];
this.jitteredViewProjectionMatrix = (mat4.multiply(
mat4.create(),
(this.jitteredProjectionMatrix as unknown) as mat4,
(this.viewport.getViewMatrix() as unknown) as mat4,
) as unknown) as number[];
}
public clearJitterProjectionMatrix() {
this.jitteredProjectionMatrix = undefined;
this.jitteredViewProjectionMatrix = undefined;
}
}

View File

@ -31,4 +31,6 @@ export interface ICameraService extends Omit<IViewport, 'syncWithMapCamera'> {
init(): void;
update(viewport: IViewport): void;
setViewProjectionMatrix(viewProjectionMatrix: number[] | undefined): void;
jitterProjectionMatrix(x: number, y: number): void;
clearJitterProjectionMatrix(): void;
}

View File

@ -107,6 +107,14 @@ export interface ILayerInitializationOptions {
*
*/
highlightColor: string | number[];
/**
* TAA
*/
enableTAA: boolean;
/**
*
*/
jitterScale: number;
onHover(pickedFeature: IPickedFeature): void;
onClick(pickedFeature: IPickedFeature): void;
}

View File

@ -93,19 +93,6 @@ export default class BasePostProcessingPass<InitializationOptions = {}>
});
},
);
// const useRenderTarget = (this.renderToScreen
// ? postProcessor.useScreenRenderTarget
// : postProcessor.useOffscreenRenderTarget
// ).bind(postProcessor);
// useRenderTarget(async () => {
// this.model.draw({
// uniforms: {
// u_Texture: postProcessor.getReadFBO(),
// },
// });
// });
}
public isEnabled() {

View File

@ -104,9 +104,9 @@ export default class PixelPickingPass implements IPass {
const originRenderFlag = this.layer.multiPassRenderer.getRenderFlag();
this.layer.multiPassRenderer.setRenderFlag(false);
// trigger hooks
layer.hooks.beforeRender.call();
layer.hooks.beforePickingEncode.call();
layer.render();
layer.hooks.afterRender.call();
layer.hooks.afterPickingEncode.call();
this.layer.multiPassRenderer.setRenderFlag(originRenderFlag);
this.alreadyInRendering = false;
@ -207,15 +207,28 @@ export default class PixelPickingPass implements IPass {
*/
private highlightPickedFeature(pickedColors: Uint8Array | undefined) {
const [r, g, b] = pickedColors;
const { clear, useFramebuffer } = this.rendererService;
// TODO: highlight pass 需要 multipass
const originRenderFlag = this.layer.multiPassRenderer.getRenderFlag();
this.layer.multiPassRenderer.setRenderFlag(false);
// 先输出到 PostProcessor
const readFBO = this.layer.multiPassRenderer.getPostProcessor().getReadFBO();
this.layer.hooks.beforeRender.call();
this.layer.hooks.beforeHighlight.call([r, g, b]);
this.layer.render();
this.layer.hooks.afterHighlight.call();
this.layer.hooks.afterRender.call();
this.layer.multiPassRenderer.setRenderFlag(originRenderFlag);
useFramebuffer(readFBO, () => {
clear({
color: [0, 0, 0, 0],
depth: 1,
stencil: 0,
framebuffer: readFBO,
});
// TODO: highlight pass 需要 multipass
const originRenderFlag = this.layer.multiPassRenderer.getRenderFlag();
this.layer.multiPassRenderer.setRenderFlag(false);
this.layer.hooks.beforeHighlight.call([r, g, b]);
this.layer.render();
this.layer.hooks.afterHighlight.call();
this.layer.hooks.afterRender.call();
this.layer.multiPassRenderer.setRenderFlag(originRenderFlag);
});
this.layer.multiPassRenderer.getPostProcessor().render(this.layer);
}
}

View File

@ -52,6 +52,7 @@ export default class PostProcessor implements IPostProcessor {
public async render(layer: ILayer) {
for (let i = 0; i < this.passes.length; i++) {
const pass = this.passes[i];
// last pass should render to screen
pass.setRenderToScreen(this.isLastEnabledPass(i));
await pass.render(layer);

View File

@ -1,75 +1,333 @@
// import { inject, injectable } from 'inversify';
// import { ILayer, ILayerService } from '../../layer/ILayerService';
// import { IPass, PassType } from '../IMultiPassRenderer';
import { inject, injectable } from 'inversify';
import { lazyInject } from '../../../index';
import blendFS from '../../../shaders/post-processing/blend.glsl';
import copyFS from '../../../shaders/post-processing/copy.glsl';
import quadVS from '../../../shaders/post-processing/quad.glsl';
import { TYPES } from '../../../types';
import { ICameraService } from '../../camera/ICameraService';
import { ILayer, ILayerService } from '../../layer/ILayerService';
import { ILogService } from '../../log/ILogService';
import { IShaderModuleService } from '../../shader/IShaderModuleService';
import { gl } from '../gl';
import { IFramebuffer } from '../IFramebuffer';
import { IModel, IModelInitializationOptions } from '../IModel';
import { IPass, PassType } from '../IMultiPassRenderer';
import { IRendererService } from '../IRendererService';
// // Generate halton sequence
// // https://en.wikipedia.org/wiki/Halton_sequence
// function halton(index: number, base: number) {
// let result = 0;
// let f = 1 / base;
// let i = index;
// while (i > 0) {
// result = result + f * (i % base);
// i = Math.floor(i / base);
// f = f / base;
// }
// return result;
// }
// Generate halton sequence
// https://en.wikipedia.org/wiki/Halton_sequence
function halton(index: number, base: number) {
let result = 0;
let f = 1 / base;
let i = index;
while (i > 0) {
result = result + f * (i % base);
i = Math.floor(i / base);
f = f / base;
}
return result;
}
// // 累加计数器
// let accumulatingId = 1;
// 累加计数器
let accumulatingId = 1;
// /**
// * TAATemporal Anti-Aliasing
// * 在需要后处理的场景中(例如 L7 的热力图需要 blur pass、PBR 中的 SSAO 环境光遮蔽),无法使用浏览器内置的 MSAA
// * 只能使用 TAA
// * @see https://yuque.antfin-inc.com/yuqi.pyq/fgetpa/ri52hv
// */
// @injectable()
// export default class TAAPass implements IPass {
// private haltonSequence: Array<[number, number]> = [];
/**
* TAATemporal Anti-Aliasing
* L7 blur passPBR SSAO 使 MSAA
* 使 TAA
* @see https://yuque.antfin-inc.com/yuqi.pyq/fgetpa/ri52hv
*/
@injectable()
export default class TAAPass implements IPass {
@lazyInject(TYPES.IRendererService)
protected readonly rendererService: IRendererService;
// // 当前累加任务 ID
// private accumulatingId: number = 0;
@lazyInject(TYPES.IShaderModuleService)
protected readonly shaderModule: IShaderModuleService;
// private frame: number = 0;
@lazyInject(TYPES.ICameraService)
protected readonly cameraService: ICameraService;
// private timer: number | undefined = undefined;
@lazyInject(TYPES.ILogService)
protected readonly logger: ILogService;
// public getType() {
// return PassType.Normal;
// }
/**
*
*/
private haltonSequence: Array<[number, number]> = [];
// public init(layer: ILayer) {
// this.scene = scene;
// this.camera = camera;
/**
* ID
*/
private accumulatingId: number = 0;
// for (let i = 0; i < 30; i++) {
// this.haltonSequence.push([halton(i, 2), halton(i, 3)]);
// }
/**
* frameID
*/
private frame: number = 0;
// // weigh accumulating
// this.blendPass = new ShaderPass(BlendShader);
// }
/**
* frame
*/
private timer: number | undefined = undefined;
// public render(layer: ILayer) {
// //
// }
private sampleRenderTarget: IFramebuffer;
private prevRenderTarget: IFramebuffer;
private outputRenderTarget: IFramebuffer;
private copyRenderTarget: IFramebuffer;
// /**
// * 是否已经完成累加
// * @return {boolean} isFinished
// */
// private isFinished() {
// return this.frame >= this.haltonSequence.length;
// }
private blendModel: IModel;
private outputModel: IModel;
private copyModel: IModel;
// private resetFrame() {
// this.frame = 0;
// }
public getType() {
return PassType.Normal;
}
// private stopAccumulating() {
// this.accumulatingId = 0;
// window.clearTimeout(this.timer);
// }
// }
public init(layer: ILayer) {
const { createFramebuffer, createTexture2D } = this.rendererService;
this.sampleRenderTarget = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
}),
});
this.prevRenderTarget = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
}),
});
this.outputRenderTarget = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
}),
});
this.copyRenderTarget = createFramebuffer({
color: createTexture2D({
width: 1,
height: 1,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
}),
});
for (let i = 0; i < 30; i++) {
this.haltonSequence.push([halton(i, 2), halton(i, 3)]);
}
this.blendModel = this.createTriangleModel('blend-pass', blendFS);
this.outputModel = this.createTriangleModel('copy-pass', copyFS, {
blend: {
enable: true,
func: {
srcRGB: gl.ONE,
dstRGB: gl.ONE_MINUS_SRC_ALPHA,
srcAlpha: gl.ONE,
dstAlpha: gl.ONE_MINUS_SRC_ALPHA,
},
equation: {
rgb: gl.FUNC_ADD,
alpha: gl.FUNC_ADD,
},
},
});
this.copyModel = this.createTriangleModel('copy-pass', copyFS);
}
public render(layer: ILayer) {
const { clear, getViewportSize, useFramebuffer } = this.rendererService;
const { width, height } = getViewportSize();
this.sampleRenderTarget.resize({ width, height });
this.prevRenderTarget.resize({ width, height });
this.outputRenderTarget.resize({ width, height });
this.copyRenderTarget.resize({ width, height });
this.resetFrame();
// 首先停止上一次的累加
this.stopAccumulating();
// 先输出到 PostProcessor
const readFBO = layer.multiPassRenderer.getPostProcessor().getReadFBO();
useFramebuffer(readFBO, () => {
clear({
color: [0, 0, 0, 0],
depth: 1,
stencil: 0,
framebuffer: readFBO,
});
// render to post processor
layer.multiPassRenderer.setRenderFlag(false);
layer.render();
layer.multiPassRenderer.setRenderFlag(true);
});
const accumulate = (id: number) => {
// 在开启新一轮累加之前,需要先结束掉之前的累加
if (!this.accumulatingId || id !== this.accumulatingId) {
return;
}
if (!this.isFinished()) {
this.doRender(layer);
requestAnimationFrame(() => {
accumulate(id);
});
}
};
this.accumulatingId = accumulatingId++;
this.timer = setTimeout(() => {
accumulate(this.accumulatingId);
}, 50);
}
private doRender(layer: ILayer) {
this.logger.info(`accumulatingId: ${this.accumulatingId}`);
const { clear, getViewportSize, useFramebuffer } = this.rendererService;
const { width, height } = getViewportSize();
const { jitterScale = 1 } = layer.getStyleOptions();
// 使用 Halton 序列抖动投影矩阵
const offset = this.haltonSequence[this.frame % this.haltonSequence.length];
this.cameraService.jitterProjectionMatrix(
((offset[0] * 2.0 - 1.0) / width) * jitterScale,
((offset[1] * 2.0 - 1.0) / height) * jitterScale,
);
// 按抖动后的投影矩阵渲染
layer.multiPassRenderer.setRenderFlag(false);
layer.hooks.beforeRender.call();
useFramebuffer(this.sampleRenderTarget, () => {
clear({
color: [0, 0, 0, 0],
depth: 1,
stencil: 0,
framebuffer: this.sampleRenderTarget,
});
layer.render();
});
layer.hooks.afterRender.call();
layer.multiPassRenderer.setRenderFlag(true);
// 混合
const layerStyleOptions = layer.getStyleOptions();
useFramebuffer(this.outputRenderTarget, () => {
this.blendModel.draw({
uniforms: {
// @ts-ignore
u_Opacity: layerStyleOptions.opacity || 1,
u_MixRatio: this.frame === 0 ? 1 : 0.9,
u_Diffuse1: this.sampleRenderTarget,
u_Diffuse2:
this.frame === 0
? layer.multiPassRenderer.getPostProcessor().getReadFBO()
: this.prevRenderTarget,
},
});
});
// 输出累加结果
if (this.frame === 0) {
clear({
color: [0, 0, 0, 0],
depth: 1,
stencil: 0,
framebuffer: this.copyRenderTarget,
});
}
if (this.frame >= 1) {
useFramebuffer(this.copyRenderTarget, () => {
this.outputModel.draw({
uniforms: {
u_Texture: this.outputRenderTarget,
},
});
});
useFramebuffer(null, () => {
this.copyModel.draw({
uniforms: {
u_Texture: this.copyRenderTarget,
},
});
});
}
// 保存前序帧结果
const tmp = this.prevRenderTarget;
this.prevRenderTarget = this.outputRenderTarget;
this.outputRenderTarget = tmp;
this.frame++;
// 恢复 jitter 后的相机
this.cameraService.clearJitterProjectionMatrix();
}
/**
*
* @return {boolean} isFinished
*/
private isFinished() {
return this.frame >= this.haltonSequence.length;
}
private resetFrame() {
this.frame = 0;
}
private stopAccumulating() {
this.accumulatingId = 0;
window.clearTimeout(this.timer);
}
private createTriangleModel(
shaderModuleName: string,
fragmentShader: string,
options?: Partial<IModelInitializationOptions>,
) {
this.shaderModule.registerModule(shaderModuleName, {
vs: quadVS,
fs: fragmentShader,
});
const { vs, fs, uniforms } = this.shaderModule.getModule(shaderModuleName);
const { createAttribute, createBuffer, createModel } = this.rendererService;
return createModel({
vs,
fs,
attributes: {
// 使用一个全屏三角形,相比 Quad 顶点数目更少
a_Position: createAttribute({
buffer: createBuffer({
data: [-4, -4, 4, -4, 0, 4],
type: gl.FLOAT,
}),
size: 2,
}),
},
uniforms: {
...uniforms,
},
depth: {
enable: false,
},
count: 3,
...options,
});
}
}

View File

@ -1,4 +1,5 @@
const TYPES = {
ISceneService: Symbol.for('ISceneService'),
IGlobalConfigService: Symbol.for('IGlobalConfigService'),
ICameraService: Symbol.for('ICameraService'),
ICoordinateSystemService: Symbol.for('ICoordinateSystemService'),
@ -14,6 +15,7 @@ const TYPES = {
IInteractionService: Symbol.for('IInteractionService'),
IControlService: Symbol.for('IControlService'),
IStyleAttributeService: Symbol.for('IStyleAttributeService'),
ILayerPlugin: Symbol.for('ILayerPlugin'),
/** multi-pass */
ClearPass: Symbol.for('ClearPass'),

View File

@ -16,6 +16,7 @@ import {
IStyleAttributeService,
IStyleAttributeUpdateOptions,
lazyInject,
lazyMultiInject,
StyleAttributeField,
StyleAttributeOption,
Triangulation,
@ -26,15 +27,7 @@ import { isFunction } from 'lodash';
// @ts-ignore
import mergeJsonSchemas from 'merge-json-schemas';
import { SyncBailHook, SyncHook } from 'tapable';
import ConfigSchemaValidationPlugin from '../plugins/ConfigSchemaValidationPlugin';
import DataMappingPlugin from '../plugins/DataMappingPlugin';
import DataSourcePlugin from '../plugins/DataSourcePlugin';
import FeatureScalePlugin from '../plugins/FeatureScalePlugin';
import MultiPassRendererPlugin from '../plugins/MultiPassRendererPlugin';
import PixelPickingPlugin from '../plugins/PixelPickingPlugin';
import RegisterStyleAttributePlugin from '../plugins/RegisterStyleAttributePlugin';
import ShaderUniformPlugin from '../plugins/ShaderUniformPlugin';
import UpdateStyleAttributePlugin from '../plugins/UpdateStyleAttributePlugin';
import baseLayerSchema from './schema';
export interface ILayerModelInitializationOptions {
@ -59,6 +52,8 @@ const defaultLayerInitializationOptions: Partial<
enablePicking: false,
enableHighlight: false,
highlightColor: 'red',
enableTAA: false,
jitterScale: 1,
};
export default class BaseLayer<ChildLayerStyleOptions = {}> implements ILayer {
@ -84,46 +79,10 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> implements ILayer {
// 每个 Layer 都有一个
public multiPassRenderer: IMultiPassRenderer;
// 插件集
public plugins: ILayerPlugin[] = [
/**
*
* @see /dev-docs/ConfigSchemaValidation.md
*/
new ConfigSchemaValidationPlugin(),
/**
* Source
*/
new DataSourcePlugin(),
/**
* StyleAttribute VertexAttribute
*/
new RegisterStyleAttributePlugin(),
/**
* Source Scale
*/
new FeatureScalePlugin(),
/**
* 使 Scale
*/
new DataMappingPlugin(),
/**
*
*/
new UpdateStyleAttributePlugin(),
/**
* Multi Pass 线
*/
new MultiPassRendererPlugin(),
/**
*
*/
new ShaderUniformPlugin(),
/**
* Encode Highlight
*/
new PixelPickingPlugin(),
];
// 注入插件集
@lazyMultiInject(TYPES.ILayerPlugin)
public plugins: ILayerPlugin[];
public sourceOption: {
data: any;
options?: ISourceCFG;

View File

@ -1,3 +1,4 @@
import { container, ILayerPlugin, TYPES } from '@l7/core';
import BaseLayer from './core/BaseLayer';
// import HeatMapLayer from './heatmap';
// import Line from './line';
@ -5,6 +6,59 @@ import BaseLayer from './core/BaseLayer';
// import Point from './point/point';
import PolygonLayer from './polygon';
// import ImageLayer from './raster';
import ConfigSchemaValidationPlugin from './plugins/ConfigSchemaValidationPlugin';
import DataMappingPlugin from './plugins/DataMappingPlugin';
import DataSourcePlugin from './plugins/DataSourcePlugin';
import FeatureScalePlugin from './plugins/FeatureScalePlugin';
import MultiPassRendererPlugin from './plugins/MultiPassRendererPlugin';
import PixelPickingPlugin from './plugins/PixelPickingPlugin';
import RegisterStyleAttributePlugin from './plugins/RegisterStyleAttributePlugin';
import ShaderUniformPlugin from './plugins/ShaderUniformPlugin';
import UpdateStyleAttributePlugin from './plugins/UpdateStyleAttributePlugin';
/**
*
* @see /dev-docs/ConfigSchemaValidation.md
*/
container
.bind<ILayerPlugin>(TYPES.ILayerPlugin)
.to(ConfigSchemaValidationPlugin);
/**
* Source
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(DataSourcePlugin);
/**
* StyleAttribute VertexAttribute
*/
container
.bind<ILayerPlugin>(TYPES.ILayerPlugin)
.to(RegisterStyleAttributePlugin);
/**
* Source Scale
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(FeatureScalePlugin);
/**
* 使 Scale
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(DataMappingPlugin);
/**
*
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(UpdateStyleAttributePlugin);
/**
* Multi Pass 线
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(MultiPassRendererPlugin);
/**
*
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(ShaderUniformPlugin);
/**
* Encode Highlight
*/
container.bind<ILayerPlugin>(TYPES.ILayerPlugin).to(PixelPickingPlugin);
export {
BaseLayer,
// PointLayer,

View File

@ -3,18 +3,19 @@ import {
ILayer,
ILayerPlugin,
ILogService,
lazyInject,
TYPES,
} from '@l7/core';
import { inject, injectable } from 'inversify';
/**
* Layer
*/
@injectable()
export default class ConfigSchemaValidationPlugin implements ILayerPlugin {
@lazyInject(TYPES.IGlobalConfigService)
@inject(TYPES.IGlobalConfigService)
private readonly configService: IGlobalConfigService;
@lazyInject(TYPES.ILogService)
@inject(TYPES.ILogService)
private readonly logger: ILogService;
public apply(layer: ILayer) {

View File

@ -9,13 +9,15 @@ import {
lazyInject,
TYPES,
} from '@l7/core';
import { inject, injectable } from 'inversify';
import { rgb2arr } from '../utils/color';
@injectable()
export default class DataMappingPlugin implements ILayerPlugin {
@lazyInject(TYPES.IGlobalConfigService)
@inject(TYPES.IGlobalConfigService)
private readonly configService: IGlobalConfigService;
@lazyInject(TYPES.ILogService)
@inject(TYPES.ILogService)
private readonly logger: ILogService;
public apply(layer: ILayer) {

View File

@ -1,5 +1,8 @@
import { ILayer, ILayerPlugin } from '@l7/core';
import Source from '@l7/source';
import { injectable } from 'inversify';
@injectable()
export default class DataSourcePlugin implements ILayerPlugin {
public apply(layer: ILayer) {
layer.hooks.init.tap('DataSourcePlugin', () => {

View File

@ -12,6 +12,7 @@ import {
import { IParseDataItem } from '@l7/source';
import { extent } from 'd3-array';
import * as d3 from 'd3-scale';
import { inject, injectable } from 'inversify';
import { isNil, isNumber, isString, uniq } from 'lodash';
const dateRegex = /^(?:(?!0000)[0-9]{4}([-/.]+)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))(\s+([01]|([01][0-9]|2[0-3])):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9]))?$/;
@ -31,11 +32,12 @@ const scaleMap = {
/**
* Source Scale StyleAttribute 使
*/
@injectable()
export default class FeatureScalePlugin implements ILayerPlugin {
@lazyInject(TYPES.IGlobalConfigService)
@inject(TYPES.IGlobalConfigService)
private readonly configService: IGlobalConfigService;
@lazyInject(TYPES.ILogService)
@inject(TYPES.ILogService)
private readonly logger: ILogService;
private scaleCache: {

View File

@ -12,8 +12,10 @@ import {
MultiPassRenderer,
PixelPickingPass,
RenderPass,
TAAPass,
TYPES,
} from '@l7/core';
import { inject, injectable } from 'inversify';
const builtinPostProcessingPassMap: {
[key: string]: new (config?: { [key: string]: any }) => IPostProcessingPass;
@ -47,11 +49,12 @@ function normalizePasses(
* ],
* })
*/
@injectable()
export default class MultiPassRendererPlugin implements ILayerPlugin {
@lazyInject(TYPES.IGlobalConfigService)
@inject(TYPES.IGlobalConfigService)
private readonly configService: IGlobalConfigService;
@lazyInject(TYPES.IRendererService)
@inject(TYPES.IRendererService)
private readonly rendererService: IRendererService;
private enabled: boolean;
@ -97,13 +100,23 @@ export default class MultiPassRendererPlugin implements ILayerPlugin {
passes: Array<string | [string, { [key: string]: unknown }]>,
) {
const multiPassRenderer = new MultiPassRenderer(layer);
const { enablePicking, enableTAA } = layer.getStyleOptions();
// clear first
multiPassRenderer.add(new ClearPass());
if (layer.getStyleOptions().enablePicking) {
// picking pass if enabled
if (enablePicking) {
multiPassRenderer.add(new PixelPickingPass());
}
multiPassRenderer.add(new RenderPass());
// TAA pass if enabled
if (enableTAA) {
multiPassRenderer.add(new TAAPass());
} else {
// render all layers in this pass
multiPassRenderer.add(new RenderPass());
}
// post processing
normalizePasses(passes).forEach(
@ -112,6 +125,7 @@ export default class MultiPassRendererPlugin implements ILayerPlugin {
multiPassRenderer.add(new PostProcessingPassClazz(pass[1]));
},
);
// 末尾为固定的 CopyPass
multiPassRenderer.add(new CopyPass());

View File

@ -8,6 +8,7 @@ import {
lazyInject,
TYPES,
} from '@l7/core';
import { inject, injectable } from 'inversify';
import { rgb2arr } from '../utils/color';
function encodePickingColor(featureIdx: number): [number, number, number] {
@ -24,8 +25,9 @@ const PickingStage = {
HIGHLIGHT: 2.0,
};
@injectable()
export default class PixelPickingPlugin implements ILayerPlugin {
@lazyInject(TYPES.IRendererService)
@inject(TYPES.IRendererService)
private readonly rendererService: IRendererService;
public apply(layer: ILayer) {
@ -51,22 +53,28 @@ export default class PixelPickingPlugin implements ILayerPlugin {
});
// 必须要与 PixelPickingPass 结合使用,因此必须开启 multiPassRenderer
// if (layer.multiPassRenderer) {
layer.hooks.beforeRender.tap('PixelPickingPlugin', () => {
layer.models.forEach((model) =>
model.addUniforms({
u_PickingStage: PickingStage.ENCODE,
}),
);
layer.hooks.beforePickingEncode.tap('PixelPickingPlugin', () => {
const { enablePicking } = layer.getStyleOptions();
if (enablePicking) {
layer.models.forEach((model) =>
model.addUniforms({
u_PickingStage: PickingStage.ENCODE,
}),
);
}
});
layer.hooks.afterRender.tap('PixelPickingPlugin', () => {
layer.models.forEach((model) =>
model.addUniforms({
u_PickingStage: PickingStage.NONE,
u_PickingColor: [0, 0, 0],
u_HighlightColor: [0, 0, 0, 0],
}),
);
layer.hooks.afterPickingEncode.tap('PixelPickingPlugin', () => {
const { enablePicking } = layer.getStyleOptions();
if (enablePicking) {
layer.models.forEach((model) =>
model.addUniforms({
u_PickingStage: PickingStage.NONE,
u_PickingColor: [0, 0, 0],
u_HighlightColor: [0, 0, 0, 0],
}),
);
}
});
layer.hooks.beforeHighlight.tap(

View File

@ -5,16 +5,17 @@ import {
ILayer,
ILayerPlugin,
ILogService,
IStyleAttributeService,
lazyInject,
TYPES,
} from '@l7/core';
import { inject, injectable } from 'inversify';
/**
* Layer indices attribute
*/
@injectable()
export default class RegisterStyleAttributePlugin implements ILayerPlugin {
@lazyInject(TYPES.ILogService)
@inject(TYPES.ILogService)
private readonly logger: ILogService;
public apply(layer: ILayer) {

View File

@ -9,6 +9,7 @@ import {
lazyInject,
TYPES,
} from '@l7/core';
import { inject, injectable } from 'inversify';
/**
* Shader Uniform
@ -17,14 +18,15 @@ import {
* @see https://yuque.antfin-inc.com/yuqi.pyq/fgetpa/doml91
* 3. Layer
*/
@injectable()
export default class ShaderUniformPlugin implements ILayerPlugin {
@lazyInject(TYPES.ICameraService)
@inject(TYPES.ICameraService)
private readonly cameraService: ICameraService;
@lazyInject(TYPES.ICoordinateSystemService)
@inject(TYPES.ICoordinateSystemService)
private readonly coordinateSystemService: ICoordinateSystemService;
@lazyInject(TYPES.IRendererService)
@inject(TYPES.IRendererService)
private readonly rendererService: IRendererService;
public apply(layer: ILayer) {

View File

@ -1,10 +1,12 @@
import { ILayer, ILayerPlugin, ILogService, lazyInject, TYPES } from '@l7/core';
import { inject, injectable } from 'inversify';
/**
* Layer indices attribute
*/
@injectable()
export default class UpdateStyleAttributePlugin implements ILayerPlugin {
@lazyInject(TYPES.ILogService)
@inject(TYPES.ILogService)
private readonly logger: ILogService;
public apply(layer: ILayer) {

View File

@ -171,9 +171,10 @@ export default class ReglModel implements IModel {
enable: enable === undefined ? false : !!enable,
func: {
srcRGB: blendFuncMap[(func && func.srcRGB) || gl.SRC_ALPHA],
srcAlpha: func && func.srcAlpha,
srcAlpha: blendFuncMap[(func && func.srcAlpha) || gl.SRC_ALPHA],
dstRGB: blendFuncMap[(func && func.dstRGB) || gl.ONE_MINUS_SRC_ALPHA],
dstAlpha: func && func.dstAlpha,
dstAlpha:
blendFuncMap[(func && func.dstAlpha) || gl.ONE_MINUS_SRC_ALPHA],
},
equation: {
rgb: blendEquationMap[(equation && equation.rgb) || gl.FUNC_ADD],

View File

@ -17,12 +17,10 @@ import {
MapType,
Point,
SceneEventList,
SceneService,
TYPES,
} from '@l7/core';
import { AMapService, MapboxService } from '@l7/maps';
import { ReglRendererService } from '@l7/renderer';
import { inject, injectable } from 'inversify';
import { Map } from 'mapbox-gl';
// 绑定渲染引擎服务
@ -83,7 +81,7 @@ class Scene {
}
// 依赖注入
this.sceneService = container.resolve(SceneService);
this.sceneService = container.get<ISceneService>(TYPES.ISceneService);
this.sceneService.init(config);
this.mapService = container.get<IMapService>(TYPES.IMapService);
this.iconService = container.get<IIconService>(TYPES.IIconService);

View File

@ -1,5 +1,8 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import Polygon from './components/Polygon';
import Blur from './components/Blur';
import TAA from './components/TAA';
storiesOf('MultiPassRenderer', module).add('blur', () => <Polygon />);
storiesOf('MultiPassRenderer', module)
.add('Blur', () => <Blur />)
.add('TAA(Temporal Anti-Aliasing)', () => <TAA />);

View File

@ -34,7 +34,8 @@ export default class Mapbox extends React.Component {
zoom: 3,
});
const layer = new PolygonLayer({
enablePicking: false,
enablePicking: true,
enableHighlight: true,
passes: [
'blurH',
[

View File

@ -0,0 +1,105 @@
// @ts-ignore
import { PolygonLayer } from '@l7/layers';
// @ts-ignore
import { Scene } from '@l7/scene';
import * as dat from 'dat.gui';
import * as React from 'react';
export default class TAA extends React.Component {
private gui: dat.GUI;
private $stats: Node;
private scene: Scene;
public componentWillUnmount() {
if (this.gui) {
this.gui.destroy();
}
if (this.$stats) {
document.body.removeChild(this.$stats);
}
this.scene.destroy();
}
public async componentDidMount() {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json',
);
const data = await response.json();
const scene = new Scene({
id: 'map',
type: 'mapbox',
style: 'mapbox://styles/mapbox/streets-v9',
center: [110.19382669582967, 50.258134],
pitch: 0,
zoom: 3,
});
const layer = new PolygonLayer({
enablePicking: true,
enableHighlight: true,
enableTAA: true,
jitterScale: 1,
// passes: [
// 'blurH',
// [
// 'blurV',
// {
// blurRadius: 8,
// },
// ],
// ],
});
layer
.source(data)
.size('name', [0, 10000, 50000, 30000, 100000])
.color('name', [
'#2E8AE6',
'#69D1AB',
'#DAF291',
'#FFD591',
'#FF7A45',
'#CF1D49',
])
.shape('fill')
.style({
opacity: 0.8,
});
scene.addLayer(layer);
scene.render();
this.scene = scene;
/*** 运行时修改样式属性 ***/
const gui = new dat.GUI();
this.gui = gui;
const styleOptions = {
jitterScale: 1,
};
const pointFolder = gui.addFolder('TAA 配置');
pointFolder
.add(styleOptions, 'jitterScale', 0, 100)
.onChange((jitterScale: boolean) => {
layer.style({
jitterScale,
});
scene.render();
});
pointFolder.open();
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}