Merge pull request #9655 from jahow/webgl-interaction
Add hit detection on the WebGL points renderer
This commit is contained in:
@@ -18,3 +18,4 @@ cloak:
|
||||
value: Your Mapbox access token from https://mapbox.com/ here
|
||||
---
|
||||
<div id="map" class="map"></div>
|
||||
<div>Current sighting: <span id="info"></span></div>
|
||||
|
||||
@@ -103,7 +103,7 @@ function loadData() {
|
||||
|
||||
loadData();
|
||||
|
||||
new Map({
|
||||
const map = new Map({
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new TileJSON({
|
||||
@@ -121,3 +121,18 @@ new Map({
|
||||
zoom: 2
|
||||
})
|
||||
});
|
||||
|
||||
const info = document.getElementById('info');
|
||||
map.on('pointermove', function(evt) {
|
||||
if (map.getView().getInteracting()) {
|
||||
return;
|
||||
}
|
||||
const pixel = evt.pixel;
|
||||
info.innerText = '';
|
||||
map.forEachFeatureAtPixel(pixel, function(feature) {
|
||||
const datetime = feature.get('datetime');
|
||||
const duration = feature.get('duration');
|
||||
const shape = feature.get('shape');
|
||||
info.innerText = 'On ' + datetime + ', lasted ' + duration + ' seconds and had a "' + shape + '" shape.';
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,15 +132,6 @@ class LayerRenderer extends Observable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
|
||||
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @return {boolean} Is there a feature at the given coordinate?
|
||||
*/
|
||||
hasFeatureAtCoordinate(coordinate, frameState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the image if not already loaded, and register the image change
|
||||
* listener if needed.
|
||||
|
||||
@@ -81,6 +81,7 @@ class WebGLLayerRenderer extends LayerRenderer {
|
||||
getShaderCompileErrors() {
|
||||
return this.helper.getShaderCompileErrors();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -231,11 +232,47 @@ export function writePointFeatureToBuffers(instructions, elementIndex, vertexBuf
|
||||
* @private
|
||||
* @return {ImageData} Image data.
|
||||
*/
|
||||
export function getBlankTexture() {
|
||||
export function getBlankImageData() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a color array based on a numerical id
|
||||
* Note: the range for each component is 0 to 1 with 256 steps
|
||||
* @param {number} id Id
|
||||
* @param {Array<number>} [opt_array] Reusable array
|
||||
* @return {Array<number>} Color array containing the encoded id
|
||||
*/
|
||||
export function colorEncodeId(id, opt_array) {
|
||||
const array = opt_array || [];
|
||||
const radix = 256;
|
||||
const divide = radix - 1;
|
||||
array[0] = Math.floor(id / radix / radix / radix) / divide;
|
||||
array[1] = (Math.floor(id / radix / radix) % radix) / divide;
|
||||
array[2] = (Math.floor(id / radix) % radix) / divide;
|
||||
array[3] = (id % radix) / divide;
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads an id from a color-encoded array
|
||||
* Note: the expected range for each component is 0 to 1 with 256 steps.
|
||||
* @param {Array<number>} color Color array containing the encoded id
|
||||
* @return {number} Decoded id
|
||||
*/
|
||||
export function colorDecodeId(color) {
|
||||
let id = 0;
|
||||
const radix = 256;
|
||||
const mult = radix - 1;
|
||||
id += Math.round(color[0] * radix * radix * radix * mult);
|
||||
id += Math.round(color[1] * radix * radix * mult);
|
||||
id += Math.round(color[2] * radix * mult);
|
||||
id += Math.round(color[3] * mult);
|
||||
return id;
|
||||
}
|
||||
|
||||
export default WebGLLayerRenderer;
|
||||
|
||||
@@ -6,7 +6,9 @@ import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../web
|
||||
import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js';
|
||||
import GeometryType from '../../geom/GeometryType.js';
|
||||
import WebGLLayerRenderer, {
|
||||
getBlankTexture,
|
||||
colorDecodeId,
|
||||
colorEncodeId,
|
||||
getBlankImageData,
|
||||
POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType,
|
||||
writePointFeatureInstructions
|
||||
} from './Layer.js';
|
||||
@@ -19,6 +21,8 @@ import {
|
||||
apply as applyTransform
|
||||
} from '../../transform.js';
|
||||
import {create as createWebGLWorker} from '../../worker/webgl.js';
|
||||
import {getUid} from '../../util.js';
|
||||
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
|
||||
|
||||
const VERTEX_SHADER = `
|
||||
precision mediump float;
|
||||
@@ -68,6 +72,26 @@ const FRAGMENT_SHADER = `
|
||||
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} Options
|
||||
* @property {function(import("../../Feature").default):number} [sizeCallback] Will be called on every feature in the
|
||||
@@ -200,7 +224,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
const options = opt_options || {};
|
||||
|
||||
const uniforms = options.uniforms || {};
|
||||
uniforms.u_texture = options.texture || getBlankTexture();
|
||||
uniforms.u_texture = options.texture || getBlankImageData();
|
||||
const projectionMatrixTransform = createTransform();
|
||||
uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform;
|
||||
|
||||
@@ -212,14 +236,17 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
this.sourceRevision_ = -1;
|
||||
|
||||
this.verticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
|
||||
this.hitVerticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
|
||||
this.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW);
|
||||
|
||||
this.program_ = this.helper.getProgram(
|
||||
options.fragmentShader || FRAGMENT_SHADER,
|
||||
options.vertexShader || VERTEX_SHADER
|
||||
);
|
||||
|
||||
this.helper.useProgram(this.program_);
|
||||
this.hitProgram_ = this.helper.getProgram(
|
||||
HIT_FRAGMENT_SHADER,
|
||||
options.vertexShader || VERTEX_SHADER
|
||||
);
|
||||
|
||||
this.sizeCallback_ = options.sizeCallback || function() {
|
||||
return 1;
|
||||
@@ -274,21 +301,55 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
*/
|
||||
this.renderInstructions_ = new Float32Array(0);
|
||||
|
||||
/**
|
||||
* @type {import("../../transform.js").Transform}
|
||||
* @private
|
||||
*/
|
||||
this.hitRenderTransform_ = createTransform();
|
||||
|
||||
/**
|
||||
* @type {import("../../transform.js").Transform}
|
||||
* @private
|
||||
*/
|
||||
this.invertHitRenderTransform_ = createTransform();
|
||||
|
||||
/**
|
||||
* These instructions are used for hit detection
|
||||
* @type {Float32Array}
|
||||
* @private
|
||||
*/
|
||||
this.hitRenderInstructions_ = new Float32Array(0);
|
||||
|
||||
/**
|
||||
* @type {WebGLRenderTarget}
|
||||
* @private
|
||||
*/
|
||||
this.hitRenderTarget_ = new WebGLRenderTarget(this.helper);
|
||||
|
||||
this.worker_ = createWebGLWorker();
|
||||
this.worker_.addEventListener('message', function(event) {
|
||||
const received = event.data;
|
||||
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
|
||||
const projectionTransform = received.projectionTransform;
|
||||
this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer);
|
||||
if (received.hitDetection) {
|
||||
this.hitVerticesBuffer_.fromArrayBuffer(received.vertexBuffer);
|
||||
this.helper.flushBufferData(this.hitVerticesBuffer_);
|
||||
} else {
|
||||
this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer);
|
||||
this.helper.flushBufferData(this.verticesBuffer_);
|
||||
}
|
||||
this.indicesBuffer_.fromArrayBuffer(received.indexBuffer);
|
||||
this.helper.flushBufferData(this.verticesBuffer_);
|
||||
this.helper.flushBufferData(this.indicesBuffer_);
|
||||
|
||||
// saves the projection transform for the current frame state
|
||||
this.renderTransform_ = projectionTransform;
|
||||
makeInverseTransform(this.invertRenderTransform_, this.renderTransform_);
|
||||
|
||||
this.renderInstructions_ = new Float32Array(event.data.renderInstructions);
|
||||
if (received.hitDetection) {
|
||||
this.hitRenderTransform_ = projectionTransform;
|
||||
makeInverseTransform(this.invertHitRenderTransform_, this.hitRenderTransform_);
|
||||
this.hitRenderInstructions_ = new Float32Array(event.data.renderInstructions);
|
||||
} else {
|
||||
this.renderTransform_ = projectionTransform;
|
||||
makeInverseTransform(this.invertRenderTransform_, this.renderTransform_);
|
||||
this.renderInstructions_ = new Float32Array(event.data.renderInstructions);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
@@ -315,6 +376,9 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
canvas.style.opacity = opacity;
|
||||
}
|
||||
|
||||
this.renderHitDetection(frameState);
|
||||
this.hitRenderTarget_.clearCachedData();
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -349,6 +413,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
|
||||
multiplyTransform(this.currentTransform_, this.invertRenderTransform_);
|
||||
|
||||
this.helper.useProgram(this.program_);
|
||||
this.helper.prepareDraw(frameState);
|
||||
|
||||
// write new data
|
||||
@@ -384,11 +449,16 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) {
|
||||
this.renderInstructions_ = new Float32Array(totalInstructionsCount);
|
||||
}
|
||||
if (!this.hitRenderInstructions_ || this.hitRenderInstructions_.length !== totalInstructionsCount) {
|
||||
this.hitRenderInstructions_ = new Float32Array(totalInstructionsCount);
|
||||
}
|
||||
|
||||
// loop on features to fill the buffer
|
||||
let feature;
|
||||
const tmpCoords = [];
|
||||
const tmpColor = [];
|
||||
let elementIndex = 0;
|
||||
let u0, v0, u1, v1, size, opacity, rotateWithView, color;
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
feature = features[i];
|
||||
if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) {
|
||||
@@ -399,19 +469,45 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
tmpCoords[1] = this.coordCallback_(feature, 1);
|
||||
applyTransform(projectionTransform, tmpCoords);
|
||||
|
||||
elementIndex = writePointFeatureInstructions(
|
||||
u0 = this.texCoordCallback_(feature, 0);
|
||||
v0 = this.texCoordCallback_(feature, 1);
|
||||
u1 = this.texCoordCallback_(feature, 2);
|
||||
v1 = this.texCoordCallback_(feature, 3);
|
||||
size = this.sizeCallback_(feature);
|
||||
opacity = this.opacityCallback_(feature);
|
||||
rotateWithView = this.rotateWithViewCallback_(feature);
|
||||
color = this.colorCallback_(feature, this.colorArray_);
|
||||
|
||||
writePointFeatureInstructions(
|
||||
this.renderInstructions_,
|
||||
elementIndex,
|
||||
tmpCoords[0],
|
||||
tmpCoords[1],
|
||||
this.texCoordCallback_(feature, 0),
|
||||
this.texCoordCallback_(feature, 1),
|
||||
this.texCoordCallback_(feature, 2),
|
||||
this.texCoordCallback_(feature, 3),
|
||||
this.sizeCallback_(feature),
|
||||
this.opacityCallback_(feature),
|
||||
this.rotateWithViewCallback_(feature),
|
||||
this.colorCallback_(feature, this.colorArray_)
|
||||
u0,
|
||||
v0,
|
||||
u1,
|
||||
v1,
|
||||
size,
|
||||
opacity,
|
||||
rotateWithView,
|
||||
color
|
||||
);
|
||||
|
||||
// for hit detection, the feature uid is saved in the opacity value
|
||||
// and the index of the opacity value is encoded in the color values
|
||||
elementIndex = writePointFeatureInstructions(
|
||||
this.hitRenderInstructions_,
|
||||
elementIndex,
|
||||
tmpCoords[0],
|
||||
tmpCoords[1],
|
||||
u0,
|
||||
v0,
|
||||
u1,
|
||||
v1,
|
||||
size,
|
||||
opacity > 0 ? Number(getUid(feature)) : 0,
|
||||
rotateWithView,
|
||||
colorEncodeId(elementIndex + 7, tmpColor)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -422,12 +518,80 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
};
|
||||
// additional properties will be sent back as-is by the worker
|
||||
message['projectionTransform'] = projectionTransform;
|
||||
|
||||
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
|
||||
this.renderInstructions_ = null;
|
||||
|
||||
/** @type import('./Layer').WebGLWorkerGenerateBuffersMessage */
|
||||
const hitMessage = {
|
||||
type: WebGLWorkerMessageType.GENERATE_BUFFERS,
|
||||
renderInstructions: this.hitRenderInstructions_.buffer
|
||||
};
|
||||
hitMessage['projectionTransform'] = projectionTransform;
|
||||
hitMessage['hitDetection'] = true;
|
||||
this.worker_.postMessage(hitMessage, [this.hitRenderInstructions_.buffer]);
|
||||
this.hitRenderInstructions_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) {
|
||||
if (!this.hitRenderInstructions_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice());
|
||||
|
||||
const data = this.hitRenderTarget_.readPixel(pixel[0], pixel[1]);
|
||||
const color = [
|
||||
data[0] / 255,
|
||||
data[1] / 255,
|
||||
data[2] / 255,
|
||||
data[3] / 255
|
||||
];
|
||||
const index = colorDecodeId(color);
|
||||
const opacity = this.hitRenderInstructions_[index];
|
||||
const uid = Math.floor(opacity).toString();
|
||||
|
||||
const source = this.getLayer().getSource();
|
||||
const feature = source.getFeatureByUid(uid);
|
||||
if (feature) {
|
||||
return callback(feature, this.getLayer());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the hit detection data to the corresponding render target
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState current frame state
|
||||
*/
|
||||
renderHitDetection(frameState) {
|
||||
const width = frameState.size[0];
|
||||
const height = frameState.size[1];
|
||||
const size = [width, height];
|
||||
|
||||
this.hitRenderTarget_.setSize(size);
|
||||
|
||||
this.helper.useProgram(this.hitProgram_);
|
||||
this.helper.prepareDrawToRenderTarget(frameState, this.hitRenderTarget_, true);
|
||||
|
||||
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
|
||||
multiplyTransform(this.currentTransform_, this.invertHitRenderTransform_);
|
||||
|
||||
this.helper.bindBuffer(this.hitVerticesBuffer_);
|
||||
this.helper.bindBuffer(this.indicesBuffer_);
|
||||
|
||||
const stride = POINT_VERTEX_STRIDE;
|
||||
const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT;
|
||||
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);
|
||||
|
||||
const renderCount = this.indicesBuffer_.getArray() ? this.indicesBuffer_.getArray().length : 0;
|
||||
this.helper.drawElements(0, renderCount);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebGLPointsLayerRenderer;
|
||||
|
||||
@@ -241,11 +241,11 @@ class VectorSource extends Source {
|
||||
this.idIndex_ = {};
|
||||
|
||||
/**
|
||||
* A lookup of features without id (keyed by getUid(feature)).
|
||||
* A lookup of features by uid (using getUid(feature)).
|
||||
* @private
|
||||
* @type {!Object<string, import("../Feature.js").default<Geometry>>}
|
||||
*/
|
||||
this.undefIdIndex_ = {};
|
||||
this.uidIndex_ = {};
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -359,10 +359,11 @@ class VectorSource extends Source {
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
} else {
|
||||
assert(!(featureKey in this.undefIdIndex_),
|
||||
}
|
||||
if (valid) {
|
||||
assert(!(featureKey in this.uidIndex_),
|
||||
30); // The passed `feature` was already added to the source
|
||||
this.undefIdIndex_[featureKey] = feature;
|
||||
this.uidIndex_[featureKey] = feature;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
@@ -489,7 +490,7 @@ class VectorSource extends Source {
|
||||
if (!this.featuresCollection_) {
|
||||
this.featureChangeKeys_ = {};
|
||||
this.idIndex_ = {};
|
||||
this.undefIdIndex_ = {};
|
||||
this.uidIndex_ = {};
|
||||
}
|
||||
} else {
|
||||
if (this.featuresRtree_) {
|
||||
@@ -770,6 +771,18 @@ class VectorSource extends Source {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a feature by its internal unique identifier (using `getUid`).
|
||||
*
|
||||
* @param {string} uid Feature identifier.
|
||||
* @return {import("../Feature.js").default<Geometry>} The feature (or `null` if not found).
|
||||
*/
|
||||
getFeatureByUid(uid) {
|
||||
const feature = this.uidIndex_[uid];
|
||||
return feature !== undefined ? feature : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the format associated with this source.
|
||||
*
|
||||
@@ -831,20 +844,13 @@ class VectorSource extends Source {
|
||||
const id = feature.getId();
|
||||
if (id !== undefined) {
|
||||
const sid = id.toString();
|
||||
if (featureKey in this.undefIdIndex_) {
|
||||
delete this.undefIdIndex_[featureKey];
|
||||
if (this.idIndex_[sid] !== feature) {
|
||||
this.removeFromIdIndex_(feature);
|
||||
this.idIndex_[sid] = feature;
|
||||
} else {
|
||||
if (this.idIndex_[sid] !== feature) {
|
||||
this.removeFromIdIndex_(feature);
|
||||
this.idIndex_[sid] = feature;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!(featureKey in this.undefIdIndex_)) {
|
||||
this.removeFromIdIndex_(feature);
|
||||
this.undefIdIndex_[featureKey] = feature;
|
||||
}
|
||||
this.removeFromIdIndex_(feature);
|
||||
this.uidIndex_[featureKey] = feature;
|
||||
}
|
||||
this.changed();
|
||||
this.dispatchEvent(new VectorSourceEvent(
|
||||
@@ -862,7 +868,7 @@ class VectorSource extends Source {
|
||||
if (id !== undefined) {
|
||||
return id in this.idIndex_;
|
||||
} else {
|
||||
return getUid(feature) in this.undefIdIndex_;
|
||||
return getUid(feature) in this.uidIndex_;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,9 +970,8 @@ class VectorSource extends Source {
|
||||
const id = feature.getId();
|
||||
if (id !== undefined) {
|
||||
delete this.idIndex_[id.toString()];
|
||||
} else {
|
||||
delete this.undefIdIndex_[featureKey];
|
||||
}
|
||||
delete this.uidIndex_[featureKey];
|
||||
this.dispatchEvent(new VectorSourceEvent(
|
||||
VectorEventType.REMOVEFEATURE, feature));
|
||||
}
|
||||
|
||||
@@ -432,6 +432,30 @@ class WebGLHelper extends Disposable {
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
gl.useProgram(this.currentProgram_);
|
||||
this.applyFrameState(frameState);
|
||||
this.applyUniforms(frameState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the render target & bind it for future draw operations.
|
||||
* This is similar to `prepareDraw`, only post processes will not be applied.
|
||||
* Note: the whole viewport will be drawn to the render target, regardless of its size.
|
||||
* @param {import("../PluggableMap.js").FrameState} frameState current frame state
|
||||
* @param {import("./RenderTarget.js").default} renderTarget Render target to draw to
|
||||
* @param {boolean} [opt_disableAlphaBlend] If true, no alpha blending will happen.
|
||||
*/
|
||||
prepareDrawToRenderTarget(frameState, renderTarget, opt_disableAlphaBlend) {
|
||||
const gl = this.getGL();
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, renderTarget.getFramebuffer());
|
||||
gl.bindTexture(gl.TEXTURE_2D, renderTarget.getTexture());
|
||||
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.ONE, opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
gl.useProgram(this.currentProgram_);
|
||||
this.applyFrameState(frameState);
|
||||
this.applyUniforms(frameState);
|
||||
}
|
||||
@@ -742,57 +766,36 @@ class WebGLHelper extends Disposable {
|
||||
// TODO: shutdown program
|
||||
|
||||
/**
|
||||
* TODO: these are not used and should be reworked
|
||||
* @param {number=} opt_wrapS wrapS.
|
||||
* @param {number=} opt_wrapT wrapT.
|
||||
* @return {WebGLTexture} The texture.
|
||||
* Will create or reuse a given webgl texture and apply the given size. If no image data
|
||||
* specified, the texture will be empty, otherwise image data will be used and the `size`
|
||||
* parameter will be ignored.
|
||||
* Note: wrap parameters are set to clamp to edge, min filter is set to linear.
|
||||
* @param {Array<number>} size Expected size of the texture
|
||||
* @param {ImageData|HTMLImageElement|HTMLCanvasElement} [opt_data] Image data/object to bind to the texture
|
||||
* @param {WebGLTexture} [opt_texture] Existing texture to reuse
|
||||
* @return {WebGLTexture} The generated texture
|
||||
* @api
|
||||
*/
|
||||
createTextureInternal(opt_wrapS, opt_wrapT) {
|
||||
createTexture(size, opt_data, opt_texture) {
|
||||
const gl = this.getGL();
|
||||
const texture = gl.createTexture();
|
||||
const texture = opt_texture || gl.createTexture();
|
||||
|
||||
// set params & size
|
||||
const level = 0;
|
||||
const internalFormat = gl.RGBA;
|
||||
const border = 0;
|
||||
const format = gl.RGBA;
|
||||
const type = gl.UNSIGNED_BYTE;
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
if (opt_data) {
|
||||
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, format, type, opt_data);
|
||||
} else {
|
||||
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, size[0], size[1], border, format, type, null);
|
||||
}
|
||||
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 (opt_wrapS !== undefined) {
|
||||
gl.texParameteri(
|
||||
gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, opt_wrapS);
|
||||
}
|
||||
if (opt_wrapT !== undefined) {
|
||||
gl.texParameteri(
|
||||
gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, opt_wrapT);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: these are not used and should be reworked
|
||||
* @param {number} width Width.
|
||||
* @param {number} height Height.
|
||||
* @param {number=} opt_wrapS wrapS.
|
||||
* @param {number=} opt_wrapT wrapT.
|
||||
* @return {WebGLTexture} The texture.
|
||||
*/
|
||||
createEmptyTexture(width, height, opt_wrapS, opt_wrapT) {
|
||||
const gl = this.getGL();
|
||||
const texture = this.createTextureInternal(opt_wrapS, opt_wrapT);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||
return texture;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TODO: these are not used and should be reworked
|
||||
* @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image.
|
||||
* @param {number=} opt_wrapS wrapS.
|
||||
* @param {number=} opt_wrapT wrapT.
|
||||
* @return {WebGLTexture} The texture.
|
||||
*/
|
||||
createTexture(image, opt_wrapS, opt_wrapT) {
|
||||
const gl = this.getGL();
|
||||
const texture = this.createTextureInternal(opt_wrapS, opt_wrapT);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
164
src/ol/webgl/RenderTarget.js
Normal file
164
src/ol/webgl/RenderTarget.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* A wrapper class to simplify rendering to a texture instead of the final canvas
|
||||
* @module ol/webgl/RenderTarget
|
||||
*/
|
||||
import {equals} from '../array.js';
|
||||
|
||||
// for pixel color reading
|
||||
const tmpArray4 = new Uint8Array(4);
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* This class is a wrapper around the association of both a `WebGLTexture` and a `WebGLFramebuffer` instances,
|
||||
* simplifying initialization and binding for rendering.
|
||||
* @api
|
||||
*/
|
||||
class WebGLRenderTarget {
|
||||
|
||||
/**
|
||||
* @param {import("./Helper.js").default} helper WebGL helper; mandatory.
|
||||
* @param {Array<number>} [opt_size] Expected size of the render target texture; note: this can be changed later on.
|
||||
*/
|
||||
constructor(helper, opt_size) {
|
||||
/**
|
||||
* @private
|
||||
* @type {import("./Helper.js").default}
|
||||
*/
|
||||
this.helper_ = helper;
|
||||
const gl = helper.getGL();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {WebGLTexture}
|
||||
*/
|
||||
this.texture_ = gl.createTexture();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {WebGLFramebuffer}
|
||||
*/
|
||||
this.framebuffer_ = gl.createFramebuffer();
|
||||
|
||||
/**
|
||||
* @type {Array<number>}
|
||||
* @private
|
||||
*/
|
||||
this.size_ = opt_size || [1, 1];
|
||||
|
||||
/**
|
||||
* @type {Uint8Array}
|
||||
* @private
|
||||
*/
|
||||
this.data_ = new Uint8Array(0);
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.dataCacheDirty_ = true;
|
||||
|
||||
this.updateSize_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the size of the render target texture. Note: will do nothing if the size
|
||||
* is already the same.
|
||||
* @param {Array<number>} size Expected size of the render target texture
|
||||
* @api
|
||||
*/
|
||||
setSize(size) {
|
||||
if (equals(size, this.size_)) {
|
||||
return;
|
||||
}
|
||||
this.size_[0] = size[0];
|
||||
this.size_[1] = size[1];
|
||||
this.updateSize_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the render target texture
|
||||
* @return {Array<number>} Size of the render target texture
|
||||
* @api
|
||||
*/
|
||||
getSize() {
|
||||
return this.size_;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will cause following calls to `#readAll` or `#readPixel` to download the content of the
|
||||
* render target into memory, which is an expensive operation.
|
||||
* This content will be kept in cache but should be cleared after each new render.
|
||||
* @api
|
||||
*/
|
||||
clearCachedData() {
|
||||
this.dataCacheDirty_ = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full content of the frame buffer as a series of r, g, b, a components
|
||||
* in the 0-255 range (unsigned byte).
|
||||
* @return {Uint8Array} Integer array of color values
|
||||
* @api
|
||||
*/
|
||||
readAll() {
|
||||
if (this.dataCacheDirty_) {
|
||||
const size = this.size_;
|
||||
const gl = this.helper_.getGL();
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_);
|
||||
gl.readPixels(0, 0, size[0], size[1], gl.RGBA, gl.UNSIGNED_BYTE, this.data_);
|
||||
this.dataCacheDirty_ = false;
|
||||
}
|
||||
return this.data_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads one pixel of the frame buffer as an array of r, g, b, a components
|
||||
* in the 0-255 range (unsigned byte).
|
||||
* @param {number} x Pixel coordinate
|
||||
* @param {number} y Pixel coordinate
|
||||
* @returns {Uint8Array} Integer array with one color value (4 components)
|
||||
* @api
|
||||
*/
|
||||
readPixel(x, y) {
|
||||
this.readAll();
|
||||
const index = Math.floor(x) + (this.size_[1] - Math.floor(y) - 1) * this.size_[0];
|
||||
tmpArray4[0] = this.data_[index * 4];
|
||||
tmpArray4[1] = this.data_[index * 4 + 1];
|
||||
tmpArray4[2] = this.data_[index * 4 + 2];
|
||||
tmpArray4[3] = this.data_[index * 4 + 3];
|
||||
return tmpArray4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {WebGLTexture} Texture to render to
|
||||
*/
|
||||
getTexture() {
|
||||
return this.texture_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {WebGLFramebuffer} Frame buffer of the render target
|
||||
*/
|
||||
getFramebuffer() {
|
||||
return this.framebuffer_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateSize_() {
|
||||
const size = this.size_;
|
||||
const gl = this.helper_.getGL();
|
||||
|
||||
this.texture_ = this.helper_.createTexture(size, null, this.texture_);
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_);
|
||||
gl.viewport(0, 0, size[0], size[1]);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture_, 0);
|
||||
|
||||
this.data_ = new Uint8Array(size[0] * size[1] * 4);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebGLRenderTarget;
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @module ol/webgl/Shader
|
||||
*/
|
||||
import {abstract} from '../util.js';
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
class WebGLShader {
|
||||
|
||||
/**
|
||||
* @param {string} source Source.
|
||||
*/
|
||||
constructor(source) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
this.source_ = source;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} Is animated?
|
||||
*/
|
||||
isAnimated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {number} Type.
|
||||
*/
|
||||
getType() {
|
||||
return abstract();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} Source.
|
||||
*/
|
||||
getSource() {
|
||||
return this.source_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default WebGLShader;
|
||||
@@ -1,5 +1,7 @@
|
||||
import WebGLLayerRenderer, {
|
||||
getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE,
|
||||
colorDecodeId,
|
||||
colorEncodeId,
|
||||
getBlankImageData, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE,
|
||||
writePointFeatureInstructions, writePointFeatureToBuffers
|
||||
} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
||||
import Layer from '../../../../../src/ol/layer/Layer.js';
|
||||
@@ -237,9 +239,9 @@ describe('ol.renderer.webgl.Layer', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('getBlankTexture', function() {
|
||||
describe('getBlankImageData', function() {
|
||||
it('creates a 1x1 white texture', function() {
|
||||
const texture = getBlankTexture();
|
||||
const texture = getBlankImageData();
|
||||
expect(texture.height).to.eql(1);
|
||||
expect(texture.width).to.eql(1);
|
||||
expect(texture.data[0]).to.eql(255);
|
||||
@@ -249,4 +251,35 @@ describe('ol.renderer.webgl.Layer', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('colorEncodeId and colorDecodeId', function() {
|
||||
it('correctly encodes and decodes ids', function() {
|
||||
expect(colorDecodeId(colorEncodeId(0))).to.eql(0);
|
||||
expect(colorDecodeId(colorEncodeId(1))).to.eql(1);
|
||||
expect(colorDecodeId(colorEncodeId(123))).to.eql(123);
|
||||
expect(colorDecodeId(colorEncodeId(12345))).to.eql(12345);
|
||||
expect(colorDecodeId(colorEncodeId(123456))).to.eql(123456);
|
||||
expect(colorDecodeId(colorEncodeId(91612))).to.eql(91612);
|
||||
expect(colorDecodeId(colorEncodeId(1234567890))).to.eql(1234567890);
|
||||
});
|
||||
|
||||
it('correctly reuses array', function() {
|
||||
const arr = [];
|
||||
expect(colorEncodeId(123, arr)).to.be(arr);
|
||||
});
|
||||
|
||||
it('is compatible with Uint8Array storage', function() {
|
||||
const encoded = colorEncodeId(91612);
|
||||
const typed = Uint8Array.of(encoded[0] * 255, encoded[1] * 255,
|
||||
encoded[2] * 255, encoded[3] * 255);
|
||||
const arr = [
|
||||
typed[0] / 255,
|
||||
typed[1] / 255,
|
||||
typed[2] / 255,
|
||||
typed[3] / 255
|
||||
];
|
||||
const decoded = colorDecodeId(arr);
|
||||
expect(decoded).to.eql(91612);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/Point
|
||||
import {get as getProjection} from '../../../../../src/ol/proj.js';
|
||||
import ViewHint from '../../../../../src/ol/ViewHint.js';
|
||||
import {POINT_VERTEX_STRIDE, WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
||||
import {create as createTransform, translate as translateTransform} from '../../../../../src/ol/transform.js';
|
||||
|
||||
|
||||
describe('ol.renderer.webgl.PointsLayer', function() {
|
||||
@@ -146,4 +147,67 @@ describe('ol.renderer.webgl.PointsLayer', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#forEachFeatureAtCoordinate', function() {
|
||||
let layer, renderer, feature;
|
||||
|
||||
beforeEach(function() {
|
||||
feature = new Feature(new Point([0, 0]));
|
||||
layer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [feature]
|
||||
})
|
||||
});
|
||||
renderer = new WebGLPointsLayerRenderer(layer, {
|
||||
sizeCallback: function() {
|
||||
return 4;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly hit detects a feature', function(done) {
|
||||
const transform = translateTransform(createTransform(), 20, 20);
|
||||
const projection = getProjection('EPSG:3857');
|
||||
const frameState = {
|
||||
viewState: {
|
||||
projection: projection,
|
||||
resolution: 1,
|
||||
rotation: 0,
|
||||
center: [0, 0]
|
||||
},
|
||||
layerStatesArray: [{}],
|
||||
layerIndex: 0,
|
||||
extent: [-20, -20, 20, 20],
|
||||
size: [40, 40],
|
||||
viewHints: [],
|
||||
coordinateToPixelTransform: transform
|
||||
};
|
||||
let found;
|
||||
const cb = function(feature) {
|
||||
found = feature;
|
||||
};
|
||||
|
||||
renderer.prepareFrame(frameState);
|
||||
renderer.worker_.addEventListener('message', function() {
|
||||
if (!renderer.hitRenderInstructions_) {
|
||||
return;
|
||||
}
|
||||
renderer.renderFrame(frameState);
|
||||
|
||||
function checkHit(x, y, expected) {
|
||||
found = null;
|
||||
renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null);
|
||||
expect(found).to.be(expected ? feature : null);
|
||||
}
|
||||
|
||||
checkHit(0, 0, true);
|
||||
checkHit(1, -2, true);
|
||||
checkHit(-2, 1, true);
|
||||
checkHit(2, 0, false);
|
||||
checkHit(1, -3, false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {bbox as bboxStrategy} from '../../../../src/ol/loadingstrategy.js';
|
||||
import {get as getProjection, transformExtent, fromLonLat} from '../../../../src/ol/proj.js';
|
||||
import VectorSource from '../../../../src/ol/source/Vector.js';
|
||||
import GeoJSON from '../../../../src/ol/format/GeoJSON.js';
|
||||
import {getUid} from '../../../../src/ol/util.js';
|
||||
|
||||
|
||||
describe('ol.source.Vector', function() {
|
||||
@@ -519,6 +520,59 @@ describe('ol.source.Vector', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('#getFeatureByUid()', function() {
|
||||
let source;
|
||||
beforeEach(function() {
|
||||
source = new VectorSource();
|
||||
});
|
||||
|
||||
it('returns a feature with an id', function() {
|
||||
const feature = new Feature();
|
||||
feature.setId('abcd');
|
||||
source.addFeature(feature);
|
||||
expect(source.getFeatureByUid(getUid(feature))).to.be(feature);
|
||||
});
|
||||
|
||||
it('returns a feature without id', function() {
|
||||
const feature = new Feature();
|
||||
source.addFeature(feature);
|
||||
expect(source.getFeatureByUid(getUid(feature))).to.be(feature);
|
||||
});
|
||||
|
||||
it('returns null when no feature is found', function() {
|
||||
const feature = new Feature();
|
||||
feature.setId('abcd');
|
||||
source.addFeature(feature);
|
||||
const wrongId = 'abcd';
|
||||
expect(source.getFeatureByUid(wrongId)).to.be(null);
|
||||
});
|
||||
|
||||
it('returns null after removing feature', function() {
|
||||
const feature = new Feature();
|
||||
feature.setId('abcd');
|
||||
source.addFeature(feature);
|
||||
const uid = getUid(feature);
|
||||
expect(source.getFeatureByUid(uid)).to.be(feature);
|
||||
source.removeFeature(feature);
|
||||
expect(source.getFeatureByUid(uid)).to.be(null);
|
||||
});
|
||||
|
||||
it('returns null after clear', function() {
|
||||
const feature = new Feature();
|
||||
feature.setId('abcd');
|
||||
source.addFeature(feature);
|
||||
const uid = getUid(feature);
|
||||
expect(source.getFeatureByUid(uid)).to.be(feature);
|
||||
source.clear();
|
||||
expect(source.getFeatureByUid(uid)).to.be(null);
|
||||
});
|
||||
|
||||
it('returns null when no features are present', function() {
|
||||
expect(source.getFeatureByUid('abcd')).to.be(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#loadFeatures', function() {
|
||||
|
||||
describe('with the "bbox" strategy', function() {
|
||||
|
||||
@@ -220,5 +220,74 @@ describe('ol.webgl.WebGLHelper', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createTexture', function() {
|
||||
let h;
|
||||
beforeEach(function() {
|
||||
h = new WebGLHelper();
|
||||
});
|
||||
|
||||
it('creates an empty texture from scratch', function() {
|
||||
const width = 4;
|
||||
const height = 4;
|
||||
const t = h.createTexture([width, height]);
|
||||
const gl = h.getGL();
|
||||
|
||||
const fb = gl.createFramebuffer();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
|
||||
const data = new Uint8Array(width * height * 4);
|
||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
gl.deleteFramebuffer(fb);
|
||||
|
||||
expect(data[0]).to.eql(0);
|
||||
expect(data[1]).to.eql(0);
|
||||
expect(data[2]).to.eql(0);
|
||||
expect(data[3]).to.eql(0);
|
||||
expect(data[4]).to.eql(0);
|
||||
expect(data[5]).to.eql(0);
|
||||
expect(data[6]).to.eql(0);
|
||||
expect(data[7]).to.eql(0);
|
||||
});
|
||||
|
||||
it('creates a texture from image data', function() {
|
||||
const width = 4;
|
||||
const height = 4;
|
||||
const canvas = document.createElement('canvas');
|
||||
const image = canvas.getContext('2d').createImageData(width, height);
|
||||
for (let i = 0; i < image.data.length; i += 4) {
|
||||
image.data[i] = 100;
|
||||
image.data[i + 1] = 150;
|
||||
image.data[i + 2] = 200;
|
||||
image.data[i + 3] = 250;
|
||||
}
|
||||
const t = h.createTexture([width, height], image);
|
||||
const gl = h.getGL();
|
||||
|
||||
const fb = gl.createFramebuffer();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
|
||||
const data = new Uint8Array(width * height * 4);
|
||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
gl.deleteFramebuffer(fb);
|
||||
|
||||
expect(data[0]).to.eql(100);
|
||||
expect(data[1]).to.eql(150);
|
||||
expect(data[2]).to.eql(200);
|
||||
expect(data[3]).to.eql(250);
|
||||
expect(data[4]).to.eql(100);
|
||||
expect(data[5]).to.eql(150);
|
||||
expect(data[6]).to.eql(200);
|
||||
expect(data[7]).to.eql(250);
|
||||
});
|
||||
|
||||
it('reuses a given texture', function() {
|
||||
const width = 4;
|
||||
const height = 4;
|
||||
const gl = h.getGL();
|
||||
const t1 = gl.createTexture();
|
||||
const t2 = h.createTexture([width, height], undefined, t1);
|
||||
expect(t1).to.be(t2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
128
test/spec/ol/webgl/rendertarget.test.js
Normal file
128
test/spec/ol/webgl/rendertarget.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import WebGLRenderTarget from '../../../../src/ol/webgl/RenderTarget.js';
|
||||
import WebGLHelper from '../../../../src/ol/webgl/Helper.js';
|
||||
|
||||
|
||||
describe('ol.webgl.RenderTarget', function() {
|
||||
let helper, testImage_4x4;
|
||||
|
||||
beforeEach(function() {
|
||||
helper = new WebGLHelper();
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
testImage_4x4 = canvas.getContext('2d').createImageData(4, 4);
|
||||
for (let i = 0; i < testImage_4x4.data.length; i += 4) {
|
||||
testImage_4x4.data[i] = 100 + i / 4;
|
||||
testImage_4x4.data[i + 1] = 100 + i / 4;
|
||||
testImage_4x4.data[i + 2] = 200 + i / 4;
|
||||
testImage_4x4.data[i + 3] = 200 + i / 4;
|
||||
}
|
||||
});
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('creates a target of size 1x1', function() {
|
||||
const rt = new WebGLRenderTarget(helper);
|
||||
expect(rt.getSize()).to.eql([1, 1]);
|
||||
});
|
||||
|
||||
it('creates a target of specified size', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [12, 34]);
|
||||
expect(rt.getSize()).to.eql([12, 34]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#setSize', function() {
|
||||
|
||||
it('updates the target size', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [12, 34]);
|
||||
expect(rt.getSize()).to.eql([12, 34]);
|
||||
rt.setSize([45, 67]);
|
||||
expect(rt.getSize()).to.eql([45, 67]);
|
||||
});
|
||||
|
||||
it('does nothing if the size has not changed', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [12, 34]);
|
||||
const spy = sinon.spy(rt, 'updateSize_');
|
||||
rt.setSize([12, 34]);
|
||||
expect(spy.called).to.be(false);
|
||||
rt.setSize([12, 345]);
|
||||
expect(spy.called).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#readAll', function() {
|
||||
|
||||
it('returns 1-pixel data with the default options', function() {
|
||||
const rt = new WebGLRenderTarget(helper);
|
||||
expect(rt.readAll().length).to.eql(4);
|
||||
});
|
||||
|
||||
it('returns the content of the texture', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [4, 4]);
|
||||
helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
|
||||
const data = rt.readAll();
|
||||
|
||||
expect(data[0]).to.eql(100);
|
||||
expect(data[1]).to.eql(100);
|
||||
expect(data[2]).to.eql(200);
|
||||
expect(data[3]).to.eql(200);
|
||||
expect(data[4]).to.eql(101);
|
||||
expect(data[5]).to.eql(101);
|
||||
expect(data[6]).to.eql(201);
|
||||
expect(data[7]).to.eql(201);
|
||||
expect(data.length).to.eql(4 * 4 * 4);
|
||||
});
|
||||
|
||||
it('does not call gl.readPixels again when #clearCachedData is not called', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [4, 4]);
|
||||
helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
|
||||
const spy = sinon.spy(rt.helper_.getGL(), 'readPixels');
|
||||
rt.readAll();
|
||||
expect(spy.callCount).to.eql(1);
|
||||
rt.readAll();
|
||||
expect(spy.callCount).to.eql(1);
|
||||
rt.clearCachedData();
|
||||
rt.readAll();
|
||||
expect(spy.callCount).to.eql(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#readPixel', function() {
|
||||
|
||||
it('returns the content of one pixel', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [4, 4]);
|
||||
helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
|
||||
|
||||
let data = rt.readPixel(0, 0);
|
||||
expect(data[0]).to.eql(112);
|
||||
expect(data[1]).to.eql(112);
|
||||
expect(data[2]).to.eql(212);
|
||||
expect(data[3]).to.eql(212);
|
||||
|
||||
data = rt.readPixel(3, 3);
|
||||
expect(data[0]).to.eql(103);
|
||||
expect(data[1]).to.eql(103);
|
||||
expect(data[2]).to.eql(203);
|
||||
expect(data[3]).to.eql(203);
|
||||
expect(data.length).to.eql(4);
|
||||
});
|
||||
|
||||
it('does not call gl.readPixels again when #clearCachedData is not called', function() {
|
||||
const rt = new WebGLRenderTarget(helper, [4, 4]);
|
||||
helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
|
||||
const spy = sinon.spy(rt.helper_.getGL(), 'readPixels');
|
||||
rt.readPixel(0, 0);
|
||||
expect(spy.callCount).to.eql(1);
|
||||
rt.readPixel(1, 1);
|
||||
expect(spy.callCount).to.eql(1);
|
||||
rt.clearCachedData();
|
||||
rt.readPixel(2, 2);
|
||||
expect(spy.callCount).to.eql(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user