diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake index a69cd1b1609..a885e182a1e 100644 --- a/cmake/core-files.cmake +++ b/cmake/core-files.cmake @@ -456,6 +456,8 @@ set(MBGL_CORE_FILES src/mbgl/util/mat4.hpp src/mbgl/util/math.cpp src/mbgl/util/math.hpp + src/mbgl/util/offscreen_texture.cpp + src/mbgl/util/offscreen_texture.hpp src/mbgl/util/premultiply.cpp src/mbgl/util/premultiply.hpp src/mbgl/util/rapidjson.hpp diff --git a/cmake/test-files.cmake b/cmake/test-files.cmake index 706d1bca591..e376bf098a4 100644 --- a/cmake/test-files.cmake +++ b/cmake/test-files.cmake @@ -97,6 +97,7 @@ set(MBGL_TEST_FILES test/util/memory.cpp test/util/merge_lines.cpp test/util/number_conversions.cpp + test/util/offscreen_texture.cpp test/util/projection.cpp test/util/run_loop.cpp test/util/text_conversions.cpp diff --git a/src/mbgl/util/offscreen_texture.cpp b/src/mbgl/util/offscreen_texture.cpp new file mode 100644 index 00000000000..e077d605720 --- /dev/null +++ b/src/mbgl/util/offscreen_texture.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include + +namespace mbgl { + +void OffscreenTexture::bind(gl::ObjectStore& store, + gl::Config& config, + std::array size) { + assert(size[0] > 0 && size[1] > 0); + + if (raster.getSize() != size) { + raster.load(PremultipliedImage(size[0], size[1], nullptr)); + raster.upload(store, config, 0); + } + + if (!fbo) { + fbo = store.createFBO(); + config.bindFramebuffer = *fbo; + MBGL_CHECK_ERROR(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + raster.getID(), 0)); + + GLenum status = MBGL_CHECK_ERROR(glCheckFramebufferStatus(GL_FRAMEBUFFER)); + if (status != GL_FRAMEBUFFER_COMPLETE) { + switch (status) { + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + throw std::runtime_error("Couldn't create framebuffer: incomplete attachment"); + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + throw std::runtime_error( + "Couldn't create framebuffer: incomplete missing attachment"); +#ifdef GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: + throw std::runtime_error("Couldn't create framebuffer: incomplete draw buffer"); +#endif +#ifdef GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: + throw std::runtime_error("Couldn't create framebuffer: incomplete read buffer"); +#endif +#ifdef GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS + case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + throw std::runtime_error("Couldn't create framebuffer: incomplete dimensions"); +#endif + + case GL_FRAMEBUFFER_UNSUPPORTED: + throw std::runtime_error("Couldn't create framebuffer: unsupported"); + default: + throw std::runtime_error("Couldn't create framebuffer: other"); + } + } + } else { + config.bindFramebuffer = *fbo; + } + + config.viewport = { { 0, 0, static_cast(size[0]), static_cast(size[1]) } }; +} + +Raster& OffscreenTexture::getTexture() { + return raster; +} + +std::array OffscreenTexture::getSize() const { + return raster.getSize(); +} + +} // namespace mbgl diff --git a/src/mbgl/util/offscreen_texture.hpp b/src/mbgl/util/offscreen_texture.hpp new file mode 100644 index 00000000000..cceb9381741 --- /dev/null +++ b/src/mbgl/util/offscreen_texture.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace mbgl { + +namespace gl { +class Config; +} // namespace gl + +class OffscreenTexture { +public: + void bind(gl::ObjectStore&, gl::Config&, std::array size); + + Raster& getTexture(); + std::array getSize() const; + +private: + mbgl::optional fbo; + Raster raster; +}; + +} // namespace mbgl diff --git a/test/fixtures/offscreen_texture/empty-red/expected.png b/test/fixtures/offscreen_texture/empty-red/expected.png new file mode 100644 index 00000000000..7ecf05ca7ca Binary files /dev/null and b/test/fixtures/offscreen_texture/empty-red/expected.png differ diff --git a/test/fixtures/offscreen_texture/render-to-fbo-composited/expected.png b/test/fixtures/offscreen_texture/render-to-fbo-composited/expected.png new file mode 100644 index 00000000000..b4d2a0b1b6f Binary files /dev/null and b/test/fixtures/offscreen_texture/render-to-fbo-composited/expected.png differ diff --git a/test/fixtures/offscreen_texture/render-to-fbo/expected.png b/test/fixtures/offscreen_texture/render-to-fbo/expected.png new file mode 100644 index 00000000000..7ecf05ca7ca Binary files /dev/null and b/test/fixtures/offscreen_texture/render-to-fbo/expected.png differ diff --git a/test/fixtures/offscreen_texture/render-to-texture/expected.png b/test/fixtures/offscreen_texture/render-to-texture/expected.png new file mode 100644 index 00000000000..7773e5ab050 Binary files /dev/null and b/test/fixtures/offscreen_texture/render-to-texture/expected.png differ diff --git a/test/util/offscreen_texture.cpp b/test/util/offscreen_texture.cpp new file mode 100644 index 00000000000..74a616134e9 --- /dev/null +++ b/test/util/offscreen_texture.cpp @@ -0,0 +1,153 @@ +#include + +#include +#include + +#include +#include + +using namespace mbgl; + +TEST(OffscreenTexture, EmptyRed) { + HeadlessView view(1.0f, 512, 256); + view.activate(); + + MBGL_CHECK_ERROR(glClearColor(1.0f, 0.0f, 0.0f, 1.0f)); + MBGL_CHECK_ERROR(glClear(GL_COLOR_BUFFER_BIT)); + + auto image = view.readStillImage(); + test::checkImage("test/fixtures/offscreen_texture/empty-red", image, 0, 0); +} + +struct Shader { + Shader(const GLchar* vertex, const GLchar* fragment) { + program = MBGL_CHECK_ERROR(glCreateProgram()); + vertexShader = MBGL_CHECK_ERROR(glCreateShader(GL_VERTEX_SHADER)); + fragmentShader = MBGL_CHECK_ERROR(glCreateShader(GL_FRAGMENT_SHADER)); + MBGL_CHECK_ERROR(glShaderSource(vertexShader, 1, &vertex, nullptr)); + MBGL_CHECK_ERROR(glCompileShader(vertexShader)); + MBGL_CHECK_ERROR(glAttachShader(program, vertexShader)); + MBGL_CHECK_ERROR(glShaderSource(fragmentShader, 1, &fragment, nullptr)); + MBGL_CHECK_ERROR(glCompileShader(fragmentShader)); + MBGL_CHECK_ERROR(glAttachShader(program, fragmentShader)); + MBGL_CHECK_ERROR(glLinkProgram(program)); + a_pos = glGetAttribLocation(program, "a_pos"); + } + + ~Shader() { + MBGL_CHECK_ERROR(glDetachShader(program, vertexShader)); + MBGL_CHECK_ERROR(glDetachShader(program, fragmentShader)); + MBGL_CHECK_ERROR(glDeleteShader(vertexShader)); + MBGL_CHECK_ERROR(glDeleteShader(fragmentShader)); + MBGL_CHECK_ERROR(glDeleteProgram(program)); + } + + GLuint program = 0; + GLuint vertexShader = 0; + GLuint fragmentShader = 0; + GLuint a_pos = 0; +}; + +struct Buffer { + Buffer(std::vector data) { + MBGL_CHECK_ERROR(glGenBuffers(1, &buffer)); + MBGL_CHECK_ERROR(glBindBuffer(GL_ARRAY_BUFFER, buffer)); + MBGL_CHECK_ERROR(glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(GLfloat), data.data(), + GL_STATIC_DRAW)); + } + + ~Buffer() { + MBGL_CHECK_ERROR(glDeleteBuffers(1, &buffer)); + } + + GLuint buffer = 0; +}; + + +TEST(OffscreenTexture, RenderToTexture) { + HeadlessView view(1.0f, 512, 256); + view.activate(); + gl::Config config; + gl::ObjectStore store; + + + MBGL_CHECK_ERROR(glEnable(GL_BLEND)); + MBGL_CHECK_ERROR(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + Shader paintShader(R"MBGL_SHADER( +attribute vec2 a_pos; +void main() { + gl_Position = vec4(a_pos, 0, 1); +} +)MBGL_SHADER", R"MBGL_SHADER( +void main() { + gl_FragColor = vec4(0, 0.8, 0, 0.8); +} +)MBGL_SHADER"); + + Shader compositeShader(R"MBGL_SHADER( +attribute vec2 a_pos; +varying vec2 v_texcoord; +void main() { + gl_Position = vec4(a_pos, 0, 1); + v_texcoord = (a_pos + 1.0) / 2.0; +} +)MBGL_SHADER", R"MBGL_SHADER( +uniform sampler2D u_texture; +varying vec2 v_texcoord; +void main() { + gl_FragColor = texture2D(u_texture, v_texcoord); +} +)MBGL_SHADER"); + + GLuint u_texture = glGetUniformLocation(compositeShader.program, "u_texture"); + + Buffer triangleBuffer({ 0, 0.5, 0.5, -0.5, -0.5, -0.5 }); + Buffer viewportBuffer({ -1, -1, 1, -1, -1, 1, 1, 1 }); + + // Make sure the texture gets destructed before we call store.reset(); + { + // First, draw red to the bound FBO. + config.clearColor = { 1, 0, 0, 1 }; + MBGL_CHECK_ERROR(glClear(GL_COLOR_BUFFER_BIT)); + + // Then, create a texture, bind it, and render yellow to that texture. This should not + // affect the originally bound FBO. + OffscreenTexture texture; + texture.bind(store, config, {{ 128, 128 }}); + + config.clearColor = { 0, 0, 0, 0 }; + MBGL_CHECK_ERROR(glClear(GL_COLOR_BUFFER_BIT)); + + config.program = paintShader.program; + MBGL_CHECK_ERROR(glBindBuffer(GL_ARRAY_BUFFER, triangleBuffer.buffer)); + MBGL_CHECK_ERROR(glEnableVertexAttribArray(paintShader.a_pos)); + MBGL_CHECK_ERROR( + glVertexAttribPointer(paintShader.a_pos, 2, GL_FLOAT, GL_FALSE, 0, nullptr)); + MBGL_CHECK_ERROR(glDrawArrays(GL_TRIANGLE_STRIP, 0, 3)); + + auto image = view.readStillImage(texture.getSize()); + test::checkImage("test/fixtures/offscreen_texture/render-to-texture", image, 0, 0); + + // Now reset the FBO back to normal and retrieve the original (restored) framebuffer. + config.reset(); + + image = view.readStillImage(); + test::checkImage("test/fixtures/offscreen_texture/render-to-fbo", image, 0, 0); + + // Now, composite the Framebuffer texture we've rendered to onto the main FBO. + config.program = compositeShader.program; + texture.getTexture().bind(store, config, 0, Raster::Scaling::Linear); + MBGL_CHECK_ERROR(glUniform1i(u_texture, 0)); + MBGL_CHECK_ERROR(glBindBuffer(GL_ARRAY_BUFFER, viewportBuffer.buffer)); + MBGL_CHECK_ERROR(glEnableVertexAttribArray(compositeShader.a_pos)); + MBGL_CHECK_ERROR( + glVertexAttribPointer(compositeShader.a_pos, 2, GL_FLOAT, GL_FALSE, 0, nullptr)); + MBGL_CHECK_ERROR(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); + + image = view.readStillImage(); + test::checkImage("test/fixtures/offscreen_texture/render-to-fbo-composited", image, 0, 0.1); + } + + store.reset(); +}