Files
openlayers/test/spec/ol/renderer/webgl/pointslayer.test.js
Olivier Guyot 23c2999cab 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.
2019-09-25 12:11:09 +02:00

360 lines
12 KiB
JavaScript

import Feature from '../../../../../src/ol/Feature.js';
import Point from '../../../../../src/ol/geom/Point.js';
import VectorLayer from '../../../../../src/ol/layer/Vector.js';
import VectorSource from '../../../../../src/ol/source/Vector.js';
import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/PointsLayer.js';
import {get as getProjection} from '../../../../../src/ol/proj.js';
import ViewHint from '../../../../../src/ol/ViewHint.js';
import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
import {compose as composeTransform, create as createTransform} from '../../../../../src/ol/transform.js';
const baseFrameState = {
viewHints: [],
viewState: {
projection: getProjection('EPSG:3857'),
resolution: 1,
rotation: 0,
center: [0, 0]
},
layerStatesArray: [{}],
layerIndex: 0,
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('constructor', function() {
let target;
beforeEach(function() {
target = document.createElement('div');
target.style.width = '256px';
target.style.height = '256px';
document.body.appendChild(target);
});
afterEach(function() {
document.body.removeChild(target);
});
it('creates a new instance', function() {
const layer = new VectorLayer({
source: new VectorSource()
});
const renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader
});
expect(renderer).to.be.a(WebGLPointsLayerRenderer);
});
});
describe('#prepareFrame', function() {
let layer, renderer, frameState;
beforeEach(function() {
layer = new VectorLayer({
source: new VectorSource()
});
renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader
});
frameState = Object.assign({
size: [2, 2],
extent: [-100, -100, 100, 100]
}, baseFrameState);
});
it('calls WebGlHelper#prepareDraw', function() {
const spy = sinon.spy(renderer.helper, 'prepareDraw');
renderer.prepareFrame(frameState);
expect(spy.called).to.be(true);
});
it('fills up a 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 = 3;
renderer.worker_.addEventListener('message', function(event) {
if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
return;
}
expect(renderer.verticesBuffer_.getArray().length).to.eql(2 * 4 * attributePerVertex);
expect(renderer.indicesBuffer_.getArray().length).to.eql(2 * 6);
expect(renderer.verticesBuffer_.getArray()[0]).to.eql(10);
expect(renderer.verticesBuffer_.getArray()[1]).to.eql(20);
expect(renderer.verticesBuffer_.getArray()[4 * attributePerVertex + 0]).to.eql(30);
expect(renderer.verticesBuffer_.getArray()[4 * attributePerVertex + 1]).to.eql(40);
done();
});
});
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) {
const source = layer.getSource();
source.addFeature(new Feature({
geometry: new Point([10, 20])
}));
source.removeFeature(source.getFeatures()[0]);
source.addFeature(new Feature({
geometry: new Point([10, 20])
}));
renderer.prepareFrame(frameState);
renderer.worker_.addEventListener('message', function(event) {
if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
return;
}
const attributePerVertex = 3;
expect(renderer.verticesBuffer_.getArray().length).to.eql(4 * attributePerVertex);
expect(renderer.indicesBuffer_.getArray().length).to.eql(6);
done();
});
});
it('rebuilds the buffers only when not interacting or animating', function() {
const spy = sinon.spy(renderer, 'rebuildBuffers_');
frameState.viewHints[ViewHint.INTERACTING] = 1;
frameState.viewHints[ViewHint.ANIMATING] = 0;
renderer.prepareFrame(frameState);
expect(spy.called).to.be(false);
frameState.viewHints[ViewHint.INTERACTING] = 0;
frameState.viewHints[ViewHint.ANIMATING] = 1;
renderer.prepareFrame(frameState);
expect(spy.called).to.be(false);
frameState.viewHints[ViewHint.INTERACTING] = 0;
frameState.viewHints[ViewHint.ANIMATING] = 0;
renderer.prepareFrame(frameState);
expect(spy.called).to.be(true);
});
it('rebuilds the buffers only when the frame extent changed', function() {
const spy = sinon.spy(renderer, 'rebuildBuffers_');
renderer.prepareFrame(frameState);
expect(spy.callCount).to.be(1);
renderer.prepareFrame(frameState);
expect(spy.callCount).to.be(1);
frameState.extent = [10, 20, 30, 40];
renderer.prepareFrame(frameState);
expect(spy.callCount).to.be(2);
});
});
describe('#forEachFeatureAtCoordinate', function() {
let layer, renderer, feature, feature2;
beforeEach(function() {
feature = new Feature({geometry: new Point([0, 0]), id: 1});
feature2 = new Feature({geometry: new Point([14, 14]), id: 2});
layer = new VectorLayer({
source: new VectorSource({
features: [feature, feature2]
})
});
renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader,
hitVertexShader: hitVertexShader,
hitFragmentShader: hitFragmentShader
});
});
it('correctly hit detects a feature', function(done) {
const transform = composeTransform(createTransform(), 20, 20, 1, -1, 0, 0, 0);
const frameState = Object.assign({
extent: [-20, -20, 20, 20],
size: [40, 40],
coordinateToPixelTransform: transform
}, baseFrameState);
let found;
const cb = function(feature) {
found = feature;
};
renderer.prepareFrame(frameState);
renderer.worker_.addEventListener('message', function() {
if (!renderer.hitRenderInstructions_) {
return;
}
renderer.prepareFrame(frameState);
renderer.renderFrame(frameState);
function checkHit(x, y, expected) {
found = null;
renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null);
expect(found).to.be(expected);
}
checkHit(0, 0, feature);
checkHit(1, -1, feature);
checkHit(-2, 2, feature);
checkHit(2, 0, null);
checkHit(1, -3, null);
checkHit(14, 14, feature2);
checkHit(15, 13, feature2);
checkHit(12, 16, feature2);
checkHit(16, 14, null);
checkHit(13, 11, null);
done();
});
});
it('correctly hit detects with pixelratio != 1', function(done) {
const transform = composeTransform(createTransform(), 20, 20, 1, -1, 0, 0, 0);
const frameState = Object.assign({
pixelRatio: 3,
extent: [-20, -20, 20, 20],
size: [40, 40],
coordinateToPixelTransform: transform
}, baseFrameState);
let found;
const cb = function(feature) {
found = feature;
};
renderer.prepareFrame(frameState);
renderer.worker_.addEventListener('message', function() {
if (!renderer.hitRenderInstructions_) {
return;
}
renderer.prepareFrame(frameState);
renderer.renderFrame(frameState);
function checkHit(x, y, expected) {
found = null;
renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null);
expect(found).to.be(expected);
}
checkHit(0, 0, feature);
checkHit(1, -1, feature);
checkHit(-2, 2, feature);
checkHit(2, 0, null);
checkHit(1, -3, null);
checkHit(14, 14, feature2);
checkHit(15, 13, feature2);
checkHit(12, 16, feature2);
checkHit(16, 14, null);
checkHit(13, 11, null);
done();
});
});
});
describe('#disposeInternal', function() {
it('terminates the worker and calls dispose on the helper', function() {
const layer = new VectorLayer({
source: new VectorSource()
});
const renderer = new WebGLPointsLayerRenderer(layer, {
vertexShader: simpleVertexShader,
fragmentShader: simpleFragmentShader
});
const spyHelper = sinon.spy(renderer.helper, 'disposeInternal');
const spyWorker = sinon.spy(renderer.worker_, 'terminate');
renderer.disposeInternal();
expect(spyHelper.called).to.be(true);
expect(spyWorker.called).to.be(true);
});
});
});