Files
filament/libs/rays/src/PathTracer.cpp
2019-05-23 22:06:43 -07:00

516 lines
20 KiB
C++

/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <rays/PathTracer.h>
#include <image/ImageOps.h>
#include <image/LinearImage.h>
#include <math/mat4.h>
#include <math/vec2.h>
#include <utils/JobSystem.h>
#ifdef FILAMENT_HAS_EMBREE
#include <embree3/rtcore.h>
#include <embree3/rtcore_ray.h>
#endif
using namespace filament::math;
using namespace image;
static constexpr size_t MIN_TILE_SIZE = 32;
static constexpr size_t MAX_TILES_COUNT = 2048;
static constexpr float inf = std::numeric_limits<float>::infinity();
static constexpr float EMPTY_SENTINEL = 2.0f;
struct PixelRectangle {
ushort2 topLeft;
ushort2 bottomRight;
};
// TODO: the following two functions should be shared with libs/ibl
static double3 hemisphereCosSample(double2 u) {
const double phi = 2 * M_PI * u.x;
const double cosTheta2 = 1 - u.y;
const double cosTheta = std::sqrt(cosTheta2);
const double sinTheta = std::sqrt(1 - cosTheta2);
return { sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta };
}
inline double2 hammersley(uint32_t i, float iN) {
constexpr float tof = 0.5f / 0x80000000U;
uint32_t bits = i;
bits = (bits << 16) | (bits >> 16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
return { i * iN, bits * tof };
}
static float3 randomPerp(float3 n) {
float3 perp = cross(n, float3{1, 0, 0});
float sqrlen = dot(perp, perp);
if (sqrlen <= std::numeric_limits<float>::epsilon()) {
perp = cross(n, float3{0, 1, 0});
sqrlen = dot(perp, perp);
}
return perp / sqrlen;
};
namespace filament {
namespace rays {
PathTracer::Builder& PathTracer::Builder::outputPlane(OutputPlane target, LinearImage image) {
mConfig.renderTargets[(int) target] = image;
return *this;
}
PathTracer::Builder& PathTracer::Builder::meshes(const SimpleMesh* meshes, size_t numMeshes) {
mConfig.meshes = meshes;
mConfig.numMeshes = numMeshes;
return *this;
}
PathTracer::Builder& PathTracer::Builder::filmCamera(const SimpleCamera& filmCamera) {
mConfig.filmCamera = filmCamera;
return *this;
}
PathTracer::Builder& PathTracer::Builder::uvCamera() {
mConfig.uvCamera = true;
return *this;
}
PathTracer::Builder& PathTracer::Builder::tileCallback(TileCallback onTile, void* userData) {
mConfig.tileCallback = onTile;
mConfig.tileUserData = userData;
return *this;
}
PathTracer::Builder& PathTracer::Builder::doneCallback(DoneCallback onDone, void* userData) {
mConfig.doneCallback = onDone;
mConfig.doneUserData = userData;
return *this;
}
PathTracer::Builder& PathTracer::Builder::samplesPerPixel(size_t numSamples) {
mConfig.samplesPerPixel = numSamples;
return *this;
}
PathTracer PathTracer::Builder::build() {
// TODO: check for valid configuration (consistent sizes etc)
return PathTracer(mConfig);
}
#ifndef FILAMENT_HAS_EMBREE
bool PathTracer::render() {
puts("Embree is not available.");
return false;
}
#else
// Note that the embree context can outlive the user-facing PathTracer object due to work
// occuring on multiple background threads.
struct EmbreeContext {
PathTracer::Config config;
std::atomic<int> numRemainingTiles;
RTCDevice device;
RTCScene scene;
};
static void renderTile(EmbreeContext* context, PixelRectangle rect) {
LinearImage& ao = context->config.renderTargets[(int) AMBIENT_OCCLUSION];
RTCScene embreeScene = context->scene;
const float inverseSampleCount = 1.0f / context->config.samplesPerPixel;
// Precompute some camera parameters.
const SimpleCamera& camera = context->config.filmCamera;
const float tnear = context->config.aoRayNear;
const float tfar = context->config.aoRayFar;
const float iw = 1.0f / ao.getWidth();
const float ih = 1.0f / ao.getHeight();
const float theta = camera.vfovDegrees * M_PI / 180;
const float f = tanf(theta / 2);
const float a = camera.aspectRatio;
const float3 org = camera.eyePosition;
const uint16_t hm1 = ao.getHeight() - 1;
// Compute the camera basis: view, right, and up vectors.
const float3 v = normalize(camera.targetPosition - org);
const float3 r = normalize(cross(v, camera.upVector));
const float3 u = cross(r, v);
// Given a pixel row and column, generate a ray from the eye through the film.
auto generateCameraRay = [=] (uint16_t row, uint16_t col) {
const uint16_t x = col;
const uint16_t y = hm1 - row;
const float s = (2.0f * (x + 0.5f) * iw - 1.0f);
const float t = (2.0f * (y + 0.5f) * ih - 1.0f);
const float3 dir = normalize(a * f * s * r - f * t * u + v);
return RTCRay {
.org_x = org.x,
.org_y = org.y,
.org_z = org.z,
.tnear = 0,
.dir_x = dir.x,
.dir_y = dir.y,
.dir_z = dir.z,
.time = 0,
.tfar = inf,
.mask = 0xffffffff
};
};
// Loop over all pixels in the tile.
for (size_t row = rect.topLeft.y, len = rect.bottomRight.y; row < len; ++row) {
for (size_t col = rect.topLeft.x, len = rect.bottomRight.x; col < len; ++col) {
RTCIntersectContext intersector;
rtcInitIntersectContext(&intersector);
RTCRay ray = generateCameraRay(row, col);
RTCRayHit rayhit { .ray = ray };
intersector.flags = RTC_INTERSECT_CONTEXT_FLAG_COHERENT;
rtcIntersect1(embreeScene, &intersector, &rayhit);
if (rayhit.ray.tfar != inf) {
intersector.flags = RTC_INTERSECT_CONTEXT_FLAG_INCOHERENT;
// TODO: For now we are using the geometric normal provided by embree which is not
// necessarily normalized.
float3 n = normalize(float3 { rayhit.hit.Ng_x, rayhit.hit.Ng_y, rayhit.hit.Ng_z });
float3 b = randomPerp(n);
float3 t = cross(n, b);
mat3 tangentFrame = {t, b, n};
RTCRay aoray {
.org_x = rayhit.ray.org_x + rayhit.ray.dir_x * rayhit.ray.tfar,
.org_y = rayhit.ray.org_y + rayhit.ray.dir_y * rayhit.ray.tfar,
.org_z = rayhit.ray.org_z + rayhit.ray.dir_z * rayhit.ray.tfar
};
float sum = 0;
for (size_t nsamp = 0, len = context->config.samplesPerPixel; nsamp < len; nsamp++) {
const double2 u = hammersley(nsamp, inverseSampleCount);
const float3 dir = tangentFrame * hemisphereCosSample(u);
aoray.dir_x = dir.x;
aoray.dir_y = dir.y;
aoray.dir_z = dir.z;
aoray.tnear = tnear;
aoray.tfar = tfar;
rtcOccluded1(embreeScene, &intersector, &aoray);
if (aoray.tfar == -inf) {
sum += 1.0f;
}
}
ao.getPixelRef(col, row)[0] = 1.0f - sum * inverseSampleCount;
}
}
}
}
static void renderTileToGbuffer(EmbreeContext* context, PixelRectangle rect) {
LinearImage& ao = context->config.renderTargets[(int) AMBIENT_OCCLUSION];
LinearImage& meshNormals = context->config.renderTargets[(int) MESH_NORMALS];
LinearImage& meshPositions = context->config.renderTargets[(int) MESH_POSITIONS];
RTCScene embreeScene = context->scene;
const float tnear = context->config.aoRayNear;
const float tfar = context->config.aoRayFar;
const float iw = 1.0f / ao.getWidth();
const float ih = 1.0f / ao.getHeight();
auto generateOrthoCameraRay = [=] (uint16_t row, uint16_t col) {
return RTCRay {
.org_x = float(col) * iw,
.org_y = float(row) * ih,
.org_z = 1,
.tnear = 0,
.dir_x = 0,
.dir_y = 0,
.dir_z = -1,
.time = 0,
.tfar = inf,
.mask = 0xffffffff
};
};
for (size_t row = rect.topLeft.y, len = rect.bottomRight.y; row < len; ++row) {
for (size_t col = rect.topLeft.x, len = rect.bottomRight.x; col < len; ++col) {
RTCIntersectContext intersector;
rtcInitIntersectContext(&intersector);
RTCRay ray = generateOrthoCameraRay(row, col);
RTCRayHit rayhit { .ray = ray };
intersector.flags = RTC_INTERSECT_CONTEXT_FLAG_COHERENT;
rtcIntersect1(embreeScene, &intersector, &rayhit);
float* position = meshPositions.getPixelRef(col, row);
float* normal = meshNormals.getPixelRef(col, row);
if (rayhit.ray.tfar != inf) {
RTCGeometry geo = rtcGetGeometry(embreeScene, rayhit.hit.geomID);
rtcInterpolate0(geo, rayhit.hit.primID, rayhit.hit.u, rayhit.hit.v,
RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, 0, position, 3);
rtcInterpolate0(geo, rayhit.hit.primID, rayhit.hit.u, rayhit.hit.v,
RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, 1, normal, 3);
// AO won't be computed until the second pass, but we show an instant preview of
// the chart shapes by setting a placeholder value in the AO map.
ao.getPixelRef(col, row)[0] = 0.5f;
} else {
normal[0] = EMPTY_SENTINEL;
}
}
}
}
static void dilateCharts(EmbreeContext* context) {
LinearImage& ao = context->config.renderTargets[(int) AMBIENT_OCCLUSION];
LinearImage& meshNormals = context->config.renderTargets[(int) MESH_NORMALS];
auto presence = [] (const LinearImage& normals, uint32_t col, uint32_t row, void*) {
return normals.getPixelRef(col, row)[0] != EMPTY_SENTINEL;
};
auto coords = image::computeCoordField(meshNormals, presence, nullptr);
auto dilated = image::voronoiFromCoordField(coords, ao);
memcpy(ao.getPixelRef(), dilated.getPixelRef(), sizeof(float) * ao.getWidth() * ao.getHeight());
}
static void renderTileFromGbuffer(EmbreeContext* context, PixelRectangle rect) {
LinearImage& ao = context->config.renderTargets[(int) AMBIENT_OCCLUSION];
LinearImage& meshNormals = context->config.renderTargets[(int) MESH_NORMALS];
LinearImage& meshPositions = context->config.renderTargets[(int) MESH_POSITIONS];
LinearImage& bentNormals = context->config.renderTargets[(int) BENT_NORMALS];
RTCScene embreeScene = context->scene;
const float inverseSampleCount = 1.0f / context->config.samplesPerPixel;
const float tnear = context->config.aoRayNear;
const float tfar = context->config.aoRayFar;
const size_t spp = context->config.samplesPerPixel;
for (size_t row = rect.topLeft.y, len = rect.bottomRight.y; row < len; ++row) {
for (size_t col = rect.topLeft.x, len = rect.bottomRight.x; col < len; ++col) {
RTCIntersectContext intersector;
rtcInitIntersectContext(&intersector);
intersector.flags = RTC_INTERSECT_CONTEXT_FLAG_INCOHERENT;
float* position = meshPositions.getPixelRef(col, row);
float* normal = meshNormals.getPixelRef(col, row);
if (normal[0] != EMPTY_SENTINEL) {
float3 n = { normal[0], normal[1], normal[2] };
float3 b = randomPerp(n);
float3 t = cross(n, b);
mat3 tangentFrame = {t, b, n};
RTCRay aoray { .org_x = position[0], .org_y = position[1], .org_z = position[2] };
float sum = 0;
float3 bentNormal = {0, 0, 0};
for (size_t nsamp = 0; nsamp < spp; nsamp++) {
const double2 u = hammersley(nsamp, inverseSampleCount);
const float3 dir = tangentFrame * hemisphereCosSample(u);
aoray.dir_x = dir.x;
aoray.dir_y = dir.y;
aoray.dir_z = dir.z;
aoray.tnear = tnear;
aoray.tfar = tfar;
rtcOccluded1(embreeScene, &intersector, &aoray);
if (aoray.tfar == -inf) {
bentNormal += dir;
sum += 1.0f;
}
}
if (bentNormals) {
bentNormal = normalize(bentNormal);
float* pBentNormal = bentNormals.getPixelRef(col, row);
pBentNormal[0] = bentNormal[0];
pBentNormal[1] = bentNormal[1];
pBentNormal[2] = bentNormal[2];
}
ao.getPixelRef(col, row)[0] = 1.0f - sum * inverseSampleCount;
}
}
}
}
template <typename RenderFn, typename CompletionFn>
void spawnTileJobs(EmbreeContext* context, RenderFn render, CompletionFn done) {
LinearImage& ao = context->config.renderTargets[(int) AMBIENT_OCCLUSION];
const size_t width = ao.getWidth();
const size_t height = ao.getHeight();
// Compute a reasonable tile size that will not create too many jobs.
int numTiles = 0;
size_t tileSize = MIN_TILE_SIZE;
while (true) {
int numCols = (width + tileSize - 1) / tileSize;
int numRows = (height + tileSize - 1) / tileSize;
numTiles = numCols * numRows;
if (numTiles <= MAX_TILES_COUNT) {
break;
}
tileSize *= 2;
}
context->numRemainingTiles = numTiles;
// Spawn one job per tile.
utils::JobSystem* js = utils::JobSystem::getJobSystem();
utils::JobSystem::Job* parent = js->createJob();
for (size_t row = 0; row < height; row += tileSize) {
for (size_t col = 0; col < width; col += tileSize) {
PixelRectangle rect;
rect.topLeft = {col, row};
rect.bottomRight = {col + tileSize, row + tileSize};
rect.bottomRight.x = std::min(rect.bottomRight.x, (uint16_t) width);
rect.bottomRight.y = std::min(rect.bottomRight.y, (uint16_t) height);
utils::JobSystem::Job* tile = utils::jobs::createJob(*js, parent, [=] {
render(context, rect);
// Decrement an atomic tile count to know when we're done.
if (--context->numRemainingTiles == 0) {
done(context);
}
});
js->run(tile);
}
}
js->run(parent);
}
bool PathTracer::render() {
const LinearImage& ao = mConfig.renderTargets[(int) AMBIENT_OCCLUSION];
const size_t width = ao.getWidth();
const size_t height = ao.getHeight();
EmbreeContext* context = new EmbreeContext { .config = mConfig };
if (!context->config.renderTargets[(int) MESH_NORMALS]) {
context->config.renderTargets[(int) MESH_NORMALS] = LinearImage(width, height, 3);
}
if (!context->config.renderTargets[(int) MESH_POSITIONS]) {
context->config.renderTargets[(int) MESH_POSITIONS] = LinearImage(width, height, 3);
}
if (!context->config.tileCallback) {
context->config.tileCallback = [] (ushort2, ushort2, void* userData) {};
}
if (!context->config.doneCallback) {
context->config.doneCallback = [] (void* userData) {};
}
// Create the embree device.
RTCDevice device = context->device = rtcNewDevice(nullptr);
rtcSetDeviceErrorFunction(device, [](void* userPtr, RTCError code, const char* str) {
printf("Embree error: %s.\n", str);
}, nullptr);
// Populates an embree scene from 3D position data, ignoring normals and UVs.
auto populate3DScene = [=] () {
for (size_t i = 0; i < context->config.numMeshes; ++i) {
const SimpleMesh& mesh = context->config.meshes[i];
RTCGeometry geo = rtcNewGeometry(device, RTC_GEOMETRY_TYPE_TRIANGLE);
rtcSetSharedGeometryBuffer(geo, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3,
mesh.positions, 0, mesh.positionsStride, mesh.numVertices);
rtcSetSharedGeometryBuffer(geo, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3,
mesh.indices, 0, sizeof(uint32_t) * 3, mesh.numIndices / 3);
rtcCommitGeometry(geo);
rtcAttachGeometry(context->scene, geo);
rtcReleaseGeometry(geo);
}
rtcCommitScene(context->scene);
};
// Populates an embree scene from 2D position data sourced from UV rather than the standard 3D
// vertex positions.
auto populate2DScene = [=] () {
for (size_t i = 0; i < context->config.numMeshes; ++i) {
const SimpleMesh& mesh = context->config.meshes[i];
RTCGeometry geo = rtcNewGeometry(device, RTC_GEOMETRY_TYPE_TRIANGLE);
rtcSetGeometryVertexAttributeCount(geo, 2);
rtcSetSharedGeometryBuffer(geo, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3,
mesh.uvs, 0, mesh.uvsStride, mesh.numVertices);
rtcSetSharedGeometryBuffer(geo, RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, 0, RTC_FORMAT_FLOAT3,
mesh.positions, 0, mesh.positionsStride, mesh.numVertices);
rtcSetSharedGeometryBuffer(geo, RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, 1, RTC_FORMAT_FLOAT3,
mesh.normals, 0, mesh.normalsStride, mesh.numVertices);
rtcSetSharedGeometryBuffer(geo, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3,
mesh.indices, 0, sizeof(uint32_t) * 3, mesh.numIndices / 3);
rtcCommitGeometry(geo);
rtcAttachGeometry(context->scene, geo);
rtcReleaseGeometry(geo);
}
rtcCommitScene(context->scene);
};
if (mConfig.uvCamera) {
context->scene = rtcNewScene(device);
populate2DScene();
// First render XYZ positions into the G-Buffer.
spawnTileJobs(context, [](EmbreeContext* context, PixelRectangle rect) {
renderTileToGbuffer(context, rect);
context->config.tileCallback(rect.topLeft, rect.bottomRight,
context->config.tileUserData);
}, [populate3DScene](EmbreeContext* context) {
rtcReleaseScene(context->scene);
// Now that the G-Buffer is ready, render the 3D scene.
context->scene = rtcNewScene(context->device);
populate3DScene();
spawnTileJobs(context, [](EmbreeContext* context, PixelRectangle rect) {
renderTileFromGbuffer(context, rect);
context->config.tileCallback(rect.topLeft, rect.bottomRight,
context->config.tileUserData);
}, [](EmbreeContext* context) {
dilateCharts(context);
context->config.doneCallback(context->config.doneUserData);
rtcReleaseScene(context->scene);
rtcReleaseDevice(context->device);
delete context;
});
});
} else {
// For the simple case of rendering from a film camera, we need only one pass.
context->scene = rtcNewScene(device);
populate3DScene();
spawnTileJobs(context, [](EmbreeContext* context, PixelRectangle rect) {
renderTile(context, rect);
context->config.tileCallback(rect.topLeft, rect.bottomRight,
context->config.tileUserData);
}, [](EmbreeContext* context) {
context->config.doneCallback(context->config.doneUserData);
rtcReleaseScene(context->scene);
rtcReleaseDevice(context->device);
delete context;
});
}
return true;
}
#endif // FILAMENT_HAS_EMBREE
} // namespace filament
} // namespace rays