diff --git a/src/ol/webgl/RenderTarget.js b/src/ol/webgl/RenderTarget.js index c9cde76b0d..7c0f6ef1f3 100644 --- a/src/ol/webgl/RenderTarget.js +++ b/src/ol/webgl/RenderTarget.js @@ -4,6 +4,8 @@ */ import {equals} from '../array.js'; +// for pixel color reading +const tmpArray4 = new Uint8Array(4); /** * @classdesc @@ -47,7 +49,13 @@ class WebGLRenderTarget { * @type {Uint8Array} * @private */ - this.data_ = new Uint8Array(1); + this.data_ = new Uint8Array(0); + + /** + * @type {boolean} + * @private + */ + this.dataCacheDirty_ = true; this.updateSize_(); } @@ -77,19 +85,50 @@ class WebGLRenderTarget { } /** - * Returns the content of the render target texture as raw data (series of r,g,b,a values) + * 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 */ - read() { - const size = this.size_; - const gl = this.helper_.getGL(); + 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_); + 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) + */ + 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 */ diff --git a/test/spec/ol/webgl/rendertarget.test.js b/test/spec/ol/webgl/rendertarget.test.js index d405fe9fab..3468b16c26 100644 --- a/test/spec/ol/webgl/rendertarget.test.js +++ b/test/spec/ol/webgl/rendertarget.test.js @@ -11,10 +11,10 @@ describe('ol.webgl.RenderTarget', function() { 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; - testImage_4x4.data[i + 1] = 150; - testImage_4x4.data[i + 2] = 200; - testImage_4x4.data[i + 3] = 250; + 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; } }); @@ -52,29 +52,77 @@ describe('ol.webgl.RenderTarget', function() { }); - describe('#readData', function() { + describe('#readAll', function() { it('returns 1-pixel data with the default options', function() { const rt = new WebGLRenderTarget(helper); - expect(rt.read().length).to.eql(4); + 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.read(); + const data = rt.readAll(); expect(data[0]).to.eql(100); - expect(data[1]).to.eql(150); + expect(data[1]).to.eql(100); 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); + 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); + }); + }); });