diff --git a/src/ol/webgl/RenderTarget.js b/src/ol/webgl/RenderTarget.js new file mode 100644 index 0000000000..c9cde76b0d --- /dev/null +++ b/src/ol/webgl/RenderTarget.js @@ -0,0 +1,124 @@ +/** + * A wrapper class to simplify rendering to a texture instead of the final canvas + * @module ol/webgl/RenderTarget + */ +import {equals} from '../array.js'; + + +/** + * @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} [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} + * @private + */ + this.size_ = opt_size || [1, 1]; + + /** + * @type {Uint8Array} + * @private + */ + this.data_ = new Uint8Array(1); + + this.updateSize_(); + } + + /** + * Changes the size of the render target texture. Note: will do nothing if the size + * is already the same. + * @param {Array} 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} Size of the render target texture + * @api + */ + getSize() { + return this.size_; + } + + /** + * Returns the content of the render target texture as raw data (series of r,g,b,a values) + * @return {Uint8Array} Integer array of color values + * @api + */ + read() { + 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_); + return this.data_; + } + + /** + * @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; diff --git a/test/spec/ol/webgl/rendertarget.test.js b/test/spec/ol/webgl/rendertarget.test.js new file mode 100644 index 0000000000..d405fe9fab --- /dev/null +++ b/test/spec/ol/webgl/rendertarget.test.js @@ -0,0 +1,80 @@ +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; + testImage_4x4.data[i + 1] = 150; + testImage_4x4.data[i + 2] = 200; + testImage_4x4.data[i + 3] = 250; + } + }); + + 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('#readData', function() { + + it('returns 1-pixel data with the default options', function() { + const rt = new WebGLRenderTarget(helper); + expect(rt.read().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(); + + 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); + expect(data.length).to.eql(4 * 4 * 4); + }); + + }); + +});