diff --git a/demos/heatmap.html b/demos/heatmap.html new file mode 100644 index 0000000000..44f99033ef --- /dev/null +++ b/demos/heatmap.html @@ -0,0 +1,59 @@ + + + + + + + + + heatmap + + + +
+ + + + + + + + diff --git a/src/core/engine/renderpass.js b/src/core/engine/renderpass.js index 9a19c47b35..70311302be 100644 --- a/src/core/engine/renderpass.js +++ b/src/core/engine/renderpass.js @@ -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; } } diff --git a/src/core/three.js b/src/core/three.js index 75cbe58b5d..3c05ab2d13 100644 --- a/src/core/three.js +++ b/src/core/three.js @@ -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, diff --git a/src/geom/buffer/heatmap.js b/src/geom/buffer/heatmap.js new file mode 100644 index 0000000000..c52c6b58ae --- /dev/null +++ b/src/geom/buffer/heatmap.js @@ -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; +} + diff --git a/src/geom/material/heatmapMateial.js b/src/geom/material/heatmapMateial.js new file mode 100644 index 0000000000..31e1064c37 --- /dev/null +++ b/src/geom/material/heatmapMateial.js @@ -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; +} diff --git a/src/geom/shader/heatmap_colorize_frag.glsl b/src/geom/shader/heatmap_colorize_frag.glsl new file mode 100644 index 0000000000..e408ca3852 --- /dev/null +++ b/src/geom/shader/heatmap_colorize_frag.glsl @@ -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; +} \ No newline at end of file diff --git a/src/geom/shader/heatmap_colorize_vert.glsl b/src/geom/shader/heatmap_colorize_vert.glsl new file mode 100644 index 0000000000..fb7bcf8ff1 --- /dev/null +++ b/src/geom/shader/heatmap_colorize_vert.glsl @@ -0,0 +1,5 @@ +varying vec2 v_uv; +void main(){ + v_uv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); +} \ No newline at end of file diff --git a/src/geom/shader/heatmap_intensity_frag.glsl b/src/geom/shader/heatmap_intensity_frag.glsl new file mode 100644 index 0000000000..a70f8dd848 --- /dev/null +++ b/src/geom/shader/heatmap_intensity_frag.glsl @@ -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); +} diff --git a/src/geom/shader/heatmap_intensity_vert.glsl b/src/geom/shader/heatmap_intensity_vert.glsl new file mode 100644 index 0000000000..504355316b --- /dev/null +++ b/src/geom/shader/heatmap_intensity_vert.glsl @@ -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; +} \ No newline at end of file diff --git a/src/layer/heatmapLayer.js b/src/layer/heatmapLayer.js new file mode 100644 index 0000000000..420a99ba97 --- /dev/null +++ b/src/layer/heatmapLayer.js @@ -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 + }; + } + + +} diff --git a/src/layer/index.js b/src/layer/index.js index 5e8fef27c6..8f5c3622ef 100644 --- a/src/layer/index.js +++ b/src/layer/index.js @@ -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'; diff --git a/src/layer/lineLayer.js b/src/layer/lineLayer.js index 4d40755100..fd103aa549 100644 --- a/src/layer/lineLayer.js +++ b/src/layer/lineLayer.js @@ -1,4 +1,3 @@ - import Layer from '../core/layer'; import * as THREE from '../core/three'; import { LineBuffer } from '../geom/buffer/index'; diff --git a/src/layer/pointLayer.js b/src/layer/pointLayer.js index a21a80b4a7..5eabb8bfad 100644 --- a/src/layer/pointLayer.js +++ b/src/layer/pointLayer.js @@ -13,7 +13,6 @@ import * as PointBuffer from '../geom/buffer/point/index'; * shape 3d cube,column, sphere * shape Model ,自定义 * image - * */ export default class PointLayer extends Layer { diff --git a/src/layer/rasterLayer.js b/src/layer/rasterLayer.js index 7a68dc8530..0c5cd44089 100644 --- a/src/layer/rasterLayer.js +++ b/src/layer/rasterLayer.js @@ -37,7 +37,6 @@ export default class RasterLayer extends Layer { const rasterMesh = new THREE.Mesh(this.geometry, material); this.add(rasterMesh); return this; - } animateFunc() {