Webgl points renderer / fix hit detection

Due to the fact that the points renderer does not know for sure
which attributes will be used for rendering, it is now mandatory
to provide both vertex/fragment shaders for rendering AND hit
detection.

The hit detection shaders should expect having an `a_hitColor`
that they should return to allow matching the feature uid.

This will be all one eventually by shader builders under the hood.
This commit is contained in:
Olivier Guyot
2019-09-25 12:11:06 +02:00
parent 109f9718f9
commit 23c2999cab
4 changed files with 140 additions and 86 deletions
+32
View File
@@ -79,6 +79,7 @@ class WebglPointsLayer extends VectorLayer {
'varying float v_year;', 'varying float v_year;',
'void main(void) {', 'void main(void) {',
// color is interpolated based on year // color is interpolated based on year
' float ratio = clamp((v_year - 1950.0) / (2013.0 - 1950.0), 0.0, 1.1);', ' float ratio = clamp((v_year - 1950.0) / (2013.0 - 1950.0), 0.0, 1.1);',
' vec3 color = mix(vec3(' + formatColor(oldColor) + '),', ' vec3 color = mix(vec3(' + formatColor(oldColor) + '),',
@@ -88,6 +89,37 @@ class WebglPointsLayer extends VectorLayer {
' gl_FragColor.rgb *= gl_FragColor.a;', ' gl_FragColor.rgb *= gl_FragColor.a;',
'}' '}'
].join(' '), ].join(' '),
hitVertexShader: [
'precision mediump float;',
'uniform mat4 u_projectionMatrix;',
'uniform mat4 u_offsetScaleMatrix;',
'uniform mat4 u_offsetRotateMatrix;',
'attribute vec2 a_position;',
'attribute float a_index;',
'attribute vec4 a_hitColor;',
'varying vec4 v_hitColor;',
'void main(void) {',
' mat4 offsetMatrix = u_offsetScaleMatrix;',
' float offsetX = a_index == 0.0 || a_index == 3.0 ? ',
' ' + formatNumber(-size / 2) + ' : ' + formatNumber(size / 2) + ';',
' float offsetY = a_index == 0.0 || a_index == 1.0 ? ',
' ' + formatNumber(-size / 2) + ' : ' + formatNumber(size / 2) + ';',
' vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);',
' gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;',
' v_hitColor = a_hitColor;',
'}'
].join(' '),
hitFragmentShader: [
'precision mediump float;',
'varying vec4 v_hitColor;',
'void main(void) {',
' gl_FragColor = v_hitColor;',
'}'
].join(' '),
texture: texture // FIXME: this doesn't work yet texture: texture // FIXME: this doesn't work yet
}); });
} }
+16 -79
View File
@@ -18,74 +18,6 @@ import {create as createWebGLWorker} from '../../worker/webgl.js';
import {getUid} from '../../util.js'; import {getUid} from '../../util.js';
import WebGLRenderTarget from '../../webgl/RenderTarget.js'; import WebGLRenderTarget from '../../webgl/RenderTarget.js';
const 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;
uniform mat4 u_offsetRotateMatrix;
varying vec2 v_texCoord;
varying float v_opacity;
varying vec4 v_color;
void main(void) {
mat4 offsetMatrix = u_offsetScaleMatrix;
if (a_rotateWithView == 1.0) {
offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;
}
vec4 offsets = offsetMatrix * vec4(a_offsets, 0.0, 0.0);
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) {
if (v_opacity == 0.0) {
discard;
}
vec4 textureColor = texture2D(u_texture, v_texCoord);
gl_FragColor = v_color * textureColor;
gl_FragColor.a *= v_opacity;
gl_FragColor.rgb *= gl_FragColor.a;
}`;
const HIT_FRAGMENT_SHADER = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
varying float v_opacity;
varying vec4 v_color;
void main(void) {
if (v_opacity == 0.0) {
discard;
}
vec4 textureColor = texture2D(u_texture, v_texCoord);
if (textureColor.a < 0.1) {
discard;
}
gl_FragColor = v_color;
}`;
/** /**
* @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different
* for each feature. * for each feature.
@@ -101,8 +33,10 @@ const HIT_FRAGMENT_SHADER = `
* * In the vertex shader as an `attribute` by prefixing it with `a_` * * In the vertex shader as an `attribute` by prefixing it with `a_`
* * In the fragment shader as a `varying` by prefixing it with `v_` * * In the fragment shader as a `varying` by prefixing it with `v_`
* Please note that these can only be numerical values. * Please note that these can only be numerical values.
* @property {string} [vertexShader] Vertex shader source * @property {string} vertexShader Vertex shader source, mandatory.
* @property {string} [fragmentShader] Fragment shader source * @property {string} fragmentShader Fragment shader source, mandatory.
* @property {string} [hitVertexShader] Vertex shader source for hit detection rendering.
* @property {string} [hitFragmentShader] Fragment shader source for hit detection rendering.
* @property {Object.<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions for the post process steps * @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. * Please note that `u_texture` is reserved for the main texture slot.
* @property {Array<import("./Layer").PostProcessesOptions>} [postProcesses] Post-processes definitions * @property {Array<import("./Layer").PostProcessesOptions>} [postProcesses] Post-processes definitions
@@ -204,11 +138,9 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
/** /**
* @param {import("../../layer/Vector.js").default} vectorLayer Vector layer. * @param {import("../../layer/Vector.js").default} vectorLayer Vector layer.
* @param {Options=} [opt_options] Options. * @param {Options=} options Options.
*/ */
constructor(vectorLayer, opt_options) { constructor(vectorLayer, options) {
const options = opt_options || {};
const uniforms = options.uniforms || {}; const uniforms = options.uniforms || {};
uniforms.u_texture = getBlankImageData(); uniforms.u_texture = getBlankImageData();
const projectionMatrixTransform = createTransform(); const projectionMatrixTransform = createTransform();
@@ -226,12 +158,12 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW); this.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW);
this.program_ = this.helper.getProgram( this.program_ = this.helper.getProgram(
options.fragmentShader || FRAGMENT_SHADER, options.fragmentShader,
options.vertexShader || VERTEX_SHADER options.vertexShader
); );
this.hitProgram_ = this.helper.getProgram( this.hitProgram_ = this.helper.getProgram(
HIT_FRAGMENT_SHADER, options.hitFragmentShader,
VERTEX_SHADER options.hitVertexShader
); );
const customAttributes = options.attributes ? const customAttributes = options.attributes ?
@@ -271,7 +203,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
size: 1, size: 1,
type: AttributeType.FLOAT type: AttributeType.FLOAT
}, { }, {
name: 'a_color', name: 'a_hitColor',
size: 4, size: 4,
type: AttributeType.FLOAT type: AttributeType.FLOAT
}, { }, {
@@ -429,6 +361,11 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.helper.makeProjectionTransform(frameState, projectionTransform); this.helper.makeProjectionTransform(frameState, projectionTransform);
const features = vectorSource.getFeatures(); const features = vectorSource.getFeatures();
// here we anticipate the amount of render instructions that we well generate
// this can be done since we know that for normal render we only have x, y as base instructions,
// and x, y, r, g, b, a and featureUid for hit render instructions
// and we also know the amount of custom attributes to append to these
const totalInstructionsCount = (2 + this.customAttributes.length) * features.length; const totalInstructionsCount = (2 + this.customAttributes.length) * features.length;
if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) { if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) {
this.renderInstructions_ = new Float32Array(totalInstructionsCount); this.renderInstructions_ = new Float32Array(totalInstructionsCount);
+1 -1
View File
@@ -14,7 +14,7 @@ const worker = self;
worker.onmessage = event => { worker.onmessage = event => {
const received = event.data; const received = event.data;
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) { if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
// This is specific to point features // This is specific to point features (x, y, index)
const baseVertexAttrsCount = 3; const baseVertexAttrsCount = 3;
const baseInstructionsCount = 2; const baseInstructionsCount = 2;
@@ -7,7 +7,6 @@ import {get as getProjection} from '../../../../../src/ol/proj.js';
import ViewHint from '../../../../../src/ol/ViewHint.js'; import ViewHint from '../../../../../src/ol/ViewHint.js';
import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js'; import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
import {compose as composeTransform, create as createTransform} from '../../../../../src/ol/transform.js'; import {compose as composeTransform, create as createTransform} from '../../../../../src/ol/transform.js';
import {getSymbolVertexShader} from '../../../../../src/ol/webgl/ShaderBuilder.js';
const baseFrameState = { const baseFrameState = {
viewHints: [], viewHints: [],
@@ -22,6 +21,54 @@ const baseFrameState = {
pixelRatio: 1 pixelRatio: 1
}; };
const simpleVertexShader = `
precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
attribute vec2 a_position;
attribute float a_index;
void main(void) {
mat4 offsetMatrix = u_offsetScaleMatrix;
float offsetX = a_index == 0.0 || a_index == 3.0 ? -2.0 : 2.0;
float offsetY = a_index == 0.0 || a_index == 1.0 ? -2.0 : 2.0;
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
}`;
const simpleFragmentShader = `
precision mediump float;
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}`;
// these shaders support hit detection
// they have a built-in size value of 4
const hitVertexShader = `
precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
attribute vec2 a_position;
attribute float a_index;
attribute vec4 a_hitColor;
varying vec4 v_hitColor;
void main(void) {
mat4 offsetMatrix = u_offsetScaleMatrix;
float offsetX = a_index == 0.0 || a_index == 3.0 ? -2.0 : 2.0;
float offsetY = a_index == 0.0 || a_index == 1.0 ? -2.0 : 2.0;
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
v_hitColor = a_hitColor;
}`;
const hitFragmentShader = `
precision mediump float;
varying vec4 v_hitColor;
void main(void) {
gl_FragColor = v_hitColor;
}`;
describe('ol.renderer.webgl.PointsLayer', function() { describe('ol.renderer.webgl.PointsLayer', function() {
describe('constructor', function() { describe('constructor', function() {
@@ -43,7 +90,10 @@ describe('ol.renderer.webgl.PointsLayer', function() {
const layer = new VectorLayer({ const layer = new VectorLayer({
source: new VectorSource() source: new VectorSource()
}); });
const renderer = new WebGLPointsLayerRenderer(layer); const renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader
});
expect(renderer).to.be.a(WebGLPointsLayerRenderer); expect(renderer).to.be.a(WebGLPointsLayerRenderer);
}); });
@@ -56,7 +106,10 @@ describe('ol.renderer.webgl.PointsLayer', function() {
layer = new VectorLayer({ layer = new VectorLayer({
source: new VectorSource() source: new VectorSource()
}); });
renderer = new WebGLPointsLayerRenderer(layer); renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader
});
frameState = Object.assign({ frameState = Object.assign({
size: [2, 2], size: [2, 2],
extent: [-100, -100, 100, 100] extent: [-100, -100, 100, 100]
@@ -95,6 +148,35 @@ describe('ol.renderer.webgl.PointsLayer', function() {
}); });
}); });
it('fills up the hit render buffer with 2 triangles per point', function(done) {
layer.getSource().addFeature(new Feature({
geometry: new Point([10, 20])
}));
layer.getSource().addFeature(new Feature({
geometry: new Point([30, 40])
}));
renderer.prepareFrame(frameState);
const attributePerVertex = 8;
renderer.worker_.addEventListener('message', function(event) {
if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
return;
}
if (!renderer.hitVerticesBuffer_.getArray()) {
return;
}
expect(renderer.hitVerticesBuffer_.getArray().length).to.eql(2 * 4 * attributePerVertex);
expect(renderer.indicesBuffer_.getArray().length).to.eql(2 * 6);
expect(renderer.hitVerticesBuffer_.getArray()[0]).to.eql(10);
expect(renderer.hitVerticesBuffer_.getArray()[1]).to.eql(20);
expect(renderer.hitVerticesBuffer_.getArray()[4 * attributePerVertex + 0]).to.eql(30);
expect(renderer.hitVerticesBuffer_.getArray()[4 * attributePerVertex + 1]).to.eql(40);
done();
});
});
it('clears the buffers when the features are gone', function(done) { it('clears the buffers when the features are gone', function(done) {
const source = layer.getSource(); const source = layer.getSource();
source.addFeature(new Feature({ source.addFeature(new Feature({
@@ -163,9 +245,10 @@ describe('ol.renderer.webgl.PointsLayer', function() {
}) })
}); });
renderer = new WebGLPointsLayerRenderer(layer, { renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: getSymbolVertexShader({ vertexShader: simpleVertexShader,
size: 4 fragmentShader: simpleFragmentShader,
}) hitVertexShader: hitVertexShader,
hitFragmentShader: hitFragmentShader
}); });
}); });
@@ -261,6 +344,8 @@ describe('ol.renderer.webgl.PointsLayer', function() {
source: new VectorSource() source: new VectorSource()
}); });
const renderer = new WebGLPointsLayerRenderer(layer, { const renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader
}); });
const spyHelper = sinon.spy(renderer.helper, 'disposeInternal'); const spyHelper = sinon.spy(renderer.helper, 'disposeInternal');