Implicit surface rendering via ray tracing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

340 lines
14 KiB

/*
* Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
*
* 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.
*
* SPDX-FileCopyrightText: Copyright (c) 2014-2022 NVIDIA CORPORATION
* SPDX-License-Identifier: Apache-2.0
*/
/*
* HDR sampling is loading an HDR image and creating an acceleration structure for
* sampling the environment.
*/
#define _USE_MATH_DEFINES
#include <numeric>
#include <chrono>
#include <array>
#include "hdr_env.hpp"
#include "nvmath/nvmath.h"
#include "stb_image.h"
#include "nvh/fileoperations.hpp"
#include "nvvk/debug_util_vk.hpp"
#include "nvvk/commands_vk.hpp"
#include "nvvk/descriptorsets_vk.hpp"
#include "nvvk/context_vk.hpp"
#include "shaders/dh_hdr.h"
#include "nvh/timesampler.hpp"
namespace nvvkhl {
// Forward declaration
std::vector<EnvAccel> createEnvironmentAccel(float*& pixels, const uint32_t& width, const uint32_t& height, float& average, float& integral);
HdrEnv::HdrEnv(nvvk::Context* ctx, nvvk::ResourceAllocator* allocator, uint32_t queueFamilyIndex)
{
setup(ctx->m_device, ctx->m_physicalDevice, queueFamilyIndex, allocator);
}
//--------------------------------------------------------------------------------------------------
//
//
void HdrEnv::setup(const VkDevice& device, const VkPhysicalDevice& /*physicalDevice*/, uint32_t familyIndex, nvvk::ResourceAllocator* allocator)
{
m_device = device;
m_alloc = allocator;
m_familyIndex = familyIndex;
m_debug.setup(device);
}
//--------------------------------------------------------------------------------------------------
//
//
void HdrEnv::destroy()
{
vkDestroyDescriptorPool(m_device, m_descPool, nullptr);
vkDestroyDescriptorSetLayout(m_device, m_descSetLayout, nullptr);
m_descPool = {};
m_descSetLayout = {};
m_alloc->destroy(m_texHdr);
m_alloc->destroy(m_accelImpSmpl);
}
//--------------------------------------------------------------------------------------------------
// Loading the HDR environment texture (HDR) and create the important accel structure
//
void HdrEnv::loadEnvironment(const std::string& hrdImage)
{
nvh::ScopedTimer st(__FUNCTION__);
m_valid = false;
if(!hrdImage.empty())
{
int32_t width{0};
int32_t height{0};
int32_t component{0};
float* pixels = nullptr;
if(stbi_is_hdr(hrdImage.c_str()) != 0)
{
pixels = stbi_loadf(hrdImage.c_str(), &width, &height, &component, STBI_rgb_alpha);
}
if(pixels != nullptr)
{
VkDeviceSize buffer_size = width * height * 4 * sizeof(float);
VkExtent2D img_size{static_cast<uint32_t>(width), static_cast<uint32_t>(height)};
m_hdrImageSize = img_size;
VkSamplerCreateInfo sampler_create_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_create_info.minFilter = VK_FILTER_LINEAR;
sampler_create_info.magFilter = VK_FILTER_LINEAR;
sampler_create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
// The map is parameterized with the U axis corresponding to the azimuthal angle, and V to the polar angle
// Therefore, in U the sampler will use VK_SAMPLER_ADDRESS_MODE_REPEAT (default), but V needs to use
// CLAMP_TO_EDGE to avoid having light leaking from one pole to another.
sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
VkFormat format = VK_FORMAT_R32G32B32A32_SFLOAT;
VkImageCreateInfo ic_info =
nvvk::makeImage2DCreateInfo(img_size, format, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT);
// We can use a different family index (1 - transfer), to allow loading in a different queue/thread than the display (0)
VkQueue queue = nullptr;
vkGetDeviceQueue(m_device, m_familyIndex, 0, &queue);
{
nvh::ScopedTimer st("Generating Acceleration structure");
{
nvvk::ScopeCommandBuffer cmd_buf(m_device, m_familyIndex, queue);
// Creating the importance sampling for the HDR and storing the info in the m_accelImpSmpl buffer
auto env_accel = createEnvironmentAccel(pixels, img_size.width, img_size.height, m_average, m_integral);
m_accelImpSmpl = m_alloc->createBuffer(cmd_buf, env_accel, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
m_debug.setObjectName(m_accelImpSmpl.buffer, "HDR_accel");
nvvk::Image image = m_alloc->createImage(cmd_buf, buffer_size, pixels, ic_info);
VkImageViewCreateInfo iv_info = nvvk::makeImageViewCreateInfo(image.image, ic_info);
m_texHdr = m_alloc->createTexture(image, iv_info, sampler_create_info);
m_debug.setObjectName(m_texHdr.image, "HDR");
}
m_alloc->finalizeAndReleaseStaging();
}
stbi_image_free(pixels);
m_valid = true;
}
}
if(!m_valid)
{ // Dummy
VkQueue queue = nullptr;
vkGetDeviceQueue(m_device, m_familyIndex, 0, &queue);
{
nvvk::ScopeCommandBuffer cmd_buf(m_device, m_familyIndex, queue);
VkImageCreateInfo image_create_info = nvvk::makeImage2DCreateInfo(VkExtent2D{1, 1}, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT);
std::vector<uint8_t> color{255, 255, 255, 255};
nvvk::Image image = m_alloc->createImage(cmd_buf, 4, color.data(), image_create_info);
VkImageViewCreateInfo iv_info = nvvk::makeImageViewCreateInfo(image.image, image_create_info);
VkSamplerCreateInfo sampler_create_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
m_texHdr = m_alloc->createTexture(image, iv_info, sampler_create_info);
m_accelImpSmpl = m_alloc->createBuffer(cmd_buf, color, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
}
m_alloc->finalizeAndReleaseStaging();
m_valid = false;
}
createDescriptorSetLayout();
}
//--------------------------------------------------------------------------------------------------
// Descriptors of the HDR and the acceleration structure
//
void HdrEnv::createDescriptorSetLayout()
{
nvvk::DescriptorSetBindings bind;
VkShaderStageFlags flags = VK_SHADER_STAGE_ALL;
bind.addBinding(EnvBindings::eHdr, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, flags); // HDR image
bind.addBinding(EnvBindings::eImpSamples, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, flags); // importance sampling
m_descPool = bind.createPool(m_device, 1);
CREATE_NAMED_VK(m_descSetLayout, bind.createLayout(m_device));
CREATE_NAMED_VK(m_descSet, nvvk::allocateDescriptorSet(m_device, m_descPool, m_descSetLayout));
std::vector<VkWriteDescriptorSet> writes;
VkDescriptorBufferInfo accel_imp_smpl{m_accelImpSmpl.buffer, 0, VK_WHOLE_SIZE};
writes.emplace_back(bind.makeWrite(m_descSet, EnvBindings::eHdr, &m_texHdr.descriptor));
writes.emplace_back(bind.makeWrite(m_descSet, EnvBindings::eImpSamples, &accel_imp_smpl));
vkUpdateDescriptorSets(m_device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//--------------------------------------------------------------------------------------------------
// Build alias map for the importance sampling: Each texel is associated to another texel, or alias,
// so that their combined intensities are a close as possible to the average of the environment map.
// This will later allow the sampling shader to uniformly select a texel in the environment, and
// select either that texel or its alias depending on their relative intensities
//
inline float buildAliasmap(const std::vector<float>& data, std::vector<EnvAccel>& accel)
{
auto size = static_cast<uint32_t>(data.size());
// Compute the integral of the emitted radiance of the environment map
// Since each element in data is already weighted by its solid angle
// the integral is a simple sum
float sum = std::accumulate(data.begin(), data.end(), 0.F);
// For each texel, compute the ratio q between the emitted radiance of the texel and the average
// emitted radiance over the entire sphere
// We also initialize the aliases to identity, ie. each texel is its own alias
auto f_size = static_cast<float>(size);
float inverse_average = f_size / sum;
for(uint32_t i = 0; i < size; ++i)
{
accel[i].q = data[i] * inverse_average;
accel[i].alias = i;
}
// Partition the texels according to their emitted radiance ratio wrt. average.
// Texels with a value q < 1 (ie. below average) are stored incrementally from the beginning of the
// array, while texels emitting higher-than-average radiance are stored from the end of the array
std::vector<uint32_t> partition_table(size);
uint32_t s = 0U;
uint32_t large = size;
for(uint32_t i = 0; i < size; ++i)
{
if(accel[i].q < 1.F)
partition_table[s++] = i;
else
partition_table[--large] = i;
}
// Associate the lower-energy texels to higher-energy ones. Since the emission of a high-energy texel may
// be vastly superior to the average,
for(s = 0; s < large && large < size; ++s)
{
// Index of the smaller energy texel
const uint32_t small_energy_index = partition_table[s];
// Index of the higher energy texel
const uint32_t high_energy_index = partition_table[large];
// Associate the texel to its higher-energy alias
accel[small_energy_index].alias = high_energy_index;
// Compute the difference between the lower-energy texel and the average
const float difference_with_average = 1.F - accel[small_energy_index].q;
// The goal is to obtain texel couples whose combined intensity is close to the average.
// However, some texels may have low energies, while others may have very high intensity
// (for example a sunset: the sky is quite dark, but the sun is still visible). In this case
// it may not be possible to obtain a value close to average by combining only two texels.
// Instead, we potentially associate a single high-energy texel to many smaller-energy ones until
// the combined average is similar to the average of the environment map.
// We keep track of the combined average by subtracting the difference between the lower-energy texel and the average
// from the ratio stored in the high-energy texel.
accel[high_energy_index].q -= difference_with_average;
// If the combined ratio to average of the higher-energy texel reaches 1, a balance has been found
// between a set of low-energy texels and the higher-energy one. In this case, we will use the next
// higher-energy texel in the partition when processing the next texel.
if(accel[high_energy_index].q < 1.0F)
large++;
}
// Return the integral of the emitted radiance. This integral will be used to normalize the probability
// distribution function (PDF) of each pixel
return sum;
}
// CIE luminance
inline float luminance(const float* color)
{
return color[0] * 0.2126F + color[1] * 0.7152F + color[2] * 0.0722F;
}
//--------------------------------------------------------------------------------------------------
// Create acceleration data for importance sampling
// See: https://arxiv.org/pdf/1901.05423.pdf
// And store the PDF into the ALPHA channel of pixels
//
inline std::vector<EnvAccel> createEnvironmentAccel(float*& pixels, const uint32_t& width, const uint32_t& height, float& average, float& integral)
{
const uint32_t rx = width;
const uint32_t ry = height;
// Create importance sampling data
std::vector<EnvAccel> env_accel(rx * ry);
std::vector<float> importance_data(rx * ry);
float cos_theta0 = 1.0F;
const float step_phi = static_cast<float>(2.0F * M_PI) / static_cast<float>(rx);
const float step_theta = static_cast<float>(M_PI) / static_cast<float>(ry);
double total = 0.0;
// For each texel of the environment map, we compute the related solid angle
// subtended by the texel, and store the weighted luminance in importance_data,
// representing the amount of energy emitted through each texel.
// Also compute the average CIE luminance to drive the tonemapping of the final image
for(uint32_t y = 0; y < ry; ++y)
{
const float theta1 = static_cast<float>(y + 1) * step_theta;
const float cos_theta1 = std::cos(theta1);
const float area = (cos_theta0 - cos_theta1) * step_phi; // solid angle
cos_theta0 = cos_theta1;
for(uint32_t x = 0; x < rx; ++x)
{
const uint32_t idx = y * rx + x;
const uint32_t idx4 = idx * 4;
float cie_luminance = luminance(&pixels[idx4]);
importance_data[idx] = area * std::max(pixels[idx4], std::max(pixels[idx4 + 1], pixels[idx4 + 2]));
total += cie_luminance;
}
}
average = static_cast<float>(total) / static_cast<float>(rx * ry);
// Build the alias map, which aims at creating a set of texel couples
// so that all couples emit roughly the same amount of energy. To this aim,
// each smaller radiance texel will be assigned an "alias" with higher emitted radiance
// As a byproduct this function also returns the integral of the radiance emitted by the environment
integral = buildAliasmap(importance_data, env_accel);
// We deduce the PDF of each texel by normalizing its emitted radiance by the radiance integral
const float inv_env_integral = 1.0F / integral;
for(uint32_t i = 0; i < rx * ry; ++i)
{
const uint32_t idx4 = i * 4;
pixels[idx4 + 3] = std::max(pixels[idx4], std::max(pixels[idx4 + 1], pixels[idx4 + 2])) * inv_env_integral;
}
return env_accel;
}
} // namespace nvvkhl