source/render/CanopyScene.cpp (399 lines of code) (raw):

/** * Copyright 2004-present Facebook. All Rights Reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #include "source/render/CanopyScene.h" #include "source/util/ThreadPool.h" namespace fb360_dep { Canopy::Canopy(const cv::Mat_<cv::Vec4f>& color, const cv::Mat_<cv::Vec3f>& mesh, GLuint program) { // tell gl about color const bool kBuildMipmaps = true; colorTexture = createTexture( color.cols, color.rows, color.ptr(), GL_RGBA16, GL_BGRA, GL_FLOAT, kBuildMipmaps); setTextureAniso(); // tell gl about mesh vertexArray = createVertexArray(); positionBuffer = createVertexAttributes( program, "position", mesh.ptr<std::array<float, 3>>(), mesh.cols * mesh.rows); indexBuffer = createBuffer(GL_ELEMENT_ARRAY_BUFFER, stripify(mesh.cols, mesh.rows)); modulo = mesh.cols; scale = {1.0 / mesh.cols, 1.0 / mesh.rows}; } void Canopy::destroy() { glDeleteBuffers(1, &indexBuffer); glDeleteBuffers(1, &positionBuffer); glDeleteTextures(1, &colorTexture); glDeleteVertexArrays(1, &vertexArray); } void Canopy::render( GLuint framebuffer, const Eigen::Projective3f& transform, const GLuint program, const float ipd) const { glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); CHECK_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE); glClearColor(0.0, 0.4, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // use <= so we never see clear depth glUseProgram(program); // tell vertex shader uniforms glUniformMatrix4fv(glGetUniformLocation(program, "transform"), 1, GL_FALSE, transform.data()); setUniform(program, "modulo", modulo); setUniform(program, "scale", scale.x(), scale.y()); setUniform(program, "ipdm", ipd); // tell fragment shader which texture to use glBindTexture(GL_TEXTURE_2D, colorTexture); // draw stuff glBindVertexArray(vertexArray); drawElements<GLuint>(GL_TRIANGLE_STRIP); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glDisable(GL_DEPTH_TEST); } // compute texVar from vertex id // transform rig space position from vertex array into clip space std::string canopyVS = R"( #version 330 core uniform int modulo; // number of vertexes per row uniform float ipdm; // positive for left eye, negative for right eye (in meters) uniform vec2 scale; // transfrom from 2D index to color texture coordinates uniform mat4 transform; // transfrom to clip-space in vec3 position; out vec2 texVar; const float kPi = 3.1415926535897932384626433832795; float ipd(const float lat) { const float kA = 25; const float kB = 0.17; return ipdm * exp( -exp(kA * (kB - 0.5 - lat / kPi)) -exp(kA * (kB - 0.5 + lat / kPi))); } float sq(const float x) { return x * x; } float sq(const vec2 x) { return dot(x, x); } float error(const vec2 xy, const float z, const float dEst) { // xy^2 = (ipd(atan(z/dEst))/2)^2 + dEst^2 + error <=> return sq(xy) - sq(ipd(atan(z / dEst)) / 2) - sq(dEst); } float solve(const vec3 p) { // for initial estimate, assume lat = atan(z/d) ~ atan(z/xy) // p.xy^2 = ipd(atan(z/d)^2 + d^2 ~ // p.xy^2 = ipd(atan(z/xy)^2 + d^2 <=> float d0 = sqrt(sq(p.xy) - sq(ipd(atan(p.z / length(p.xy))))); // refine with a few iterations of newton-raphson // two iterations get error below 2.4e-07 radians at 0.2 m // one iteration gets the same result at 0.7 m // and no iterations are required beyond 6.3 meters const int iterations = 2; for (int i = 0; i < iterations; ++i) { const float kSmidgen = 1e-3; float d1 = (1 + kSmidgen) * d0; float e0 = error(p.xy, p.z, d0); float e1 = error(p.xy, p.z, d1); float de = (e1 - e0) / (d1 - d0); d0 -= e0 / de; } return d0; } vec3 eye(const vec3 p) { float dEst = solve(p); float ipdEst = ipd(atan(p.z / dEst)); float eNorm = ipdEst / 2; float k = -dEst / eNorm; mat2 A = mat2(1.0, k, -k, 1.0); // column major! return vec3(inverse(A) * p.xy, 0); } void main() { // compute the color texture coordinates from the vertex id texVar = scale * vec2(gl_VertexID % modulo + 0.5, gl_VertexID / modulo + 0.5); vec3 pos = position; if (ipdm != 0) { // adjust position when rendering stereo pos -= eye(pos); } // apply transform gl_Position = transform * vec4(pos, 1); } )"; // read color from sampler // modulate by how much mesh has been stretched std::string canopyFS = R"( #version 330 core uniform sampler2D sampler; in vec2 texVar; out vec4 color; void main() { color = texture(sampler, texVar); if (color.a == 0) { discard; } // call a = dtexVar/dx and b = dtexVar/dy. a and b describe a parallelogram // in the camera image corresponding to your screen space pixel. if you // move one pixel in x, you move a in the camera image, and if you move one // pixel vertically, you move b in the camera image. if you move unit // vectors in other directions, your movement describes an ellipse in the // camera image. the minor axis of the ellipse corresponds to the worst // direction that you can move in because that is the finest grain // information you are looking for in the camera image // so, what is the length of the minor axis? well, the squared length of a // move in camera space corresponding to [cos(d), sin(d)] - a unit vector // move in direction d - in screen space is: // (cos(d) * a + sin(d) * b)^2 = // cos(d)^2 * a^2 + sin(d)^2 * b^2 + 2 cos(d) sin(d) a.b = // (1 + cos(2d))/2 a^2 + (1 - cos(2d))/2 b^2 + sin(2d) a.b = // (a^2 + b^2)/2 + (a^2 - b^2)/2 cos(2d) + a.b sin(2d) = // (a^2 + b^2)/2 + [(a^2 - b^2)/2, a.b].[cos(2d), sin(2d)] // it can thus be seen that the squared length varies between: // (a^2 + b^2)/2 -/+ |[(a^2 - b^2)/2, a.b]| vec2 a = dFdx(texVar), b = dFdy(texVar); float aa = dot(a, a), bb = dot(b, b), ab = dot(a, b); float minor = (aa + bb) / 2 - length(vec2((aa - bb) / 2, ab)); // make contribution proportional to minor axis color.a *= minor; // alpha is a cone, 1 in the center, epsilon at edges const float eps = 1.0f / 255.0f; // max granularity float cone = max(eps, 1 - 2 * length(texVar - 0.5)); color.a *= cone; } )"; std::string canopyFS_SVD = R"( #version 330 core uniform sampler2D sampler; in vec2 texVar; out vec4 color; void main() { color = texture(sampler, texVar); if (color.a == 0) { discard; } // call a = dtexVar/dx and b = dtexVar/dy. a and b describe a parallelogram // in the camera image corresponding to your screen space pixel as before but a more complete // method is to compute an area invariant version of this using the ratio of the singular values // of the matrix that takes a square pixel into the parallelogram. vec2 v1 = dFdx(texVar), v2 = dFdy(texVar); float a = v1.x; float b = v1.y; float c = v2.x; float d = v2.y; float s1 = a*a + b*b + c*c + d*d; float sb = a*a + b*b - c*c - d*d; float sc = a*c + b*d; float s2 = sqrt(sb*sb + 4*sc*sc); float sigma1 = sqrt((s1 + s2) / 2); float sigma2 = sqrt((s1 - s2) / 2); color.a *= sigma2 / sigma1; // alpha is a cone, 1 in the center, epsilon at edges const float eps = 1.0f / 255.0f; // max granularity float cone = max(eps, 1 - 2 * length(texVar - 0.5)); color.a *= cone; } )"; // read color from sampler and apply soft max std::string accumulateFS = R"( #version 330 core uniform sampler2D sampler; uniform bool alphaBlend; in vec2 texVar; out vec4 color; void main() { color = texture(sampler, texVar); if (alphaBlend) { // we want weight, w = k^a - 1 = e^(log(k) * a) - 1 const float kLogK = 30; color.a = exp(kLogK * color.a) - 1; } } )"; // read color from sampler, converting from pre-multiplied alpha std::string unpremulFS = R"( #version 330 core uniform sampler2D sampler; in vec2 texVar; out vec4 color; void main() { color = texture(sampler, texVar); color /= color.a; } )"; // merge a texture into the accumulate buffer static void accumulate(GLuint framebuffer, GLuint texture, const GLuint program, const bool alphaBlend) { glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); CHECK_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE); // set up blend equations to accumulate premultiplied alpha // dst.rgb += src.a * src.rgb <=> dst.rgb = src.a * src.rgb + 1 * dst.rgb // dst.a += src.a <=> dst.a = 1 * src.a + 1 * dst.a glEnable(GL_BLEND); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE); glUseProgram(program); setUniform(program, "alphaBlend", alphaBlend); glBindTexture(GL_TEXTURE_2D, texture); fullscreen(program, "tex"); glDisable(GL_BLEND); } void CanopyScene::render( const GLuint framebuffer, const Eigen::Projective3f& transform, const float ipd, const bool alphaBlend) const { // framebuffer used to accumulate all the cameras GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); GLuint accumulateBuffer = createFramebuffer(); GLuint accumulateTexture = createFramebufferTexture(viewport[2], viewport[3], GL_RGBA32F); glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); // framebuffer used to render a single canopy GLuint canopyBuffer = createFramebuffer(); GLuint canopyTexture = createFramebufferTexture(viewport[2], viewport[3], GL_RGBA32F); GLuint canopyDepth = createFramebufferDepth(viewport[2], viewport[3]); // accumulate all the canopies into the accumulateBuffer for (auto& canopy : canopies) { canopy.render(canopyBuffer, transform, canopyProgram, ipd); accumulate(accumulateBuffer, canopyTexture, accumulateProgram, alphaBlend); } // clean up canopy framebuffer glDeleteRenderbuffers(1, &canopyDepth); glDeleteTextures(1, &canopyTexture); glDeleteFramebuffers(1, &canopyBuffer); // un-premultiply out of the accumulation buffer into framebuffer glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glUseProgram(unpremulProgram); glBindTexture(GL_TEXTURE_2D, accumulateTexture); fullscreen(unpremulProgram, "tex"); // clean up glDeleteTextures(1, &accumulateTexture); glDeleteFramebuffers(1, &accumulateBuffer); } template <typename T> GLuint createCubemapTexture( const T& scene, const int edge, const Eigen::Vector3f& position, const float ipd, const bool alphaBlend) { // create cubemap framebuffer GLuint framebuffer = createFramebuffer(); GLuint cubemap = createFramebufferCubemapTexture(edge, edge, GL_RGBA32F); glViewport(0, 0, edge, edge); // 90 degree frustum const float kNearZ = 0.1f; // meters Eigen::Projective3f projection = frustum(-kNearZ, kNearZ, -kNearZ, kNearZ, kNearZ); // render each cube face const int kFaceCount = 6; for (int face = 0; face < kFaceCount; ++face) { glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, cubemap, 0); CHECK_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE); // from https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_cube_map.txt // major axis // direction target sc tc ma // ---------- ------------------------------- --- --- --- // +rx TEXTURE_CUBE_MAP_POSITIVE_X_EXT -rz -ry rx // -rx TEXTURE_CUBE_MAP_NEGATIVE_X_EXT +rz -ry rx // +ry TEXTURE_CUBE_MAP_POSITIVE_Y_EXT +rx +rz ry // -ry TEXTURE_CUBE_MAP_NEGATIVE_Y_EXT +rx -rz ry // +rz TEXTURE_CUBE_MAP_POSITIVE_Z_EXT +rx -ry rz // -rz TEXTURE_CUBE_MAP_NEGATIVE_Z_EXT -rx -ry rz static const Eigen::Vector3f table[kFaceCount][3] = { {Eigen::Vector3f::UnitX(), -Eigen::Vector3f::UnitZ(), -Eigen::Vector3f::UnitY()}, {-Eigen::Vector3f::UnitX(), Eigen::Vector3f::UnitZ(), -Eigen::Vector3f::UnitY()}, {Eigen::Vector3f::UnitY(), Eigen::Vector3f::UnitX(), Eigen::Vector3f::UnitZ()}, {-Eigen::Vector3f::UnitY(), Eigen::Vector3f::UnitX(), -Eigen::Vector3f::UnitZ()}, {Eigen::Vector3f::UnitZ(), Eigen::Vector3f::UnitX(), -Eigen::Vector3f::UnitY()}, {-Eigen::Vector3f::UnitZ(), -Eigen::Vector3f::UnitX(), -Eigen::Vector3f::UnitY()}}; Eigen::Affine3f transform; transform.linear().row(0) = table[face][1]; // sc from table transform.linear().row(1) = table[face][2]; // tc from table transform.linear().row(2) = -table[face][0]; // -major axis direction from table transform.translation().setZero(); transform.translate(-position); scene.render(framebuffer, projection * transform, ipd, alphaBlend); } // clean up glDeleteFramebuffers(1, &framebuffer); // bind and return the cubemap glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap); return cubemap; } // render scene from position as a cubemap with edge by edge pixel faces, stacked vertically cv::Mat_<cv::Vec4f> CanopyScene::cubemap( int edge, const Eigen::Vector3f& position, const float ipd, const bool alphaBlend) const { GLuint cubemap = createCubemapTexture(*this, edge, position, ipd, alphaBlend); // opengl's origin is bottom-left whereas opencv uses top-left // so stick faces into result from bottom to top, then flip the whole thing upside-down const int kFaceCount = 6; cv::Mat_<cv::Vec4f> result(kFaceCount * edge, edge); for (int face = 0; face < kFaceCount; ++face) { cv::Vec4f* dst = &result((kFaceCount - 1 - face) * edge, 0); glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_BGRA, GL_FLOAT, dst); } const int kUpsideDownCode = 0; cv::flip(result, result, kUpsideDownCode); // clean up and return glDeleteTextures(1, &cubemap); return result; } // render equirect from cubemap std::string equirectFS = R"( #version 330 core uniform samplerCube sampler; in vec2 texVar; out vec4 color; void main() { // remap texVar to equirect direction with z up, BUT // - flip texVar.x because latitude grows in the negative direction of the z-axis // - flip texVar.y because glReadPixels reads the image from the bottom up const float PI = 3.1415926535897932384626433832795; float lon = (1 - texVar.x) * 2.0 * PI; // 360 .. 0 degrees float lat = -(texVar.y - 0.5) * PI; // 180 .. -180 degrees vec3 direction = vec3( cos(lat) * cos(lon), cos(lat) * sin(lon), sin(lat)); color = texture(sampler, direction); } )"; // render scene from position as an equirect that is height pixels tall and twice as wide cv::Mat_<cv::Vec4f> CanopyScene::equirect( int height, const Eigen::Vector3f& position, const float ipd, const bool alphaBlend) const { // use the equirect height for the cube edge to provide plenty of resolution GLuint cubemap = createCubemapTexture(*this, height, position, ipd, alphaBlend); // create the framebuffer int width = 2 * height; GLuint fbo = createFramebuffer(); GLuint color = createFramebufferColor(width, height, GL_RGBA32F); glViewport(0, 0, width, height); // set up the program GLuint program = createProgram(fullscreenVertexShader(), equirectFS); #ifdef GL_TEXTURE_CUBE_MAP_SEAMLESS glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); #endif setTextureWrap<GL_TEXTURE_CUBE_MAP>(GL_CLAMP_TO_EDGE); setLinearFiltering<GL_TEXTURE_CUBE_MAP>(); setTextureAniso<GL_TEXTURE_CUBE_MAP>(); // render and read the result fullscreen(program); glReadBuffer(GL_COLOR_ATTACHMENT0); cv::Mat_<cv::Vec4f> equirect(height, width); glReadPixels(0, 0, equirect.cols, equirect.rows, GL_BGRA, GL_FLOAT, equirect.ptr()); // clean up and return glDeleteProgram(program); glDeleteRenderbuffers(1, &color); glDeleteFramebuffers(1, &fbo); glDeleteTextures(1, &cubemap); return equirect; } static cv::Mat_<cv::Vec3f> disparityMesh(const cv::Mat_<float>& disparity, Camera camera) { // use camera disparity to compute a world coordinate mesh camera = camera.rescale({disparity.cols, disparity.rows}); cv::Mat_<cv::Vec3f> mesh(disparity.rows, disparity.cols); for (int y = 0; y < disparity.rows; ++y) { for (int x = 0; x < disparity.cols; ++x) { float distance = 1.0f / disparity(y, x); Camera::Vector3 rig = camera.rig({x + 0.5, y + 0.5}, distance); mesh(y, x) = cv::Vec3f(rig[0], rig[1], rig[2]); } } return mesh; } cv::Mat_<cv::Vec4f> alphaFov(const cv::Mat_<cv::Vec4f>& color, Camera camera) { // knock out pixels outside fov cv::Mat_<cv::Vec4f> result(color.rows, color.cols); camera = camera.rescale({result.cols, result.rows}); for (int y = 0; y < result.rows; ++y) { for (int x = 0; x < result.cols; ++x) { result(y, x) = color(y, x); result(y, x)[3] = camera.isOutsideImageCircle({x + 0.5, y + 0.5}) ? 0 : 1; } } return result; } CanopyScene::CanopyScene( const Camera::Rig& cameras, const std::vector<cv::Mat_<float>>& disparities, const std::vector<cv::Mat_<cv::Vec4f>>& colors, const bool onScreen) { // create the programs canopyProgram = createProgram(canopyVS, onScreen ? canopyFS : canopyFS_SVD); accumulateProgram = createProgram(fullscreenVertexShader(), accumulateFS); unpremulProgram = createProgram(fullscreenVertexShader(), unpremulFS); // prepare images and meshes for canopies in parallel std::vector<cv::Mat_<cv::Vec4f>> images(ssize(cameras)); std::vector<cv::Mat_<cv::Vec3f>> meshes(ssize(cameras)); ThreadPool threads; for (ssize_t i = 0; i < ssize(cameras); ++i) { threads.spawn([&, i] { images[i] = alphaFov(colors[i], cameras[i]); meshes[i] = disparityMesh(disparities[i], cameras[i]); }); } threads.join(); // create the canopies for (ssize_t i = 0; i < ssize(images); ++i) { canopies.emplace_back(images[i], meshes[i], canopyProgram); } } CanopyScene::~CanopyScene() { for (Canopy& canopy : canopies) { canopy.destroy(); } glDeleteProgram(unpremulProgram); glDeleteProgram(accumulateProgram); glDeleteProgram(canopyProgram); } } // namespace fb360_dep