Merge pull request #9090 from jahow/add-webgl-points-example

Add texture & color to the WebGL points renderer
This commit is contained in:
Tim Schaub
2018-12-29 22:12:12 -07:00
committed by GitHub
6 changed files with 80578 additions and 31 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,17 @@
---
layout: example.html
title: Icon Sprites with WebGL
shortdesc: Rendering many icons with WebGL
docs: >
This example shows how to use `ol/renderer/webgl/PointsLayer` to render
a very large amount of sprites. The above map is based on a dataset from the National UFO Reporting Center: each
icon marks a UFO sighting according to its reported shape (disk, light, fireball...). The older the sighting, the redder
the icon.
A very simple sprite atlas is used in the form of a PNG file containing all icons on a grid. Then, the `texCoordCallback`
option of the `ol/renderer/webgl/PointsLayer` constructor is used to specify which sprite to use according to the sighting shape.
The dataset contains around 80k points and can be found here: https://www.kaggle.com/NUFORC/ufo-sightings
tags: "webgl, icon, sprite, point, ufo"
---
<div id="map" class="map"></div>

View File

@@ -0,0 +1,124 @@
import Map from '../src/ol/Map.js';
import View from '../src/ol/View.js';
import TileLayer from '../src/ol/layer/Tile.js';
import TileJSON from '../src/ol/source/TileJSON';
import Feature from '../src/ol/Feature';
import Point from '../src/ol/geom/Point';
import VectorLayer from '../src/ol/layer/Vector';
import {Vector} from '../src/ol/source';
import {fromLonLat} from '../src/ol/proj';
import WebGLPointsLayerRenderer from '../src/ol/renderer/webgl/PointsLayer';
import {lerp} from '../src/ol/math';
const features = [];
const vectorSource = new Vector({
features: [],
attributions: 'National UFO Reporting Center'
});
const texture = document.createElement('img');
texture.src = 'data/ufo_shapes.png';
// This describes the content of the associated sprite sheet
// coords are u0, v0, u1, v1 for a given shape
const shapeTextureCoords = {
'light': [0, 0.5, 0.25, 0],
'sphere': [0.25, 0.5, 0.5, 0],
'circle': [0.25, 0.5, 0.5, 0],
'disc': [0.5, 0.5, 0.75, 0],
'oval': [0.5, 0.5, 0.75, 0],
'triangle': [0.75, 0.5, 1, 0],
'fireball': [0, 1, 0.25, 0.5],
'default': [0.75, 1, 1, 0.5]
};
const oldColor = [255, 160, 110];
const newColor = [180, 255, 200];
class WebglPointsLayer extends VectorLayer {
createRenderer() {
return new WebGLPointsLayerRenderer(this, {
texture: texture,
colorCallback: function(feature, vertex, component) {
// component at index 3 is alpha
if (component === 3) {
return 1;
}
// color is interpolated based on year (min is 1910, max is 2013)
// please note: most values are between 2000-2013
const ratio = (feature.get('year') - 1950) / (2013 - 1950);
return lerp(oldColor[component], newColor[component], ratio * ratio) / 255;
},
texCoordCallback: function(feature, component) {
let coords = shapeTextureCoords[feature.get('shape')];
if (!coords) {
coords = shapeTextureCoords['default'];
}
return coords[component];
},
sizeCallback: function() {
return 16;
}
});
}
}
function loadData() {
const client = new XMLHttpRequest();
client.open('GET', 'data/csv/ufo_sighting_data.csv');
client.onload = function() {
const csv = client.responseText;
let curIndex;
let prevIndex = 0;
let line;
while ((curIndex = csv.indexOf('\n', prevIndex)) > 0) {
line = csv.substr(prevIndex, curIndex - prevIndex).split(',');
prevIndex = curIndex + 1;
// skip header
if (prevIndex === 0) {
continue;
}
const coords = fromLonLat([parseFloat(line[5]), parseFloat(line[4])]);
// only keep valid points
if (isNaN(coords[0]) || isNaN(coords[1])) {
continue;
}
features.push(new Feature({
datetime: line[0],
year: parseInt(/[0-9]{4}/.exec(line[0])[0]), // extract the year as int
shape: line[2],
duration: line[3],
geometry: new Point(coords)
}));
}
vectorSource.addFeatures(features);
};
client.send();
}
loadData();
new Map({
layers: [
new TileLayer({
source: new TileJSON({
url: 'https://api.tiles.mapbox.com/v3/mapbox.world-dark.json?secure',
crossOrigin: 'anonymous'
})
}),
new WebglPointsLayer({
source: vectorSource
})
],
target: document.getElementById('map'),
view: new View({
center: [0, 4000000],
zoom: 2
})
});

View File

@@ -14,6 +14,7 @@ const VERTEX_SHADER = `
attribute float a_rotateWithView;
attribute vec2 a_offsets;
attribute float a_opacity;
attribute vec4 a_color;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
@@ -21,6 +22,7 @@ const VERTEX_SHADER = `
varying vec2 v_texCoord;
varying float v_opacity;
varying vec4 v_color;
void main(void) {
mat4 offsetMatrix = u_offsetScaleMatrix;
@@ -31,21 +33,26 @@ const VERTEX_SHADER = `
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
v_texCoord = a_texCoord;
v_opacity = a_opacity;
v_color = a_color;
}`;
const FRAGMENT_SHADER = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
varying float v_opacity;
varying vec4 v_color;
void main(void) {
gl_FragColor.rgb = vec3(1.0, 1.0, 1.0);
float alpha = v_opacity;
if (alpha == 0.0) {
if (v_opacity == 0.0) {
discard;
}
gl_FragColor.a = alpha;
vec4 textureColor = texture2D(u_texture, v_texCoord);
gl_FragColor = v_color * textureColor;
gl_FragColor.a *= v_opacity;
gl_FragColor.rgb *= gl_FragColor.a;
}`;
/**
@@ -65,16 +72,25 @@ const FRAGMENT_SHADER = `
* source to compute the coordinate of the quad center on screen (in pixels). This is only done on source change.
* The second argument is 0 for `x` component and 1 for `y`.
* @property {function(import("../../Feature").default, number):number} [texCoordCallback] Will be called on every feature in the
* source to compute the texture coordinates of each corner of the quad. This is only done on source change.
* The second argument is 0 for `u0` component, 1 or `v0`, 2 for `u1`, and 3 for `v1`.
* source to compute the texture coordinates of each corner of the quad (without effect if no `texture` option defined). This is only done on source change.
* The second argument is 0 for `u0` component, 1 for `v0`, 2 for `u1`, and 3 for `v1`.
* @property {function(import("../../Feature").default, number, number):number} [colorCallback] Will be called on every feature in the
* source to compute the color of each corner of the quad. This is only done on source change.
* The second argument is 0 for bottom left, 1 for bottom right, 2 for top right and 3 for top left
* The third argument is 0 for red, 1 for green, 2 for blue and 3 for alpha
* The return value should be between 0 and 1.
* @property {function(import("../../Feature").default):number} [opacityCallback] Will be called on every feature in the
* source to compute the opacity of the quad on screen (from 0 to 1). This is only done on source change.
* Note: this is multiplied with the color of the point which can also have an alpha value < 1.
* @property {function(import("../../Feature").default):boolean} [rotateWithViewCallback] Will be called on every feature in the
* source to compute whether the quad on screen must stay upwards (`false`) or follow the view rotation (`true`).
* This is only done on source change.
* @property {HTMLCanvasElement|HTMLImageElement|ImageData} [texture] Texture to use on points. `texCoordCallback` and `sizeCallback`
* must be defined for this to have any effect.
* @property {string} [vertexShader] Vertex shader source
* @property {string} [fragmentShader] Fragment shader source
* @property {Object.<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions for the post process steps
* Please note that `u_texture` is reserved for the main texture slot.
* @property {Array<PostProcessesOptions>} [postProcesses] Post-processes definitions
*/
@@ -90,6 +106,25 @@ const FRAGMENT_SHADER = `
* * `vec2 a_offsets`
* * `float a_rotateWithView`
* * `float a_opacity`
* * `float a_color`
*
* The following uniform is used for the main texture: `u_texture`.
*
* Please note that the main shader output should have premultiplied alpha, otherwise the colors will be blended
* additively.
*
* Points are rendered as quads with the following structure:
*
* (u0, v1) (u1, v1)
* [3]----------[2]
* |` |
* | ` |
* | ` |
* | ` |
* | ` |
* | ` |
* [0]----------[1]
* (u0, v0) (u1, v0)
*
* This uses {@link module:ol/webgl/Helper~WebGLHelper} internally.
*
@@ -98,11 +133,13 @@ const FRAGMENT_SHADER = `
* * Vertex shader:
* ```
* precision mediump float;
*
* attribute vec2 a_position;
* attribute vec2 a_texCoord;
* attribute float a_rotateWithView;
* attribute vec2 a_offsets;
* attribute float a_opacity;
* attribute vec4 a_color;
*
* uniform mat4 u_projectionMatrix;
* uniform mat4 u_offsetScaleMatrix;
@@ -110,6 +147,7 @@ const FRAGMENT_SHADER = `
*
* varying vec2 v_texCoord;
* varying float v_opacity;
* varying vec4 v_color;
*
* void main(void) {
* mat4 offsetMatrix = u_offsetScaleMatrix;
@@ -120,6 +158,7 @@ const FRAGMENT_SHADER = `
* gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
* v_texCoord = a_texCoord;
* v_opacity = a_opacity;
* v_color = a_color;
* }
* ```
*
@@ -127,16 +166,20 @@ const FRAGMENT_SHADER = `
* ```
* precision mediump float;
*
* uniform sampler2D u_texture;
*
* varying vec2 v_texCoord;
* varying float v_opacity;
* varying vec4 v_color;
*
* void main(void) {
* gl_FragColor.rgb = vec3(1.0, 1.0, 1.0);
* float alpha = v_opacity;
* if (alpha == 0.0) {
* if (v_opacity == 0.0) {
* discard;
* }
* gl_FragColor.a = alpha;
* vec4 textureColor = texture2D(u_texture, v_texCoord);
* gl_FragColor = v_color * textureColor;
* gl_FragColor.a *= v_opacity;
* gl_FragColor.rgb *= gl_FragColor.a;
* }
* ```
*
@@ -153,9 +196,11 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
const options = opt_options || {};
const uniforms = options.uniforms || {};
uniforms.u_texture = options.texture || this.getDefaultTexture();
this.helper_ = new WebGLHelper({
postProcesses: options.postProcesses,
uniforms: options.uniforms
uniforms: uniforms
});
this.sourceRevision_ = -1;
@@ -183,6 +228,9 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
this.texCoordCallback_ = options.texCoordCallback || function(feature, index) {
return index < 2 ? 0 : 1;
};
this.colorCallback_ = options.colorCallback || function(feature, index, component) {
return 1;
};
this.rotateWithViewCallback_ = options.rotateWithViewCallback || function() {
return false;
};
@@ -218,6 +266,8 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
const vectorLayer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer());
const vectorSource = /** @type {import("../../source/Vector.js").default} */ (vectorLayer.getSource());
const stride = 12;
this.helper_.prepareDraw(frameState);
if (this.sourceRevision_ < vectorSource.getRevision()) {
@@ -242,14 +292,29 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
const size = this.sizeCallback_(feature);
const opacity = this.opacityCallback_(feature);
const rotateWithView = this.rotateWithViewCallback_(feature) ? 1 : 0;
const stride = 8;
const v0_r = this.colorCallback_(feature, 0, 0);
const v0_g = this.colorCallback_(feature, 0, 1);
const v0_b = this.colorCallback_(feature, 0, 2);
const v0_a = this.colorCallback_(feature, 0, 3);
const v1_r = this.colorCallback_(feature, 1, 0);
const v1_g = this.colorCallback_(feature, 1, 1);
const v1_b = this.colorCallback_(feature, 1, 2);
const v1_a = this.colorCallback_(feature, 1, 3);
const v2_r = this.colorCallback_(feature, 2, 0);
const v2_g = this.colorCallback_(feature, 2, 1);
const v2_b = this.colorCallback_(feature, 2, 2);
const v2_a = this.colorCallback_(feature, 2, 3);
const v3_r = this.colorCallback_(feature, 3, 0);
const v3_g = this.colorCallback_(feature, 3, 1);
const v3_b = this.colorCallback_(feature, 3, 2);
const v3_a = this.colorCallback_(feature, 3, 3);
const baseIndex = this.verticesBuffer_.getArray().length / stride;
this.verticesBuffer_.getArray().push(
x, y, -size / 2, -size / 2, u0, v0, opacity, rotateWithView,
x, y, +size / 2, -size / 2, u1, v0, opacity, rotateWithView,
x, y, +size / 2, +size / 2, u1, v1, opacity, rotateWithView,
x, y, -size / 2, +size / 2, u0, v1, opacity, rotateWithView
x, y, -size / 2, -size / 2, u0, v0, opacity, rotateWithView, v0_r, v0_g, v0_b, v0_a,
x, y, +size / 2, -size / 2, u1, v0, opacity, rotateWithView, v1_r, v1_g, v1_b, v1_a,
x, y, +size / 2, +size / 2, u1, v1, opacity, rotateWithView, v2_r, v2_g, v2_b, v2_a,
x, y, -size / 2, +size / 2, u0, v1, opacity, rotateWithView, v3_r, v3_g, v3_b, v3_a
);
this.indicesBuffer_.getArray().push(
baseIndex, baseIndex + 1, baseIndex + 3,
@@ -263,11 +328,12 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
this.helper_.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);
const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT;
this.helper_.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * 8, 0);
this.helper_.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * 8, bytesPerFloat * 2);
this.helper_.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * 8, bytesPerFloat * 4);
this.helper_.enableAttributeArray(DefaultAttrib.OPACITY, 1, FLOAT, bytesPerFloat * 8, bytesPerFloat * 6);
this.helper_.enableAttributeArray(DefaultAttrib.ROTATE_WITH_VIEW, 1, FLOAT, bytesPerFloat * 8, bytesPerFloat * 7);
this.helper_.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * stride, 0);
this.helper_.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 2);
this.helper_.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 4);
this.helper_.enableAttributeArray(DefaultAttrib.OPACITY, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 6);
this.helper_.enableAttributeArray(DefaultAttrib.ROTATE_WITH_VIEW, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 7);
this.helper_.enableAttributeArray(DefaultAttrib.COLOR, 4, FLOAT, bytesPerFloat * stride, bytesPerFloat * 8);
return true;
}
@@ -280,6 +346,18 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
getShaderCompileErrors() {
return this.helper_.getShaderCompileErrors();
}
/**
* Returns a texture of 1x1 pixel, white
* @private
* @return {ImageData} Image data.
*/
getDefaultTexture() {
const canvas = document.createElement('canvas');
const image = canvas.getContext('2d').createImageData(1, 1);
image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255;
return image;
}
}
export default WebGLPointsLayerRenderer;

View File

@@ -57,11 +57,12 @@ export const DefaultAttrib = {
TEX_COORD: 'a_texCoord',
OPACITY: 'a_opacity',
ROTATE_WITH_VIEW: 'a_rotateWithView',
OFFSETS: 'a_offsets'
OFFSETS: 'a_offsets',
COLOR: 'a_color'
};
/**
* @typedef {number|Array<number>|HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} UniformLiteralValue
* @typedef {number|Array<number>|HTMLCanvasElement|HTMLImageElement|ImageData} UniformLiteralValue
*/
/**
@@ -527,7 +528,7 @@ class WebGLHelper extends Disposable {
value = typeof uniform.value === 'function' ? uniform.value(frameState) : uniform.value;
// apply value based on type
if (value instanceof HTMLCanvasElement || value instanceof ImageData) {
if (value instanceof HTMLCanvasElement || value instanceof HTMLImageElement || value instanceof ImageData) {
// create a texture & put data
if (!uniform.texture) {
uniform.texture = gl.createTexture();
@@ -537,13 +538,7 @@ class WebGLHelper extends Disposable {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
if (value instanceof ImageData) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, value.width, value.height, 0,
gl.UNSIGNED_BYTE, new Uint8Array(value.data));
} else {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value);
}
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value);
// fill texture slots by increasing index
gl.uniform1i(this.getUniformLocation(uniform.name), textureSlot++);