source/render/RigScene.cpp (934 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/RigScene.h"
#include <fstream>
#include <glog/logging.h>
#define STB_IMAGE_IMPLEMENTATION
#pragma GCC diagnostic push
#if !defined(__has_warning)
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
#elif __has_warning("-Wmisleading-indentation")
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
#endif
#if defined(__has_warning)
#if __has_warning("-Wimplicit-fallthrough")
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
#endif // __has_warning("-Wimplicit-fallthrough")
#endif // defined(__has_warning)
#include "source/thirdparty/stb_image.h"
#pragma GCC diagnostic pop
#include <folly/Format.h>
namespace fb360_dep {
const float kUnit = 1.0f; // change this to 1.0e-2f if rig is in cm
const float kWhiteZ = 1.0f; // this distance is mapped to white (meters)
const float kR = 1.0f / kUnit;
// Resolution of direction textures.
// Must match the texture scaling in cameraVS inside createPrograms
// In cameraVS::main and cameraMeshVS::main
// texVarScaled = (0.5 + texVar * (kDirections - 1)) / kDirections;
const int kDirections = 128;
void RigScene::destroyFramebuffers() {
glDeleteTextures(1, &accumulateTexture);
glDeleteFramebuffers(1, &accumulateFBO);
glDeleteRenderbuffers(1, &cameraDepth);
glDeleteTextures(1, &cameraTexture);
glDeleteFramebuffers(1, &cameraFBO);
cameraFBO = 0; // flag as destroyed
}
void RigScene::createFramebuffers(const int w, const int h) {
// framebuffer used to render each camera
cameraFBO = createFramebuffer();
// NB: for peak performance in a headset, this could be GL_SRGB8_ALPHA8
cameraTexture = createFramebufferTexture(w, h, GL_RGBA16);
cameraDepth = createFramebufferDepth(w, h);
// framebuffer used to accumulate all the cameras
accumulateFBO = createFramebuffer();
accumulateTexture = createFramebufferTexture(w, h, GL_RGBA32F);
}
void RigScene::createPrograms() {
// input is depth
// texVar is computed from the instance id and scale and offset
// position is computed by a lookup in the direction texture
const std::string cameraVS = R"(
#version 330 core
uniform mat4 transform;
uniform vec3 camera;
uniform int modulo;
uniform vec2 scale;
uniform sampler2D directions;
in vec2 offset; // per-vertex offset
uniform bool isDepth;
in float depth; // per-instance depth
out vec2 texVar;
uniform float kIPD = 0; // positive for left eye, negative for right eye
const float kPi = 3.1415926535897932384626433832795;
float ipd(const float lat) {
const float kA = 25;
const float kB = 0.17;
return kIPD * 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() {
ivec2 instance = ivec2(gl_InstanceID % modulo, gl_InstanceID / modulo);
vec2 dirVar = scale * (instance + offset);
texVar = scale * (instance + (isDepth ? vec2(0.5) : offset));
// We want the direction texture to align the first and last values to the
// edge of each row/column, it'll have (kDirections - 1) texels instead of
// kDirections, so we need to scale by (kDirections - 1) / kDirections
// Also, kDirections buckets are originally defined at the leftmost edge
// of pixels, not the center. We make up for this by shifting the input
// locations by 0.5 / (num texels), where num texels = kDirections - 1
const float kDirections = 128;
vec2 texVarScaled = (0.5 + dirVar * (kDirections - 1)) / kDirections;
vec3 direction = texture(directions, texVarScaled).xyz;
vec3 rig = camera + depth * direction;
if (kIPD != 0) { // adjust rig when rendering stereo
rig -= eye(rig);
}
gl_Position = transform * vec4(rig, 1);
}
)";
// an error, e, ortho to the ray will result in an angular error of
// x ~ tan x = 1/d * e
// if the error is parallel to the ray - and the viewer is r away from the
// ray origin - it will result in an angular error of
// x ~ tan x ~ r / d^2 * e
// we can simply do a mesh simplification with these error metrics
// errors in depth are scaled by r / d^2
// errors orthogonal to depth are scaled by 1 / d
// or we can do mesh simplification in an equi-error space
// this could be optimized using standard mesh simplification
// the actual mesh consists of points at direction(x, y) * depth(x, y)
// approximation of equi-error mesh:
// (a, b, c) = (x, y, k * r / depth(x, y))
// real coordinates can be recovered as:
// x = a, y = b, depth = k * r / c
// error introduced in (a, b) will be scaled on the direction sphere by
// 1 / focal // increasingly inaccurate for large fov lenses
// and when projected out to depth, (a, b) errors will be scaled by
// d / focal
// error introduced in c will cause errors in depth of
// k * r / c^2 = // using 1/c' = -1/c^2
// d^2 / (k * r) // using d = k * r / c <=> c = k * r / d
// from this we can compute k
// we want the angular error from ortho to be the same as from parallel
// so we want
// 1 / d * ortho error = r / d^2 * parallel error <=>
// 1 / d * d / focal = r / d^2 * d^2 / (k * r) <=>
// k = focal
// input is a, b, c
// s, t, and depth are recovered from a, b, c
// position is computed by a lookup in the direction texture
const std::string cameraMeshVS = R"(
#version 330 core
uniform mat4 transform;
uniform vec3 camera;
uniform float focalR;
uniform vec2 scale;
uniform sampler2D directions;
uniform bool forceMono;
in vec3 abc;
out vec2 texVar;
void main() {
// recover (s,t) from (a,b)
texVar = scale * abc.xy;
// recover depth from c
float depth = forceMono ? focalR / 50.0 : focalR / abc.z;
// scale direction texture coordinates; see cameraVS for discussion
const float kDirections = 128;
vec2 texVarScaled = (0.5 + texVar * (kDirections - 1)) / kDirections;
vec3 direction = texture(directions, texVarScaled).xyz;
gl_Position = transform * vec4(camera + depth * direction, 1);
}
)";
const std::string fullscreenVS = R"(
#version 330 core
in vec2 tex;
out vec2 texVar;
void main() {
gl_Position = vec4(2 * tex - 1, 0, 1);
texVar = tex;
}
)";
const std::string passthroughFS = R"(
#version 330 core
uniform sampler2D sampler;
in vec2 texVar;
out vec4 color;
void main() {
color = texture(sampler, texVar);
}
)";
const std::string cameraFS = R"(
#version 330 core
uniform int debug;
uniform sampler2D sampler;
in vec2 texVar;
out vec4 color;
void main() {
color = texture(sampler, texVar);
// 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;
}
)";
const std::string effectFS = R"(
#version 330 core
uniform float effect;
uniform sampler2D sampler;
in vec2 texVar;
out vec4 color;
void main() {
color = texture(sampler, texVar);
vec4 cyan = vec4(0.5, 1.0, 1.0, 1.0);
color += cyan
* smoothstep(1/(effect - 0.5), 1/effect, gl_FragCoord.w)
* smoothstep(1/(effect + 0.5), 1/effect, gl_FragCoord.w);
// alpha is a cone, 1 in the center, 0 at edges
float cone = max(0, 1 - 2 * length(texVar - 0.5));
color.a = cone;
}
)";
const std::string exponentialFS = R"(
#version 330 core
uniform sampler2D sampler;
in vec2 texVar;
out vec4 color;
void main() {
color = texture(sampler, texVar);
color.a = exp(30 * color.a) - 1;
}
)";
const std::string resolveFS = R"(
#version 330 core
uniform float fade;
uniform sampler2D sampler;
in vec2 texVar;
out vec4 color;
void main() {
vec4 premul = texture(sampler, texVar);
color.rgb = fade * premul.rgb / premul.a;
color.a = premul.a;
}
)";
cameraProgram = createProgram(cameraVS, cameraFS);
cameraMeshProgram = createProgram(cameraMeshVS, cameraFS);
effectMeshProgram = createProgram(cameraMeshVS, effectFS);
updateProgram = createProgram(fullscreenVS, exponentialFS);
resolveProgram = createProgram(fullscreenVS, resolveFS);
}
void RigScene::destroyPrograms() {
glDeleteProgram(resolveProgram);
glDeleteProgram(updateProgram);
glDeleteProgram(effectMeshProgram);
glDeleteProgram(cameraMeshProgram);
glDeleteProgram(cameraProgram);
}
static MatrixDepth loadPfm(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
CHECK(file) << "can't open " << filename;
// first line is identifier
std::string identifier;
file >> identifier;
CHECK_EQ(identifier, "Pf") << "expected grayscale PFM file";
// second line is width and height
int width, height;
file >> width >> height;
// third line is scale with endianness encoded in the sign
double scale;
file >> scale;
CHECK_LT(scale, 0) << "expected little endian PFM file";
// skip 1 whitespace character
file.ignore();
// get the rest
MatrixDepth result(height, width);
file.read((char*)result.data(), result.size() * sizeof(float));
return result;
}
static MatrixDepth fakePfm(const int width, const int height, const float depth) {
return MatrixDepth::Constant(height, width, depth);
}
// glDeleteTextures appears to be slow. recycle
static bool kRecycle = true;
static std::vector<GLuint> recycledTextures;
void recycleTexture(const GLuint texture) {
if (texture != 0) { // glDeleteTexture silently ignores 0
if (kRecycle) {
recycledTextures.push_back(texture);
} else {
glDeleteTextures(1, &texture);
}
}
}
GLuint bindRecycledTexture() {
if (recycledTextures.empty()) {
return createTexture();
}
GLuint result = recycledTextures.back();
recycledTextures.pop_back();
glBindTexture(GL_TEXTURE_2D, result);
return result;
}
void emptyRecycling() {
while (!recycledTextures.empty()) {
glDeleteTextures(1, &recycledTextures.back());
recycledTextures.pop_back();
}
}
static GLuint linearTexture2D(
const int width,
const int height,
const GLenum dstformat, // GL_RGB32F, for example
const GLenum srcformat, // GL_RGBA, for example
const GLenum srctype, // GL_UNSIGNED_BYTE, for example
const GLvoid* data) {
GLuint result = bindRecycledTexture();
glTexImage2D(
GL_TEXTURE_2D,
0, // level
dstformat,
width,
height,
0, // border
srcformat,
srctype,
data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
return result;
}
static GLuint linearCompressedTexture2D(
const int width,
const int height,
const GLenum format, // GL_COMPRESSED_RGBA_BPTC_UNORM, for example
const GLvoid* data,
const size_t size) {
GLuint result = bindRecycledTexture();
glCompressedTexImage2D(
GL_TEXTURE_2D,
0, // level
format,
width,
height,
0, // border
GLsizei(size),
data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
return result;
}
// create rainbow texture with white a distance of kWhite from camera
static GLuint fakeTexture(const MatrixDepth& depth) {
static const std::vector<Eigen::Vector3f> colors = {
{0, 0, 0}, {1, 0, 0}, {1, 0, 1}, {0, 0, 1}, {0, 1, 1}, {0, 1, 0}, {1, 1, 0}, {1, 1, 1}};
std::vector<uint8_t> fake(depth.size() * 4);
for (int y = 0; y < depth.rows(); ++y) {
for (int x = 0; x < depth.cols(); ++x) {
float value = kWhiteZ / (kUnit * depth(y, x));
Eigen::Vector3f color;
if (value < 0) {
color = colors.front();
} else if (value < 1) {
float v = value * (colors.size() - 1);
int i = static_cast<int>(v);
float f = v - i;
color = (1 - f) * colors[i] + f * colors[i + 1];
} else {
color = colors.back();
}
uint8_t* p = &fake[(depth.cols() * y + x) * 4];
for (int i = 0; i < 3; ++i) {
p[i] = static_cast<uint8_t>(color[i] * 255 + 0.5f);
}
p[3] = 255;
}
}
return linearTexture2D(
static_cast<int>(depth.cols()),
static_cast<int>(depth.rows()),
GL_SRGB8_ALPHA8,
GL_RGBA,
GL_UNSIGNED_BYTE,
fake.data());
}
// create grayscale texture with white a distance of kWhite from rig
template <typename T>
static GLuint fakeTexture(const MatrixDepth& depth, const Camera& camera) {
std::vector<T> fake(depth.size() * 4);
for (int y = 0; y < depth.rows(); ++y) {
for (int x = 0; x < depth.cols(); ++x) {
const Camera::Vector2 normalized((x + 0.5) / depth.cols(), (y + 0.5) / depth.rows());
const Camera::Vector2 pixel = normalized.cwiseProduct(camera.resolution);
const Camera::Vector3 rig = camera.rig(pixel).pointAt(depth(y, x));
const float value = std::min(1.0f, kWhiteZ / (kUnit * static_cast<float>(rig.norm())));
T* p = &fake[(depth.cols() * y + x) * 4];
for (int i = 0; i < 3; ++i) {
p[i] = static_cast<T>(value * std::numeric_limits<T>::max() + 0.5f);
}
p[3] = std::numeric_limits<T>::max();
}
}
return linearTexture2D(
static_cast<int>(depth.cols()),
static_cast<int>(depth.rows()),
RigScene::getInternalRGBAFormat(T()),
GL_RGBA,
getType(T()),
fake.data());
}
template <typename T>
static void debugSaveBinary(const std::string& filename, const T& data) {
const bool kSaveBinaries = false;
if (kSaveBinaries) {
std::ofstream file(filename, std::ios::binary);
file.write(reinterpret_cast<const char*>(data.data()), data.size() * sizeof(data[0]));
}
}
static GLuint loadImageTexture(const std::string& filename) {
// load color from an image file as 4 channels (rgba) per pixel
const int kDstChannels = 4;
int width, height, channels;
uint8_t* data = stbi_load(filename.c_str(), &width, &height, &channels, kDstChannels);
if (!data) {
return 0;
}
using VectorXb = Eigen::Matrix<uint8_t, Eigen::Dynamic, 1>;
debugSaveBinary(
boost::filesystem::path(filename).replace_extension(".rgba").string(),
Eigen::Map<VectorXb>(data, width * height * kDstChannels));
// hand it to opengl
GLuint result = linearTexture2D(width, height, GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, data);
// clean up and return
stbi_image_free(data);
return result;
}
static bool isBC7Supported() {
static bool cacheValid = false;
static bool cache = false;
if (cacheValid) {
return cache;
}
GLint count;
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
for (GLint i = 0; i < count; ++i) {
const char* ext = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, i));
if (strstr(ext, "texture_compression_bptc")) {
cache = true;
break;
}
}
if (cacheValid) {
return cache;
}
cacheValid = true; // atomic would avoid harmless possible overreporting
if (cache) {
return cache;
}
// BC7 not supported, report what is
LOG(INFO) << "BC7 (BPTC) not supported:";
for (GLint i = 0; i < count; ++i) {
const char* ext = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, i));
if (strstr(ext, "texture_compression_")) {
LOG(INFO) << folly::sformat("- supported: {}", ext);
}
}
return cache;
}
static void read32(uint32_t& dst, std::ifstream& file) {
file.read((char*)&dst, sizeof(uint32_t));
}
static GLuint loadDdsTexture(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) {
return 0;
}
// first there's the signature
uint32_t signature;
read32(signature, file);
CHECK_EQ(signature, 'D' << 0 | 'D' << 8 | 'S' << 16 | ' ' << 24);
/* then a DDS_HEADER
typedef struct {
DWORD dwSize;
DWORD dwFlags;
DWORD dwHeight;
DWORD dwWidth;
DWORD dwPitchOrLinearSize;
DWORD dwDepth;
DWORD dwMipMapCount;
DWORD dwReserved1[11];
DDS_PIXELFORMAT ddspf;
DWORD dwCaps;
DWORD dwCaps2;
DWORD dwCaps3;
DWORD dwCaps4;
DWORD dwReserved2;
} DDS_HEADER;
*/
std::vector<uint32_t> ddsHeader(1);
read32(ddsHeader[0], file);
CHECK_EQ(ddsHeader[0] % sizeof(uint32_t), 0);
ddsHeader.resize(ddsHeader[0] / sizeof(uint32_t));
file.read((char*)&ddsHeader[1], ddsHeader[0] - sizeof(uint32_t));
uint32_t width = ddsHeader[3]; // dwWidth
uint32_t height = ddsHeader[2]; // dwHeight
uint32_t size = ddsHeader[4]; // dwPitchOrLinearSize
uint32_t format = ddsHeader[20]; // dwFourCC inside ddspf
if (format == ('D' << 0 | 'X' << 8 | '1' << 16 | '0' << 24)) {
/* "DX10" indicates the presence of a DDS_HEADER_DXT10
typedef struct {
DXGI_FORMAT dxgiFormat;
D3D10_RESOURCE_DIMENSION resourceDimension;
UINT miscFlag;
UINT arraySize;
UINT miscFlags2;
} DDS_HEADER_DXT10;
*/
uint32_t ddsHeaderDxt10[5];
for (uint32_t& v : ddsHeaderDxt10) {
read32(v, file);
}
format = ddsHeaderDxt10[0]; // dxgiFormat
}
GLenum glFormat;
if (format == 99) { // BC7_UNORM_SRGB
if (!isBC7Supported()) {
return 0;
}
#ifndef GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM
#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D
#endif
glFormat = GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM;
} else {
CHECK(0) << ".dds file is not BC7_UNORM_SRGB (99) format";
}
// read the bytes
std::vector<uint8_t> compressed(size);
file.read((char*)compressed.data(), compressed.size());
debugSaveBinary(boost::filesystem::path(filename).replace_extension(".bc7").string(), compressed);
// hand it to opengl
return linearCompressedTexture2D(width, height, glFormat, compressed.data(), compressed.size());
}
static GLuint loadTexture(const std::string& filename) {
GLuint result = loadDdsTexture(filename + ".dds");
if (!result) {
result = loadImageTexture(filename + ".png");
}
if (!result) {
result = loadImageTexture(filename + ".jpg");
}
CHECK(result != 0) << "can't load image " << filename;
return result;
}
static MatrixDepth downscale(const MatrixDepth& hires, const int factor) {
// point-sample
int offset = factor / 2;
MatrixDepth result(hires.rows() / factor, hires.cols() / factor);
for (int y = 0; y < result.rows(); ++y) {
for (int x = 0; x < result.cols(); ++x) {
result(y, x) = hires(y * factor + offset, x * factor + offset);
}
}
return result;
}
static RigScene::Subframe createMeshSubframe(
const std::string& imagePrefix,
const std::string& depthPrefix,
const GLuint program) {
RigScene::Subframe subframe;
subframe.vertexArray = createVertexArray();
// read .obj file
std::vector<Eigen::Vector3f> vertexes;
std::vector<Eigen::Vector3i> faces;
std::string depth = depthPrefix + ".obj";
std::ifstream file(depth);
CHECK(file) << "can't open " << depth;
while (file) {
std::string first;
file >> first;
if (first == "v") {
Eigen::Vector3f v;
file >> v.x() >> v.y() >> v.z();
vertexes.push_back(v);
} else if (first == "f") {
Eigen::Vector3i f;
file >> f.x() >> f.y() >> f.z();
f -= f.Constant(1); // first vertex in an .obj file is 1
faces.push_back(f);
}
std::string skip;
getline(file, skip); // skip rest of line
}
debugSaveBinary(imagePrefix + ".vtx", vertexes);
debugSaveBinary(imagePrefix + ".idx", faces);
// pass vertexes and faces to opengl
GLuint meshVBO = createVertexAttributes(getAttribLocation(program, "abc"), vertexes);
GLuint meshIBO = createBuffer(GL_ELEMENT_ARRAY_BUFFER, faces);
subframe.indexCount = 3 * static_cast<GLsizei>(faces.size());
Eigen::Vector3f maximum(0, 0, 0);
for (const Eigen::Vector3f& v : vertexes) {
maximum = maximum.cwiseMax(v);
}
subframe.size = {maximum.x() + 0.5f, maximum.y() + 0.5f};
LOG(INFO) << folly::sformat(
"loaded {}x{} mesh, {} vertexes, {} faces",
subframe.size.x(),
subframe.size.y(),
vertexes.size(),
faces.size());
// load color
subframe.colorTexture = loadTexture(imagePrefix);
// clean up buffers
glBindVertexArray(0);
glDeleteBuffers(1, &meshIBO);
glDeleteBuffers(1, &meshVBO);
return subframe;
}
static RigScene::Subframe createPointCloudSubframeFromMemory(
const GLuint texture,
MatrixDepth& depthMap,
const GLuint program) {
RigScene::Subframe subframe;
subframe.colorTexture = texture;
// create vertex buffer for per-instance depth
subframe.vertexArray = createVertexArray();
using Vector1f = Eigen::Matrix<float, 1, 1>;
GLuint depthVBO = createVertexAttributes(
getAttribLocation(program, "depth"),
Eigen::Map<Eigen::Matrix<Vector1f, Eigen::Dynamic, 1>>(
reinterpret_cast<Vector1f*>(depthMap.data()), depthMap.size()));
glVertexAttribDivisor(getAttribLocation(program, "depth"), 1);
subframe.size = {depthMap.cols(), depthMap.rows()};
// create vertex buffer for vertex offsets
const float kRadius = 1.0;
std::vector<Eigen::Vector2f> offsets = {
{0.5f - kRadius, 0.5f - kRadius},
{0.5f + kRadius, 0.5f - kRadius},
{0.5f - kRadius, 0.5f + kRadius},
{0.5f + kRadius, 0.5f + kRadius},
};
GLuint offsetVBO = createVertexAttributes(getAttribLocation(program, "offset"), offsets);
// clean up buffers
glBindVertexArray(0);
glDeleteBuffers(1, &offsetVBO);
glDeleteBuffers(1, &depthVBO);
return subframe;
}
static RigScene::Subframe createPointCloudSubframe(
const std::string& imagePrefix,
const std::string& depthPrefix,
const GLuint program) {
// load depth map from file
MatrixDepth depthMap = depthPrefix.empty()
? fakePfm(kDirections, kDirections, static_cast<float>(Camera::kNearInfinity))
: loadPfm(depthPrefix + ".pfm");
const int kDownscaleFactor = 1;
if (kDownscaleFactor != 1) {
depthMap = downscale(depthMap, kDownscaleFactor);
}
const GLuint texture = imagePrefix.empty() ? fakeTexture(depthMap) : loadTexture(imagePrefix);
return createPointCloudSubframeFromMemory(texture, depthMap, program);
}
RigScene::Subframe RigScene::createSubframe(
const Camera& camera,
const GLuint buffer,
const uint64_t offset,
const folly::dynamic& layout) const {
Subframe subframe;
subframe.vertexArray = createVertexArray();
const int w(static_cast<int>(camera.resolution.x()));
const int h(static_cast<int>(camera.resolution.y()));
// PBO for color
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer);
if (isBC7Supported()) {
subframe.colorTexture = linearCompressedTexture2D(
w,
h,
GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM,
(GLvoid*)(layout[".bc7"]["offset"].getInt() - offset),
w * h); // bc7 is 1 byte/pixel
} else {
subframe.colorTexture = linearTexture2D(
w,
h,
GL_SRGB8_ALPHA8,
GL_RGBA,
GL_UNSIGNED_BYTE,
(GLvoid*)(layout[".rgba"]["offset"].getInt() - offset));
}
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
// VBO for vertexes
glBindBuffer(GL_ARRAY_BUFFER, buffer);
GLint location = getAttribLocation(cameraMeshProgram, "abc");
glVertexAttribPointer(
location, 3, GL_FLOAT, GL_TRUE, 0, (GLvoid*)(layout[".vtx"]["offset"].getInt() - offset));
glEnableVertexAttribArray(location);
// IBO for indexes
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
subframe.indexCount = static_cast<GLsizei>(layout[".idx"]["size"].getInt() / sizeof(uint32_t));
subframe.indexOffset = (GLvoid*)(layout[".idx"]["offset"].getInt() - offset);
subframe.size = {w, h};
// unbind vertex array before deleting buffer so vertex array keeps it alive
glBindVertexArray(0);
glDeleteBuffers(1, &buffer);
return subframe;
}
RigScene::Subframe RigScene::createSubframe(
const std::string& id,
const std::string& images,
const std::string& depths) const {
const std::string image = images.empty() ? "" : images + '/' + id;
const std::string depth = depths.empty() ? "" : depths + '/' + id + "_depth";
return useMesh ? createMeshSubframe(image, depth, cameraMeshProgram)
: createPointCloudSubframe(image, depth, cameraProgram);
}
std::vector<RigScene::Subframe> RigScene::createFrame(
const std::string& images,
const std::string& depths) const {
std::vector<RigScene::Subframe> subframes(rig.size());
for (int i = 0; i < int(subframes.size()); ++i) {
LOG(INFO) << folly::sformat("load subframe for {}", rig[i].id);
subframes[i] = createSubframe(rig[i].id, images, depths);
}
return subframes;
}
RigScene::Subframe RigScene::createPointCloudSubframeFromData(
const uint8_t* colorData,
uint16_t* depthData,
const int colorWidth,
const int colorHeight,
const int depthWidth,
const int depthHeight,
const float depthScale = 1.0f) const {
// Color
GLuint colorTexture = linearTexture2D(
colorWidth, colorHeight, GL_SRGB8_ALPHA8, GL_BGRA, GL_UNSIGNED_BYTE, colorData);
// Depth
using MatrixDepth16 = Eigen::Matrix<uint16_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
MatrixDepth16 depthMatrix16 = Eigen::Map<MatrixDepth16>(depthData, depthHeight, depthWidth);
MatrixDepth depthMatrix = depthMatrix16.cast<float>();
depthMatrix *= depthScale;
return createPointCloudSubframeFromMemory(colorTexture, depthMatrix, cameraProgram);
}
void RigScene::destroyFrame(std::vector<Subframe>& subframes) const {
for (Subframe& subframe : subframes) {
recycleTexture(subframe.colorTexture);
glDeleteVertexArrays(1, &subframe.vertexArray);
}
subframes.clear();
}
GLuint RigScene::createDirection(const Camera& camera) {
// create direction texture that tabelizes the rig() function
std::vector<Eigen::Vector3f> directions;
for (int y = 0; y < kDirections; ++y) {
for (int x = 0; x < kDirections; ++x) {
Camera::Vector2 c(x, y);
Camera::Vector2 pixel = c.cwiseProduct(camera.resolution) / (kDirections - 1);
Camera::Vector3 direction = camera.rig(pixel).direction();
if (isDepthZCoord) {
Camera::Real factor = -camera.pixelToCamera(pixel).z();
direction /= factor;
}
directions.push_back(direction.cast<float>());
}
}
return linearTexture2D(kDirections, kDirections, GL_RGB32F, GL_RGB, GL_FLOAT, directions.data());
}
// render a full-screen triangle with the given program and texture
void RigScene::fullscreen(GLuint program, GLuint texture, GLuint target) {
glUseProgram(program);
static const int kUnit = 0;
connectUnitWithTextureAndUniform(kUnit, target, texture, program, "sampler");
GLuint vertexArray = createVertexArray();
const std::vector<Eigen::Vector2f> tex{
{0, 0},
{0, 2},
{2, 0},
};
GLuint buffer = createVertexAttributes(program, "tex", tex);
glDrawArrays(GL_TRIANGLES, 0, 3); // draw the triangle
glDeleteBuffers(1, &buffer);
glDeleteVertexArrays(1, &vertexArray);
}
void RigScene::clearSubframe() const {
glBindFramebuffer(GL_FRAMEBUFFER, cameraFBO);
CHECK_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
GLuint RigScene::getProgram() const {
if (useMesh) {
return effect != 0 ? effectMeshProgram : cameraMeshProgram;
}
return cameraProgram;
}
void RigScene::renderSubframe(const int subframeIndex, const bool wireframe) const {
CHECK(subframeIndex < int(subframes.size()));
const RigScene::Subframe& subframe = subframes[subframeIndex];
const Camera& camera = rig[subframeIndex];
const GLuint directionTexture = directionTextures[subframeIndex];
GLuint program = getProgram();
glUseProgram(program);
// send camera position to gl
Eigen::Vector3f position = camera.position.cast<float>();
glUniform3fv(getUniformLocation(program, "camera"), 1, position.data());
setUniform(program, "scale", 1.0f / subframe.size.x(), 1.0f / subframe.size.y());
// activate direction texture for this camera
const int kDirectionUnit = 0;
connectUnitWith2DTextureAndUniform(kDirectionUnit, directionTexture, program, "directions");
// debug flags, if used
if (glGetUniformLocation(program, "debug") != -1) {
setUniform(program, "debug", debug);
}
// activate color texture for this camera
const int kColorUnit = 1;
connectUnitWith2DTextureAndUniform(kColorUnit, subframe.colorTexture, program, "sampler");
// activate vertex array for this camera
glPolygonMode(GL_FRONT_AND_BACK, wireframe ? GL_LINE : GL_FILL);
glBindVertexArray(subframe.vertexArray);
glEnable(GL_DEPTH_TEST);
if (useMesh) {
if (effect != 0) {
setUniform(program, "effect", effect);
}
float focalR = static_cast<float>(camera.getScalarFocal() * kR);
setUniform(program, "focalR", focalR);
setUniform(program, "forceMono", forceMono);
glDrawElements(GL_TRIANGLES, subframe.indexCount, GL_UNSIGNED_INT, subframe.indexOffset);
if (!backgroundSubframes.empty() && renderBackground) {
const RigScene::Subframe& backgroundSubframe = backgroundSubframes[subframeIndex];
const int kBackgroundColorUnit = 2;
connectUnitWith2DTextureAndUniform(
kBackgroundColorUnit, backgroundSubframe.colorTexture, program, "sampler");
glBindVertexArray(backgroundSubframe.vertexArray);
glDrawElements(
GL_TRIANGLES,
backgroundSubframe.indexCount,
GL_UNSIGNED_INT,
backgroundSubframe.indexOffset);
}
} else {
setUniform(program, "modulo", subframe.size.x());
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, subframe.size.prod());
setUniform(program, "isDepth", isDepth);
}
glDisable(GL_DEPTH_TEST);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
GLint RigScene::clearAccumulation() {
// save the currently bound framebuffer
GLint result;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &result);
// destroy existing framebuffers if they're the wrong size
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
const int w = viewport[2];
const int h = viewport[3];
if (cameraFBO != 0) {
glBindTexture(GL_TEXTURE_2D, cameraTexture);
int cw;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &cw);
int ch;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &ch);
if (cw != w || ch != h) {
destroyFramebuffers();
}
}
if (cameraFBO == 0) {
createFramebuffers(w, h);
}
glBindFramebuffer(GL_FRAMEBUFFER, accumulateFBO);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_FRAMEBUFFER_SRGB);
if (/* DISABLE CODE */ (false)) {
glCullFace(GL_FRONT);
glEnable(GL_CULL_FACE);
}
return result;
}
void RigScene::updateAccumulation() const {
// add camera buffer to accumulation buffer
glBindFramebuffer(GL_FRAMEBUFFER, accumulateFBO);
CHECK_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE);
// set up blend equations to accumulate premultiplied alpha
// dst.rgb = src.a * src.rgb + 1 * dst.rgb
// dst.a = 1 * src.a + 1 * dst.a
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
fullscreen(updateProgram, cameraTexture);
glDisable(GL_BLEND);
}
void RigScene::resolveAccumulation(GLint fbo, float fade) const {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glUseProgram(resolveProgram);
setUniform(resolveProgram, "fade", fade);
fullscreen(resolveProgram, accumulateTexture);
}
static Eigen::Matrix4f computeTransform(const Eigen::Matrix4f& view) {
// rig is specified in cm using z-is-up convention
// view is specified in m using y-is-up convention
Eigen::Matrix4f model;
model << kUnit, 0, 0, 0, 0, 0, kUnit, 0, 0, -kUnit, 0, 0, 0, 0, 0, 1;
return view * model;
}
void RigScene::updateTransform(const Eigen::Matrix4f& transform) const {
GLuint program = getProgram();
glUseProgram(program);
glUniformMatrix4fv(getUniformLocation(program, "transform"), 1, GL_FALSE, transform.data());
}
static bool
isVisible(const Camera& camera, const Camera::Vector2& frac, const Eigen::Matrix4f& transform) {
Camera::Vector2 pixel = frac.cwiseProduct(camera.resolution);
Eigen::Vector3f rig = camera.rigNearInfinity(pixel).cast<float>();
Eigen::Vector4f clip = transform * Eigen::Vector4f(rig.x(), rig.y(), rig.z(), 1);
return clip.w() > 0 && -clip.w() < clip.x() && clip.x() < clip.w() && -clip.w() < clip.y() &&
clip.y() < clip.w();
}
static bool isVisible(const Camera& camera, const Eigen::Matrix4f& transform) {
const int kIntervalsPerEdge = 3;
for (int y = 0; y <= kIntervalsPerEdge; ++y) {
for (int x = 0; x <= kIntervalsPerEdge; ++x) {
if (y == 0 || y == kIntervalsPerEdge) {
if (x == 0 || x == kIntervalsPerEdge) {
continue; // don't check the corners
}
}
Camera::Vector2 frac(
x / Camera::Real(kIntervalsPerEdge), y / Camera::Real(kIntervalsPerEdge));
if (isVisible(camera, frac, transform)) {
return true;
}
}
}
return false;
}
void RigScene::render(
const Eigen::Matrix4f& projview,
const float displacementMeters,
const bool doCulling,
const bool wireframe) {
Eigen::Matrix4f transform = computeTransform(projview);
updateTransform(transform);
GLint fbo = clearAccumulation();
culled.resize(rig.size());
for (int i = 0; i < int(rig.size()); ++i) {
culled[i] = doCulling && !isVisible(rig[i], transform);
if (i < int(subframes.size()) && subframes[i].isValid() && !culled[i]) {
clearSubframe();
renderSubframe(i, wireframe);
updateAccumulation();
}
}
const float kBeginFade = 0.5f;
const float kEndFade = 0.75f;
const float kMinimumFade = 0.05f;
// 0 until kBeginFade, then ramp down do kMinimumFade at kEndFade
const float fade = kMinimumFade +
(1.0f - kMinimumFade) *
std::max(0.0f, std::min(1.0f, (displacementMeters - kEndFade) / (kBeginFade - kEndFade)));
resolveAccumulation(fbo, fade * fade); // square to die off faster
CHECK_EQ(glGetError(), GL_NO_ERROR);
}
RigScene::~RigScene() {
if (cameraFBO) { // framebuffers are created lazily
destroyFramebuffers();
}
destroyFrame(subframes);
for (const GLuint& texture : directionTextures) {
recycleTexture(texture);
}
destroyPrograms();
emptyRecycling();
}
RigScene::RigScene(const Camera::Rig& rig, const bool useMesh, const bool isDepthZCoord)
: useMesh(useMesh), isDepthZCoord(isDepthZCoord), rig(rig) {
createPrograms();
for (const Camera& camera : rig) {
directionTextures.push_back(createDirection(camera));
}
cameraFBO = 0; // mark cameraFBO as uninitialized
}
RigScene::RigScene(const std::string& rigPath, const bool useMesh, const bool isDepthZCoord)
: RigScene(Camera::loadRig(rigPath), useMesh, isDepthZCoord) {}
RigScene::RigScene(
const std::string& rigPath,
const std::string& imageDir,
const std::string& depthDir,
const bool useMesh,
const bool isDepthZCoord)
: RigScene(rigPath, useMesh, isDepthZCoord) {
subframes = createFrame(imageDir, depthDir);
}
template <typename T>
void RigScene::createSubframes(
const Camera::Rig& rig,
std::vector<MatrixDepth>& depthMaps,
const std::vector<std::vector<T>>& images,
const std::vector<int>& imageWidths,
const std::vector<int>& imageHeights) {
const size_t numCameras = rig.size();
CHECK_GT(numCameras, 0);
CHECK_EQ(numCameras, depthMaps.size());
CHECK_EQ(numCameras, images.size());
subframes.resize(numCameras);
isDepth = images[0].empty();
for (size_t i = 0; i < numCameras; ++i) {
GLuint texture;
if (isDepth) {
texture = fakeTexture<T>(depthMaps[i], rig[i]);
} else {
CHECK_EQ(images[i].size(), imageWidths[i] * imageHeights[i] * 4);
texture = linearTexture2D(
imageWidths[i],
imageHeights[i],
getInternalRGBAFormat(T()),
GL_RGBA,
getType(T()),
images[i].data());
}
subframes[i] = createPointCloudSubframeFromMemory(texture, depthMaps[i], cameraProgram);
}
}
template <>
RigScene::RigScene(
const Camera::Rig& rig,
std::vector<MatrixDepth>& depthMaps,
const std::vector<std::vector<uint8_t>>& images,
const std::vector<int>& imageWidths,
const std::vector<int>& imageHeights)
: RigScene(rig, false) {
createSubframes(rig, depthMaps, images, imageWidths, imageHeights);
}
template <>
RigScene::RigScene(
const Camera::Rig& rig,
std::vector<MatrixDepth>& depthMaps,
const std::vector<std::vector<uint16_t>>& images,
const std::vector<int>& imageWidths,
const std::vector<int>& imageHeights)
: RigScene(rig, false) {
createSubframes(rig, depthMaps, images, imageWidths, imageHeights);
}
} // namespace fb360_dep