feat heatmap layer

This commit is contained in:
mipha.ly@alibaba-inc.com 2019-02-27 14:50:10 +08:00 committed by 刘叶
parent 712e983b3e
commit 6ed876befe
14 changed files with 375 additions and 10 deletions

59
demos/heatmap.html Normal file
View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="geometry" content="diagram">
<link rel="stylesheet" href="./assets/common.css">
<title>heatmap</title>
<style>
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://webapi.amap.com/maps?v=1.4.8&key=15cd8a57710d40c9b7c0e3cc120f1200&plugin=Map3D"></script>
<script src="./assets/jquery-3.2.1.min.js"></script>
<script src="./assets/dat.gui.min.js"></script>
<script src="../build/L7.js"></script>
<script>
const scene = new L7.Scene({
id: 'map',
mapStyle: 'dark', // 样式URL
center: [ -155, 60 ],
pitch: 0,
zoom: 4.5
});
window.scene = scene;
scene.on('loaded', () => {
$.get('./data/earthquakes.geojson', data => {
scene.HeatmapLayer({
zIndex: 2
})
.source(data)
.size('mag', [ 0, 1 ]) // weight映射通道
.style({
intensity: 1,
radius: 10,
rampColors: {
colors: [ 'rgba(33,102,172,0)', 'rgb(103,169,207)', 'rgb(209,229,240)', 'rgb(253,219,199)', 'rgb(239,138,98)', 'rgb(178,24,43)' ],
positions: [ 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 ]
}
})
.render();
/* scene.PointLayer({
zIndex: 3
})
.source(data)
.shape('2d:circle')
.size(2)
.color('#EE2C2C')
.render();*/
});
});
</script>
</body>
</html>

View File

@ -1,25 +1,28 @@
import * as THREE from '../three';
export class RenderPass {
export default class RenderPass {
constructor(cfg) {
this.scene;
this.camera = cfg.camera;
this.renderer = cfg.renderer;
this.clearColor = cfg.clear.clearColor;
this.clearAlpha = cfg.clear.clearAlpha;
this.size = cfg.size ? cfg.size : cfg.renderer.getSize();
this._init(cfg);
}
_init() {
this.scene = new THREE.Scene();
const parameters = { minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
const parameters = {
// minFilter: THREE.NearestFilter,
// magFilter: THREE.NearestFilter,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
stencilBuffer: false,
depthBuffer: false
};
const size = this.renderer.getSize();
this.pass = new THREE.WebGLRenderTarget(size.width, size.height, parameters);
this.pass = new THREE.WebGLRenderTarget(this.size.width, this.size.height, parameters);
this.originClearColor = this.renderer.getClearColor();
this.originClearAlpha = this.renderer.getClearAlpha();
this.texture = this.pass.texture;
@ -35,7 +38,8 @@ export class RenderPass {
render() {
this.renderer.setClearColor(this.clearColor, this.clearAlpha);
this.renderer.render(this.scene, this.camera, this.pass, true); // this.pass,true
this.renderer.setClearColor(this.clearColor, this.clearAlpha);
this.renderer.render(this.scene, this.camera, this.pass, true);
this.renderer.setClearColor(this.originClearColor, this.originClearAlpha);
this.texture = this.pass.texture;
}
}

View File

@ -11,7 +11,9 @@ export { Mesh } from 'three/src/objects/Mesh.js';
export { Texture } from 'three/src/textures/Texture.js';
export { WebGLRenderTarget } from 'three/src/renderers/WebGLRenderTarget.js';
export { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
export { OrthographicCamera } from 'three/src/cameras/OrthographicCamera.js';
export { BufferGeometry } from 'three/src/core/BufferGeometry.js';
export { PlaneBufferGeometry } from 'three/src/geometries/PlaneGeometry.js';
export { Raycaster } from 'three/src/core/Raycaster.js';
export { Matrix4 } from 'three/src/math/Matrix4.js';
export { Matrix3 } from 'three/src/math/Matrix3.js';
@ -20,7 +22,9 @@ export { Vector4 } from 'three/src/math/Vector4.js';
export { Vector3 } from 'three/src/math/Vector3.js';
export { Vector2 } from 'three/src/math/Vector2.js';
export { ShaderMaterial } from 'three/src/materials/ShaderMaterial.js';
export { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial.js';
export { DataTexture } from 'three/src/textures/DataTexture.js';
export { Color } from 'three/src/math/Color.js';
export {
Float64BufferAttribute,
Float32BufferAttribute,

105
src/geom/buffer/heatmap.js Normal file
View File

@ -0,0 +1,105 @@
import BufferBase from './bufferBase';
import { colorScales } from '../../attr/colorscales';
import * as THREE from '../../core/three';
export default class HeatmapBuffer extends BufferBase {
geometryBuffer() {
const coordinates = this.get('coordinates');
const properties = this.get('properties');
const positions = [];
const dirs = [];
const weights = [];
const indices = [];
// 组织顶点数据
coordinates.forEach((geo, index) => {
const totalIndex = index * 4;
const props = properties[index];
const radius = props.size;
/* const tl = this._addVertex(geo, -1, -1);
const tr = this._addVertex(geo, 1, -1);
const bl = this._addVertex(geo, -1, 1);
const br = this._addVertex(geo, 1, 1);*/
const dir = this._addDir(-1, 1);
const dir1 = this._addDir(1, 1);
const dir2 = this._addDir(-1, -1);
const dir3 = this._addDir(1, -1);
positions.push(...geo, ...geo, ...geo, ...geo, ...geo, ...geo);
dirs.push(...dir, ...dir2, ...dir3, ...dir1, ...dir, ...dir3);
// dirs.push(...this._addDir(-1, 1), ...this._addDir(1, 1), ...this._addDir(-1, -1), ...this._addDir(1, -1));
weights.push(radius, radius, radius, radius, radius, radius);
indices.push(totalIndex, totalIndex + 2, totalIndex + 3, totalIndex, totalIndex + 3, totalIndex + 1);
});
this.attributes = {
vertices: positions,
indices,
dirs,
weights
};
}
_addVertex(position, dirX, dirY) {
const x = (position[0] * 2) + ((dirX + 1) / 2);
const y = (position[1] * 2) + ((dirY + 1) / 2);
const z = position[2];
return [ x, y, z ];
}
_addDir(dirX, dirY) {
const x = (dirX + 1) / 2;
const y = (dirY + 1) / 2;
return [ x, y ];
}
}
export function createColorRamp(colors) {
const colorImageData = getColorRamp(colors);
const colorTexture = getTexture(colorImageData);
return colorTexture;
}
function getColorRamp(name) {
let colorscale = name;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 1;
const gradient = ctx.createLinearGradient(0, 0, 256, 0);
let data = null;
if (typeof (colorscale) === 'string') {
colorscale = colorScales[name];
}
if (Object.prototype.toString.call(colorscale) === '[object Object]') {
const min = colorscale.positions[0];
const max = colorscale.positions[colorscale.positions.length - 1];
for (let i = 0; i < colorscale.colors.length; ++i) {
const value = (colorscale.positions[i] - min) / (max - min);
gradient.addColorStop(value, colorscale.colors[i]);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 256, 1);
data = new Uint8ClampedArray(ctx.getImageData(0, 0, 256, 1).data);
}
if (Object.prototype.toString.call(colorscale) === '[object Uint8Array]') {
data = ctx.createImageData(256, 1);
}
return new ImageData(data, 16, 16);
}
function getTexture(image) {
const texture = new THREE.Texture(image);
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
texture.format = THREE.RGBAFormat;
texture.type = THREE.UnsignedByteType;
texture.needsUpdate = true;
return texture;
}

View File

@ -0,0 +1,33 @@
import * as THREE from '../../core/three';
import Material from './material';
import heatmap_intensity_vert from '../shader/heatmap_intensity_vert.glsl';
import heatmap_intensity_frag from '../shader/heatmap_intensity_frag.glsl';
import heatmap_colorize_vert from '../shader/heatmap_colorize_vert.glsl';
import heatmap_colorize_frag from '../shader/heatmap_colorize_frag.glsl';
export function HeatmapIntensityMaterial(opt) {
const material = new Material({
uniforms: {
u_intensity: { value: opt.intensity },
u_radius: { value: opt.radius },
u_zoom: { value: opt.zoom }
},
vertexShader: heatmap_intensity_vert,
fragmentShader: heatmap_intensity_frag,
transparent: true,
blending: THREE.AdditiveBlending
});
return material;
}
export function HeatmapColorizeMaterial(opt) {
const material = new Material({
uniforms: {
u_texture: { value: opt.texture },
u_colorRamp: { value: opt.colorRamp }
},
vertexShader: heatmap_colorize_vert,
fragmentShader: heatmap_colorize_frag
});
return material;
}

View File

@ -0,0 +1,9 @@
uniform sampler2D u_texture;
uniform sampler2D u_colorRamp;
varying vec2 v_uv;
void main(){
float intensity = texture2D(u_texture,v_uv).r;
vec4 color = texture2D(u_colorRamp,vec2(0.5,1.0-intensity));
gl_FragColor = color * intensity;
}

View File

@ -0,0 +1,5 @@
varying vec2 v_uv;
void main(){
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

View File

@ -0,0 +1,11 @@
precision highp float;
varying float v_weight;
varying vec2 v_extrude;
void main(){
float GAUSS_COEF = 0.3989422804014327;
float d = -0.5 * 3.0 * 3.0 * dot(v_extrude, v_extrude);
float val = v_weight * 10.0 * GAUSS_COEF * exp(d);
gl_FragColor = vec4(val, val, val, 1.0);
}

View File

@ -0,0 +1,22 @@
precision highp float;
attribute float a_weight;
attribute vec2 a_dir;
uniform float u_intensity;
uniform float u_radius;
uniform float u_zoom;
varying vec2 v_extrude;
varying float v_weight;
void main(){
v_weight = a_weight;
float GAUSS_COEF = 0.3989422804014327;
float ZERO = 1.0 / 255.0 / 16.0;
float extrude_x = a_dir.x * 2.0 -1.0;
float extrude_y = a_dir.y * 2.0 -1.0;
vec2 extrude_dir = normalize(vec2(extrude_x,extrude_y));
float S = sqrt(-2.0 * log(ZERO / a_weight / u_intensity / GAUSS_COEF)) / 3.0;
v_extrude = extrude_dir * S;
vec2 extrude = v_extrude * u_radius * pow(2.0,20.0-min(u_zoom,9.0));
vec4 pos = vec4( position.xy+ extrude, 0.0, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * pos;
}

113
src/layer/heatmapLayer.js Normal file
View File

@ -0,0 +1,113 @@
import Layer from '../core/layer';
import HeatmapBuffer from '../geom/buffer/heatmap';
import { createColorRamp } from '../geom/buffer/heatmap';
import { HeatmapIntensityMaterial, HeatmapColorizeMaterial } from '../geom/material/heatmapMateial';
import Renderpass from '../core/engine/renderpass';
import * as THREE from '../core/three';
export default class HeatmapLayer extends Layer {
render() {
this.init();
const bbox = this._calBoundingBox(this.layerSource.geoData);
const colors = this.get('styleOptions').rampColors;
this.colorRamp = createColorRamp(colors);
this._createIntensityPass(bbox);
this._createColorizePass(bbox);
}
_createIntensityPass(bbox) {
const source = this.layerSource;
const style = this.get('styleOptions');
const StyleData = this.StyleData;
// get attributes data
const buffer = new HeatmapBuffer({
coordinates: source.geoData,
properties: StyleData
});
const attributes = buffer.attributes;
// create geometery
const geometry = new THREE.BufferGeometry();
// geometry.setIndex(attributes.indices);
geometry.addAttribute('position', new THREE.Float32BufferAttribute(attributes.vertices, 3));
geometry.addAttribute('a_dir', new THREE.Float32BufferAttribute(attributes.dirs, 2));
geometry.addAttribute('a_weight', new THREE.Float32BufferAttribute(attributes.weights, 1));
// set material
const material = new HeatmapIntensityMaterial({
intensity: style.intensity,
radius: style.radius,
zoom: this.scene.getZoom()
});
const mesh = new THREE.Mesh(geometry, material);
// set camera
const passOrth = new THREE.OrthographicCamera(bbox.width / -2, bbox.width / 2, bbox.height / 2, bbox.height / -2, 1, 10000);
passOrth.position.set(bbox.minX + bbox.width / 2, bbox.minY + bbox.height / 2, 1000);
// renderpass
const renderpass = new Renderpass({
renderer: this.scene._engine._renderer,
camera: passOrth,
size: {
width: 10000,
height: 10000 * (bbox.height / bbox.width)
},
clear: {
clearColor: 0x000000,
clearAlpha: 1.0
}
});
renderpass.add(mesh);
renderpass.render();
this.intensityPass = renderpass;
const scene = this.scene;
render();
function render() {
requestAnimationFrame(render);
renderpass.render();
mesh.material.uniforms.u_zoom.value = scene.getZoom();
}
}
_createColorizePass(bbox) {
// create plane geometry
const geometery = new THREE.PlaneBufferGeometry(bbox.width, bbox.height);
const material = new HeatmapColorizeMaterial({
texture: this.intensityPass.texture,
colorRamp: this.colorRamp
});
const mesh = new THREE.Mesh(geometery, material);
mesh.position.set(bbox.minX + bbox.width / 2, bbox.minY + bbox.height / 2, 0.0);
this.add(mesh);
}
_calBoundingBox(positions) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (let i = 0; i < positions.length; i++) {
const p = positions[i];
if (p[0] < minX) {
minX = p[0];
} else if (p[0] > maxX) {
maxX = p[0];
}
if (p[1] < minY) {
minY = p[1];
} else if (p[1] > maxY) {
maxY = p[1];
}
}
const width = maxX - minX;
const height = maxY - minY;
return {
minX,
maxX,
minY,
maxY,
width,
height
};
}
}

View File

@ -4,12 +4,14 @@ import PointLayer from './pointLayer';
import LineLayer from './lineLayer';
import ImageLayer from './imageLayer';
import RasterLayer from './rasterLayer';
import HeatmapLayer from './heatmapLayer';
registerLayer('PolygonLayer', PolygonLayer);
registerLayer('PointLayer', PointLayer);
registerLayer('LineLayer', LineLayer);
registerLayer('ImageLayer', ImageLayer);
registerLayer('RasterLayer', RasterLayer);
registerLayer('HeatmapLayer', HeatmapLayer);
export { LAYER_MAP } from './factory';
export { default as PolygonLayer } from './polygonLayer';
@ -17,4 +19,5 @@ export { default as PointLayer } from './pointLayer';
export { default as LineLayer } from './lineLayer';
export { default as ImageLayer } from './imageLayer';
export { default as RasterLayer } from './rasterLayer';
export { default as HeatmapLayer } from './heatmapLayer';

View File

@ -1,4 +1,3 @@
import Layer from '../core/layer';
import * as THREE from '../core/three';
import { LineBuffer } from '../geom/buffer/index';

View File

@ -13,7 +13,6 @@ import * as PointBuffer from '../geom/buffer/point/index';
* shape 3d cubecolumn, sphere
* shape Model ,自定义
* image
*
*/
export default class PointLayer extends Layer {

View File

@ -37,7 +37,6 @@ export default class RasterLayer extends Layer {
const rasterMesh = new THREE.Mesh(this.geometry, material);
this.add(rasterMesh);
return this;
}
animateFunc() {