diff --git a/src/appleseed.studio/mainwindow/renderingsettingswindow.cpp b/src/appleseed.studio/mainwindow/renderingsettingswindow.cpp index 3557d24215..ffe063d11b 100644 --- a/src/appleseed.studio/mainwindow/renderingsettingswindow.cpp +++ b/src/appleseed.studio/mainwindow/renderingsettingswindow.cpp @@ -939,6 +939,7 @@ namespace combobox->setToolTip(m_params_metadata.get_path("lighting_engine.help")); combobox->addItem("Unidirectional Path Tracer", "pt"); // combobox->addItem("Bidirectional Path Tracer", "bdpt"); + combobox->addItem("Guided Path Tracing", "gpt"); combobox->addItem("Stochastic Progressive Photon Mapping", "sppm"); construct(config, combobox); } @@ -1080,7 +1081,7 @@ namespace const int DefaultMaxDiffuseBounces = 3; const int max_bounces = - get_config(config, construct_bounce_param_path(bounce_type), default_max_bounces); + get_config(config, construct_bounce_param_path(widget_key_prefix, bounce_type), default_max_bounces); const std::string widget_max_bounce_key = widget_key_prefix + ".bounces.max_" + bounce_type + "_bounces"; @@ -1107,12 +1108,12 @@ namespace ? get_widget(widget_key_prefix + ".bounces.max_" + bounce_type + "_bounces") : -1; - set_config(config, construct_bounce_param_path(bounce_type), max_bounces); + set_config(config, construct_bounce_param_path(widget_key_prefix, bounce_type), max_bounces); } - static std::string construct_bounce_param_path(const std::string& bounce_type) + static std::string construct_bounce_param_path(const std::string& prefix, const std::string& bounce_type) { - return "pt.max_" + bounce_type + "_bounces"; + return prefix + ".max_" + bounce_type + "_bounces"; } }; @@ -1334,6 +1335,406 @@ namespace } }; + // + // Guided Path Tracer panel. + // + + class GuidedPathTracerPanel + : public LightingEnginePanel + { + Q_OBJECT + + public: + explicit GuidedPathTracerPanel(const Configuration& config, QWidget* parent = nullptr) + : LightingEnginePanel("Guided Path Tracer", parent) + { + fold(); + + QVBoxLayout* layout = new QVBoxLayout(); + container()->setLayout(layout); + + create_path_guiding_settings(layout); + + QGroupBox* groupbox = new QGroupBox("Components"); + layout->addWidget(groupbox); + + QVBoxLayout* sublayout = new QVBoxLayout(); + groupbox->setLayout(sublayout); + + sublayout->addWidget(create_checkbox("lighting_components.dl", "Direct Lighting")); + sublayout->addWidget(create_checkbox("lighting_components.ibl", "Image-Based Lighting")); + sublayout->addWidget(create_checkbox("lighting_components.caustics", "Caustics")); + + create_separate_bounce_settings_group(layout, "gpt", "gpt.max_bounces"); + + create_pt_volume_settings(layout); + create_pt_advanced_settings(layout); + + create_direct_link("lighting_components.dl", "gpt.enable_dl"); + create_direct_link("lighting_components.ibl", "gpt.enable_ibl"); + create_direct_link("lighting_components.caustics", "gpt.enable_caustics"); + + create_direct_link("gpt.bounces.rr_start_bounce", "gpt.rr_min_path_length"); + + create_direct_link("volume.distance_samples", "gpt.volume_distance_samples"); + create_direct_link("volume.optimize_for_lights_outside_volumes", "gpt.optimize_for_lights_outside_volumes"); + + create_direct_link("advanced.next_event_estimation", "gpt.next_event_estimation"); + + create_direct_link("advanced.dl.light_samples", "gpt.dl_light_samples"); + create_direct_link("advanced.dl.low_light_threshold", "gpt.dl_low_light_threshold"); + + create_direct_link("advanced.ibl.env_samples", "gpt.ibl_env_samples"); + + create_direct_link("advanced.light_sampler.algorithm", "light_sampler.algorithm"); + create_direct_link("advanced.light_sampler.enable_importance_sampling", "light_sampler.enable_importance_sampling"); + + create_direct_link("advanced.record_light_paths", "gpt.record_light_paths"); + + create_direct_link("advanced.clamp_roughness", "gpt.clamp_roughness"); + + create_direct_link("guiding.samples_per_pass", "gpt.samples_per_pass"); + create_direct_link("guiding.spatial_filter", "gpt.spatial_filter"); + create_direct_link("guiding.directional_filter", "gpt.directional_filter"); + create_direct_link("guiding.bsdf_sampling_fraction", "gpt.bsdf_sampling_fraction"); + create_direct_link("guiding.fixed_bsdf_sampling_fraction_value", "gpt.fixed_bsdf_sampling_fraction_value"); + create_direct_link("guiding.learning_rate", "gpt.learning_rate"); + create_direct_link("guiding.iteration_progression", "gpt.iteration_progression"); + create_direct_link("guiding.guided_bounce_mode", "gpt.guided_bounce_mode"); + create_direct_link("guiding.save_tree_iterations", "gpt.save_tree_iterations"); + create_direct_link("guiding.file_path", "gpt.file_path"); + + load_directly_linked_values(config); + + slot_changed_bsdf_sampling_fraction_mode(m_sampling_fraction_combobox->currentIndex()); + slot_changed_save_iterations_mode(m_save_iterations_combobox->currentIndex()); + + load_global_max_bounce_settings(config, "gpt", "gpt.max_bounces", 8); + load_separate_bounce_settings(config, "gpt", "guided", 8); + load_separate_bounce_settings(config, "gpt", "diffuse", 3); + load_separate_bounce_settings(config, "gpt", "glossy", 8); + load_separate_bounce_settings(config, "gpt", "specular", 8); + load_separate_bounce_settings(config, "gpt", "volume", 8, false); + + set_widget("advanced.unlimited_ray_intensity", !config.get_parameters().exist_path("gpt.max_ray_intensity")); + set_widget("advanced.max_ray_intensity", get_config(config, "gpt.max_ray_intensity", 1.0)); + } + + void save_config(Configuration& config) const override + { + save_directly_linked_values(config); + + save_bounce_settings(config, "gpt", "gpt.max_bounces"); + save_separate_bounce_settings(config, "gpt", "guided"); + save_separate_bounce_settings(config, "gpt", "diffuse"); + save_separate_bounce_settings(config, "gpt", "glossy"); + save_separate_bounce_settings(config, "gpt", "specular"); + save_separate_bounce_settings(config, "gpt", "volume", false); + + if (get_widget("advanced.unlimited_ray_intensity")) + config.get_parameters().remove_path("gpt.max_ray_intensity"); + else set_config(config, "gpt.max_ray_intensity", get_widget("advanced.max_ray_intensity")); + } + + private: + QComboBox* m_sampling_fraction_combobox; + QDoubleSpinBox* m_bsdf_sampling_fraction_spinbox; + QDoubleSpinBox* m_learning_rate_spinbox; + QComboBox* m_save_iterations_combobox; + QLineEdit* m_path_line_edit; + QWidget* m_browse_button; + ParamArray m_file_save_settings; + + void create_pt_volume_settings(QVBoxLayout* parent) + { + QGroupBox* groupbox = new QGroupBox("Participating Media"); + parent->addWidget(groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + groupbox->setLayout(layout); + + QFormLayout* sublayout = create_form_layout(); + layout->addLayout(sublayout); + + QSpinBox* volume_distance_samples = + create_integer_input("volume.distance_samples", 1, 1000, 1); + volume_distance_samples->setToolTip( + m_params_metadata.get_path("gpt.volume_distance_samples.help")); + sublayout->addRow("Volume Distance Samples:", volume_distance_samples); + + sublayout->addRow( + create_checkbox("volume.optimize_for_lights_outside_volumes", "Optimize for Lights Outside Volumes")); + } + + void create_pt_advanced_settings(QVBoxLayout* parent) + { + CollapsibleSectionWidget* collapsible_section = new CollapsibleSectionWidget("Advanced"); + parent->addWidget(collapsible_section); + + QVBoxLayout* layout = new QVBoxLayout(); + + create_pt_advanced_nee_settings(layout); + create_pt_advanced_optimization_settings(layout); + create_pt_advanced_diag_settings(layout); + collapsible_section->set_content_layout(layout); + } + + void create_pt_advanced_nee_settings(QVBoxLayout* parent) + { + QGroupBox* groupbox = create_checkable_groupbox("advanced.next_event_estimation", "Next Event Estimation"); + parent->addWidget(groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + groupbox->setLayout(layout); + + create_pt_advanced_nee_lightsampler_settings(layout); + create_pt_advanced_nee_dl_settings(layout); + create_pt_advanced_nee_ibl_settings(layout); + create_pt_advanced_nee_max_ray_intensity_settings(layout); + } + + void create_pt_advanced_nee_lightsampler_settings(QVBoxLayout* parent) + { + QFormLayout* sublayout = create_form_layout(); + parent->addLayout(sublayout); + + QComboBox* light_sampler = create_combobox("advanced.light_sampler.algorithm"); + light_sampler->setToolTip(m_params_metadata.get_path("light_sampler.algorithm.help")); + light_sampler->addItem("CDF", "cdf"); + light_sampler->addItem("Light Tree", "lighttree"); + sublayout->addRow("Light Sampler:", light_sampler); + + sublayout->addRow(create_checkbox("advanced.light_sampler.enable_importance_sampling", "Enable Importance Sampling")); + } + + void create_pt_advanced_nee_dl_settings(QVBoxLayout* parent) + { + QGroupBox* groupbox = new QGroupBox("Direct Lighting"); + parent->addWidget(groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + groupbox->setLayout(layout); + + QFormLayout* sublayout = create_form_layout(); + layout->addLayout(sublayout); + + QDoubleSpinBox* light_samples = create_double_input("advanced.dl.light_samples", 0.0, 1000000.0, 3, 1.0); + light_samples->setToolTip(m_params_metadata.get_path("gpt.dl_light_samples.help")); + sublayout->addRow("Light Samples:", light_samples); + + QDoubleSpinBox* low_light_threshold = create_double_input("advanced.dl.low_light_threshold", 0.0, 1000.0, 3, 0.1); + low_light_threshold->setToolTip(m_params_metadata.get_path("gpt.dl_low_light_threshold.help")); + sublayout->addRow("Low Light Threshold:", low_light_threshold); + } + + void create_pt_advanced_nee_ibl_settings(QVBoxLayout* parent) + { + QGroupBox* groupbox = new QGroupBox("Image-Based Lighting"); + parent->addWidget(groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + groupbox->setLayout(layout); + + QHBoxLayout* sublayout = create_horizontal_layout(); + layout->addLayout(sublayout); + + QDoubleSpinBox* env_samples = create_double_input("advanced.ibl.env_samples", 0.0, 1000000.0, 3, 1.0); + env_samples->setToolTip(m_params_metadata.get_path("gpt.ibl_env_samples.help")); + sublayout->addLayout(create_form_layout("Environment Samples:", env_samples)); + } + + void create_pt_advanced_nee_max_ray_intensity_settings(QVBoxLayout* parent) + { + QDoubleSpinBox* max_ray_intensity = create_double_input("advanced.max_ray_intensity", 0.0, 1.0e4, 1, 0.1); + max_ray_intensity->setToolTip(m_params_metadata.get_path("gpt.max_ray_intensity.help")); + + QCheckBox* unlimited_ray_intensity = create_checkbox("advanced.unlimited_ray_intensity", "Unlimited"); + parent->addLayout(create_form_layout("Max Ray Intensity:", create_horizontal_group(max_ray_intensity, unlimited_ray_intensity))); + connect(unlimited_ray_intensity, SIGNAL(toggled(bool)), max_ray_intensity, SLOT(setDisabled(bool))); + } + + void create_pt_advanced_optimization_settings(QVBoxLayout* parent) + { + QGroupBox* diag_groupbox = new QGroupBox("Optimizations"); + parent->addWidget(diag_groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + diag_groupbox->setLayout(layout); + + QFormLayout* sublayout = create_form_layout(); + layout->addLayout(sublayout); + + sublayout->addRow(create_checkbox("advanced.clamp_roughness", "Clamp Roughness")); + } + + void create_pt_advanced_diag_settings(QVBoxLayout* parent) + { + QGroupBox* diag_groupbox = new QGroupBox("Diagnostics"); + parent->addWidget(diag_groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + diag_groupbox->setLayout(layout); + + QFormLayout* sublayout = create_form_layout(); + layout->addLayout(sublayout); + + sublayout->addRow(create_checkbox("advanced.record_light_paths", "Record Light Paths")); + } + + void create_path_guiding_settings(QVBoxLayout* parent) + { + QGroupBox* groupbox = new QGroupBox("Path Guiding"); + parent->insertWidget(1, groupbox); + + QVBoxLayout* layout = create_vertical_layout(); + groupbox->setLayout(layout); + + QFormLayout* sublayout = create_form_layout(); + layout->addLayout(sublayout); + + QSpinBox* samples_per_pass = + create_integer_input("guiding.samples_per_pass", 1, 32, 1); + samples_per_pass->setToolTip( + m_params_metadata.get_path("gpt.samples_per_pass.help")); + //samples_per_pass->setValue(4); + sublayout->addRow("Samples Per Pass:", samples_per_pass); + + QComboBox* spatial_filter_combobox = create_combobox("guiding.spatial_filter"); + spatial_filter_combobox->setToolTip(m_params_metadata.get_path("gpt.spatial_filter.help")); + spatial_filter_combobox->addItem("Stochastic", "stochastic"); + spatial_filter_combobox->addItem("Box", "box"); + spatial_filter_combobox->addItem("Nearest", "nearest"); + + sublayout->addRow("Spatial Filter:", spatial_filter_combobox); + + QComboBox* directional_filter_combobox = create_combobox("guiding.directional_filter"); + directional_filter_combobox->setToolTip(m_params_metadata.get_path("gpt.directional_filter.help")); + directional_filter_combobox->addItem("Box", "box"); + directional_filter_combobox->addItem("Nearest", "nearest"); + + sublayout->addRow("Directional Filter:", directional_filter_combobox); + + m_sampling_fraction_combobox = create_combobox("guiding.bsdf_sampling_fraction"); + m_sampling_fraction_combobox->setToolTip(m_params_metadata.get_path("gpt.bsdf_sampling_fraction.help")); + m_sampling_fraction_combobox->addItem("Learn", "learn"); + m_sampling_fraction_combobox->addItem("Fixed", "fixed"); + + sublayout->addRow("BSDF Sampling Fraction Mode:", m_sampling_fraction_combobox); + + m_bsdf_sampling_fraction_spinbox = + create_double_input("guiding.fixed_bsdf_sampling_fraction_value", 0, 1, 2, 0.02); + m_bsdf_sampling_fraction_spinbox->setToolTip( + m_params_metadata.get_path("gpt.fixed_bsdf_sampling_fraction_value.help")); + //bsdf_sampling_fraction->setValue(0.5); + set_widget_width_for_text(m_bsdf_sampling_fraction_spinbox, "0.50", SpinBoxMargin, SpinBoxMinWidth); + sublayout->addRow("Fixed BSDF Sampling Fraction:", m_bsdf_sampling_fraction_spinbox); + + m_learning_rate_spinbox = + create_double_input("guiding.learning_rate", 0.01, 1.0, 2, 0.01); + m_learning_rate_spinbox->setToolTip( + m_params_metadata.get_path("gpt.learning_rate.help")); + m_learning_rate_spinbox->setValue(0.01); + set_widget_width_for_text(m_learning_rate_spinbox, "0.01", SpinBoxMargin, SpinBoxMinWidth); + sublayout->addRow("Learning Rate:", m_learning_rate_spinbox); + + connect( + m_sampling_fraction_combobox, SIGNAL(currentIndexChanged(int)), + SLOT(slot_changed_bsdf_sampling_fraction_mode(const int))); + + QComboBox* iteration_combobox = create_combobox("guiding.iteration_progression"); + iteration_combobox->setToolTip(m_params_metadata.get_path("gpt.iteration_progression.help")); + iteration_combobox->addItem("Combine Iterations", "combine"); + iteration_combobox->addItem("Automatic Cut-Off", "automatic"); + + sublayout->addRow("Iteration Progression:", iteration_combobox); + + m_save_iterations_combobox = create_combobox("guiding.save_tree_iterations"); + m_save_iterations_combobox->setToolTip(m_params_metadata.get_path("gpt.save_tree_iterations.help")); + m_save_iterations_combobox->addItem("None", "none"); + m_save_iterations_combobox->addItem("All", "all"); + m_save_iterations_combobox->addItem("Final", "final"); + + sublayout->addRow("Save iterations:", m_save_iterations_combobox); + + QVBoxLayout* file_layout = create_vertical_layout(); + m_path_line_edit = create_line_edit("guiding.file_path"); + m_browse_button = new QPushButton("Browse"); + m_browse_button->setToolTip(m_params_metadata.get_path("gpt.file_path.help")); + m_browse_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + file_layout->addWidget(m_path_line_edit); + file_layout->addWidget(m_browse_button); + sublayout->addRow("File Path:", file_layout); + + connect( + m_save_iterations_combobox, SIGNAL(currentIndexChanged(int)), + SLOT(slot_changed_save_iterations_mode(const int))); + + connect( + m_browse_button, SIGNAL(pressed()), + SLOT(slot_browse_button_pressed())); + + QComboBox* bounce_mode_combobox = create_combobox("guiding.guided_bounce_mode"); + bounce_mode_combobox->setToolTip(m_params_metadata.get_path("gpt.guided_bounce_mode.help")); + bounce_mode_combobox->addItem("Learned Distribution", "learn"); + bounce_mode_combobox->addItem("Strictly Diffuse", "strictly_diffuse"); + bounce_mode_combobox->addItem("Strictly Glossy", "strictly_glossy"); + bounce_mode_combobox->addItem("Prefer Diffuse", "prefer_diffuse"); + bounce_mode_combobox->addItem("Prefer Glossy", "prefer_glossy"); + + sublayout->addRow("Guided Bounce Mode:", bounce_mode_combobox); + create_guided_bounce_settings(sublayout, "gpt"); + } + + void create_guided_bounce_settings(QFormLayout* layout, const std::string& prefix) + { + const std::string widget_base_key = prefix + ".bounces."; + + QSpinBox* max_guided_bounces = create_integer_input(widget_base_key + "max_guided_bounces", 0, 100, 1); + + QCheckBox* unlimited_guided_bounces = create_checkbox(widget_base_key + "unlimited_guided_bounces", "Unlimited"); + + layout->addRow("Max Guided Bounces:", create_horizontal_group(max_guided_bounces, unlimited_guided_bounces)); + connect(unlimited_guided_bounces, SIGNAL(toggled(bool)), max_guided_bounces, SLOT(setDisabled(bool))); + } + + private slots: + void slot_changed_bsdf_sampling_fraction_mode(const int index) + { + const QString bsdf_sampling_mode = m_sampling_fraction_combobox->itemData(index).value(); + + m_bsdf_sampling_fraction_spinbox->setEnabled(bsdf_sampling_mode == "fixed"); + m_learning_rate_spinbox->setEnabled(bsdf_sampling_mode == "learn"); + } + + void slot_changed_save_iterations_mode(const int index) + { + const QString save_iterations_mode = m_save_iterations_combobox->itemData(index).value(); + + const bool enable_browsing = save_iterations_mode == "all" || save_iterations_mode == "final"; + m_browse_button->setEnabled(enable_browsing); + m_path_line_edit->setEnabled(enable_browsing); + } + + void slot_browse_button_pressed() + { + QFileDialog::Options options; + + QString filepath = + get_save_filename( + this, + "Save...", + "SD tree files (*.sdt)", + m_file_save_settings, + "", + options); + + if (!filepath.isEmpty()) + { + m_path_line_edit->setText(filepath); + } + } + }; + // // Stochastic Progressive Photon Mapping panel. // @@ -1801,6 +2202,7 @@ void RenderingSettingsWindow::create_panels(const Configuration& config) } m_panels.push_back(new UnidirectionalPathTracerPanel(config)); + m_panels.push_back(new GuidedPathTracerPanel(config)); if (!interactive) m_panels.push_back(new SPPMPanel(config)); diff --git a/src/appleseed/CMakeLists.txt b/src/appleseed/CMakeLists.txt index 4dde54042a..1c10e789e0 100644 --- a/src/appleseed/CMakeLists.txt +++ b/src/appleseed/CMakeLists.txt @@ -1211,6 +1211,23 @@ source_group ("renderer\\kernel\\lighting\\pt" FILES ${renderer_kernel_lighting_pt_sources} ) +set (renderer_kernel_lighting_gpt_sources + renderer/kernel/lighting/gpt/gptlightingengine.cpp + renderer/kernel/lighting/gpt/gptlightingengine.h + renderer/kernel/lighting/gpt/gptparameters.h + renderer/kernel/lighting/gpt/gptparameters.cpp + renderer/kernel/lighting/gpt/gptpasscallback.h + renderer/kernel/lighting/gpt/gptpasscallback.cpp + renderer/kernel/lighting/gpt/pathguidedsampler.h + renderer/kernel/lighting/gpt/pathguidedsampler.cpp +) +list (APPEND appleseed_sources + ${renderer_kernel_lighting_gpt_sources} +) +source_group ("renderer\\kernel\\lighting\\gpt" FILES + ${renderer_kernel_lighting_gpt_sources} +) + set (renderer_kernel_lighting_sppm_sources renderer/kernel/lighting/sppm/sppmimporton.h renderer/kernel/lighting/sppm/sppmimportonmap.cpp @@ -1243,6 +1260,7 @@ set (renderer_kernel_lighting_sources renderer/kernel/lighting/directlightingintegrator.h renderer/kernel/lighting/forwardlightsampler.cpp renderer/kernel/lighting/forwardlightsampler.h + renderer/kernel/lighting/guidedpathtracer.h renderer/kernel/lighting/ilightingengine.h renderer/kernel/lighting/imagebasedlighting.cpp renderer/kernel/lighting/imagebasedlighting.h @@ -1264,6 +1282,8 @@ set (renderer_kernel_lighting_sources renderer/kernel/lighting/pathvertex.cpp renderer/kernel/lighting/pathvertex.h renderer/kernel/lighting/scatteringmode.h + renderer/kernel/lighting/sdtree.h + renderer/kernel/lighting/sdtree.cpp renderer/kernel/lighting/tracer.cpp renderer/kernel/lighting/tracer.h renderer/kernel/lighting/volumelightingintegrator.cpp @@ -1420,6 +1440,10 @@ set (renderer_kernel_rendering_sources renderer/kernel/rendering/tilecallbackcollection.h renderer/kernel/rendering/timedrenderercontroller.cpp renderer/kernel/rendering/timedrenderercontroller.h + renderer/kernel/rendering/variancetrackingshadingresultframebuffer.cpp + renderer/kernel/rendering/variancetrackingshadingresultframebuffer.h + renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.cpp + renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.h ) list (APPEND appleseed_sources ${renderer_kernel_rendering_sources} diff --git a/src/appleseed/renderer/kernel/lighting/gpt/gptlightingengine.cpp b/src/appleseed/renderer/kernel/lighting/gpt/gptlightingengine.cpp new file mode 100644 index 0000000000..ee63df5293 --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/gptlightingengine.cpp @@ -0,0 +1,1445 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// Interface header. +#include "gptlightingengine.h" + +// appleseed.renderer headers. +#include "renderer/global/globallogger.h" +#include "renderer/global/globaltypes.h" +#include "renderer/kernel/aov/aovcomponents.h" +#include "renderer/kernel/lighting/directlightingintegrator.h" +#include "renderer/kernel/lighting/imagebasedlighting.h" +#include "renderer/kernel/lighting/lightpathrecorder.h" +#include "renderer/kernel/lighting/lightpathstream.h" +#include "renderer/kernel/lighting/guidedpathtracer.h" +#include "renderer/kernel/lighting/pathvertex.h" +#include "renderer/kernel/lighting/scatteringmode.h" +#include "renderer/kernel/lighting/volumelightingintegrator.h" +#include "renderer/kernel/shading/shadingcomponents.h" +#include "renderer/kernel/shading/shadingcontext.h" +#include "renderer/kernel/shading/shadingpoint.h" +#include "renderer/modeling/bsdf/bsdf.h" +#include "renderer/modeling/edf/edf.h" +#include "renderer/modeling/environment/environment.h" +#include "renderer/modeling/environmentedf/environmentedf.h" +#include "renderer/modeling/scene/scene.h" +#include "renderer/utility/spectrumclamp.h" +#include "renderer/utility/stochasticcast.h" + +// appleseed.foundation headers. +#include "foundation/containers/dictionary.h" +#include "foundation/math/mis.h" +#include "foundation/math/population.h" +#include "foundation/math/vector.h" +#include "foundation/string/string.h" +#include "foundation/utility/statistics.h" + +// Standard headers. +#include +#include +#include +#include +#include + +// Forward declarations. +namespace renderer { class BackwardLightSampler; } +namespace renderer { class PixelContext; } +namespace renderer { class TextureCache; } + +using namespace foundation; +using namespace std; + +namespace renderer +{ + +namespace +{ + // + // Guided Path Tracing lighting engine. + // + // Implementation of "Practical Path Guiding for Efficient Light-Transport Simulation" [Müller et al. 2017]. + // + + class GPTLightingEngine + : public ILightingEngine + { + public: + GPTLightingEngine( + STree* sd_tree, + const BackwardLightSampler& light_sampler, + LightPathRecorder& light_path_recorder, + const GPTParameters& params) + : m_sd_tree(sd_tree) + , m_params(params) + , m_light_sampler(light_sampler) + , m_light_path_stream( + m_params.m_record_light_paths + ? light_path_recorder.create_stream() + : nullptr) + , m_path_count(0) + , m_inf_volume_ray_warnings(0) + { + } + + void release() override + { + delete this; + } + + void print_settings() const override + { + m_params.print(); + } + + void compute_lighting( + SamplingContext& sampling_context, + const PixelContext& pixel_context, + const ShadingContext& shading_context, + const ShadingPoint& shading_point, + ShadingComponents& radiance, // output radiance, in W.sr^-1.m^-2 + AOVComponents& aov_components) override + { + if (m_light_path_stream) + { + m_light_path_stream->begin_path( + pixel_context, + shading_point.get_scene().get_render_data().m_active_camera, + shading_point.get_ray().m_org); + } + + if (m_params.m_next_event_estimation) + { + do_compute_lighting( + sampling_context, + shading_context, + shading_point, + radiance, + aov_components); + } + else + { + do_compute_lighting( + sampling_context, + shading_context, + shading_point, + radiance, + aov_components); + } + + if (m_light_path_stream) + m_light_path_stream->end_path(); + } + + template + void do_compute_lighting( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingPoint& shading_point, + ShadingComponents& radiance, // output radiance, in W.sr^-1.m^-2 + AOVComponents& aov_components) + { + PathVisitor path_visitor( + m_sd_tree, + m_params, + m_light_sampler, + sampling_context, + shading_context, + shading_point.get_scene(), + radiance, + aov_components, + m_light_path_stream); + + VolumeVisitor volume_visitor( + m_params, + m_light_sampler, + sampling_context, + shading_context, + shading_point.get_scene(), + radiance, + m_inf_volume_ray_warnings); + + GuidedPathTracer path_tracer( // false = not adjoint + m_sd_tree, + path_visitor, + volume_visitor, + m_params.m_bsdf_sampling_fraction_mode, + m_params.m_guided_bounce_mode, + m_params.m_rr_min_path_length, + m_params.m_max_bounces == ~size_t(0) ? ~size_t(0) : m_params.m_max_bounces + 1, + m_params.m_max_guided_bounces, + m_params.m_max_diffuse_bounces == ~size_t(0) ? ~size_t(0) : m_params.m_max_diffuse_bounces + 1, + m_params.m_max_glossy_bounces, + m_params.m_max_specular_bounces, + m_params.m_max_volume_bounces, + m_params.m_clamp_roughness, + shading_context.get_max_iterations()); + + const size_t path_length = + path_tracer.trace( + sampling_context, + shading_context, + shading_point); + + // Update statistics. + ++m_path_count; + m_path_length.insert(path_length); + } + + StatisticsVector get_statistics() const override + { + Statistics stats; + stats.insert("path count", m_path_count); + stats.insert("path length", m_path_length); + + return StatisticsVector::make("path tracing statistics", stats); + } + + private: + const GPTParameters m_params; + STree* m_sd_tree; + const BackwardLightSampler& m_light_sampler; + LightPathStream* m_light_path_stream; + + std::uint64_t m_path_count; + Population m_path_length; + + size_t m_inf_volume_ray_warnings; + static const size_t MaxInfVolumeRayWarnings = 5; + + // + // Base path visitor. + // + + class PathVisitorBase + { + public: + void on_first_diffuse_bounce( + const PathVertex& vertex, + const Spectrum& albedo) + { + m_aov_components.m_albedo = albedo; + } + + bool accept_scattering( + const ScatteringMode::Mode prev_mode, + const ScatteringMode::Mode next_mode) + { + assert(next_mode != ScatteringMode::None); + + if (!m_params.m_enable_caustics) + { + // Don't follow paths leading to caustics. + if (ScatteringMode::has_diffuse_or_volume(prev_mode) && + ScatteringMode::has_glossy_or_specular(next_mode)) + return false; + + // Ignore light emission after glossy-to-specular bounces to prevent another class of fireflies. + if (ScatteringMode::has_glossy(prev_mode) && + ScatteringMode::has_specular(next_mode)) + m_omit_emitted_light = true; + } + + return true; + } + + protected: + const GPTParameters& m_params; + const BackwardLightSampler& m_light_sampler; + SamplingContext& m_sampling_context; + const ShadingContext& m_shading_context; + const EnvironmentEDF* m_env_edf; + ShadingComponents& m_path_radiance; + AOVComponents& m_aov_components; + LightPathStream* m_light_path_stream; + bool m_omit_emitted_light; + STree* m_sd_tree; + + PathVisitorBase( + STree* sd_tree, + const GPTParameters& params, + const BackwardLightSampler& light_sampler, + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const Scene& scene, + ShadingComponents& path_radiance, + AOVComponents& aov_components, + LightPathStream* light_path_stream) + : m_params(params) + , m_light_sampler(light_sampler) + , m_sampling_context(sampling_context) + , m_shading_context(shading_context) + , m_env_edf(scene.get_environment()->get_environment_edf()) + , m_path_radiance(path_radiance) + , m_aov_components(aov_components) + , m_light_path_stream(light_path_stream) + , m_omit_emitted_light(false) + , m_sd_tree(sd_tree) + { + } + }; + + // + // Path visitor without next event estimation. + // + + class PathVisitorSimple + : public PathVisitorBase + { + public: + PathVisitorSimple( + STree* sd_tree, + const GPTParameters& params, + const BackwardLightSampler& light_sampler, + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const Scene& scene, + ShadingComponents& path_radiance, + AOVComponents& aov_components, + LightPathStream* light_path_stream) + : PathVisitorBase( + sd_tree, + params, + light_sampler, + sampling_context, + shading_context, + scene, + path_radiance, + aov_components, + light_path_stream) + { + } + + void on_miss(const PathVertex &vertex, GPTVertexPath& guided_path) + { + assert(vertex.m_prev_mode != ScatteringMode::None); + + // Can't look up the environment if there's no environment EDF. + if (m_env_edf == nullptr) + return; + + // When IBL is disabled, the environment should still be reflected by glossy and specular surfaces. + if (!m_params.m_enable_ibl && vertex.m_prev_mode == ScatteringMode::Diffuse) + return; + + // Evaluate the environment EDF. + Spectrum env_radiance(Spectrum::Illuminance); + float env_prob; + m_env_edf->evaluate( + m_shading_context, + -Vector3f(vertex.m_outgoing.get_value()), + env_radiance, + env_prob); + + // Update path radiance. + env_radiance *= vertex.m_throughput; + m_path_radiance.add_emission( + vertex.m_path_length, + vertex.m_aov_mode, + env_radiance); + + guided_path.add_radiance(env_radiance); + } + + void on_hit(const PathVertex &vertex, GPTVertexPath& guided_path) + { + // Emitted light contribution. + if ((!m_omit_emitted_light || m_params.m_enable_caustics) && + vertex.m_edf && + vertex.m_cos_on > 0.0 && + (vertex.m_path_length > 2 || m_params.m_enable_dl) && + (vertex.m_path_length < 2 || (vertex.m_edf->get_flags() & EDF::CastIndirectLight))) + { + // Compute the emitted radiance. + Spectrum emitted_radiance(Spectrum::Illuminance); + vertex.compute_emitted_radiance(m_shading_context, emitted_radiance); + + // Record light path event. + if (m_light_path_stream) + m_light_path_stream->hit_emitter(vertex, emitted_radiance); + + // Apply path throughput. + emitted_radiance *= vertex.m_throughput; + + // Update path radiance. + m_path_radiance.add_emission( + vertex.m_path_length, + vertex.m_aov_mode, + emitted_radiance); + + guided_path.add_radiance(emitted_radiance); + } + else + { + // Record light path event. + if (m_light_path_stream) + m_light_path_stream->hit_reflector(vertex); + } + } + + void on_scatter( + PathVertex& vertex, + GPTVertexPath& guided_path, + const float bsdf_sampling_fraction, + const bool enable_path_guiding) + { + // When caustics are disabled, disable glossy and specular components after a diffuse or volume bounce. + // Note that accept_scattering() is later going to return false in this case. + if (!m_params.m_enable_caustics) + { + if (vertex.m_prev_mode == ScatteringMode::Diffuse || + vertex.m_prev_mode == ScatteringMode::Volume) + vertex.m_scattering_modes &= ~(ScatteringMode::Glossy | ScatteringMode::Specular); + } + } + }; + + // + // Path visitor with next event estimation. + // + + class PathVisitorNextEventEstimation + : public PathVisitorBase + { + public: + PathVisitorNextEventEstimation( + STree* sd_tree, + const GPTParameters& params, + const BackwardLightSampler& light_sampler, + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const Scene& scene, + ShadingComponents& path_radiance, + AOVComponents& aov_components, + LightPathStream* light_path_stream) + : PathVisitorBase( + sd_tree, + params, + light_sampler, + sampling_context, + shading_context, + scene, + path_radiance, + aov_components, + light_path_stream) + , m_is_indirect_lighting(false) + { + } + + void on_miss(const PathVertex &vertex, GPTVertexPath& guided_path) + { + assert(vertex.m_prev_mode != ScatteringMode::None); + + // Can't look up the environment if there's no environment EDF. + if (m_env_edf == nullptr) + return; + + // When IBL is disabled, the environment should still be reflected by glossy and specular surfaces. + if (!m_params.m_enable_ibl && vertex.m_prev_mode == ScatteringMode::Diffuse) + return; + + // Evaluate the environment EDF. + Spectrum env_radiance(Spectrum::Illuminance); + float env_prob; + m_env_edf->evaluate( + m_shading_context, + -Vector3f(vertex.m_outgoing.get_value()), + env_radiance, + env_prob); + + // This may happen for points of the environment map with infinite components, + // which are then excluded from importance sampling and thus have zero weight. + if (env_prob == 0.0f) + return; + + // Multiple importance sampling. + if (vertex.m_prev_mode != ScatteringMode::Specular) + { + assert(vertex.m_prev_prob > 0.0f); + const float env_sample_count = max(m_params.m_ibl_env_sample_count, 1.0f); + const float mis_weight = + mis_power2( + 1.0f * vertex.m_prev_prob, + env_sample_count * env_prob); + env_radiance *= mis_weight; + } + + // Apply path throughput. + env_radiance *= vertex.m_throughput; + + // Optionally clamp secondary rays contribution. + if (m_params.m_has_max_ray_intensity && vertex.m_path_length > 1 && vertex.m_prev_mode != ScatteringMode::Specular) + clamp_contribution(env_radiance, m_params.m_max_ray_intensity); + + // Update path radiance. + m_path_radiance.add_emission( + vertex.m_path_length, + vertex.m_aov_mode, + env_radiance); + + if(!m_params.m_enable_ibl) + guided_path.add_radiance(env_radiance); + else + guided_path.add_indirect_radiance(env_radiance); + + } + + void on_hit(const PathVertex& vertex, GPTVertexPath& guided_path) + { + // Emitted light contribution. + if ((!m_omit_emitted_light || m_params.m_enable_caustics) && + vertex.m_edf && + vertex.m_cos_on > 0.0 && + (vertex.m_path_length > 2 || m_params.m_enable_dl) && + (vertex.m_path_length < 2 || (vertex.m_edf->get_flags() & EDF::CastIndirectLight))) + { + // Compute the emitted radiance. + Spectrum emitted_radiance(0.0f); + add_emitted_light_contribution(vertex, emitted_radiance); + + // Record light path event. + if (m_light_path_stream) + m_light_path_stream->hit_emitter(vertex, emitted_radiance); + + // Apply path throughput. + emitted_radiance *= vertex.m_throughput; + + // Optionally clamp secondary rays contribution. + if (m_params.m_has_max_ray_intensity && vertex.m_path_length > 1 && vertex.m_prev_mode != ScatteringMode::Specular) + clamp_contribution(emitted_radiance, m_params.m_max_ray_intensity); + + // Update path radiance. + m_path_radiance.add_emission( + vertex.m_path_length, + vertex.m_aov_mode, + emitted_radiance); + + guided_path.add_indirect_radiance(emitted_radiance); + } + else + { + // Record light path event. + if (m_light_path_stream) + m_light_path_stream->hit_reflector(vertex); + } + } + + void on_scatter( + PathVertex& vertex, + GPTVertexPath& guided_path, + const float bsdf_sampling_fraction, + const bool enable_path_guiding) + { + assert(vertex.m_scattering_modes != ScatteringMode::None); + + // Any light contribution after a diffuse or glossy bounce is considered indirect. + if (ScatteringMode::has_diffuse_or_glossy_or_volume(vertex.m_prev_mode)) + m_is_indirect_lighting = true; + + // When caustics are disabled, disable glossy and specular components after a diffuse or volume bounce. + if (!m_params.m_enable_caustics) + { + if (vertex.m_prev_mode == ScatteringMode::Diffuse || + vertex.m_prev_mode == ScatteringMode::Volume) + vertex.m_scattering_modes &= ~(ScatteringMode::Glossy | ScatteringMode::Specular); + } + + // Terminate the path if all scattering modes are disabled. + if (vertex.m_scattering_modes == ScatteringMode::None) + return; + + DirectShadingComponents vertex_radiance; + + if (vertex.m_bssrdf == nullptr) + { + // If we have an OSL shader, we need to choose one of the closures and set + // its shading basis into the shading point for the DirectLightingIntegrator + // to use it. + if (m_params.m_enable_dl || m_params.m_enable_ibl) + { + const Material::RenderData& material_data = + vertex.m_shading_point->get_material()->get_render_data(); + if (material_data.m_shader_group) + { + // todo: don't split if there's only one closure. + m_sampling_context.split_in_place(2, 1); + m_shading_context.choose_bsdf_closure_shading_basis( + *vertex.m_shading_point, + m_sampling_context.next2()); + } + } + } + + // Direct lighting contribution. + if (m_params.m_enable_dl || vertex.m_path_length > 1) + { + if (vertex.m_bsdf) + { + add_direct_lighting_contribution_bsdf( + *vertex.m_shading_point, + vertex.m_outgoing, + *vertex.m_bsdf, + vertex.m_bsdf_data, + vertex.m_scattering_modes, + vertex_radiance, + m_light_path_stream, + bsdf_sampling_fraction, + enable_path_guiding); + } + } + + // Image-based lighting contribution. + if (m_params.m_enable_ibl && m_env_edf) + { + if (vertex.m_bsdf) + { + add_image_based_lighting_contribution_bsdf( + *vertex.m_shading_point, + vertex.m_outgoing, + *vertex.m_bsdf, + vertex.m_bsdf_data, + vertex.m_scattering_modes, + vertex_radiance, + m_light_path_stream, + bsdf_sampling_fraction, + enable_path_guiding); + } + } + + // Apply path throughput. + vertex_radiance *= vertex.m_throughput; + + // Optionally clamp secondary rays contribution. + if (m_params.m_has_max_ray_intensity && vertex.m_path_length > 1 && vertex.m_prev_mode != ScatteringMode::Specular) + clamp_contribution(vertex_radiance, m_params.m_max_ray_intensity); + + // Update path radiance. + m_path_radiance.add( + vertex.m_path_length, + vertex.m_aov_mode, + vertex_radiance); + + guided_path.add_radiance(vertex_radiance.m_beauty); + } + + private: + bool m_is_indirect_lighting; + + void add_emitted_light_contribution( + const PathVertex& vertex, + Spectrum& vertex_radiance) + { + // Compute the emitted radiance. + Spectrum emitted_radiance(Spectrum::Illuminance); + vertex.compute_emitted_radiance(m_shading_context, emitted_radiance); + + // Multiple importance sampling. + if (vertex.m_prev_mode != ScatteringMode::Specular) + { + const float light_sample_count = max(m_params.m_dl_light_sample_count, 1.0f); + const float mis_weight = + mis_power2( + 1.0f * vertex.get_bsdf_prob_area(), + light_sample_count * vertex.get_light_prob_area(m_light_sampler)); + emitted_radiance *= mis_weight; + } + + // Add emitted light contribution. + vertex_radiance += emitted_radiance; + } + + void add_direct_lighting_contribution_bsdf( + const ShadingPoint& shading_point, + const Dual3d& outgoing, + const BSDF& bsdf, + const void* bsdf_data, + const int scattering_modes, + DirectShadingComponents& vertex_radiance, + LightPathStream* light_path_stream, + const float bsdf_sampling_fraction, + const bool enable_path_guiding) + { + DirectShadingComponents dl_radiance; + + const size_t light_sample_count = + stochastic_cast( + m_sampling_context, + m_params.m_dl_light_sample_count); + + if (light_sample_count == 0) + return; + + const PathGuidedSampler path_guided_sampler( + enable_path_guiding, + m_params.m_guided_bounce_mode, + m_sd_tree->get_d_tree(foundation::Vector3f(shading_point.get_point())), + bsdf_sampling_fraction, + bsdf, + bsdf_data, + scattering_modes, // bsdf_sampling_modes (unused) + shading_point, + m_sd_tree->is_built()); + + // This path will be extended via BSDF sampling: sample the lights only. + const DirectLightingIntegrator integrator( + m_shading_context, + m_light_sampler, + path_guided_sampler, + shading_point.get_time(), + scattering_modes, // light_sampling_modes + 1, // material_sample_count + light_sample_count, + m_params.m_dl_low_light_threshold, + m_is_indirect_lighting); + integrator.compute_outgoing_radiance_light_sampling_low_variance( + m_sampling_context, + MISPower2, + outgoing, + dl_radiance, + light_path_stream); + + // Divide by the sample count when this number is less than 1. + if (m_params.m_rcp_dl_light_sample_count > 0.0f) + dl_radiance *= m_params.m_rcp_dl_light_sample_count; + + // Add direct lighting contribution. + vertex_radiance += dl_radiance; + } + + void add_image_based_lighting_contribution_bsdf( + const ShadingPoint& shading_point, + const Dual3d& outgoing, + const BSDF& bsdf, + const void* bsdf_data, + const int scattering_modes, + DirectShadingComponents& vertex_radiance, + LightPathStream* light_path_stream, + const float bsdf_sampling_fraction, + const bool enable_path_guiding) + { + DirectShadingComponents ibl_radiance; + + const size_t env_sample_count = + stochastic_cast( + m_sampling_context, + m_params.m_ibl_env_sample_count); + + const PathGuidedSampler path_guided_sampler( + enable_path_guiding, + m_params.m_guided_bounce_mode, + m_sd_tree->get_d_tree(foundation::Vector3f(shading_point.get_point())), + bsdf_sampling_fraction, + bsdf, + bsdf_data, + scattering_modes, // bsdf_sampling_modes (unused) + shading_point, + m_sd_tree->is_built()); + + // This path will be extended via BSDF sampling: sample the environment only. + compute_ibl_environment_sampling( + m_sampling_context, + m_shading_context, + *m_env_edf, + outgoing, + path_guided_sampler, + scattering_modes, + 1, // bsdf_sample_count + env_sample_count, + ibl_radiance, + light_path_stream); + + // Divide by the sample count when this number is less than 1. + if (m_params.m_rcp_ibl_env_sample_count > 0.0f) + ibl_radiance *= m_params.m_rcp_ibl_env_sample_count; + + // Add image-based lighting contribution. + vertex_radiance += ibl_radiance; + } + }; + + // + // Base volume visitor. + // + + class VolumeVisitorBase + { + public: + bool accept_scattering(const ScatteringMode::Mode prev_mode) + { + return true; + } + + void on_scatter(PathVertex& vertex) + { + // When caustics are disabled, disable glossy and specular components after a diffuse or volume bounce. + // Note that accept_scattering() is later going to return false in this case. + if (!m_params.m_enable_caustics) + { + if (vertex.m_prev_mode == ScatteringMode::Diffuse || + vertex.m_prev_mode == ScatteringMode::Volume) + vertex.m_scattering_modes &= ~(ScatteringMode::Glossy | ScatteringMode::Specular); + } + } + + protected: + const GPTParameters& m_params; + const BackwardLightSampler& m_light_sampler; + SamplingContext& m_sampling_context; + const ShadingContext& m_shading_context; + ShadingComponents& m_path_radiance; + const EnvironmentEDF* m_env_edf; + bool m_is_indirect_lighting; + size_t& m_inf_volume_ray_warnings; + + VolumeVisitorBase( + const GPTParameters& params, + const BackwardLightSampler& light_sampler, + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const Scene& scene, + ShadingComponents& path_radiance, + size_t& inf_volume_ray_warnings) + : m_params(params) + , m_light_sampler(light_sampler) + , m_sampling_context(sampling_context) + , m_shading_context(shading_context) + , m_path_radiance(path_radiance) + , m_env_edf(scene.get_environment()->get_environment_edf()) + , m_is_indirect_lighting(false) + , m_inf_volume_ray_warnings(inf_volume_ray_warnings) + { + } + }; + + // + // Volume visitor without next event estimation. + // + + class VolumeVisitorSimple + : public VolumeVisitorBase + { + public: + VolumeVisitorSimple( + const GPTParameters& params, + const BackwardLightSampler& light_sampler, + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const Scene& scene, + ShadingComponents& path_radiance, + size_t& inf_volume_ray_warnings) + : VolumeVisitorBase( + params, + light_sampler, + sampling_context, + shading_context, + scene, + path_radiance, + inf_volume_ray_warnings) + { + } + + void visit_ray(PathVertex& vertex, const ShadingRay& volume_ray, GPTVertexPath& guided_path) + { + // Any light contribution after a diffuse, glossy or volume bounce is considered indirect. + if (ScatteringMode::has_diffuse_or_glossy_or_volume(vertex.m_scattering_modes)) + m_is_indirect_lighting = true; + } + }; + + // + // Volume visitor with next event estimation. + // + + class VolumeVisitorDistanceSampling + : public VolumeVisitorBase + { + public: + VolumeVisitorDistanceSampling( + const GPTParameters& params, + const BackwardLightSampler& light_sampler, + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const Scene& scene, + ShadingComponents& path_radiance, + size_t& inf_volume_ray_warnings) + : VolumeVisitorBase( + params, + light_sampler, + sampling_context, + shading_context, + scene, + path_radiance, + inf_volume_ray_warnings) + { + } + + float sample_distance( + const ShadingRay& volume_ray, + const float extinction, + float& distance) + { + m_sampling_context.split_in_place(1, 1); + + if (!volume_ray.is_finite()) + { + // Sample distance. + distance = sample_exponential_distribution( + m_sampling_context.next2(), extinction); + + // Calculate PDF of this distance sample. + return exponential_distribution_pdf(distance, extinction); + } + else + { + // Sample distance. + const float ray_length = static_cast(volume_ray.get_length()); + distance = sample_exponential_distribution_on_segment( + m_sampling_context.next2(), extinction, 0.0f, ray_length); + + // Calculate PDF of this distance sample. + return exponential_distribution_on_segment_pdf( + distance, extinction, 0.0f, ray_length); + } + } + + void visit_ray(PathVertex &vertex, const ShadingRay &volume_ray, GPTVertexPath &guided_path) + { + // Any light contribution after a diffuse, glossy or volume bounce is considered indirect. + if (ScatteringMode::has_diffuse_or_glossy_or_volume(vertex.m_scattering_modes)) + m_is_indirect_lighting = true; + + if (!volume_ray.is_finite()) + { + if (m_inf_volume_ray_warnings < MaxInfVolumeRayWarnings) + RENDERER_LOG_WARNING("volume ray of infinite length encountered."); + else if (m_inf_volume_ray_warnings == MaxInfVolumeRayWarnings) + RENDERER_LOG_WARNING("more volume rays of infinite length found, " + "omitting warning messages for brevity."); + ++m_inf_volume_ray_warnings; + } + + const ShadingRay::Medium* medium = volume_ray.get_current_medium(); + assert(medium != nullptr); + const Volume* volume = medium->get_volume(); + assert(volume != nullptr); + + const size_t light_sample_count = + stochastic_cast( + m_sampling_context, + m_params.m_dl_light_sample_count); + + VolumeLightingIntegrator integrator( + m_shading_context, + m_light_sampler, + *volume, + volume_ray, + vertex.m_volume_data, + *vertex.m_shading_point, + vertex.m_scattering_modes, + m_params.m_distance_sample_count, + light_sample_count, + m_params.m_dl_low_light_threshold, + m_is_indirect_lighting); + + DirectShadingComponents radiance; + if (m_params.m_enable_equiangular_sampling) + { + integrator.compute_radiance_combined_sampling( + m_sampling_context, + MISPower2, + radiance); + } + else + { + integrator.compute_radiance_exponential_sampling( + m_sampling_context, + MISPower2, + radiance); + } + + radiance *= vertex.m_throughput; + m_path_radiance.add(vertex.m_path_length, vertex.m_aov_mode, radiance); + + guided_path.add_radiance(radiance.m_beauty); + } + }; + }; +} + + +// +// PTLightingEngineFactory class implementation. +// + +Dictionary GPTLightingEngineFactory::get_params_metadata() +{ + Dictionary metadata; + + metadata.dictionaries().insert( + "spatial_filter", + Dictionary() + .insert("type", "enum") + .insert("values", "stochastic|box|nearest") + .insert("default", "stochastic") + .insert("label", "Spatial Filter") + .insert("help", "Spatial filtering mode for SD-tree recordings") + .insert( + "options", + Dictionary() + .insert( + "stochastic", + Dictionary() + .insert("label", "Stochastic") + .insert("help", "Randomly offset position of recording")) + .insert( + "box", + Dictionary() + .insert("label", "Box") + .insert("help", "Record radiance in nearby S-tree leaf nodes")) + .insert( + "nearest", + Dictionary() + .insert("label", "Nearest") + .insert("help", "Record radiance to the nearest S-tree leaf node only")))); + + metadata.dictionaries().insert( + "directional_filter", + Dictionary() + .insert("type", "enum") + .insert("values", "box|nearest") + .insert("default", "box") + .insert("label", "Directional Filter") + .insert("help", "Directional mode for SD-tree recordings") + .insert( + "options", + Dictionary() + .insert( + "box", + Dictionary() + .insert("label", "Box") + .insert("help", "Record radiance in nearby D-tree leaf nodes")) + .insert( + "nearest", + Dictionary() + .insert("label", "Nearest") + .insert("help", "Record radiance to the nearest D-tree leaf node only")))); + + metadata.dictionaries().insert( + "bsdf_sampling_fraction", + Dictionary() + .insert("type", "enum") + .insert("values", "learn|fixed") + .insert("default", "learn") + .insert("label", "BSDF Sampling Fraction") + .insert("help", "BSDF Sampling Fraction Mode") + .insert( + "options", + Dictionary() + .insert( + "learn", + Dictionary() + .insert("label", "Learn BSDF Sampling Fraction") + .insert("help", "Automatically learn the optimal sampling fraction at each spatial leaf node")) + .insert( + "fixed", + Dictionary() + .insert("label", "Fixed BSDF Sampling Fraction") + .insert("help", "Use a fixed sampling fraction")))); + + metadata.dictionaries().insert( + "iteration_progression", + Dictionary() + .insert("type", "enum") + .insert("values", "combine|automatic") + .insert("default", "combine") + .insert("label", "Learning Iteration Progression") + .insert("help", "Behavior of the iterative radiance distribution learning") + .insert( + "options", + Dictionary() + .insert( + "combine", + Dictionary() + .insert("label", "Combine Iterations") + .insert("help", "Combine iterations based on their estimated variances")) + .insert( + "automatic", + Dictionary() + .insert("label", "Automatic Final Iteration") + .insert("help", "Initiate a final iteration when projected variance estimate increases")))); + + metadata.dictionaries().insert( + "guided_bounce_mode", + Dictionary() + .insert("type", "enum") + .insert("values", "learn|strictly_diffuse|strictly_glossy|prefer_diffuse|prefer_glossy") + .insert("default", "learn") + .insert("label", "Scattering Mode for Path Guided Bounces") + .insert("help", "How path guided bounces should be treated internally") + .insert( + "options", + Dictionary() + .insert( + "learn", + Dictionary() + .insert("label", "Learned Distribution") + .insert("help", "Guided bounce modes are based on the learned radiance distribution")) + .insert( + "strictly_diffuse", + Dictionary() + .insert("label", "Strictly Diffuse") + .insert("help", "Guided bounces are always treated as Diffuse")) + .insert( + "strictly_glossy", + Dictionary() + .insert("label", "Strictly Glossy") + .insert("help", "Guided bounces are always treated as Glossy")) + .insert( + "prefer_diffuse", + Dictionary() + .insert("label", "Prefer Diffuse") + .insert("help", "Guided bounces are treated as Diffuse if remaining modes allow, Glossy otherwise")) + .insert( + "prefer_glossy", + Dictionary() + .insert("label", "Prefer Glossy") + .insert("help", "Guided bounces are treated as Glossy if remaining modes allow, Diffuse otherwise")))); + + metadata.dictionaries().insert( + "save_tree_iterations", + Dictionary() + .insert("type", "enum") + .insert("values", "none|all|final") + .insert("default", "none") + .insert("label", "Save option for SD tree structure") + .insert("help", "Set which SD tree iterations to save to disk") + .insert( + "options", + Dictionary() + .insert( + "none", + Dictionary() + .insert("label", "None") + .insert("help", "Do not save the SD tree")) + .insert( + "all", + Dictionary() + .insert("label", "All") + .insert("help", "Save all SD tree iterations to disk")) + .insert( + "final", + Dictionary() + .insert("label", "Final") + .insert("help", "Save final SD tree iteration to disk")))); + + metadata.dictionaries().insert( + "file_path", + Dictionary() + .insert("type", "text") + .insert("default", "") + .insert("label", "File Path") + .insert("help", "Path to disk location where files will be saved")); + + metadata.dictionaries().insert( + "guided_bounce_mode", + Dictionary() + .insert("type", "enum") + .insert("values", "learn|strictly_diffuse|strictly_glossy|prefer_diffuse|prefer_glossy") + .insert("default", "learn") + .insert("label", "Scattering Mode for Path Guided Bounces") + .insert("help", "How path guided bounces should be treated internally") + .insert( + "options", + Dictionary() + .insert( + "learn", + Dictionary() + .insert("label", "Learned Distribution") + .insert("help", "Guided bounce modes are based on the learned radiance distribution")) + .insert( + "strictly_diffuse", + Dictionary() + .insert("label", "Strictly Diffuse") + .insert("help", "Guided bounces are always treated as Diffuse")) + .insert( + "strictly_glossy", + Dictionary() + .insert("label", "Strictly Glossy") + .insert("help", "Guided bounces are always treated as Glossy")) + .insert( + "prefer_diffuse", + Dictionary() + .insert("label", "Prefer Diffuse") + .insert("help", "Guided bounces are treated as Diffuse if remaining modes allow, Glossy otherwise")) + .insert( + "prefer_glossy", + Dictionary() + .insert("label", "Prefer Glossy") + .insert("help", "Guided bounces are treated as Glossy if remaining modes allow, Diffuse otherwise")))); + + metadata.dictionaries().insert( + "samples_per_pass", + Dictionary() + .insert("type", "int") + .insert("default", "4") + .insert("unlimited", "false") + .insert("min", "1") + .insert("max", "32") + .insert("label", "Samples Per Pass") + .insert("help", "Number of samples for one path guiding pass")); + + metadata.dictionaries().insert( + "fixed_bsdf_sampling_fraction_value", + Dictionary() + .insert("type", "float") + .insert("default", "0.5") + .insert("unlimited", "false") + .insert("min", "0.0") + .insert("max", "1.0") + .insert("label", "Fixed BSDF Sampling Fraction") + .insert("help", "Ratio between BSDF sampling and SD-tree sampling")); + + metadata.dictionaries().insert( + "learning_rate", + Dictionary() + .insert("type", "float") + .insert("default", "0.01") + .insert("unlimited", "false") + .insert("min", "0.001") + .insert("max", "0.5") + .insert("label", "BSDF Sampling Fraction Learning Rate") + .insert("help", "BSDF Sampling Fraction Learning Rate")); + + metadata.dictionaries().insert( + "dl_light_samples", + Dictionary() + .insert("type", "float") + .insert("default", "1.0") + .insert("label", "Light Samples") + .insert("help", "Number of samples used to estimate direct lighting")); + + metadata.dictionaries().insert( + "enable_dl", + Dictionary() + .insert("type", "bool") + .insert("default", "true") + .insert("label", "Enable Direct Lighting") + .insert("help", "Enable direct lighting")); + + metadata.dictionaries().insert( + "enable_ibl", + Dictionary() + .insert("type", "bool") + .insert("default", "on") + .insert("label", "Enable IBL") + .insert("help", "Enable image-based lighting")); + + metadata.dictionaries().insert( + "enable_caustics", + Dictionary() + .insert("type", "bool") + .insert("default", "false") + .insert("label", "Enable Caustics") + .insert("help", "Enable caustics")); + + metadata.dictionaries().insert( + "max_bounces", + Dictionary() + .insert("type", "int") + .insert("default", "8") + .insert("unlimited", "true") + .insert("min", "0") + .insert("label", "Max Bounces") + .insert("help", "Maximum number of bounces")); + + metadata.dictionaries().insert( + "max_guided_bounces", + Dictionary() + .insert("type", "int") + .insert("default", "8") + .insert("unlimited", "true") + .insert("min", "0") + .insert("label", "Max Guided Bounces") + .insert("help", "Maximum number of path guided bounces")); + + metadata.dictionaries().insert( + "max_diffuse_bounces", + Dictionary() + .insert("type", "int") + .insert("default", "3") + .insert("unlimited", "true") + .insert("min", "0") + .insert("label", "Max Diffuse Bounces") + .insert("help", "Maximum number of diffuse bounces")); + + metadata.dictionaries().insert( + "max_glossy_bounces", + Dictionary() + .insert("type", "int") + .insert("default", "8") + .insert("unlimited", "true") + .insert("min", "0") + .insert("label", "Max Glossy Bounces") + .insert("help", "Maximum number of glossy bounces")); + + metadata.dictionaries().insert( + "max_specular_bounces", + Dictionary() + .insert("type", "int") + .insert("default", "8") + .insert("unlimited", "true") + .insert("min", "0") + .insert("label", "Max Specular Bounces") + .insert("help", "Maximum number of specular bounces")); + + metadata.dictionaries().insert( + "max_volume_bounces", + Dictionary() + .insert("type", "int") + .insert("default", "0") + .insert("unlimited", "false") + .insert("min", "0") + .insert("label", "Max Volume Bounces") + .insert("help", "Maximum number of volume scattering events (0 = single scattering)")); + + metadata.dictionaries().insert( + "rr_min_path_length", + Dictionary() + .insert("type", "int") + .insert("default", "6") + .insert("min", "1") + .insert("label", "Russian Roulette Start Bounce") + .insert("help", "Consider pruning low contribution paths starting with this bounce")); + + metadata.dictionaries().insert( + "next_event_estimation", + Dictionary() + .insert("type", "bool") + .insert("default", "true") + .insert("label", "Next Event Estimation") + .insert("help", "Explicitly connect path vertices to light sources to improve efficiency")); + + metadata.dictionaries().insert( + "dl_light_samples", + Dictionary() + .insert("type", "float") + .insert("default", "1.0") + .insert("label", "Light Samples") + .insert("help", "Number of samples used to estimate direct lighting")); + + metadata.dictionaries().insert( + "dl_low_light_threshold", + Dictionary() + .insert("type", "float") + .insert("default", "0.0") + .insert("label", "Low Light Threshold") + .insert("help", "Light contribution threshold to disable shadow rays")); + + metadata.dictionaries().insert( + "ibl_env_samples", + Dictionary() + .insert("type", "float") + .insert("default", "1.0") + .insert("label", "IBL Samples") + .insert("help", "Number of samples used to estimate environment lighting")); + + metadata.dictionaries().insert( + "clamp_roughness", + Dictionary() + .insert("type", "bool") + .insert("default", "false") + .insert("label", "Clamp BSDF roughness") + .insert("help", "Clamp BSDF roughness parameter to a maximum level to reduce fireflies in glossy reflections")); + + metadata.dictionaries().insert( + "max_ray_intensity", + Dictionary() + .insert("type", "float") + .insert("default", "1.0") + .insert("unlimited", "true") + .insert("min", "0.0") + .insert("label", "Max Ray Intensity") + .insert("help", "Clamp intensity of rays (after the first bounce) to this value to reduce fireflies")); + + metadata.dictionaries().insert( + "volume_distance_samples", + Dictionary() + .insert("type", "int") + .insert("default", "2") + .insert("unlimited", "true") + .insert("min", "1") + .insert("label", "Volume Distance Samples") + .insert("help", "Number of distance samples for volume rendering")); + + metadata.dictionaries().insert( + "optimize_for_lights_outside_volumes", + Dictionary() + .insert("type", "bool") + .insert("default", "false") + .insert("label", "Optimize for Lights Outside Volumes") + .insert("help", "Optimize distance sampling for lights that are located outside volumes")); + + metadata.dictionaries().insert( + "record_light_paths", + Dictionary() + .insert("type", "bool") + .insert("default", "false") + .insert("label", "Record Light Paths") + .insert("help", "Record light paths in memory to later allow visualizing them or saving them to disk")); + + return metadata; +} + +GPTLightingEngineFactory::GPTLightingEngineFactory( + STree* sd_tree, + const BackwardLightSampler& light_sampler, + LightPathRecorder& light_path_recorder, + const GPTParameters& params) + : m_sd_tree(sd_tree) + , m_light_sampler(light_sampler) + , m_light_path_recorder(light_path_recorder) + , m_params(params) +{ +} + +void GPTLightingEngineFactory::release() +{ + delete this; +} + +ILightingEngine* GPTLightingEngineFactory::create() +{ + return + new GPTLightingEngine( + m_sd_tree, + m_light_sampler, + m_light_path_recorder, + m_params); +} + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/lighting/gpt/gptlightingengine.h b/src/appleseed/renderer/kernel/lighting/gpt/gptlightingengine.h new file mode 100644 index 0000000000..618ef4a07b --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/gptlightingengine.h @@ -0,0 +1,78 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/kernel/lighting/gpt/gptparameters.h" +#include "renderer/kernel/lighting/ilightingengine.h" +#include "renderer/kernel/lighting/sdtree.h" + +// appleseed.foundation headers. +#include "foundation/platform/compiler.h" + +// Forward declarations. +namespace foundation { class Dictionary; } +namespace renderer { class BackwardLightSampler; } +namespace renderer { class LightPathRecorder; } + +namespace renderer +{ + +// +// Implementation of "Practical Path Guiding for Efficient Light-Transport Simulation" [Müller et al. 2017]. +// + +class GPTLightingEngineFactory + : public ILightingEngineFactory +{ + public: + // Return parameters metadata. + static foundation::Dictionary get_params_metadata(); + + // Constructor. + GPTLightingEngineFactory( + STree* sd_tree, + const BackwardLightSampler& light_sampler, + LightPathRecorder& light_path_recorder, + const GPTParameters& params); + + // Delete this instance. + void release() override; + + // Return a new path tracing lighting engine instance. + ILightingEngine* create() override; + + private: + STree* m_sd_tree; + const BackwardLightSampler& m_light_sampler; + LightPathRecorder& m_light_path_recorder; + GPTParameters m_params; +}; + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/lighting/gpt/gptparameters.cpp b/src/appleseed/renderer/kernel/lighting/gpt/gptparameters.cpp new file mode 100644 index 0000000000..395c5cdafc --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/gptparameters.cpp @@ -0,0 +1,403 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// Interface header. +#include "gptparameters.h" + +// appleseed.renderer headers. +#include "renderer/global/globallogger.h" +#include "renderer/utility/paramarray.h" +#include "renderer/utility/settingsparsing.h" + +// appleseed.foundation headers. +#include "foundation/string/string.h" + +// Standard headers. +#include + +using namespace foundation; +using namespace std; + +namespace renderer +{ +SpatialFilter get_spatial_filter(const ParamArray& params) +{ + const std::string name = params.get_required("spatial_filter", "stochastic"); + + if (name == "nearest") + { + return SpatialFilter::Nearest; + } + else if (name == "box") + { + return SpatialFilter::Box; + } + else if (name == "stochastic") + { + return SpatialFilter::Stochastic; + } + else + { + RENDERER_LOG_WARNING("Unknown parameter for spatial filter"); + return SpatialFilter::Stochastic; + } +} + +DirectionalFilter get_directional_filter(const ParamArray& params) +{ + const std::string name = params.get_required("directional_filter", "box"); + + if (name == "nearest") + { + return DirectionalFilter::Nearest; + } + else if (name == "box") + { + return DirectionalFilter::Box; + } + else + { + RENDERER_LOG_WARNING("Unknown parameter for directional filter"); + return DirectionalFilter::Box; + } +} + +IterationProgression get_iteration_progression(const ParamArray& params) +{ + const std::string name = params.get_required("iteration_progression", "combine"); + + if (name == "automatic") + { + return IterationProgression::Automatic; + } + else if (name == "combine") + { + return IterationProgression::Combine; + } + else + { + RENDERER_LOG_WARNING("Unknown parameter for iteration progression"); + return IterationProgression::Combine; + } +} + +BSDFSamplingFractionMode get_bsdf_sampling_fraction_mode(const ParamArray& params) +{ + const std::string name = params.get_required("bsdf_sampling_fraction", "learn"); + + if (name == "fixed") + { + return BSDFSamplingFractionMode::Fixed; + } + else if (name == "learn") + { + return BSDFSamplingFractionMode::Learn; + } + else + { + RENDERER_LOG_WARNING("Unknown parameter for bsdf sampling fraction mode"); + return BSDFSamplingFractionMode::Learn; + } +} + +GuidedBounceMode get_guided_bounce_mode(const ParamArray& params) +{ + const std::string name = params.get_required("guided_bounce_mode", "learn"); + + if (name == "learn") + { + return GuidedBounceMode::Learn; + } + else if (name == "strictly_diffuse") + { + return GuidedBounceMode::StrictlyDiffuse; + } + else if (name == "strictly_glossy") + { + return GuidedBounceMode::StrictlyGlossy; + } + else if (name == "prefer_diffuse") + { + return GuidedBounceMode::PreferDiffuse; + } + else if (name == "prefer_glossy") + { + return GuidedBounceMode::PreferGlossy; + } + else + { + RENDERER_LOG_WARNING("Unknown parameter for guided bounce mode"); + return GuidedBounceMode::Learn; + } +} + +SaveMode get_save_mode(const ParamArray& params) +{ + const std::string name = params.get_required("save_tree_iterations", "none"); + + if (name == "none") + { + return SaveMode::None; + } + else if (name == "all") + { + return SaveMode::All; + } + else if (name == "final") + { + return SaveMode::Final; + } + else + { + RENDERER_LOG_WARNING("Unknown parameter for save mode"); + return SaveMode::None; + } +} + +GPTParameters::GPTParameters(const ParamArray& params) + : m_samples_per_pass(params.get_optional("samples_per_pass", 4)) + , m_fixed_bsdf_sampling_fraction(params.get_optional("fixed_bsdf_sampling_fraction_value", 0.5f)) + , m_learning_rate(params.get_optional("learning_rate", 0.01f)) + , m_bsdf_sampling_fraction_mode(get_bsdf_sampling_fraction_mode(params)) + , m_guided_bounce_mode(get_guided_bounce_mode(params)) + , m_save_mode(get_save_mode(params)) + , m_save_path(params.get_optional("file_path", "none")) + , m_directional_filter(get_directional_filter(params)) + , m_spatial_filter(get_spatial_filter(params)) + , m_iteration_progression(get_iteration_progression(params)) + , m_enable_dl(params.get_optional("enable_dl", true)) + , m_enable_ibl(params.get_optional("enable_ibl", true)) + , m_enable_caustics(params.get_optional("enable_caustics", false)) + , m_max_bounces(fixup_bounces(params.get_optional("max_bounces", 8))) + , m_max_diffuse_bounces(fixup_bounces(params.get_optional("max_diffuse_bounces", 3))) + , m_max_guided_bounces(fixup_bounces(params.get_optional("max_guided_bounces", 8))) + , m_max_glossy_bounces(fixup_bounces(params.get_optional("max_glossy_bounces", 8))) + , m_max_specular_bounces(fixup_bounces(params.get_optional("max_specular_bounces", 8))) + , m_max_volume_bounces(fixup_bounces(params.get_optional("max_volume_bounces", 8))) + , m_clamp_roughness(params.get_optional("clamp_roughness", false)) + , m_rr_min_path_length(fixup_path_length(params.get_optional("rr_min_path_length", 6))) + , m_next_event_estimation(params.get_optional("next_event_estimation", true)) + , m_dl_light_sample_count(params.get_optional("dl_light_samples", 1.0f)) + , m_dl_low_light_threshold(params.get_optional("dl_low_light_threshold", 0.0f)) + , m_ibl_env_sample_count(params.get_optional("ibl_env_samples", 1.0f)) + , m_has_max_ray_intensity(params.strings().exist("max_ray_intensity")) + , m_max_ray_intensity(params.get_optional("max_ray_intensity", 0.0f)) + , m_distance_sample_count(params.get_optional("volume_distance_samples", 2)) + , m_enable_equiangular_sampling(!params.get_optional("optimize_for_lights_outside_volumes", false)) + , m_record_light_paths(params.get_optional("record_light_paths", false)) +{ + // Precompute the reciprocal of the number of light samples. + m_rcp_dl_light_sample_count = + m_dl_light_sample_count > 0.0f && m_dl_light_sample_count < 1.0f + ? 1.0f / m_dl_light_sample_count + : 0.0f; + + // Precompute the reciprocal of the number of environment samples. + m_rcp_ibl_env_sample_count = + m_ibl_env_sample_count > 0.0f && m_ibl_env_sample_count < 1.0f + ? 1.0f / m_ibl_env_sample_count + : 0.0f; +} + +void GPTParameters::print() const +{ + std::string bsdf_mode_string = "bsdf sampling mode "; + + switch (m_bsdf_sampling_fraction_mode) + { + case BSDFSamplingFractionMode::Fixed: + bsdf_mode_string += "Fixed\n"; + bsdf_mode_string += " fixed bsdf sampling fraction " + pretty_scalar(m_fixed_bsdf_sampling_fraction, 2); + break; + + case BSDFSamplingFractionMode::Learn: + bsdf_mode_string += "Learn\n"; + bsdf_mode_string += " learning rate " + pretty_scalar(m_learning_rate, 2); + break; + + default: + break; + } + + std::string iteration_progression_string; + + switch (m_iteration_progression) + { + case IterationProgression::Automatic: + iteration_progression_string = "Automatic"; + break; + + case IterationProgression::Combine: + iteration_progression_string = "Combine"; + break; + + default: + break; + } + + std::string directional_filter_string; + + switch (m_directional_filter) + { + case DirectionalFilter::Nearest: + directional_filter_string = "Nearest"; + break; + + case DirectionalFilter::Box: + directional_filter_string = "Box"; + break; + + default: + break; + } + + std::string spatial_filter_string; + + switch (m_spatial_filter) + { + case SpatialFilter::Nearest: + spatial_filter_string = "Nearest"; + break; + + case SpatialFilter::Box: + spatial_filter_string = "Box"; + break; + + case SpatialFilter::Stochastic: + spatial_filter_string = "Stochastic"; + break; + + default: + break; + } + + std::string bounce_mode_string; + + switch (m_guided_bounce_mode) + { + case GuidedBounceMode::Learn: + bounce_mode_string = "Learned Distribution"; + break; + + case GuidedBounceMode::StrictlyDiffuse: + bounce_mode_string = "Strictly Diffuse"; + break; + + case GuidedBounceMode::StrictlyGlossy: + bounce_mode_string = "Strictly Glossy"; + break; + + case GuidedBounceMode::PreferDiffuse: + bounce_mode_string = "Prefer Diffuse"; + break; + + case GuidedBounceMode::PreferGlossy: + bounce_mode_string = "Prefer Glossy"; + break; + + default: + break; + } + + std::string save_mode_string; + + switch (m_save_mode) + { + case SaveMode::None: + save_mode_string = "None"; + break; + + case SaveMode::All: + save_mode_string = "All"; + break; + + case SaveMode::Final: + save_mode_string = "Final"; + break; + + default: + break; + } + + RENDERER_LOG_INFO( + "guided path tracer settings:\n" + " samples per pass %s\n" + " iteration progression %s\n" + " %s\n" + " spatial filter %s\n" + " directional filter %s\n" + " guided bounce mode %s\n" + " save iterations %s\n" + " save path %s\n" + " direct lighting %s\n" + " ibl %s\n" + " caustics %s\n" + " max bounces %s\n" + " max path guided bounces %s\n" + " max diffuse bounces %s\n" + " max glossy bounces %s\n" + " max specular bounces %s\n" + " max volume bounces %s\n" + " russian roulette start bounce %s\n" + " next event estimation %s\n" + " dl light samples %s\n" + " dl light threshold %s\n" + " ibl env samples %s\n" + " max ray intensity %s\n" + " volume distance samples %s\n" + " equiangular sampling %s\n" + " clamp roughness %s", + pretty_uint(m_samples_per_pass).c_str(), + iteration_progression_string.c_str(), + bsdf_mode_string.c_str(), + spatial_filter_string.c_str(), + directional_filter_string.c_str(), + bounce_mode_string.c_str(), + save_mode_string.c_str(), + m_save_path.c_str(), + m_enable_dl ? "on" : "off", + m_enable_ibl ? "on" : "off", + m_enable_caustics ? "on" : "off", + m_max_bounces == ~size_t(0) ? "unlimited" : pretty_uint(m_max_bounces).c_str(), + m_max_guided_bounces == ~size_t(0) ? "unlimited" : pretty_uint(m_max_guided_bounces).c_str(), + m_max_diffuse_bounces == ~size_t(0) ? "unlimited" : pretty_uint(m_max_diffuse_bounces).c_str(), + m_max_glossy_bounces == ~size_t(0) ? "unlimited" : pretty_uint(m_max_glossy_bounces).c_str(), + m_max_specular_bounces == ~size_t(0) ? "unlimited" : pretty_uint(m_max_specular_bounces).c_str(), + m_max_volume_bounces == ~size_t(0) ? "unlimited" : pretty_uint(m_max_volume_bounces).c_str(), + m_rr_min_path_length == ~size_t(0) ? "unlimited" : pretty_uint(m_rr_min_path_length).c_str(), + m_next_event_estimation ? "on" : "off", + pretty_scalar(m_dl_light_sample_count).c_str(), + pretty_scalar(m_dl_low_light_threshold, 3).c_str(), + pretty_scalar(m_ibl_env_sample_count).c_str(), + m_has_max_ray_intensity ? pretty_scalar(m_max_ray_intensity).c_str() : "unlimited", + pretty_int(m_distance_sample_count).c_str(), + m_enable_equiangular_sampling ? "on" : "off", + m_clamp_roughness ? "on" : "off"); +} + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/lighting/gpt/gptparameters.h b/src/appleseed/renderer/kernel/lighting/gpt/gptparameters.h new file mode 100644 index 0000000000..f6070a214f --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/gptparameters.h @@ -0,0 +1,147 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/global/globaltypes.h" + +// Standard headers. +#include +#include + +// Forward declarations. +namespace renderer +{ +class ParamArray; +} + +namespace renderer +{ +enum class SpatialFilter +{ + Nearest, + Stochastic, + Box +}; + +enum class DirectionalFilter +{ + Nearest, + Box +}; + +enum class IterationProgression +{ + Combine, + Automatic +}; + +enum class BSDFSamplingFractionMode +{ + Learn, + Fixed +}; + +enum class GuidedBounceMode +{ + Learn, + StrictlyDiffuse, + StrictlyGlossy, + PreferDiffuse, + PreferGlossy +}; + +enum class SaveMode +{ + None, + All, + Final +}; + +struct GPTParameters +{ + explicit GPTParameters(const ParamArray& params); + + void print() const; + + const size_t m_samples_per_pass; + const IterationProgression m_iteration_progression; + const SpatialFilter m_spatial_filter; + const DirectionalFilter m_directional_filter; + const BSDFSamplingFractionMode m_bsdf_sampling_fraction_mode; + const GuidedBounceMode m_guided_bounce_mode; + const SaveMode m_save_mode; + const std::string m_save_path; + const float m_fixed_bsdf_sampling_fraction; + const float m_learning_rate; + + + const bool m_enable_dl; // is direct lighting enabled? + const bool m_enable_ibl; // is image-based lighting enabled? + const bool m_enable_caustics; // are caustics enabled? + + const size_t m_max_bounces; // maximum number of bounces, ~0 for unlimited + const size_t m_max_guided_bounces; // maximum number of path guided bounces, ~0 for unlimited + const size_t m_max_diffuse_bounces; // maximum number of diffuse bounces, ~0 for unlimited + const size_t m_max_glossy_bounces; // maximum number of glossy bounces, ~0 for unlimited + const size_t m_max_specular_bounces; // maximum number of specular bounces, ~0 for unlimited + const size_t m_max_volume_bounces; // maximum number of volume scattering events, ~0 for unlimited + + const bool m_clamp_roughness; + + const size_t m_rr_min_path_length; // minimum path length before Russian Roulette kicks in, ~0 for unlimited + const bool m_next_event_estimation; // use next event estimation? + + const float m_dl_light_sample_count; // number of light samples used to estimate direct illumination + const float m_dl_low_light_threshold; // light contribution threshold to disable shadow rays + const float m_ibl_env_sample_count; // number of environment samples used to estimate IBL + float m_rcp_dl_light_sample_count; + float m_rcp_ibl_env_sample_count; + + const bool m_has_max_ray_intensity; + const float m_max_ray_intensity; + + const size_t m_distance_sample_count; // number of distance samples for volume rendering + const bool m_enable_equiangular_sampling; // optimize for lights that are located outside volumes + + const bool m_record_light_paths; + + + static size_t fixup_bounces(const int x) + { + return x == -1 ? ~size_t(0) : x; + } + + static size_t fixup_path_length(const size_t x) + { + return x == 0 ? ~size_t(0) : x; + } +}; + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/lighting/gpt/gptpasscallback.cpp b/src/appleseed/renderer/kernel/lighting/gpt/gptpasscallback.cpp new file mode 100644 index 0000000000..19497f1009 --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/gptpasscallback.cpp @@ -0,0 +1,274 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// Interface header. +#include "gptpasscallback.h" + +// appleseed.renderer headers. +#include "renderer/global/globallogger.h" +#include "renderer/global/globaltypes.h" +#include "renderer/modeling/frame/frame.h" + +// appleseed.foundation headers. +#include "foundation/platform/types.h" +#include "foundation/utility/job/iabortswitch.h" +#include "foundation/string/string.h" + +// Standard headers. +#include +#include + +using namespace foundation; +using namespace std; + +namespace renderer +{ + +const size_t ImageBufferCapacity = 4; + +GPTPassCallback::GPTPassCallback( + const GPTParameters& params, + STree* sd_tree, + const size_t sample_budget, + const size_t max_passes) + : m_params(params) + , m_sd_tree(sd_tree) + , m_passes_left_curr_iter(0) + , m_passes_rendered(0) + , m_last_extrapolated_variance(std::numeric_limits::infinity()) + , m_sample_budget(sample_budget) + , m_iter(0) + , m_is_final_iter(false) + , m_var_increase(false) +{ + m_max_passes = m_sample_budget / m_params.m_samples_per_pass; + + if (m_max_passes > max_passes) + m_max_passes = max_passes; + + m_remaining_passes = m_max_passes; +} + +void GPTPassCallback::release() +{ + delete this; +} + +void GPTPassCallback::on_pass_begin( + const Frame& frame, + JobQueue& job_queue, + IAbortSwitch& abort_switch) +{ + if (m_passes_left_curr_iter > 0) + return; + + // New iteration. + + // Prepare pass. + m_num_passes_curr_iter = m_passes_left_curr_iter = std::min(size_t(1) << m_iter, m_remaining_passes); + + if (m_is_final_iter || m_remaining_passes - m_passes_left_curr_iter < 2 * m_passes_left_curr_iter) + { + m_passes_left_curr_iter = m_remaining_passes; + m_is_final_iter = true; + m_sd_tree->start_final_iteration(); + } + + if (!m_var_increase && m_iter > 0) + { + // Clear the frame and build the tree. + m_framebuffer->clear(); + m_sd_tree->build(m_iter); + + switch (m_params.m_save_mode) + { + case SaveMode::All: + m_sd_tree->write_to_disk(m_iter, true); + break; + + case SaveMode::Final: + if(m_is_final_iter) + m_sd_tree->write_to_disk(m_iter, false); + break; + + default: + break; + } + } + + ++m_iter; +} + +bool GPTPassCallback::on_pass_end( + const Frame& frame, + JobQueue& job_queue, + IAbortSwitch& abort_switch) +{ + ++m_passes_rendered; + --m_passes_left_curr_iter; + --m_remaining_passes; + + if (m_passes_rendered >= m_max_passes || abort_switch.is_aborted()) + { + const float variance = m_framebuffer->estimator_variance(); + RENDERER_LOG_INFO("Final iteration variance estimate: %s", pretty_scalar(variance, 7).c_str()); + + if (m_params.m_iteration_progression == IterationProgression::Combine) + { + image_to_buffer(frame.image(), 1.0f / variance); + combine_iterations(frame); + } + + return true; + } + + if (m_passes_left_curr_iter == 0) + { + // Update the variance projection. + const size_t remaining_passes_at_curr_iter_start = m_remaining_passes + m_num_passes_curr_iter; + const size_t samples_rendered = m_passes_rendered * m_params.m_samples_per_pass; + const float variance = m_framebuffer->estimator_variance(); + const float current_extraplolated_variance = + variance * m_num_passes_curr_iter / remaining_passes_at_curr_iter_start; + + RENDERER_LOG_INFO("Variance: %s", pretty_scalar(variance, 7).c_str()); + + RENDERER_LOG_INFO("Extrapolated variance:\n Previous: %s\n Current: %s\n", + pretty_scalar(m_last_extrapolated_variance, 7).c_str(), + pretty_scalar(current_extraplolated_variance, 7).c_str()); + + if (m_params.m_iteration_progression == IterationProgression::Automatic && samples_rendered > 256 && + current_extraplolated_variance > m_last_extrapolated_variance) + { + RENDERER_LOG_INFO("Extrapolated variance is increasing, initiating final iteration"); + m_var_increase = m_is_final_iter = true; + } + + m_last_extrapolated_variance = current_extraplolated_variance; + + if (m_params.m_iteration_progression == IterationProgression::Combine) + image_to_buffer(frame.image(), 1.0f / variance); + } + + return false; +} + +void GPTPassCallback::set_framebuffer( + VarianceTrackingShadingResultFrameBufferFactory* framebuffer) +{ + m_framebuffer = framebuffer; +} + +void GPTPassCallback::image_to_buffer( + const Image& image, + const float inverse_variance) +{ + // Store rendered images. + if (m_image_buffer.size() == ImageBufferCapacity) + { + m_image_buffer.pop_front(); + m_inverse_variance_buffer.pop_front(); + } + m_image_buffer.push_back(image); + m_inverse_variance_buffer.push_back(inverse_variance); +} + +// Inverse variance weighted sample combination [Müller 2019] +void GPTPassCallback::combine_iterations(const Frame& frame) +{ + assert(m_image_buffer.size() > 0 && m_inverse_variance_buffer.size() == m_image_buffer.size()); + + // No need to weight single image. + if (m_image_buffer.size() == 1) + return; + + float total_inverse_variances = 0.0f; + + for (const float& var : m_inverse_variance_buffer) + { + total_inverse_variances += var; + } + + // If no variance was detected use the last image as is. + if (total_inverse_variances <= 0.0f) + return; + + // Calculate the weights. + std::string weight_str = "["; + + for (float& var : m_inverse_variance_buffer) + { + var /= total_inverse_variances; + weight_str += pretty_scalar(var, 3) + ", "; + } + weight_str = weight_str.substr(0, weight_str.size() - 2) + "]"; // subtract last comma + + RENDERER_LOG_INFO( + "Applying inverse variance weighted sample combination on last %s images with weights %s", + pretty_uint(m_image_buffer.size()).c_str(), + weight_str.c_str()); + + CanvasProperties image_properties = frame.image().properties(); + + // Arrays to store pixel channels. + std::unique_ptr final_pixel(new float[image_properties.m_channel_count]); + std::unique_ptr temp_pixel(new float[image_properties.m_channel_count]); + + for (size_t y = 0; y < image_properties.m_canvas_height; ++y) + for (size_t x = 0; x < image_properties.m_canvas_width; ++x) + { + auto image_itr = m_image_buffer.cbegin(); + auto weight_itr = m_inverse_variance_buffer.cbegin(); + + // Clear the final pixel receiver. + float* channel = final_pixel.get(); + for (size_t c = 0; c < image_properties.m_channel_count; ++c) + channel[c] = 0.0f; + + // Iterate over the stored images. + for (size_t i = 0; i < m_image_buffer.size(); ++i) + { + // Get image's pixel data. + image_itr->get_pixel(x, y, temp_pixel.get(), image_properties.m_channel_count); + + // Add inverse variance weighted channels to final pixel. + float* final_channel = final_pixel.get(); + float* temp_channel = temp_pixel.get(); + for (size_t c = 0; c < image_properties.m_channel_count; ++c) + final_channel[c] += temp_channel[c] * (*weight_itr); + + ++image_itr; + ++weight_itr; + } + + // Set the final pixel in the frame. + frame.image().set_pixel(x, y, final_pixel.get(), image_properties.m_channel_count); + } +} + +} // namespace renderer \ No newline at end of file diff --git a/src/appleseed/renderer/kernel/lighting/gpt/gptpasscallback.h b/src/appleseed/renderer/kernel/lighting/gpt/gptpasscallback.h new file mode 100644 index 0000000000..3bb85e737b --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/gptpasscallback.h @@ -0,0 +1,109 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/kernel/lighting/gpt/gptparameters.h" +#include "renderer/kernel/lighting/sdtree.h" +#include "renderer/kernel/rendering/ipasscallback.h" +#include "renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.h" + +// appleseed.foundation headers. +#include "foundation/image/image.h" + +// Standard headers. +#include + +// Forward declarations. +namespace foundation { class IAbortSwitch; } +namespace foundation { class JobQueue; } +namespace renderer { class Frame; } + +namespace renderer +{ + +// +// This class is responsible for the path guiding budget balancing logic. +// + +class GPTPassCallback + : public IPassCallback +{ + public: + // Constructor. + GPTPassCallback( + const GPTParameters& params, + STree* sd_tree, + const size_t sample_budget, + const size_t max_passes); + + // Delete this instance. + void release() override; + + // This method is called at the beginning of a pass. + void on_pass_begin( + const Frame& frame, + foundation::JobQueue& job_queue, + foundation::IAbortSwitch& abort_switch) override; + + // This method is called at the end of a pass. + bool on_pass_end( + const Frame& frame, + foundation::JobQueue& job_queue, + foundation::IAbortSwitch& abort_switch) override; + + void set_framebuffer( + VarianceTrackingShadingResultFrameBufferFactory* framebuffer); + + private: + void image_to_buffer( + const foundation::Image& image, + const float inverse_variance); + + void combine_iterations(const Frame& frame); + + const GPTParameters m_params; + size_t m_iter; + size_t m_max_passes; + size_t m_passes_rendered; + size_t m_remaining_passes; + size_t m_passes_left_curr_iter; + size_t m_num_passes_curr_iter; + STree* m_sd_tree; + size_t m_sample_budget; + float m_last_extrapolated_variance; + bool m_is_final_iter; + bool m_var_increase; + VarianceTrackingShadingResultFrameBufferFactory* m_framebuffer; + + std::list m_image_buffer; + std::list m_inverse_variance_buffer; +}; + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/lighting/gpt/pathguidedsampler.cpp b/src/appleseed/renderer/kernel/lighting/gpt/pathguidedsampler.cpp new file mode 100644 index 0000000000..e820aea5dd --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/pathguidedsampler.cpp @@ -0,0 +1,265 @@ +// Interface header. +#include "pathguidedsampler.h" + +// appleseed.renderer headers. +#include "renderer/kernel/lighting/tracer.h" +#include "renderer/kernel/shading/directshadingcomponents.h" +#include "renderer/kernel/shading/shadingcontext.h" +#include "renderer/kernel/shading/shadingpoint.h" +#include "renderer/modeling/bsdf/bsdf.h" +#include "renderer/modeling/bsdf/bsdfsample.h" +#include "renderer/modeling/volume/volume.h" + +// appleseed.foundation headers. +#include "foundation/math/scalar.h" + +// Standard headers. +#include + +using namespace foundation; +using namespace std; + +namespace renderer +{ + +PathGuidedSampler::PathGuidedSampler( + const bool enable_path_guiding, + GuidedBounceMode guided_bounce_mode, + DTree* d_tree, + const float bsdf_sampling_fraction, + const BSDF& bsdf, + const void* bsdf_data, + const int bsdf_sampling_modes, + const ShadingPoint& shading_point, + const bool sd_tree_is_built) + : BSDFSampler( + bsdf, + bsdf_data, + bsdf_sampling_modes, + shading_point) + , m_enable_path_guiding(enable_path_guiding) + , m_d_tree(d_tree) + , m_bsdf_sampling_fraction(bsdf_sampling_fraction) + , m_sd_tree_is_built(sd_tree_is_built) + , m_guided_bounce_mode(guided_bounce_mode) +{ + assert(m_d_tree); + assert(m_bsdf_sampling_fraction >= 0.0f && m_bsdf_sampling_fraction <= 1.0f); +} + +bool PathGuidedSampler::sample( + SamplingContext& sampling_context, + const Dual3d& outgoing, + Dual3f& incoming, + DirectShadingComponents& value, + float& pdf) const +{ + BSDFSample bsdf_sample; + float d_tree_pdf; + + sample( + sampling_context, + bsdf_sample, + outgoing, + pdf, + d_tree_pdf); + + // Filter scattering modes. + if (!(m_bsdf_sampling_modes & bsdf_sample.get_mode())) + return false; + + incoming = bsdf_sample.m_incoming; + value = bsdf_sample.m_value; + + return true; +} + +float PathGuidedSampler::evaluate( + const Vector3f& outgoing, + const Vector3f& incoming, + const int light_sampling_modes, + DirectShadingComponents& value) const +{ + const float bsdf_pdf = m_bsdf.evaluate( + m_bsdf_data, + false, // not adjoint + true, + m_local_geometry, + outgoing, + incoming, + light_sampling_modes, + value); + + float d_tree_pdf; + return guided_path_extension_pdf(incoming, bsdf_pdf, d_tree_pdf, false); +} + +bool PathGuidedSampler::sample( + SamplingContext& sampling_context, + BSDFSample& bsdf_sample, + const Dual3d& outgoing, + float& wi_pdf, + float& d_tree_pdf) const +{ + if (!m_sd_tree_is_built || m_bsdf.is_purely_specular() || !m_enable_path_guiding) + { + m_bsdf.sample( + sampling_context, + m_bsdf_data, + false, + true, // multiply by |cos(incoming, normal)| + m_local_geometry, + Dual3f(outgoing), + m_bsdf_sampling_modes, + bsdf_sample); + + wi_pdf = guided_path_extension_pdf( + bsdf_sample.m_incoming.get_value(), + bsdf_sample.get_probability(), + d_tree_pdf, + true); + + return false; + } + + sampling_context.split_in_place(1, 1); + const float s = sampling_context.next2(); + + if (s < m_bsdf_sampling_fraction) + { + m_bsdf.sample( + sampling_context, + m_bsdf_data, + false, + true, // multiply by |cos(incoming, normal)| + m_local_geometry, + Dual3f(outgoing), + m_bsdf_sampling_modes, + bsdf_sample); + + if (bsdf_sample.get_mode() == ScatteringMode::None) + return false; + + if (bsdf_sample.get_mode() == ScatteringMode::Specular) + { + d_tree_pdf = 0.0f; + wi_pdf = m_bsdf_sampling_fraction; + return false; + } + + wi_pdf = guided_path_extension_pdf( + bsdf_sample.m_incoming.get_value(), + bsdf_sample.get_probability(), + d_tree_pdf, + false); + + return false; + } + else + { + DTreeSample d_tree_sample; + m_d_tree->sample(sampling_context, d_tree_sample, enable_modes_before_sampling(m_bsdf_sampling_modes)); + const ScatteringMode::Mode scattering_mode = set_mode_after_sampling(d_tree_sample.scattering_mode); + + if (scattering_mode == ScatteringMode::None) + { + // Terminate. + bsdf_sample.set_to_scattering(scattering_mode, 0.0f); + return true; + } + + bsdf_sample.m_incoming = foundation::Dual3f(d_tree_sample.direction); + d_tree_pdf = d_tree_sample.pdf; + + const float bsdf_pdf = m_bsdf.evaluate( + m_bsdf_data, + false, // not adjoint + true, // multiply by |cos(incoming, normal)| + m_local_geometry, + Vector3f(outgoing.get_value()), + bsdf_sample.m_incoming.get_value(), + m_bsdf_sampling_modes, + bsdf_sample.m_value); + + if (bsdf_pdf == 0.0f) + { + // Reject invalid directions. + bsdf_sample.set_to_scattering(ScatteringMode::None, bsdf_pdf); + return true; + } + else + { + bsdf_sample.set_to_scattering(scattering_mode, bsdf_pdf); + } + + wi_pdf = guided_path_extension_pdf(bsdf_sample.m_incoming.get_value(), bsdf_pdf, d_tree_pdf, true); + + return true; + } +} + +float PathGuidedSampler::guided_path_extension_pdf( + const foundation::Vector3f& incoming, + const float& bsdf_pdf, + float& d_tree_pdf, + const bool d_tree_pdf_is_set) const +{ + if (!m_sd_tree_is_built || m_bsdf.is_purely_specular() || !m_enable_path_guiding) + { + d_tree_pdf = 0.0f; + return bsdf_pdf; + } + + if (!d_tree_pdf_is_set) + d_tree_pdf = m_d_tree->pdf(incoming, enable_modes_before_sampling(m_bsdf_sampling_modes)); + + return lerp(d_tree_pdf, bsdf_pdf, m_bsdf_sampling_fraction); +} + +const int PathGuidedSampler::enable_modes_before_sampling( + const int modes) const +{ + if (m_guided_bounce_mode == GuidedBounceMode::Learn) + return modes; + else + return ScatteringMode::Diffuse | ScatteringMode::Glossy; +} + +ScatteringMode::Mode PathGuidedSampler::set_mode_after_sampling( + const ScatteringMode::Mode sampled_mode) const +{ + switch (m_guided_bounce_mode) + { + case GuidedBounceMode::Learn: + return sampled_mode; + break; + + case GuidedBounceMode::StrictlyDiffuse: + return ScatteringMode::has_diffuse(m_bsdf_sampling_modes) ? + ScatteringMode::Diffuse : ScatteringMode::None; + break; + + case GuidedBounceMode::StrictlyGlossy: + return ScatteringMode::has_glossy(m_bsdf_sampling_modes) ? + ScatteringMode::Glossy : ScatteringMode::None; + break; + + case GuidedBounceMode::PreferDiffuse: + return ScatteringMode::has_diffuse(m_bsdf_sampling_modes) ? + ScatteringMode::Diffuse : ScatteringMode::has_glossy(m_bsdf_sampling_modes) ? + ScatteringMode::Glossy : ScatteringMode::None; + break; + + case GuidedBounceMode::PreferGlossy: + return ScatteringMode::has_glossy(m_bsdf_sampling_modes) ? + ScatteringMode::Glossy : ScatteringMode::has_diffuse(m_bsdf_sampling_modes) ? + ScatteringMode::Diffuse : ScatteringMode::None; + break; + + default: + return sampled_mode; + break; + } +} + +} //namespace renderer \ No newline at end of file diff --git a/src/appleseed/renderer/kernel/lighting/gpt/pathguidedsampler.h b/src/appleseed/renderer/kernel/lighting/gpt/pathguidedsampler.h new file mode 100644 index 0000000000..bfd86a8361 --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/gpt/pathguidedsampler.h @@ -0,0 +1,72 @@ +#pragma once + +// appleseed.renderer headers. +#include "renderer/kernel/lighting/materialsamplers.h" +#include "renderer/kernel/lighting/sdtree.h" +#include "renderer/modeling/bsdf/bsdfsample.h" + + +namespace renderer +{ + +// +// Sampler acting as a wrapper for path guided sampling at shading points for the implementation of +// "Practical Path Guiding for Efficient Light-Transport Simulation" [Müller et al. 2017]. +// + +class PathGuidedSampler + : public BSDFSampler +{ + public: + PathGuidedSampler( + const bool enable_path_guiding, + const GuidedBounceMode guided_bounce_mode, + DTree* d_tree, + const float bsdf_sampling_fraction, + const BSDF& bsdf, + const void* bsdf_data, + const int bsdf_sampling_modes, + const ShadingPoint& shading_point, + const bool sd_tree_is_built); + + bool sample( + SamplingContext& sampling_context, + const foundation::Dual3d& outgoing, + foundation::Dual3f& incoming, + DirectShadingComponents& value, + float& pdf) const override; + + bool sample( + SamplingContext& sampling_context, + BSDFSample& bsdf_sample, + const foundation::Dual3d& outgoing, + float& wi_pdf, + float& d_tree_pdf) const; + + float evaluate( + const foundation::Vector3f& outgoing, + const foundation::Vector3f& incoming, + const int light_sampling_modes, + DirectShadingComponents& value) const override; + + private: + float guided_path_extension_pdf( + const foundation::Vector3f& incoming, + const float& bsdf_pdf, + float& d_tree_pdf, + const bool d_tree_pdf_is_set) const; + + const int enable_modes_before_sampling( + const int sample_modes) const; + + ScatteringMode::Mode set_mode_after_sampling( + const ScatteringMode::Mode sampled_mode) const; + + DTree* m_d_tree; + const float m_bsdf_sampling_fraction; + const bool m_sd_tree_is_built; + const bool m_enable_path_guiding; + const GuidedBounceMode m_guided_bounce_mode; +}; + +} // namespace render \ No newline at end of file diff --git a/src/appleseed/renderer/kernel/lighting/guidedpathtracer.h b/src/appleseed/renderer/kernel/lighting/guidedpathtracer.h new file mode 100644 index 0000000000..9e97361f39 --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/guidedpathtracer.h @@ -0,0 +1,1067 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/global/globallogger.h" +#include "renderer/global/globaltypes.h" +#include "renderer/kernel/aov/aovcomponents.h" +#include "renderer/kernel/intersection/intersector.h" +#include "renderer/kernel/lighting/gpt/pathguidedsampler.h" +#include "renderer/kernel/lighting/pathvertex.h" +#include "renderer/kernel/lighting/scatteringmode.h" +#include "renderer/kernel/lighting/sdtree.h" +#include "renderer/kernel/shading/shadingcontext.h" +#include "renderer/kernel/shading/shadingpoint.h" +#include "renderer/kernel/shading/shadingray.h" +#include "renderer/modeling/bsdf/bsdf.h" +#include "renderer/modeling/bsdf/bsdfsample.h" +#include "renderer/modeling/bssrdf/bssrdf.h" +#include "renderer/modeling/bssrdf/bssrdfsample.h" +#include "renderer/modeling/input/source.h" +#include "renderer/modeling/material/material.h" +#include "renderer/modeling/scene/objectinstance.h" +#include "renderer/modeling/shadergroup/shadergroup.h" +#include "renderer/modeling/volume/volume.h" + +// appleseed.foundation headers. +#include "foundation/core/concepts/noncopyable.h" +#include "foundation/math/dual.h" +#include "foundation/math/ray.h" +#include "foundation/math/rr.h" +#include "foundation/math/sampling/mappings.h" +#include "foundation/math/scalar.h" +#include "foundation/math/vector.h" +#include "foundation/memory/arena.h" +#include "foundation/string/string.h" + +// Standard headers. +#include +#include +#include + +namespace renderer +{ + +// +// A path tracer implementing "Practical Path Guiding for Efficient Light-Transport Simulation" [Müller et al. 2017]. +// +// The PathVisitor class must conform to the following prototype: +// +// struct PathVisitor +// { +// void on_first_diffuse_bounce(const PathVertex& vertex); +// +// bool accept_scattering( +// const ScatteringMode::Mode prev_mode, +// const ScatteringMode::Mode next_mode) const; +// +// void on_miss(const PathVertex& vertex, GPTVertexPath& guided_path); +// void on_hit(const PathVertex& vertex, GPTVertexPath& guided_path); +// void on_scatter(PathVertex& vertex, GPTVertexPath& guided_path); +// }; +// +// The VolumeVisitor class must conform to the following prototype: +// +// struct VolumeVisitor +// { +// bool accept_scattering( +// const ScatteringMode::Mode prev_mode); +// +// void on_scatter(PathVertex& vertex); +// void visit_ray(PathVertex& vertex, const ShadingRay& volume_ray); +// }; +// + +template +class GuidedPathTracer + : public foundation::NonCopyable +{ + public: + GuidedPathTracer( + STree* sd_tree, + PathVisitor& path_visitor, + VolumeVisitor& volume_visitor, + const BSDFSamplingFractionMode bsdf_sampling_fraction_mode, + const GuidedBounceMode guided_bounce_mode, + const size_t rr_min_path_length, + const size_t max_bounces, + const size_t max_guided_bounces, + const size_t max_diffuse_bounces, + const size_t max_glossy_bounces, + const size_t max_specular_bounces, + const size_t max_volume_bounces, + const bool clamp_roughness, + const size_t max_iterations = 1000, + const double near_start = 0.0); // abort tracing if the first ray is shorter than this + + size_t trace( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingRay& ray, + const ShadingPoint* parent_shading_point = nullptr, + const bool clear_arena = true); + + size_t trace( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingPoint& shading_point, + const bool clear_arena = true); + + const ShadingPoint& get_path_vertex(const size_t i) const; + + private: + STree* m_sd_tree; + PathVisitor& m_path_visitor; + VolumeVisitor& m_volume_visitor; + const BSDFSamplingFractionMode m_bsdf_sampling_fraction_mode; + const GuidedBounceMode m_guided_bounce_mode; + const size_t m_rr_min_path_length; + const size_t m_max_bounces; + const size_t m_max_guided_bounces; + const size_t m_max_diffuse_bounces; + const size_t m_max_glossy_bounces; + const size_t m_max_specular_bounces; + const size_t m_max_volume_bounces; + const bool m_clamp_roughness; + const size_t m_max_iterations; + const double m_near_start; + size_t m_guided_bounces; + size_t m_diffuse_bounces; + size_t m_glossy_bounces; + size_t m_specular_bounces; + size_t m_volume_bounces; + size_t m_iterations; + foundation::Arena m_shading_point_arena; + + // Determine whether a ray can pass through a surface with a given alpha value. + static bool pass_through( + SamplingContext& sampling_context, + const Alpha alpha); + + // Use Russian Roulette to cut the path without introducing bias. + bool continue_path_rr( + SamplingContext& sampling_context, + PathVertex& vertex); + + // Apply path visitor and sample BSDF in a given path vertex. + // If all checks are passed, build a bounced ray that continues in the sampled direction + // and return true, otherwise return false. + bool process_bounce( + SamplingContext& sampling_context, + PathVertex& vertex, + BSDFSample& sample, + ShadingRay& ray, + GPTVertexPath& guide_vertices); + + // This method performs raymarching across the volume. + // Returns whether the path should be continued. + bool march( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingRay& ray, + PathVertex& vertex, + ShadingPoint& shading_point, + GPTVertexPath& guided_path); +}; + + +// +// PathTracer class implementation. +// + +template +inline GuidedPathTracer::GuidedPathTracer( + STree* sd_tree, + PathVisitor& path_visitor, + VolumeVisitor& volume_visitor, + const BSDFSamplingFractionMode bsdf_sampling_fraction_mode, + const GuidedBounceMode guided_bounce_mode, + const size_t rr_min_path_length, + const size_t max_bounces, + const size_t max_guided_bounces, + const size_t max_diffuse_bounces, + const size_t max_glossy_bounces, + const size_t max_specular_bounces, + const size_t max_volume_bounces, + const bool clamp_roughness, + const size_t max_iterations, + const double near_start) + : m_sd_tree(sd_tree) + , m_path_visitor(path_visitor) + , m_volume_visitor(volume_visitor) + , m_bsdf_sampling_fraction_mode(bsdf_sampling_fraction_mode) + , m_guided_bounce_mode(guided_bounce_mode) + , m_rr_min_path_length(rr_min_path_length) + , m_max_bounces(max_bounces) + , m_max_guided_bounces(max_guided_bounces) + , m_max_diffuse_bounces(max_diffuse_bounces) + , m_max_glossy_bounces(max_glossy_bounces) + , m_max_specular_bounces(max_specular_bounces) + , m_max_volume_bounces(max_volume_bounces) + , m_clamp_roughness(clamp_roughness) + , m_max_iterations(max_iterations) + , m_near_start(near_start) +{ +} + +template +inline size_t GuidedPathTracer::trace( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingRay& ray, + const ShadingPoint* parent_shading_point, + const bool clear_arena) +{ + ShadingPoint shading_point; + shading_context.get_intersector().trace(ray, shading_point, parent_shading_point); + + return + trace( + sampling_context, + shading_context, + shading_point, + clear_arena); +} + +template +size_t GuidedPathTracer::trace( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingPoint& shading_point, + const bool clear_arena) +{ + // Terminate the path if the first hit is too close to the origin. + if (shading_point.hit_surface() && shading_point.get_distance() < m_near_start) + return 1; + + PathVertex vertex(sampling_context); + vertex.m_path_length = 1; + vertex.m_scattering_modes = ScatteringMode::All; + vertex.m_throughput.set(1.0f); + vertex.m_shading_point = &shading_point; + vertex.m_prev_mode = ScatteringMode::Specular; + vertex.m_prev_prob = BSDF::DiracDelta; + vertex.m_aov_mode = ScatteringMode::None; + + // This variable tracks the beginning of the path segment inside the current medium. + // While it is properly initialized when entering a medium, we also initialize it + // here to silence a gcc warning. + foundation::Vector3d medium_start(0.0); + + m_guided_bounces = 0; + m_diffuse_bounces = 0; + m_glossy_bounces = 0; + m_specular_bounces = 0; + m_volume_bounces = 0; + m_iterations = 0; + + GPTVertexPath guided_path; + + while (true) + { + if (clear_arena) + shading_context.get_arena().clear(); + + ShadingPoint* next_shading_point = m_shading_point_arena.allocate(); + +#ifndef NDEBUG + // Save the sampling context at the beginning of the iteration. + const SamplingContext backup_sampling_context(sampling_context); + + // Resume execution here to reliably reproduce problems downstream. + sampling_context = backup_sampling_context; +#endif + + // Put a hard limit on the number of iterations. + if (m_iterations++ == m_max_iterations) + { + if (m_max_bounces < m_max_iterations) + { + RENDERER_LOG_WARNING( + "reached hard iteration limit (%s), breaking path tracing loop.", + foundation::pretty_int(m_max_iterations).c_str()); + } + break; + } + + // Retrieve the ray. + const ShadingRay& ray = vertex.get_ray(); + assert(foundation::is_normalized(ray.m_dir)); + + // Compute the outgoing direction at this vertex. + // Derivation: + // ray.dir + d(ray.dir)/dx = ray.dx.dir + // outgoing = -ray.dir + // d(outgoing)/dx = d(-ray.dir)/dx = ray.dir - ray.dx.dir + vertex.m_outgoing = + ray.m_has_differentials + ? foundation::Dual3d( + -ray.m_dir, + ray.m_dir - ray.m_rx.m_dir, + ray.m_dir - ray.m_ry.m_dir) + : foundation::Dual3d(-ray.m_dir); + + // Terminate the path if the ray didn't hit anything. + if (!vertex.m_shading_point->hit_surface()) + { + m_path_visitor.on_miss(vertex, guided_path); + break; + } + + // Retrieve the material at the shading point. + const Material* material = vertex.get_material(); + + // Terminate the path if the surface has no material. + if (material == nullptr) + break; + + // Retrieve the material's render data. + const Material::RenderData& material_data = material->get_render_data(); + + // Retrieve the object instance at the shading point. + const ObjectInstance& object_instance = vertex.m_shading_point->get_object_instance(); + + // Determine whether the ray is entering or leaving a medium. + const bool entering = vertex.m_shading_point->is_entering(); + + // Handle false intersections. + if (ray.get_current_medium() && + ray.get_current_medium()->m_object_instance->get_medium_priority() > object_instance.get_medium_priority() && + material_data.m_bsdf != nullptr) + { + // Construct a ray that continues in the same direction as the incoming ray. + ShadingRay next_ray( + vertex.get_point(), + ray.m_dir, + ray.m_time, + ray.m_flags, + ray.m_depth); + + // Advance the differentials if the ray has them. + if (ray.m_has_differentials) + { + next_ray.m_rx = ray.m_rx; + next_ray.m_ry = ray.m_ry; + next_ray.m_rx.m_org = ray.m_rx.point_at(ray.m_tmax); + next_ray.m_ry.m_org = ray.m_ry.point_at(ray.m_tmax); + next_ray.m_has_differentials = true; + } + + // Initialize the ray's medium list. + if (entering) + { + // Execute the OSL shader if there is one. + if (material_data.m_shader_group) + { + shading_context.execute_osl_shading( + *material_data.m_shader_group, + *vertex.m_shading_point); + } + + const void* data = material_data.m_bsdf->evaluate_inputs(shading_context, *vertex.m_shading_point); + const float ior = material_data.m_bsdf->sample_ior(sampling_context, data); + next_ray.add_medium(ray, &object_instance, material, ior); + } + else next_ray.remove_medium(ray, &object_instance); + + // Trace the ray. + shading_context.get_intersector().trace( + next_ray, + *next_shading_point, + vertex.m_shading_point); + + // Update the pointers to the shading points and loop. + vertex.m_shading_point = next_shading_point; + continue; + } + + // Handle alpha mapping. + if (vertex.m_path_length > 1) + { + Alpha alpha = vertex.m_shading_point->get_alpha(); + + // Apply OSL transparency if needed. + if (material_data.m_shader_group && + material_data.m_shader_group->has_transparency()) + { + Alpha a; + shading_context.execute_osl_transparency( + *material_data.m_shader_group, + *vertex.m_shading_point, + a); + alpha *= a; + } + + if (pass_through(sampling_context, alpha)) + { + // Construct a ray that continues in the same direction as the incoming ray. + ShadingRay next_ray( + vertex.get_point(), + ray.m_dir, + ray.m_time, + ray.m_flags, + ray.m_depth); // ray depth does not increase when passing through an alpha-mapped surface + + // Advance the differentials if the ray has them. + if (ray.m_has_differentials) + { + next_ray.m_rx = ray.m_rx; + next_ray.m_ry = ray.m_ry; + next_ray.m_rx.m_org = ray.m_rx.point_at(ray.m_tmax); + next_ray.m_ry.m_org = ray.m_ry.point_at(ray.m_tmax); + next_ray.m_has_differentials = true; + } + + // Inherit the medium list from the parent ray. + next_ray.copy_media_from(ray); + + // Trace the ray. + shading_context.get_intersector().trace( + next_ray, + *next_shading_point, + vertex.m_shading_point); + + // Update the pointers to the shading points and loop. + vertex.m_shading_point = next_shading_point; + continue; + } + } + + // Execute the OSL shader if there is one. + if (material_data.m_shader_group) + { + shading_context.execute_osl_shading( + *material_data.m_shader_group, + *vertex.m_shading_point); + } + + // Retrieve the EDF, the BSDF and the BSSRDF. + vertex.m_edf = + vertex.m_shading_point->is_curve_primitive() ? nullptr : material_data.m_edf; + vertex.m_bsdf = material_data.m_bsdf; + vertex.m_bssrdf = material_data.m_bssrdf; + + // We allow materials with both a BSDF and a BSSRDF. + // When both are present, pick one to extend the path. + if (vertex.m_bsdf && vertex.m_bssrdf) + { + sampling_context.split_in_place(1, 1); + if (sampling_context.next2() < 0.5f) + vertex.m_bsdf = nullptr; + else vertex.m_bssrdf = nullptr; + vertex.m_throughput *= 2.0f; + } + + // Evaluate the inputs of the BSDF. + if (vertex.m_bsdf) + { + vertex.m_bsdf_data = + vertex.m_bsdf->evaluate_inputs(shading_context, *vertex.m_shading_point); + } + + // Evaluate the inputs of the BSSRDF. + if (vertex.m_bssrdf) + { + vertex.m_bssrdf_data = + vertex.m_bssrdf->evaluate_inputs(shading_context, *vertex.m_shading_point); + } + + // Let the path visitor handle a hit. + // cos(outgoing, normal) is used for changes of probability measure. In the case of subsurface + // scattering, we purposely compute it at the outgoing vertex even though it may be used at + // the incoming vertex if we need to change the probability of reaching this vertex by BSDF + // sampling from projected solid angle measure to area measure. + vertex.m_cos_on = foundation::dot(vertex.m_outgoing.get_value(), vertex.get_shading_normal()); + m_path_visitor.on_hit(vertex, guided_path); + + // Use Russian Roulette to cut the path without introducing bias. + if (!continue_path_rr(sampling_context, vertex)) + break; + + // Honor the global bounce limit. + const size_t bounces = vertex.m_path_length - 1; + if (bounces == m_max_bounces) + break; + + // Determine which scattering modes are still enabled. + if (m_diffuse_bounces >= m_max_diffuse_bounces) + vertex.m_scattering_modes &= ~ScatteringMode::Diffuse; + if (m_glossy_bounces >= m_max_glossy_bounces) + vertex.m_scattering_modes &= ~ScatteringMode::Glossy; + if (m_specular_bounces >= m_max_specular_bounces) + vertex.m_scattering_modes &= ~ScatteringMode::Specular; + if (m_volume_bounces >= m_max_volume_bounces) + vertex.m_scattering_modes &= ~ScatteringMode::Volume; + + // Terminate path if no scattering event is possible. + if (vertex.m_scattering_modes == ScatteringMode::None) + break; + + BSDFSample bsdf_sample; + + // Subsurface scattering. + BSSRDFSample bssrdf_sample; + if (vertex.m_bssrdf) + { + // Sample the BSSRDF and terminate the path if no incoming point is found. + if (!vertex.m_bssrdf->sample( + shading_context, + sampling_context, + vertex.m_bssrdf_data, + *vertex.m_shading_point, + foundation::Vector3f(vertex.m_outgoing.get_value()), + vertex.m_scattering_modes, + bssrdf_sample, + bsdf_sample)) + break; + + // Update the path throughput. + vertex.m_throughput *= bssrdf_sample.m_value; + vertex.m_throughput /= bssrdf_sample.m_probability; + + // Switch to the BSSRDF's BRDF. + vertex.m_shading_point = &bssrdf_sample.m_incoming_point; + vertex.m_bsdf = bssrdf_sample.m_brdf; + vertex.m_bsdf_data = bssrdf_sample.m_brdf_data; + } + + // Terminate the path if no above-surface scattering possible. + if (vertex.m_bsdf == nullptr && material->get_render_data().m_volume == nullptr) + break; + + // In case there is no BSDF, the current ray will be continued without increasing its depth. + ShadingRay next_ray( + vertex.get_point(), + ray.m_dir, + ray.m_time, + ray.m_flags, + ray.m_depth); + + if (vertex.m_bsdf != nullptr) + { + // If there is a BSDF, compute the bounce. + const bool continue_path = + process_bounce(sampling_context, vertex, bsdf_sample, next_ray, guided_path); + + // Terminate the path if this scattering event is not accepted. + if (!continue_path) + break; + } + + // Build the medium list of the scattered ray. + const foundation::Vector3d& geometric_normal = vertex.get_geometric_normal(); + const bool crossing_interface = vertex.m_bssrdf == nullptr && + foundation::dot(vertex.m_outgoing.get_value(), geometric_normal) * + foundation::dot(next_ray.m_dir, geometric_normal) < 0.0; + if (crossing_interface) + { + // Ray goes under the surface: + // inherit the medium list of the parent ray and add/remove the current medium. + if (entering) + { + const float ior = vertex.m_bsdf == nullptr ? 1.0f : + vertex.m_bsdf->sample_ior(sampling_context, vertex.m_bsdf_data); + next_ray.add_medium(ray, &object_instance, vertex.get_material(), ior); + } + else + { + next_ray.remove_medium(ray, &object_instance); + } + } + else + { + // Reflected ray: + // inherit the medium list of the parent ray. + next_ray.copy_media_from(ray); + } + + // Compute absorption for the segment inside the medium. + const ShadingRay::Medium* prev_medium = ray.get_current_medium(); + if (prev_medium != nullptr && prev_medium->m_material != nullptr) + { + const Material::RenderData& render_data = prev_medium->m_material->get_render_data(); + + if (render_data.m_bsdf) + { + // Execute the OSL shader if there is one. + if (render_data.m_shader_group) + { + shading_context.execute_osl_shading( + *render_data.m_shader_group, + *vertex.m_shading_point); + } + + const void* data = render_data.m_bsdf->evaluate_inputs(shading_context, *vertex.m_shading_point); + const float distance = static_cast(norm(vertex.get_point() - medium_start)); + Spectrum absorption; + render_data.m_bsdf->compute_absorption(data, distance, absorption); + vertex.m_throughput *= absorption; + } + } + + medium_start = vertex.get_point(); + + const ShadingRay::Medium* current_medium = next_ray.get_current_medium(); + if (current_medium != nullptr && + current_medium->get_volume() != nullptr) + { + // This ray is being cast into a participating medium. + if (!march( + sampling_context, + shading_context, + next_ray, + vertex, + *next_shading_point, + guided_path)) + break; + } + else + { + // This ray is being cast into an ordinary medium. + shading_context.get_intersector().trace( + next_ray, + *next_shading_point, + vertex.m_shading_point); + } + + // Update the pointers to the shading points. + vertex.m_parent_shading_point = vertex.m_shading_point; + vertex.m_shading_point = next_shading_point; + } + + if (!m_sd_tree->is_final_iteration()) + guided_path.record_to_tree( + *m_sd_tree, + sampling_context); + + return vertex.m_path_length; +} + +template +inline bool GuidedPathTracer::pass_through( + SamplingContext& sampling_context, + const Alpha alpha) +{ + // Fully opaque: never pass through. + if (alpha[0] >= 1.0f) + return false; + + // Fully transparent: always pass through. + if (alpha[0] <= 0.0f) + return true; + + // Generate a uniform sample in [0,1). + sampling_context.split_in_place(1, 1); + const float s = sampling_context.next2(); + + return s >= alpha[0]; +} + +template +inline bool GuidedPathTracer::continue_path_rr( + SamplingContext& sampling_context, + PathVertex& vertex) +{ + // Don't apply Russian Roulette for the first few bounces. + if (vertex.m_path_length <= m_rr_min_path_length) + return true; + + // Generate a uniform sample in [0,1). + sampling_context.split_in_place(1, 1); + const float s = sampling_context.next2(); + + // Compute the probability of extending this path. + const float scattering_prob = + std::min(foundation::max_value(vertex.m_throughput), 0.99f); + + // Russian Roulette. + if (!foundation::pass_rr(scattering_prob, s)) + return false; + + // Adjust throughput to account for terminated paths. + assert(scattering_prob > 0.0f); + vertex.m_throughput /= scattering_prob; + + return true; +} + +template +bool GuidedPathTracer::process_bounce( + SamplingContext& sampling_context, + PathVertex& vertex, + BSDFSample& sample, + ShadingRay& next_ray, + GPTVertexPath& guided_path) +{ + foundation::Vector3f voxel_size; + DTree *d_tree = m_sd_tree->get_d_tree(foundation::Vector3f(vertex.get_point()), voxel_size); + const float sampling_fraction = d_tree->bsdf_sampling_fraction(); + const bool enable_path_guiding = m_guided_bounces < m_max_guided_bounces; + + // Let the path visitor handle the scattering event. + m_path_visitor.on_scatter(vertex, guided_path, sampling_fraction, enable_path_guiding); + + // Terminate the path if all scattering modes are disabled. + if (vertex.m_scattering_modes == ScatteringMode::None) + return false; + + float wi_pdf, d_tree_pdf; + + PathGuidedSampler sampler( + enable_path_guiding, + m_guided_bounce_mode, + d_tree, + sampling_fraction, + *vertex.m_bsdf, + vertex.m_bsdf_data, + vertex.m_scattering_modes, + *vertex.m_shading_point, + m_sd_tree->is_built()); + + bool is_path_guided = sampler.sample( + sampling_context, + sample, + vertex.m_outgoing, + wi_pdf, + d_tree_pdf); + + // Terminate the path if it gets absorbed. + if (sample.get_mode() == ScatteringMode::None) + return false; + + if (!is_path_guided) + { + // Above-surface scattering. + if (vertex.m_bssrdf == nullptr) + { + next_ray.m_min_roughness = m_clamp_roughness ? sample.m_min_roughness : 0.0f; + + if (vertex.m_path_length == 1 && sample.get_mode() == ScatteringMode::Diffuse) + m_path_visitor.on_first_diffuse_bounce(vertex, sample.m_aov_components.m_albedo); + } + else + { + // Since the BSDF is already sampled during BSSRDF sampling, it is not sampled here. + // However, we need to check if the corresponding mode is still enabled. + if ((sample.get_mode() & vertex.m_scattering_modes) == 0) + sample.set_to_absorption(); + } + + // Terminate the path if this scattering event is not accepted. + if (!m_path_visitor.accept_scattering(vertex.m_prev_mode, sample.get_mode())) + return false; + } + + // Save the scattering properties for MIS at light-emitting vertices. + vertex.m_prev_mode = sample.get_mode(); + vertex.m_prev_prob = wi_pdf; + + // Update the AOV scattering mode only for the first bounce. + if (vertex.m_path_length == 1) + vertex.m_aov_mode = sample.get_mode(); + + // Update path throughput. + if (wi_pdf != BSDF::DiracDelta) + sample.m_value /= wi_pdf; + vertex.m_throughput *= sample.m_value.m_beauty; + + // Add a vertex to the guided path + const bool is_delta = sample.get_mode() == ScatteringMode::Specular; + + if (!guided_path.is_full() && !m_sd_tree->is_final_iteration() && (!is_delta || + (m_bsdf_sampling_fraction_mode == BSDFSamplingFractionMode::Learn && m_sd_tree->is_built()))) + { + guided_path.add_vertex( + GPTVertex{ + d_tree, + voxel_size, + foundation::Vector3f(vertex.get_point()), + sample.m_incoming.get_value(), + vertex.m_throughput, + wi_pdf != BSDF::DiracDelta ? sample.m_value.m_beauty * wi_pdf : sample.m_value.m_beauty, + renderer::Spectrum(0.0f), + wi_pdf, + sample.get_probability(), + d_tree_pdf, + is_delta + } + ); + } + + // Update bounce counters. + ++vertex.m_path_length; + m_guided_bounces += is_path_guided ? 1 : 0; + m_diffuse_bounces += (sample.get_mode() >> ScatteringMode::DiffuseBitShift) & 1; + m_glossy_bounces += (sample.get_mode() >> ScatteringMode::GlossyBitShift) & 1; + m_specular_bounces += (sample.get_mode() >> ScatteringMode::SpecularBitShift) & 1; + + // Construct the scattered ray. + const ShadingRay& ray = vertex.get_ray(); + next_ray.m_org = vertex.m_shading_point->get_point(); + next_ray.m_dir = foundation::improve_normalization<2>(foundation::Vector3d(sample.m_incoming.get_value())); + next_ray.m_time = ray.m_time; + next_ray.m_flags = ScatteringMode::get_vis_flags(sample.get_mode()), + next_ray.m_depth = ray.m_depth + 1; + + // Compute scattered ray differentials. + if (sample.m_incoming.has_derivatives()) + { + next_ray.m_rx.m_org = next_ray.m_org + vertex.m_shading_point->get_dpdx(); + next_ray.m_ry.m_org = next_ray.m_org + vertex.m_shading_point->get_dpdy(); + next_ray.m_rx.m_dir = next_ray.m_dir + foundation::Vector3d(sample.m_incoming.get_dx()); + next_ray.m_ry.m_dir = next_ray.m_dir + foundation::Vector3d(sample.m_incoming.get_dy()); + next_ray.m_has_differentials = true; + } + + return true; +} + +template +bool GuidedPathTracer::march( + SamplingContext& sampling_context, + const ShadingContext& shading_context, + const ShadingRay& ray, + PathVertex& vertex, + ShadingPoint& exit_point, + GPTVertexPath& guided_path) +{ + if (!continue_path_rr(sampling_context, vertex)) + return false; + + if (vertex.m_scattering_modes == ScatteringMode::None) + return false; + + const ShadingRay::Medium* medium = ray.get_current_medium(); + const Volume* volume = medium->get_volume(); + + // Trace the ray across the volume. + exit_point.clear(); + shading_context.get_intersector().trace( + ray, + exit_point, + vertex.m_shading_point); + + while (true) + { + shading_context.get_arena().clear(); + + // Put a hard limit on the number of iterations. + if (m_iterations++ == m_max_iterations) + { + RENDERER_LOG_WARNING( + "reached hard iteration limit (%s), breaking path tracing loop.", + foundation::pretty_int(m_max_iterations).c_str()); + return false; + } + + // Apply Russian Roulette. + if (!continue_path_rr(sampling_context, vertex)) + return false; + + if (m_volume_bounces >= m_max_volume_bounces) + vertex.m_scattering_modes &= ~ScatteringMode::Volume; + + const ShadingRay& volume_ray = exit_point.get_ray(); + + // Evaluate and prepare volume inputs. + void* data = volume->evaluate_inputs(shading_context, volume_ray); + volume->prepare_inputs(shading_context.get_arena(), volume_ray, data); + vertex.m_volume_data = data; + + // Apply the visitor to this ray. + m_volume_visitor.visit_ray(vertex, volume_ray, guided_path); + + if ((vertex.m_scattering_modes & ScatteringMode::Volume) == 0) + { + // No more scattering events are allowed: + // update the ray transmission and continue path tracing. + Spectrum transmission; + volume->evaluate_transmission( + vertex.m_volume_data, + volume_ray, + transmission); + vertex.m_throughput *= transmission; + break; + } + + // Retrieve extinction spectrum. + const Spectrum& extinction_coef = + volume->extinction_coefficient(vertex.m_volume_data, volume_ray); + + // Sample channel uniformly at random. + sampling_context.split_in_place(1, 1); + const float s = sampling_context.next2(); + const size_t channel = foundation::truncate(s * Spectrum::size()); + const bool extinction_is_null = extinction_coef[channel] < 1.0e-6f; + + // Sample distance. + float distance_sample, distance_pdf; + if (extinction_is_null) + { + distance_sample = 0.0f; + distance_pdf = 0.0f; + } + else + { + sampling_context.split_in_place(1, 1); + distance_sample = + foundation::sample_exponential_distribution( + sampling_context.next2(), + extinction_coef[channel]); + distance_pdf = + foundation::exponential_distribution_pdf( + distance_sample, + extinction_coef[channel]); + } + + // Continue path tracing if sampled distance exceeds total length of the ray, + // otherwise process the scattering event. + if (extinction_is_null || volume_ray.m_tmax < distance_sample) + { + Spectrum transmission; + volume->evaluate_transmission( + vertex.m_volume_data, + volume_ray, + transmission); + vertex.m_throughput *= transmission; + vertex.m_throughput /= // equivalent to multiplying by MIS weight + foundation::average_value(transmission); // and then dividing by transmission[channel] + break; + } + + // + // Bounce. + // + + ShadingPoint* next_shading_point = m_shading_point_arena.allocate(); + shading_context.get_intersector().make_volume_shading_point( + *next_shading_point, + volume_ray, + distance_sample); + + // Terminate the path if this scattering event is not accepted. + if (!m_volume_visitor.accept_scattering(vertex.m_prev_mode)) + return false; + + // Let the volume visitor handle the scattering event. + m_volume_visitor.on_scatter(vertex); + + // Terminate the path if all scattering modes are disabled. + if (vertex.m_scattering_modes == ScatteringMode::None) + return false; + + // Retrieve scattering spectrum. + const Spectrum& scattering_coef = + volume->scattering_coefficient(vertex.m_volume_data, volume_ray); + + // Evaluate transmission between the origin and the sampled distance. + Spectrum transmission; + volume->evaluate_transmission( + vertex.m_volume_data, + volume_ray, + distance_sample, + transmission); + + // Compute MIS weight. + // MIS terms are: + // - scattering albedo, + // - throughput of the entire path up to the sampled point. + // Reference: "Practical and Controllable Subsurface Scattering + // for Production Path Tracing", p. 1 [ACM 2016 Article]. + float mis_weights_sum = 0.0f; + for (size_t i = 0, e = Spectrum::size(); i < e; ++i) + { + if (extinction_coef[i] > 1.0e-6f) + { + const float probability = + foundation::exponential_distribution_pdf( + distance_sample, + extinction_coef[i]); + mis_weights_sum += foundation::square(probability); + } + } + if (mis_weights_sum < 1.0e-6f) + return false; // no scattering + const float current_mis_weight = + Spectrum::size() * + foundation::square(distance_pdf) / + mis_weights_sum; + + vertex.m_throughput *= scattering_coef; + vertex.m_throughput *= transmission; + vertex.m_throughput *= current_mis_weight / distance_pdf; + + // Sample phase function. + foundation::Vector3f incoming; + const float pdf = volume->sample( + sampling_context, + vertex.m_volume_data, + volume_ray, + distance_sample, + incoming); + + // Save the scattering properties for MIS at light-emitting vertices. + vertex.m_prev_mode = ScatteringMode::Volume; + vertex.m_prev_prob = pdf; + + // Update the AOV scattering mode only for the first bounce. + if (vertex.m_path_length == 1) + vertex.m_aov_mode = ScatteringMode::Volume; + + ++m_volume_bounces; + ++vertex.m_path_length; + + // Continue the ray in in-scattered direction. + ShadingRay next_ray( + volume_ray.m_org + static_cast(distance_sample) * volume_ray.m_dir, + foundation::improve_normalization<2>(foundation::Vector3d(incoming)), + volume_ray.m_time, + volume_ray.m_flags, + volume_ray.m_depth + 1); + next_ray.copy_media_from(volume_ray); + + // Update the pointer to the shading points. + vertex.m_shading_point = next_shading_point; + + // Trace the ray across the volume. + exit_point.clear(); + shading_context.get_intersector().trace(next_ray, exit_point, nullptr); + } + + return true; +} + +template +inline const ShadingPoint& GuidedPathTracer::get_path_vertex(const size_t i) const +{ + return reinterpret_cast(m_shading_point_arena.get_storage())[i]; +} + +} // namespace renderer \ No newline at end of file diff --git a/src/appleseed/renderer/kernel/lighting/materialsamplers.h b/src/appleseed/renderer/kernel/lighting/materialsamplers.h index 81d3db8072..8ea50e59c1 100644 --- a/src/appleseed/renderer/kernel/lighting/materialsamplers.h +++ b/src/appleseed/renderer/kernel/lighting/materialsamplers.h @@ -135,7 +135,7 @@ class BSDFSampler const int light_sampling_modes, DirectShadingComponents& value) const override; - private: + protected: const BSDF& m_bsdf; const void* m_bsdf_data; const int m_bsdf_sampling_modes; diff --git a/src/appleseed/renderer/kernel/lighting/sdtree.cpp b/src/appleseed/renderer/kernel/lighting/sdtree.cpp new file mode 100644 index 0000000000..a733c991fe --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/sdtree.cpp @@ -0,0 +1,1433 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// Interface header. +#include "sdtree.h" + +// appleseed.renderer headers. +#include "renderer/global/globallogger.h" +#include "renderer/modeling/camera/camera.h" + +// appleseed.foundation headers. +#include "foundation/math/scalar.h" +#include "foundation/math/sampling/mappings.h" +#include "foundation/string/string.h" + +// Standard headers. +#include +#include +#include +#include + +using namespace foundation; + +namespace renderer +{ + +// +// SD-Tree implementation for "Practical Path Guiding for Efficient Light-Transport Simulation" [Müller et al. 2017]. +// + +const size_t SpatialSubdivisionThreshold = 4000; // TODO: make this dependent on the filter types +const float DTreeThreshold = 0.01f; +const size_t DTreeMaxDepth = 20; +const float DTreeGlossyAreaFraction = 0.1f; +const float DTreeGlossyEnergyThreshold = 0.7f; + +// Sampling fraction optimization constants. + +const float Beta1 = 0.9f; +const float Beta2 = 0.999f; +const float OptimizationEpsilon = 1e-8f; +const float Regularization = 0.01f; + +// Helper functions. + +void atomic_add( + std::atomic& atomic, + const float value) +{ + float current = atomic.load(std::memory_order_relaxed); + while (!atomic.compare_exchange_weak(current, current + value)) + ; +} + +inline float logistic(float x) +{ + return 1.0f / (1.0f + std::exp(-x)); +} + +Vector2f cartesian_to_cylindrical( + const Vector3f& direction) +{ + const float cos_theta = direction.z; + float phi = std::atan2(direction.y, direction.x); + + if (phi < 0.0f) + phi += TwoPi(); + + // D-tree directions are stored as 2D [cos(theta), phi] coordinates to preserve area. + // Theta is the angle with z-Axis to ensure compatibility with SD-tree visualizer [Müller et.al. 2017] + return Vector2f( + (cos_theta + 1.0f) * 0.5f, + phi * RcpTwoPi()); +} + +Vector3f cylindrical_to_cartesian( + const Vector2f& cylindrical_direction) +{ + assert(cylindrical_direction[0] >= 0.0f && cylindrical_direction[0] < 1.0f); + assert(cylindrical_direction[1] >= 0.0f && cylindrical_direction[1] < 1.0f); + + const float phi = TwoPi() * cylindrical_direction[1]; + const float cos_theta = 2.0f * cylindrical_direction[0] - 1.0f; + const float sin_theta = std::sqrt(1.0f - cos_theta * cos_theta); + + return Vector3f( + std::cos(phi) * sin_theta, + std::sin(phi) * sin_theta, + cos_theta); +} + +template +void write( + std::ofstream& outstream, + const T data) +{ + outstream.write(reinterpret_cast(&data), sizeof(T)); +} + +// Node structure compatible with SD tree visualizer [Müller et al. 2017]. + +struct VisualizerNode +{ + std::array sums; + std::array children; +}; + +// QuadTreeNode implementation. + +QuadTreeNode::QuadTreeNode( + const bool create_children, + const float radiance_sum) + : m_is_leaf(!create_children) + , m_current_iter_radiance_sum(radiance_sum) + , m_previous_iter_radiance_sum(radiance_sum) +{ + if(create_children) + { + m_upper_left_node.reset(new QuadTreeNode(false)); + m_upper_right_node.reset(new QuadTreeNode(false)); + m_lower_right_node.reset(new QuadTreeNode(false)); + m_lower_left_node.reset(new QuadTreeNode(false)); + } +} + +QuadTreeNode::QuadTreeNode(const QuadTreeNode& other) + : m_current_iter_radiance_sum(other.m_current_iter_radiance_sum.load(std::memory_order_relaxed)) + , m_previous_iter_radiance_sum(other.m_previous_iter_radiance_sum) + , m_is_leaf(other.m_is_leaf) +{ + if(!other.m_is_leaf) + { + m_upper_left_node.reset(new QuadTreeNode(*other.m_upper_left_node)); + m_upper_right_node.reset(new QuadTreeNode(*other.m_upper_right_node)); + m_lower_right_node.reset(new QuadTreeNode(*other.m_lower_right_node)); + m_lower_left_node.reset(new QuadTreeNode(*other.m_lower_left_node)); + } +} + +void QuadTreeNode::add_radiance( + Vector2f& direction, + const float radiance) +{ + if(m_is_leaf) + atomic_add(m_current_iter_radiance_sum, radiance); + else + choose_node(direction)->add_radiance(direction, radiance); +} + +void QuadTreeNode::add_radiance( + const AABB2f& splat_aabb, + const AABB2f& node_aabb, + const float radiance) +{ + const AABB2f intersection_aabb(AABB2f::intersect(splat_aabb, node_aabb)); + + if(!intersection_aabb.is_valid()) + return; + + const float intersection_volume = intersection_aabb.volume(); + + if(intersection_volume <= 0.0f) + return; + + if(m_is_leaf) + { + atomic_add(m_current_iter_radiance_sum, radiance * intersection_volume); + } + else + { + // Create each child's AABB and recursively add radiance. + const Vector2f node_size = node_aabb.extent(); + AABB2f child_aabb(node_aabb.min, node_aabb.min + 0.5f * node_size); + m_upper_left_node->add_radiance(splat_aabb, child_aabb, radiance); + + child_aabb.translate(Vector2f(0.5f * node_size.x, 0.0f)); + m_upper_right_node->add_radiance(splat_aabb, child_aabb, radiance); + + child_aabb.translate(Vector2f(0.0f, 0.5f * node_size.x)); + m_lower_right_node->add_radiance(splat_aabb, child_aabb, radiance); + + child_aabb.translate(Vector2f(-0.5f * node_size.x, 0.0f)); + m_lower_left_node->add_radiance(splat_aabb, child_aabb, radiance); + } +} + +size_t QuadTreeNode::max_depth() const +{ + if(m_is_leaf) + return 1; + + size_t max_child_depth = m_upper_left_node->max_depth(); + max_child_depth = std::max(m_upper_right_node->max_depth(), max_child_depth); + max_child_depth = std::max(m_lower_right_node->max_depth(), max_child_depth); + max_child_depth = std::max(m_lower_left_node->max_depth(), max_child_depth); + return 1 + max_child_depth; +} + +size_t QuadTreeNode::node_count() const +{ + if(m_is_leaf) + return 1; + + return 1 + + m_upper_left_node->node_count() + + m_upper_right_node->node_count() + + m_lower_right_node->node_count() + + m_lower_left_node->node_count(); +} + +float QuadTreeNode::radiance_sum() const +{ + return m_previous_iter_radiance_sum; +} + +float QuadTreeNode::build_radiance_sums() +{ + if(m_is_leaf) + { + m_previous_iter_radiance_sum = m_current_iter_radiance_sum.load(std::memory_order_relaxed); + return m_previous_iter_radiance_sum; + } + + m_previous_iter_radiance_sum = 0.0f; + m_previous_iter_radiance_sum += m_upper_left_node->build_radiance_sums(); + m_previous_iter_radiance_sum += m_upper_right_node->build_radiance_sums(); + m_previous_iter_radiance_sum += m_lower_right_node->build_radiance_sums(); + m_previous_iter_radiance_sum += m_lower_left_node->build_radiance_sums(); + return m_previous_iter_radiance_sum; +} + +// Implementation of Algorithm 4 in Practical Path Guiding complementary PDF [Müller et.al. 2017] +// https://tom94.net/data/publications/mueller17practical/mueller17practical-supp.pdf + +void QuadTreeNode::restructure( + const float total_radiance_sum, + const float subdiv_threshold, + std::vector< + std::pair>* sorted_energy_ratios, + const size_t depth) +{ + const float fraction = m_previous_iter_radiance_sum / total_radiance_sum; + + // Check if this node satisfies subdivision criterion. + if(fraction > subdiv_threshold && depth < DTreeMaxDepth) + { + if(m_is_leaf) + { + // Create new children. + m_is_leaf = false; + const float quarter_sum = 0.25f * m_previous_iter_radiance_sum; + m_upper_left_node.reset(new QuadTreeNode(false, quarter_sum)); + m_upper_right_node.reset(new QuadTreeNode(false, quarter_sum)); + m_lower_right_node.reset(new QuadTreeNode(false, quarter_sum)); + m_lower_left_node.reset(new QuadTreeNode(false, quarter_sum)); + } + + // Recursively ensure children satisfy subdivision criterion. + m_upper_left_node->restructure(total_radiance_sum, subdiv_threshold, sorted_energy_ratios, depth + 1); + m_upper_right_node->restructure(total_radiance_sum, subdiv_threshold, sorted_energy_ratios, depth + 1); + m_lower_right_node->restructure(total_radiance_sum, subdiv_threshold, sorted_energy_ratios, depth + 1); + m_lower_left_node->restructure(total_radiance_sum, subdiv_threshold, sorted_energy_ratios, depth + 1); + } + else if(!m_is_leaf) + { + // If this interior node does not satisfy the subdivision criterion + // revert it into a leaf. + m_is_leaf = true; + m_upper_left_node.reset(nullptr); + m_upper_right_node.reset(nullptr); + m_lower_right_node.reset(nullptr); + m_lower_left_node.reset(nullptr); + } + + if(sorted_energy_ratios != nullptr && !m_is_leaf && m_upper_left_node->m_is_leaf) + { + const std::pair ratio( + std::pow(0.25f, static_cast(depth - 1)), + 4.0f * m_upper_left_node->radiance_sum() / total_radiance_sum); + + auto insert_pos = std::lower_bound(sorted_energy_ratios->cbegin(), sorted_energy_ratios->cend(), ratio); + sorted_energy_ratios->insert(insert_pos, ratio); + } + + m_current_iter_radiance_sum.store(0.0f, std::memory_order_relaxed); +} + +void QuadTreeNode::reset() +{ + m_upper_left_node.reset(new QuadTreeNode(false)); + m_upper_right_node.reset(new QuadTreeNode(false)); + m_lower_right_node.reset(new QuadTreeNode(false)); + m_lower_left_node.reset(new QuadTreeNode(false)); + + m_is_leaf = false; + m_current_iter_radiance_sum.store(0.0f, std::memory_order_relaxed); + m_previous_iter_radiance_sum = 0.0f; +} + +// Implementation of Algorithm 2 in Practical Path Guiding complementary PDF [Müller et.al. 2017] +// https://tom94.net/data/publications/mueller17practical/mueller17practical-supp.pdf + +float QuadTreeNode::pdf( + Vector2f& direction) const +{ + if(m_is_leaf) + return RcpFourPi(); + + const QuadTreeNode* sub_node = choose_node(direction); + const float factor = 4.0f * sub_node->m_previous_iter_radiance_sum / m_previous_iter_radiance_sum; + return factor * sub_node->pdf(direction); +} + +const Vector2f QuadTreeNode::sample( + Vector2f& sample, + float& pdf) const +{ + pdf = 1.0f; // initiate to one for recursive sampling routine + return sample_recursive(sample, pdf); +} + +// Implementation of Algorithm 1 in Practical Path Guiding complementary pdf [Müller et.al. 2017] +// https://tom94.net/data/publications/mueller17practical/mueller17practical-supp.pdf + +const Vector2f QuadTreeNode::sample_recursive( + Vector2f& sample, + float& pdf) const +{ + assert(sample.x >= 0.0f && sample.x <= 1.0f); + assert(sample.y >= 0.0f && sample.y <= 1.0f); + + // Ensure each sample dimension is < 1.0 after renormalization in previous recursive step. + if(sample.x >= 1.0f) + sample.x = std::nextafter(1.0f, 0.0f); + + if(sample.y >= 1.0f) + sample.y = std::nextafter(1.0f, 0.0f); + + if(m_is_leaf) + { + pdf *= RcpFourPi(); + return sample; + } + + const float upper_left = m_upper_left_node->m_previous_iter_radiance_sum; + const float upper_right = m_upper_right_node->m_previous_iter_radiance_sum; + const float lower_right = m_lower_right_node->m_previous_iter_radiance_sum; + const float lower_left = m_lower_left_node->m_previous_iter_radiance_sum; + const float sum_left_half = upper_left + lower_left; + const float sum_right_half = upper_right + lower_right; + + float factor = sum_left_half / m_previous_iter_radiance_sum; + + // Sample child nodes with probability proportional to their energy. + if(sample.x < factor) + { + sample.x /= factor; + factor = upper_left / sum_left_half; + + if(sample.y < factor) + { + sample.y /= factor; + const Vector2f sampled_direction = + Vector2f(0.0f, 0.0f) + 0.5f * m_upper_left_node->sample(sample, pdf); + + const float probability_factor = 4.0f * upper_left / m_previous_iter_radiance_sum; + pdf *= probability_factor; + return sampled_direction; + } + + sample.y = (sample.y - factor) / (1.0f - factor); + const Vector2f sampled_direction = + Vector2f(0.0f, 0.5f) + 0.5f * m_lower_left_node->sample(sample, pdf); + + const float probability_factor = 4.0f * lower_left / m_previous_iter_radiance_sum; + pdf *= probability_factor; + return sampled_direction; + } + else + { + sample.x = (sample.x - factor) / (1.0f - factor); + factor = upper_right / sum_right_half; + + if (sample.y < factor) + { + sample.y /= factor; + const Vector2f sampled_direction = + Vector2f(0.5f, 0.0f) + 0.5f * m_upper_right_node->sample(sample, pdf); + + const float probability_factor = 4.0f * upper_right / m_previous_iter_radiance_sum; + pdf *= probability_factor; + return sampled_direction; + } + + sample.y = (sample.y - factor) / (1.0f - factor); + const Vector2f sampled_direction = + Vector2f(0.5f, 0.5f) + 0.5f * m_lower_right_node->sample(sample, pdf); + + const float probability_factor = 4.0f * lower_right / m_previous_iter_radiance_sum; + pdf *= probability_factor; + return sampled_direction; + } +} + +size_t QuadTreeNode::depth( + Vector2f& direction) const +{ + if(m_is_leaf) + return 1; + + return 1 + choose_node(direction)->depth(direction); +} + +QuadTreeNode* QuadTreeNode::choose_node( + Vector2f& direction) const +{ + if(direction.x < 0.5f) + { + direction.x *= 2.0f; + if(direction.y < 0.5f) + { + direction.y *= 2.0f; + return m_upper_left_node.get(); + } + else + { + direction.y = direction.y * 2.0f - 1.0f; + return m_lower_left_node.get(); + } + } + else + { + direction.x = direction.x * 2.0f - 1.0f; + if(direction.y < 0.5f) + { + direction.y *= 2.0f; + return m_upper_right_node.get(); + } + else + { + direction.y = direction.y * 2.0f - 1.0f; + return m_lower_right_node.get(); + } + } +} + +void QuadTreeNode::flatten( + std::list& nodes) const +{ + nodes.push_back({}); + VisualizerNode& node = nodes.back(); + + node.sums[0] = m_upper_left_node->m_previous_iter_radiance_sum; + if(m_upper_left_node->m_is_leaf) + { + node.children[0] = 0; + } + else + { + const size_t next_index = nodes.size(); + m_upper_left_node->flatten(nodes); + node.children[0] = next_index; + } + + node.sums[1] = m_upper_right_node->m_previous_iter_radiance_sum; + if(m_upper_right_node->m_is_leaf) + { + node.children[1] = 0; + } + else + { + const size_t next_index = nodes.size(); + m_upper_right_node->flatten(nodes); + node.children[1] = next_index; + } + + node.sums[2] = m_lower_left_node->m_previous_iter_radiance_sum; + if (m_lower_left_node->m_is_leaf) + { + node.children[2] = 0; + } + else + { + const size_t next_index = nodes.size(); + m_lower_left_node->flatten(nodes); + node.children[2] = next_index; + } + + node.sums[3] = m_lower_right_node->m_previous_iter_radiance_sum; + if(m_lower_right_node->m_is_leaf) + { + node.children[3] = 0; + } + else + { + const size_t next_index = nodes.size(); + m_lower_right_node->flatten(nodes); + node.children[3] = next_index; + } +} + +struct DTreeRecord +{ + Vector3f direction; + float radiance; + float wi_pdf; + float bsdf_pdf; + float d_tree_pdf; + float sample_weight; + float product; + bool is_delta; +}; + +// DTree implementation. + +DTree::DTree( + const GPTParameters& parameters) + : m_parameters(parameters) + , m_root_node(true) + , m_current_iter_sample_weight(0.0f) + , m_previous_iter_sample_weight(0.0f) + , m_optimization_step_count(0) + , m_first_moment(0.0f) + , m_second_moment(0.0f) + , m_theta(0.0f) + , m_is_built(false) + , m_scattering_mode(ScatteringMode::Diffuse) +{ + m_atomic_flag.clear(std::memory_order_release); +} + +DTree::DTree( + const DTree& other) + : m_parameters(other.m_parameters) + , m_current_iter_sample_weight(other.m_current_iter_sample_weight.load(std::memory_order_relaxed)) + , m_previous_iter_sample_weight(other.m_previous_iter_sample_weight) + , m_root_node(other.m_root_node) + , m_optimization_step_count(other.m_optimization_step_count) + , m_first_moment(other.m_first_moment) + , m_second_moment(other.m_second_moment) + , m_theta(other.m_theta) + , m_is_built(other.m_is_built) + , m_scattering_mode(other.m_scattering_mode) +{ + m_atomic_flag.clear(std::memory_order_release); +} + +void DTree::record( + const DTreeRecord& d_tree_record) +{ + if(m_parameters.m_bsdf_sampling_fraction_mode == BSDFSamplingFractionMode::Learn && m_is_built && d_tree_record.product > 0.0f) + optimization_step(d_tree_record); // also optimizes delta records + + if(d_tree_record.is_delta || d_tree_record.wi_pdf <= 0.0f) + return; + + atomic_add(m_current_iter_sample_weight, d_tree_record.sample_weight); + + const float radiance = d_tree_record.radiance / d_tree_record.wi_pdf * d_tree_record.sample_weight; + + Vector2f direction = cartesian_to_cylindrical(d_tree_record.direction); + + switch (m_parameters.m_directional_filter) + { + case DirectionalFilter::Nearest: + m_root_node.add_radiance(direction, radiance); + break; + + case DirectionalFilter::Box: + { + // Determine the node size at the direction. + const size_t leaf_depth = depth(direction); + const Vector2f leaf_size(std::pow(0.25f, static_cast(leaf_depth - 1))); + const AABB2f node_aabb(Vector2f(0.0f), Vector2f(1.0f)); + const AABB2f splat_aabb(direction - 0.5f * leaf_size, direction + 0.5f * leaf_size); + + if(!splat_aabb.is_valid()) + return; + + m_root_node.add_radiance(splat_aabb, node_aabb, radiance / splat_aabb.volume()); + break; + } + default: + break; + } +} + +void DTree::sample( + SamplingContext& sampling_context, + DTreeSample& d_tree_sample, + const int modes) const +{ + if((modes & m_scattering_mode) == 0) + { + d_tree_sample.scattering_mode = ScatteringMode::None; + d_tree_sample.pdf = 0.0f; + return; + } + + sampling_context.split_in_place(2, 1); + Vector2f s = sampling_context.next2(); + + if (m_previous_iter_sample_weight <= 0.0f || m_root_node.radiance_sum() <= 0.0f) + { + d_tree_sample.direction = sample_sphere_uniform(s); + d_tree_sample.pdf = RcpFourPi(); + d_tree_sample.scattering_mode = ScatteringMode::Diffuse; + } + else + { + const Vector2f direction = m_root_node.sample(s, d_tree_sample.pdf); + d_tree_sample.direction = cylindrical_to_cartesian(direction); + d_tree_sample.scattering_mode = m_scattering_mode; + } +} + +float DTree::pdf( + const Vector3f& direction, + const int modes) const +{ + if ((modes & m_scattering_mode) == 0) + return 0.0f; + + if(m_previous_iter_sample_weight <= 0.0f || m_root_node.radiance_sum() <= 0.0f) + return RcpFourPi(); + + Vector2f dir = cartesian_to_cylindrical(direction); + return m_root_node.pdf(dir); +} + +void DTree::halve_sample_weight() +{ + m_current_iter_sample_weight = 0.5f * m_current_iter_sample_weight.load(std::memory_order_relaxed); + m_previous_iter_sample_weight *= 0.5f; +} + +size_t DTree::node_count() const +{ + return m_root_node.node_count(); +} + +size_t DTree::max_depth() const +{ + return m_root_node.max_depth(); +} + +size_t DTree::depth( + const Vector2f& direction) const +{ + Vector2f local_direction = direction; + + return m_root_node.depth(local_direction); +} + +ScatteringMode::Mode DTree::get_scattering_mode() const +{ + return m_scattering_mode; +} + +void DTree::build() +{ + m_previous_iter_sample_weight = m_current_iter_sample_weight.load(std::memory_order_relaxed); + m_root_node.build_radiance_sums(); +} + +void DTree::restructure( + const float subdiv_threshold) +{ + m_is_built = true; + m_current_iter_sample_weight.store(0.0f, std::memory_order_relaxed); + const float radiance_sum = m_root_node.radiance_sum(); + + // Reset D-Trees that did not collect radiance. + if(radiance_sum <= 0.0f) + { + m_root_node.reset(); + m_scattering_mode = ScatteringMode::Diffuse; + m_optimization_step_count = 0; + m_first_moment = 0.0f; + m_second_moment = 0.0f; + m_theta = 0.0f; + return; + } + + std::vector> sorted_energy_ratios; + m_root_node.restructure( + radiance_sum, subdiv_threshold, + m_parameters.m_guided_bounce_mode == GuidedBounceMode::Learn ? + &sorted_energy_ratios : nullptr); + + // Determine what ScatteringMode should be assigned to directions sampled from this D-tree. + if(m_parameters.m_guided_bounce_mode == GuidedBounceMode::Learn) + { + float area_fraction_sum = 0.0f; + float energy_fraction_sum = 0.0f; + bool is_glossy = false; + auto itr = sorted_energy_ratios.cbegin(); + + while(itr != sorted_energy_ratios.cend() && area_fraction_sum + itr->first < DTreeGlossyAreaFraction) + { + area_fraction_sum += itr->first; + energy_fraction_sum += itr->second; + + // If a significant part of the energy is stored in a small subset of directions + // treat bounces as Glossy, otherwise treat them as Diffuse. + if(energy_fraction_sum > DTreeGlossyEnergyThreshold) + { + is_glossy = true; + break; + } + + ++itr; + } + + m_scattering_mode = is_glossy ? ScatteringMode::Glossy : ScatteringMode::Diffuse; + } +} + +float DTree::sample_weight() const +{ + return m_previous_iter_sample_weight; +} + +float DTree::mean() const +{ + if (m_previous_iter_sample_weight <= 0.0f) + return 0.0f; + + return m_root_node.radiance_sum() * (1.0f / m_previous_iter_sample_weight) * RcpFourPi(); +} + +float DTree::bsdf_sampling_fraction() const +{ + if(m_parameters.m_bsdf_sampling_fraction_mode == BSDFSamplingFractionMode::Learn) + return logistic(m_theta); + else + return m_parameters.m_fixed_bsdf_sampling_fraction; +} + +void DTree::acquire_optimization_spin_lock() +{ + while(m_atomic_flag.test_and_set(std::memory_order_acquire)) + ; +} + +void DTree::release_optimization_spin_lock() +{ + m_atomic_flag.clear(std::memory_order_release); +} + +// BSDF sampling fraction optimization procedure. +// Implementation of Algorithm 3 in chapter "Practical Path Guiding in Production" [Müller 2019] +// released in "Path Guiding in Production" Siggraph Course 2019, [Vorba et. al. 2019] +// Implements the stochastic-gradient-based Adam optimizer [Kingma and Ba 2014] + +void DTree::optimization_step( + const DTreeRecord& d_tree_record) +{ + acquire_optimization_spin_lock(); + + const float sampling_fraction = bsdf_sampling_fraction(); + const float combined_pdf = sampling_fraction * d_tree_record.bsdf_pdf + + (1.0f - sampling_fraction) * d_tree_record.d_tree_pdf; + + const float d_sampling_fraction = -d_tree_record.product * + (d_tree_record.bsdf_pdf - d_tree_record.d_tree_pdf) / + (d_tree_record.wi_pdf * combined_pdf); + + const float d_theta = d_sampling_fraction * sampling_fraction * (1.0f - sampling_fraction); + const float reg_gradient = m_theta * Regularization; + const float gradient = (d_theta + reg_gradient) * d_tree_record.sample_weight; + + adam_step(gradient); + + release_optimization_spin_lock(); +} + +void DTree::adam_step( + const float gradient) +{ + ++m_optimization_step_count; + const float debiased_learning_rate = m_parameters.m_learning_rate * + std::sqrt(1.0f - std::pow(Beta2, static_cast(m_optimization_step_count))) / + (1.0f - std::pow(Beta1, static_cast(m_optimization_step_count))); + + m_first_moment = Beta1 * m_first_moment + (1.0f - Beta1) * gradient; + m_second_moment = Beta2 * m_second_moment + (1.0f - Beta2) * gradient * gradient; + m_theta -= debiased_learning_rate * m_first_moment / (std::sqrt(m_second_moment) + OptimizationEpsilon); + + m_theta = clamp(m_theta, -20.0f, 20.0f); +} + +void DTree::write_to_disk( + std::ofstream& os) const +{ + std::list nodes; + m_root_node.flatten(nodes); + + write(os, mean()); + write(os, static_cast(sample_weight())); + write(os, static_cast(nodes.size())); + + for(const auto& n : nodes) + for(int i = 0; i < 4; ++i) + { + write(os, n.sums[i]); + write(os, static_cast(n.children[i])); + } +} + +// Struct used to gather SD-tree statistics. + +struct DTreeStatistics +{ + DTreeStatistics() + : num_d_trees(0) + , min_d_tree_depth(std::numeric_limits::max()) + , max_d_tree_depth(0) + , average_d_tree_depth(0.0f) + , min_d_tree_nodes(std::numeric_limits::max()) + , max_d_tree_nodes(0) + , average_d_tree_nodes(0.0f) + , min_sample_weight(std::numeric_limits::max()) + , max_sample_weight(0.0f) + , average_sample_weight(0.0f) + , min_sampling_fraction(std::numeric_limits::max()) + , max_sampling_fraction(0.0f) + , average_sampling_fraction(0.0f) + , min_mean_radiance(std::numeric_limits::max()) + , max_mean_radiance(0.0f) + , average_mean_radiance(0.0f) + , glossy_d_tree_fraction(0.0f) + , num_s_tree_nodes(0) + , min_s_tree_depth(std::numeric_limits::max()) + , max_s_tree_depth(0) + , average_s_tree_depth(0.0f) + {} + + size_t num_d_trees; + size_t min_d_tree_depth; + size_t max_d_tree_depth; + float average_d_tree_depth; + size_t min_d_tree_nodes; + size_t max_d_tree_nodes; + float average_d_tree_nodes; + float min_sample_weight; + float max_sample_weight; + float average_sample_weight; + float min_sampling_fraction; + float max_sampling_fraction; + float average_sampling_fraction; + float min_mean_radiance; + float max_mean_radiance; + float average_mean_radiance; + float glossy_d_tree_fraction; + + size_t num_s_tree_nodes; + size_t min_s_tree_depth; + size_t max_s_tree_depth; + float average_s_tree_depth; + + void build() + { + if(num_d_trees <= 0) + return; + + average_d_tree_depth /= num_d_trees; + average_s_tree_depth /= num_d_trees; + average_d_tree_nodes /= num_d_trees; + average_mean_radiance /= num_d_trees; + average_sample_weight /= num_d_trees; + glossy_d_tree_fraction /= num_d_trees; + average_sampling_fraction /= num_d_trees; + } +}; + +// STreeNode implementation. + +STreeNode::STreeNode( + const GPTParameters& parameters) + : m_axis(0) + , m_d_tree(new DTree(parameters)) +{} + +STreeNode::STreeNode( + const unsigned int parent_axis, + const DTree* parent_d_tree) + : m_axis((parent_axis + 1) % 3) + , m_d_tree(new DTree(*parent_d_tree)) +{ + m_d_tree->halve_sample_weight(); +} + +DTree* STreeNode::get_d_tree( + Vector3f& point, + Vector3f& size) +{ + if(is_leaf()) + return m_d_tree.get(); + else + { + size[m_axis] *= 0.5f; + return choose_node(point)->get_d_tree(point, size); + } +} + +// Implementation of Algorithm 3 in Practical Path Guiding complementary pdf [Müller et.al. 2017] +// https://tom94.net/data/publications/mueller17practical/mueller17practical-supp.pdf + +void STreeNode::subdivide( + const size_t required_samples) +{ + if(is_leaf()) + { + if (m_d_tree->sample_weight() > required_samples) + subdivide(); + else + return; + } + + m_first_node->subdivide(required_samples); + m_second_node->subdivide(required_samples); +} + +void STreeNode::subdivide() +{ + if(is_leaf()) + { + m_first_node.reset(new STreeNode(m_axis, m_d_tree.get())); + m_second_node.reset(new STreeNode(m_axis, m_d_tree.get())); + m_d_tree.reset(nullptr); + } +} + +void STreeNode::record( + const AABB3f& splat_aabb, + const AABB3f& node_aabb, + const DTreeRecord& d_tree_record) +{ + const AABB3f intersection_aabb(AABB3f::intersect(splat_aabb, node_aabb)); + + if(!intersection_aabb.is_valid()) + return; + + const float intersection_volume = intersection_aabb.volume(); + + if(intersection_volume <= 0.0f) + return; + + if(is_leaf()) + { + m_d_tree->record( + DTreeRecord{ + d_tree_record.direction, + d_tree_record.radiance, + d_tree_record.wi_pdf, + d_tree_record.bsdf_pdf, + d_tree_record.d_tree_pdf, + d_tree_record.sample_weight * intersection_volume, + d_tree_record.product, + d_tree_record.is_delta}); + } + else + { + const Vector3f node_size = node_aabb.extent(); + Vector3f offset(0.0f); + offset[m_axis] = node_size[m_axis] * 0.5f; + + m_first_node->record(splat_aabb, AABB3f(node_aabb.min, node_aabb.max - offset), d_tree_record); + m_second_node->record(splat_aabb, AABB3f(node_aabb.min + offset, node_aabb.max), d_tree_record); + } +} + +void STreeNode::restructure( + const float subdiv_threshold) +{ + if(is_leaf()) + { + m_d_tree->restructure(subdiv_threshold); + } + else + { + m_first_node->restructure(subdiv_threshold); + m_second_node->restructure(subdiv_threshold); + } +} + +void STreeNode::build() +{ + if(is_leaf()) + { + m_d_tree->build(); + } + else + { + m_first_node->build(); + m_second_node->build(); + } +} + +void STreeNode::gather_statistics( + DTreeStatistics& statistics, + const size_t depth) const +{ + statistics.num_s_tree_nodes++; + if(is_leaf()) + { + ++statistics.num_d_trees; + const size_t d_tree_depth = m_d_tree->max_depth(); + statistics.max_d_tree_depth = std::max(statistics.max_d_tree_depth, d_tree_depth); + statistics.min_d_tree_depth = std::min(statistics.min_d_tree_depth, d_tree_depth); + statistics.average_d_tree_depth += d_tree_depth; + + const float mean_radiance = m_d_tree->mean(); + statistics.max_mean_radiance = std::max(statistics.max_mean_radiance, mean_radiance); + statistics.min_mean_radiance = std::min(statistics.min_mean_radiance, mean_radiance); + statistics.average_mean_radiance += mean_radiance; + + const size_t node_count = m_d_tree->node_count(); + statistics.max_d_tree_nodes = std::max(statistics.max_d_tree_nodes, node_count); + statistics.min_d_tree_nodes = std::min(statistics.min_d_tree_nodes, node_count); + statistics.average_d_tree_nodes += node_count; + + const float sample_weight = m_d_tree->sample_weight(); + statistics.max_sample_weight = std::max(statistics.max_sample_weight, sample_weight); + statistics.min_sample_weight = std::min(statistics.min_sample_weight, sample_weight); + statistics.average_sample_weight += sample_weight; + + if(m_d_tree->get_scattering_mode() == ScatteringMode::Glossy) + statistics.glossy_d_tree_fraction += 1.0f; + + const float bsdf_sampling_fraction = m_d_tree->bsdf_sampling_fraction(); + statistics.min_sampling_fraction = std::min(statistics.min_sampling_fraction, bsdf_sampling_fraction); + statistics.max_sampling_fraction = std::max(statistics.max_sampling_fraction, bsdf_sampling_fraction); + statistics.average_sampling_fraction += bsdf_sampling_fraction; + + statistics.max_s_tree_depth = std::max(statistics.max_s_tree_depth, depth); + statistics.min_s_tree_depth = std::min(statistics.min_s_tree_depth, depth); + statistics.average_s_tree_depth += depth; + } + else + { + m_first_node->gather_statistics(statistics, depth + 1); + m_second_node->gather_statistics(statistics, depth + 1); + } +} + +STreeNode* STreeNode::choose_node( + Vector3f& point) const +{ + if(point[m_axis] < 0.5f) + { + point[m_axis] *= 2.0f; + return m_first_node.get(); + } + else + { + point[m_axis] = (point[m_axis] - 0.5f) * 2.0f; + return m_second_node.get(); + } +} + +bool STreeNode::is_leaf() const +{ + return m_d_tree != nullptr; +} + +void STreeNode::write_to_disk( + std::ofstream& os, + const AABB3f& aabb) const +{ + if(is_leaf()) + { + if(m_d_tree->sample_weight() > 0.0) + { + const Vector3f extent = aabb.extent(); + write(os, aabb.min.x); + write(os, aabb.min.y); + write(os, aabb.min.z); + write(os, extent.x); + write(os, extent.y); + write(os, extent.z); + + m_d_tree->write_to_disk(os); + } + } + else + { + AABB3f child_aabb(aabb); + const float half_extent = 0.5f * aabb.extent()[m_axis]; + child_aabb.max[m_axis] -= half_extent; + m_first_node->write_to_disk(os, child_aabb); + + child_aabb.min[m_axis] += half_extent; + child_aabb.max[m_axis] += half_extent; + m_second_node->write_to_disk(os, child_aabb); + } +} + +// STree implementation. + +STree::STree( + const Scene& scene, + const GPTParameters& parameters) + : m_parameters(parameters) + , m_scene_aabb(scene.compute_bbox()) + , m_is_built(false) + , m_is_final_iteration(false) + , m_scene(scene) +{ + m_root_node.reset(new STreeNode(m_parameters)); + + // Grow the AABB into a cube for nicer hierarchical subdivisions [Müller et. al. 2017]. + const Vector3f size = m_scene_aabb.extent(); + const float maxSize = max_value(size); + m_scene_aabb.max = m_scene_aabb.min + Vector3f(maxSize); +} + +DTree* STree::get_d_tree( + const Vector3f& point, + Vector3f& d_tree_voxel_size) +{ + d_tree_voxel_size = m_scene_aabb.extent(); + Vector3f transformed_point = point - m_scene_aabb.min; + transformed_point /= d_tree_voxel_size; + + return m_root_node->get_d_tree(transformed_point, d_tree_voxel_size); +} + +DTree* STree::get_d_tree( + const Vector3f& point) +{ + Vector3f d_tree_voxel_size; + return get_d_tree(point, d_tree_voxel_size); +} + +void STree::record( + DTree* d_tree, + const Vector3f& point, + const Vector3f& d_tree_node_size, + DTreeRecord& d_tree_record, + SamplingContext& sampling_context) +{ + assert(std::isfinite(d_tree_record.radiance)); + assert(d_tree_record.radiance >= 0.0f); + assert(std::isfinite(d_tree_record.product)); + assert(d_tree_record.product >= 0.0f); + assert(std::isfinite(d_tree_record.sample_weight)); + assert(d_tree_record.sample_weight >= 0.0f); + + switch (m_parameters.m_spatial_filter) + { + case SpatialFilter::Nearest: + d_tree->record(d_tree_record); + break; + + case SpatialFilter::Stochastic: + { + // Jitter the position of the record. + sampling_context.split_in_place(3, 1); + + Vector3f offset = d_tree_node_size; + offset *= (sampling_context.next2() - Vector3f(0.5f)); + Vector3f jittered_point = clip_vector_to_aabb(point + offset); + + DTree* stochastic_d_tree = get_d_tree(jittered_point); + stochastic_d_tree->record(d_tree_record); + break; + } + + case SpatialFilter::Box: + box_filter_splat(point, d_tree_node_size, d_tree_record); + break; + } +} + +void STree::build( + const size_t iteration) +{ + // Build D-tree radiance and sample weight sums first. + m_root_node->build(); + + const size_t required_samples = static_cast(SpatialSubdivisionThreshold * std::pow(2.0f, iteration * 0.5f)); + + // First refine the S-tree then refine the D-tree at each spatial leaf. + m_root_node->subdivide(required_samples); + m_root_node->restructure(DTreeThreshold); + + DTreeStatistics statistics; + m_root_node->gather_statistics(statistics); + statistics.build(); + + RENDERER_LOG_INFO( + "SD-Tree statistics: [min, max, avg]\n" + "S-Tree:\n" + " Node Count = %s\n" + " S-Tree depth = [%s, %s, %s]\n" + "D-Tree:\n" + " Tree Count = %s\n" + " Node Count = [%s, %s, %s]\n" + " D-Tree Depth = [%s, %s, %s]\n" + " Mean Radiance = [%s, %s, %s]\n" + " Sample Weight = [%s, %s, %s]\n" + " BSDF Sampling Fraction = [%s, %s, %s]\n" + " Glossy D-Tree Fraction = %s\n", + pretty_uint(statistics.num_s_tree_nodes).c_str(), + pretty_uint(statistics.min_s_tree_depth).c_str(), + pretty_uint(statistics.max_s_tree_depth).c_str(), + pretty_scalar(statistics.average_s_tree_depth, 2).c_str(), + pretty_uint(statistics.num_d_trees).c_str(), + pretty_uint(statistics.min_d_tree_nodes).c_str(), + pretty_uint(statistics.max_d_tree_nodes).c_str(), + pretty_scalar(statistics.average_d_tree_nodes, 1).c_str(), + pretty_uint(statistics.min_d_tree_depth).c_str(), + pretty_uint(statistics.max_d_tree_depth).c_str(), + pretty_scalar(statistics.average_d_tree_depth, 2).c_str(), + pretty_scalar(statistics.min_mean_radiance, 3).c_str(), + pretty_scalar(statistics.max_mean_radiance, 3).c_str(), + pretty_scalar(statistics.average_mean_radiance, 3).c_str(), + pretty_scalar(statistics.min_sample_weight, 3).c_str(), + pretty_scalar(statistics.max_sample_weight, 3).c_str(), + pretty_scalar(statistics.average_sample_weight, 3).c_str(), + pretty_scalar(statistics.min_sampling_fraction, 3).c_str(), + pretty_scalar(statistics.max_sampling_fraction, 3).c_str(), + pretty_scalar(statistics.average_sampling_fraction, 3).c_str(), + pretty_scalar(statistics.glossy_d_tree_fraction, 3).c_str()); + + m_is_built = true; +} + +bool STree::is_built() const +{ + return m_is_built; +} + +void STree::start_final_iteration() +{ + m_is_final_iteration = true; +} + +bool STree::is_final_iteration() const +{ + return m_is_final_iteration; +} + +void STree::box_filter_splat( + const Vector3f& point, + const Vector3f& d_tree_node_size, + DTreeRecord& d_tree_record) +{ + const AABB3f splat_aabb(point - d_tree_node_size * 0.5f, point + d_tree_node_size * 0.5f); + + assert(splat_aabb.is_valid() && splat_aabb.volume() > 0.0f); + + d_tree_record.sample_weight /= splat_aabb.volume(); + m_root_node->record(AABB3f(point - d_tree_node_size * 0.5f, point + d_tree_node_size * 0.5f), m_scene_aabb, d_tree_record); +} + +Vector3f STree::clip_vector_to_aabb( + const Vector3f& point) +{ + Vector3f result = point; + for (int i = 0; i < Vector3f::Dimension; ++i) + { + result[i] = std::min(std::max(result[i], m_scene_aabb.min[i]), m_scene_aabb.max[i]); + } + return result; +} + +void STree::write_to_disk( + const size_t iteration, + const bool append_iteration) const +{ + std::string file_path = m_parameters.m_save_path; + + if(append_iteration) + { + const std::string file_extension_str = ".sdt"; + std::ostringstream suffix; + suffix << "-" << std::setfill('0') << std::setw(2) << iteration << file_extension_str; + + file_path = + file_path.substr( + 0, + file_path.length() - file_extension_str.length()) + + suffix.str(); + } + + std::ofstream os( + file_path, + std::ios::out | std::ios::binary); + + if(!os.is_open()) + { + RENDERER_LOG_WARNING("Could not open file \"%s\" for writing.", file_path.c_str()); + return; + } + + const Camera *camera = m_scene.get_render_data().m_active_camera; + + if (camera == nullptr) + { + RENDERER_LOG_WARNING("Could not retrieve active camera."); + return; + } + + const float shutter_mid_time = camera->get_shutter_middle_time(); + Matrix4f camera_matrix = camera->transform_sequence().evaluate(shutter_mid_time).get_local_to_parent(); + + // Rotate 180 degrees around y to conform to the visualizer tool's z-axis convention. + const Matrix4f rotate_y = Matrix4f::make_rotation_y(Pi()); + camera_matrix = camera_matrix * rotate_y; + + write(os, camera_matrix(0, 0)); write(os, camera_matrix(0, 1)); write(os, camera_matrix(0, 2)); write(os, camera_matrix(0, 3)); + write(os, camera_matrix(1, 0)); write(os, camera_matrix(1, 1)); write(os, camera_matrix(1, 2)); write(os, camera_matrix(1, 3)); + write(os, camera_matrix(2, 0)); write(os, camera_matrix(2, 1)); write(os, camera_matrix(2, 2)); write(os, camera_matrix(2, 3)); + write(os, camera_matrix(3, 0)); write(os, camera_matrix(3, 1)); write(os, camera_matrix(3, 2)); write(os, camera_matrix(3, 3)); + + m_root_node->write_to_disk(os, m_scene_aabb); +} + +// GPTVertex implementation. + +void GPTVertex::add_radiance( + const renderer::Spectrum& radiance) +{ + m_radiance += radiance; +} + +void GPTVertex::record_to_tree( + STree& sd_tree, + SamplingContext& sampling_context) +{ + Spectrum incoming_radiance; + Spectrum product; + + for(size_t i = 0; i < Spectrum::size(); ++i) + { + // Check if components are valid. + if (!std::isfinite(m_radiance[i]) || m_radiance[i] < 0.0f || + !std::isfinite(m_bsdf_value[i]) || m_bsdf_value[i] < 0.0f) + { + return; + } + + const float rcp_factor = m_throughput[i] == 0.0f ? 0.0f : 1.0f / m_throughput[i]; + + incoming_radiance[i] = m_radiance[i] * rcp_factor; + product[i] = incoming_radiance[i] * m_bsdf_value[i]; + } + + DTreeRecord d_tree_record{ + m_direction, + average_value(incoming_radiance), + m_wi_pdf, + m_bsdf_pdf, + m_d_tree_pdf, + 1.0f, + average_value(product), + m_is_delta}; + + sd_tree.record(m_d_tree, m_point, m_d_tree_node_size, d_tree_record, sampling_context); +} + +// GPTVertexPath implementation. + +GPTVertexPath::GPTVertexPath() + : m_path_index(0) +{ +} + +void GPTVertexPath::add_vertex( + const GPTVertex& vertex) +{ + if(m_path_index < m_path.size()) + m_path[m_path_index++] = vertex; +} + +void GPTVertexPath::add_radiance( + const renderer::Spectrum& r) +{ + for(int i = 0; i < m_path_index; ++i) + m_path[i].add_radiance(r); +} + +void GPTVertexPath::add_indirect_radiance( + const renderer::Spectrum& r) +{ + for(int i = 0; i < m_path_index - 1; ++i) + m_path[i].add_radiance(r); +} + +bool GPTVertexPath::is_full() const +{ + return m_path_index >= m_path.size(); +} + +void GPTVertexPath::record_to_tree( + STree& sd_tree, + SamplingContext& sampling_context) +{ + for(int i = 0; i < m_path_index; ++i) + m_path[i].record_to_tree( + sd_tree, + sampling_context); +} + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/lighting/sdtree.h b/src/appleseed/renderer/kernel/lighting/sdtree.h new file mode 100644 index 0000000000..edabfc47b5 --- /dev/null +++ b/src/appleseed/renderer/kernel/lighting/sdtree.h @@ -0,0 +1,399 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/global/globaltypes.h" +#include "renderer/kernel/lighting/gpt/gptparameters.h" +#include "renderer/kernel/lighting/scatteringmode.h" +#include "renderer/modeling/scene/scene.h" + +// appleseed.foundation headers. +#include "foundation/math/aabb.h" +#include "foundation/math/vector.h" + +// Standard headers. +#include +#include +#include +#include +#include +#include +#include + +// +// SD-Tree implementation for "Practical Path Guiding for Efficient Light-Transport Simulation" [Müller et al. 2017]. +// + +namespace renderer { + +// Forward declarations. +struct DTreeRecord; +struct DTreeStatistics; +struct VisualizerNode; + +struct DTreeSample +{ + foundation::Vector3f direction; + float pdf; + ScatteringMode::Mode scattering_mode; +}; + +// The node type for the D-Tree. + +class QuadTreeNode +{ + public: + QuadTreeNode( + const bool create_children, + const float radiance_sum = 0.0f); + + QuadTreeNode( + const QuadTreeNode& other); + + QuadTreeNode& operator=(const QuadTreeNode& other) = delete; + + // Recursively add radiance unfiltered. + void add_radiance( + foundation::Vector2f& direction, + const float radiance); + + // Recursively add radiance filtered. + void add_radiance( + const foundation::AABB2f& splat_aabb, + const foundation::AABB2f& node_aabb, + const float radiance); + + size_t max_depth() const; + size_t node_count() const; + float radiance_sum() const; + + // Recursively sum and store each node's children's radiance. + float build_radiance_sums(); + + // Recursively restructure the D-tree based on the directional radiance distribution. + void restructure( + const float total_radiance_sum, + const float subdiv_threshold, + std::vector< + std::pair>* sorted_energy_ratios, + const size_t depth = 1); + + // Reset to state of an initial root node. + void reset(); + + // Sample a direction in cylindrical coordinates based on the directional radiance distribution. + const foundation::Vector2f sample( + foundation::Vector2f& sample, + float& pdf) const; + + float pdf( + foundation::Vector2f& direction) const; + + size_t depth( + foundation::Vector2f& direction) const; + + // Flatten the node based D-tree representation to a list format compatible with the visualizer tool. + void flatten( + std::list& nodes) const; + + private: + // Recursively sample a direction based on the directional radiance distribution. + const foundation::Vector2f sample_recursive( + foundation::Vector2f& sample, + float& pdf) const; + + QuadTreeNode* choose_node( + foundation::Vector2f& direction) const; + + std::unique_ptr m_upper_left_node; + std::unique_ptr m_upper_right_node; + std::unique_ptr m_lower_right_node; + std::unique_ptr m_lower_left_node; + + // The active radiance sum for recording incoming light. + std::atomic m_current_iter_radiance_sum; + + // The last completed iteration's radiance sum to guide the direction sampling. + float m_previous_iter_radiance_sum; + + bool m_is_leaf; +}; + +// The D-tree interface. + +class DTree +{ + public: + DTree( + const GPTParameters& parameters); + + DTree( + const DTree& other); + + // Record radiance to the D-tree. + void record( + const DTreeRecord& d_tree_record); + + // Sample a direction based on the directional radiance distribution. + void sample( + SamplingContext& sampling_context, + DTreeSample& d_tree_sample, + const int modes) const; + + float pdf( + const foundation::Vector3f& direction, + const int modes) const; + + // Divide the tree's sample weight by two. + void halve_sample_weight(); + size_t node_count() const; + size_t max_depth() const; + size_t depth( + const foundation::Vector2f& direction) const; + + // Recursively sum the directional radiance contributions from leaf to root. + void build(); + + // Recursively restructure the D-tree based on the directional radiance distribution. + void restructure( + const float subdiv_threshold); + + float sample_weight() const; + float mean() const; + float bsdf_sampling_fraction() const; + ScatteringMode::Mode get_scattering_mode() const; + + void write_to_disk( + std::ofstream& os) const; + + private: + void acquire_optimization_spin_lock(); + void release_optimization_spin_lock(); + + // Perform an bsdf sampling fraction optimization step. + void optimization_step( + const DTreeRecord& d_tree_record); + + void adam_step( + const float gradient); + + QuadTreeNode m_root_node; + std::atomic m_current_iter_sample_weight; + float m_previous_iter_sample_weight; + bool m_is_built; + ScatteringMode::Mode m_scattering_mode; + + // BSDF sampling fraction optimization variables. + std::atomic_flag m_atomic_flag; + size_t m_optimization_step_count; + float m_first_moment; + float m_second_moment; + float m_theta; + + const GPTParameters& m_parameters; +}; + +// The S-tree node class. + +class STreeNode +{ + public: + STreeNode( + const GPTParameters& parameters); + + STreeNode( + const unsigned int parent_axis, + const DTree* parent_d_tree); + + // Get the D-tree at a scene position. + DTree* get_d_tree( + foundation::Vector3f& point, + foundation::Vector3f& size); + + // Recursively refine the spatial resolution. + void subdivide( + const size_t required_samples); + + // Record radiance box filtered. + void record( + const foundation::AABB3f& splat_aabb, + const foundation::AABB3f& node_aabb, + const DTreeRecord& d_tree_record); + + // Refine the D-tree at spatial leaf nodes. + void restructure( + const float subdiv_threshold); + + // Build the D-tree at spatial leaf nodes. + void build(); + + void gather_statistics( + DTreeStatistics& statistics, + const size_t depth = 1) const; + + void write_to_disk( + std::ofstream& os, + const foundation::AABB3f& aabb) const; + + private: + STreeNode* choose_node( + foundation::Vector3f& point) const; + + void subdivide(); + bool is_leaf() const; + + std::unique_ptr m_first_node; + std::unique_ptr m_second_node; + + // This member is only set if the node is a leaf node and nullptr otherwise. + std::unique_ptr m_d_tree; + + // This node's split axis. + unsigned int m_axis; +}; + +// The root class of the SD-tree. + +class STree { + public: + STree( + const renderer::Scene& scene, + const GPTParameters& parameters); + + // Get the D-tree and its size at a scene position. + DTree* get_d_tree( + const foundation::Vector3f& point, + foundation::Vector3f& d_tree_voxel_size); + + // Get the D-tree at a scene position. + DTree* get_d_tree( + const foundation::Vector3f& point); + + // Record radiance. + void record( + DTree* d_tree, + const foundation::Vector3f& point, + const foundation::Vector3f& d_tree_node_size, + DTreeRecord& d_tree_record, + SamplingContext& sampling_context); + + // Refine the SD-tree's radiance distribution after an iteration has completed. + void build( + const size_t iteration); + + bool is_built() const; + + void start_final_iteration(); + + bool is_final_iteration() const; + + void write_to_disk( + const size_t iteration, + const bool append_iteration) const; + + private: + void box_filter_splat( + const foundation::Vector3f& point, + const foundation::Vector3f& d_tree_node_size, + DTreeRecord& d_tree_record); + + /// Clip a point to lie within the scene bounding box. + foundation::Vector3f clip_vector_to_aabb( + const foundation::Vector3f& point); + + const GPTParameters m_parameters; + const renderer::Scene& m_scene; + foundation::AABB3f m_scene_aabb; + std::unique_ptr m_root_node; + bool m_is_built; + bool m_is_final_iteration; +}; + +// Guided path tracing vertex used for keeping track of information at scattering events along a path. + +class GPTVertex +{ + public: + + // Add radiance conbributions lying further beyond this vertex' scattering event. + void add_radiance( + const renderer::Spectrum& radiance); + + // Record the accumulated radiance to the sd_tree on path completion. + void record_to_tree( + STree& sd_tree, + SamplingContext& sampling_context); + + DTree* m_d_tree; + foundation::Vector3f m_d_tree_node_size; + foundation::Vector3f m_point; + foundation::Vector3f m_direction; + renderer::Spectrum m_throughput; + renderer::Spectrum m_bsdf_value; + renderer::Spectrum m_radiance; + float m_wi_pdf; + float m_bsdf_pdf; + float m_d_tree_pdf; + bool m_is_delta; +}; + +// A trail of guided path tracing vertices. + +class GPTVertexPath +{ + public: + GPTVertexPath(); + + // Add a vertex to the path. + void add_vertex( + const GPTVertex& vertex); + + // Add radiance to all vertices in the path. + void add_radiance( + const renderer::Spectrum& r); + + // Add radiance to all but the last vertex in the path. + void add_indirect_radiance( + const renderer::Spectrum& r); + + // Record the accumulated radiance of all vertices in the path to the sd_tree on path completion. + void record_to_tree( + STree& sd_tree, + SamplingContext& sampling_context); + + // Test if this path has capacity for another vertex. + bool is_full() const; + + private: + std::array m_path; // a path can hold 32 vertices + int m_path_index; // index into the next free array position +}; + +} // namespace renderer \ No newline at end of file diff --git a/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.cpp b/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.cpp index b5e940b2cf..02ac7a5ae2 100644 --- a/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.cpp +++ b/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.cpp @@ -169,7 +169,7 @@ void SPPMPassCallback::on_pass_begin( } } -void SPPMPassCallback::on_pass_end( +bool SPPMPassCallback::on_pass_end( const Frame& frame, JobQueue& job_queue, IAbortSwitch& abort_switch) @@ -225,6 +225,8 @@ void SPPMPassCallback::on_pass_end( pretty_time(m_stopwatch.get_seconds()).c_str()); ++m_pass_number; + + return false; } SPPMLightingEngineWorkingSet& SPPMPassCallback::acquire_working_set() diff --git a/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.h b/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.h index 3743810c57..eed5b31a87 100644 --- a/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.h +++ b/src/appleseed/renderer/kernel/lighting/sppm/sppmpasscallback.h @@ -93,7 +93,7 @@ class SPPMPassCallback foundation::IAbortSwitch& abort_switch) override; // This method is called at the end of a pass. - void on_pass_end( + bool on_pass_end( const Frame& frame, foundation::JobQueue& job_queue, foundation::IAbortSwitch& abort_switch) override; diff --git a/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.cpp b/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.cpp index 8ef4e456d1..ca639f4598 100644 --- a/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.cpp +++ b/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.cpp @@ -80,4 +80,11 @@ void EphemeralShadingResultFrameBufferFactory::destroy( delete framebuffer; } +size_t EphemeralShadingResultFrameBufferFactory::get_total_channel_count( + const size_t aov_count) const +{ + return ShadingResultFrameBuffer::get_total_channel_count( + aov_count); +} + } // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.h b/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.h index b40941ee04..ba2638c8af 100644 --- a/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.h +++ b/src/appleseed/renderer/kernel/rendering/ephemeralshadingresultframebufferfactory.h @@ -63,6 +63,9 @@ class EphemeralShadingResultFrameBufferFactory void destroy( ShadingResultFrameBuffer* framebuffer) override; + + size_t get_total_channel_count( + const size_t aov_count) const override; }; } // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.cpp b/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.cpp index 49b02ce006..dfc5d9ae74 100644 --- a/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.cpp +++ b/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.cpp @@ -64,9 +64,6 @@ #include #include -// Forward declarations. -namespace renderer { class IRendererController; } - using namespace boost; using namespace foundation; @@ -331,6 +328,7 @@ namespace set_current_thread_name("pass_manager"); const size_t start_pass = m_frame.get_initial_pass(); + bool do_loop_break = false; // // Rendering passes. @@ -389,12 +387,16 @@ namespace if (m_pass_callback) { assert(!m_job_queue.has_scheduled_or_running_jobs()); - m_pass_callback->on_pass_end(m_frame, m_job_queue, m_abort_switch); + do_loop_break = m_pass_callback->on_pass_end(m_frame, m_job_queue, m_abort_switch); assert(!m_job_queue.has_scheduled_or_running_jobs()); } // Optionally write a checkpoint file to disk. m_frame.save_checkpoint(m_framebuffer_factory, pass); + + // Break the loop if the callback indicated a render end. + if(do_loop_break) + break; } // Check abort flag. diff --git a/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.h b/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.h index 27e3d8c863..fb5918f188 100644 --- a/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.h +++ b/src/appleseed/renderer/kernel/rendering/generic/genericframerenderer.h @@ -43,6 +43,7 @@ namespace renderer { class IPassCallback; } namespace renderer { class IShadingResultFrameBufferFactory; } namespace renderer { class ITileCallbackFactory; } namespace renderer { class ITileRendererFactory; } +namespace renderer { class IRendererController; } namespace renderer { diff --git a/src/appleseed/renderer/kernel/rendering/ipasscallback.h b/src/appleseed/renderer/kernel/rendering/ipasscallback.h index 3532d00b86..ce0a67fa94 100644 --- a/src/appleseed/renderer/kernel/rendering/ipasscallback.h +++ b/src/appleseed/renderer/kernel/rendering/ipasscallback.h @@ -60,7 +60,8 @@ class APPLESEED_DLLSYMBOL IPassCallback foundation::IAbortSwitch& abort_switch) = 0; // This method is called at the end of a pass. - virtual void on_pass_end( + // A return value of true indicates that + virtual bool on_pass_end( const Frame& frame, foundation::JobQueue& job_queue, foundation::IAbortSwitch& abort_switch) = 0; diff --git a/src/appleseed/renderer/kernel/rendering/ishadingresultframebufferfactory.h b/src/appleseed/renderer/kernel/rendering/ishadingresultframebufferfactory.h index bdf844c4d2..b450088ff4 100644 --- a/src/appleseed/renderer/kernel/rendering/ishadingresultframebufferfactory.h +++ b/src/appleseed/renderer/kernel/rendering/ishadingresultframebufferfactory.h @@ -57,6 +57,9 @@ class IShadingResultFrameBufferFactory virtual void destroy( ShadingResultFrameBuffer* framebuffer) = 0; + + virtual size_t get_total_channel_count( + const size_t aov_count) const = 0; }; } // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.cpp b/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.cpp index 5e5debba59..48619fe8d9 100644 --- a/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.cpp +++ b/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.cpp @@ -105,4 +105,11 @@ void PermanentShadingResultFrameBufferFactory::destroy( { } +size_t PermanentShadingResultFrameBufferFactory::get_total_channel_count( + const size_t aov_count) const +{ + return ShadingResultFrameBuffer::get_total_channel_count( + aov_count); +} + } // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.h b/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.h index 4ce926f744..bbc88fa0f4 100644 --- a/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.h +++ b/src/appleseed/renderer/kernel/rendering/permanentshadingresultframebufferfactory.h @@ -71,6 +71,9 @@ class PermanentShadingResultFrameBufferFactory void destroy( ShadingResultFrameBuffer* framebuffer) override; + size_t get_total_channel_count( + const size_t aov_count) const override; + private: std::vector m_framebuffers; }; diff --git a/src/appleseed/renderer/kernel/rendering/renderercomponents.cpp b/src/appleseed/renderer/kernel/rendering/renderercomponents.cpp index d2bae577ff..8fcb6a2aa4 100644 --- a/src/appleseed/renderer/kernel/rendering/renderercomponents.cpp +++ b/src/appleseed/renderer/kernel/rendering/renderercomponents.cpp @@ -32,6 +32,9 @@ // appleseed.renderer headers. #include "renderer/global/globallogger.h" #include "renderer/kernel/lighting/bdpt/bdptlightingengine.h" +#include "renderer/kernel/lighting/gpt/gptlightingengine.h" +#include "renderer/kernel/lighting/gpt/gptparameters.h" +#include "renderer/kernel/lighting/gpt/gptpasscallback.h" #include "renderer/kernel/lighting/lighttracing/lighttracingsamplegenerator.h" #include "renderer/kernel/lighting/pt/ptlightingengine.h" #include "renderer/kernel/lighting/sppm/sppmlightingengine.h" @@ -51,9 +54,11 @@ #include "renderer/kernel/rendering/generic/generictilerenderer.h" #include "renderer/kernel/rendering/permanentshadingresultframebufferfactory.h" #include "renderer/kernel/rendering/progressive/progressiveframerenderer.h" +#include "renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.h" #include "renderer/kernel/shading/oslshadingsystem.h" #include "renderer/kernel/texturing/oiiotexturesystem.h" #include "renderer/modeling/project/project.h" +#include "renderer/modeling/scene/scene.h" #include "renderer/utility/paramarray.h" // OpenImageIO headers. @@ -62,6 +67,7 @@ #include "foundation/platform/_endoiioheaders.h" // Standard headers. +#include #include using namespace foundation; @@ -122,9 +128,6 @@ RendererComponents::RendererComponents( bool RendererComponents::create() { - if (!create_shading_result_framebuffer_factory()) - return false; - if (!create_lighting_engine_factory()) return false; @@ -137,10 +140,13 @@ bool RendererComponents::create() if (!create_pixel_renderer_factory()) return false; + if (!create_shading_result_framebuffer_factory()) + return false; + if (!create_tile_renderer_factory()) return false; - if (!create_frame_renderer()) + if (!create_frame_renderer_factory()) return false; return true; @@ -191,7 +197,53 @@ bool RendererComponents::create_lighting_engine_factory() new PTLightingEngineFactory( *m_backward_light_sampler, m_project.get_light_path_recorder(), - get_child_and_inherit_globals(m_params, "pt"))); // todo: change to "pt_lighting_engine"? + get_child_and_inherit_globals(m_params, "pt"))); // todo: change to "pt_lighting_engine"? + + return true; + } + else if (name == "gpt") + { + const std::string pixel_renderer_name = m_params.get_optional("pixel_renderer", ""); + + if (pixel_renderer_name != "uniform") + { + RENDERER_LOG_ERROR("cannot use guided path tracing with pixel renderer other than uniform pixel renderer."); + return false; + } + + const std::string framebuffer_name = m_params.get_optional("shading_result_framebuffer", ""); + + if (framebuffer_name != "permanent") + { + RENDERER_LOG_ERROR("cannot use guided path tracing without permanent shading result framebuffer."); + return false; + } + + m_backward_light_sampler.reset( + new BackwardLightSampler( + m_scene, + get_child_and_inherit_globals(m_params, "light_sampler"))); + + const GPTParameters gpt_parameters( + get_child_and_inherit_globals(m_params, "gpt")); + + m_sd_tree.reset(new STree(m_scene, gpt_parameters)); + + ParamArray uniform_pixel_renderer_params = get_child_and_inherit_globals(m_params, "uniform_pixel_renderer"); + + m_pass_callback.reset( + new GPTPassCallback( + gpt_parameters, + m_sd_tree.get(), + uniform_pixel_renderer_params.get_required("samples", 64), + m_params.get_optional("passes", std::numeric_limits::max()))); + + m_lighting_engine_factory.reset( + new GPTLightingEngineFactory( + m_sd_tree.get(), + *m_backward_light_sampler, + m_project.get_light_path_recorder(), + gpt_parameters)); return true; } @@ -225,7 +277,7 @@ bool RendererComponents::create_lighting_engine_factory() const SPPMParameters sppm_params( get_child_and_inherit_globals(m_params, "sppm")); - SPPMPassCallback* sppm_pass_callback = + SPPMPassCallback *sppm_pass_callback = new SPPMPassCallback( m_scene, *m_forward_light_sampler, @@ -367,11 +419,22 @@ bool RendererComponents::create_pixel_renderer_factory() return false; } + ParamArray params = get_child_and_inherit_globals(m_params, "uniform_pixel_renderer"); + + const std::string lighting_engine_name = m_params.get_required("lighting_engine", ""); + + if (lighting_engine_name == "gpt") + { + const GPTParameters gpt_parameters( + get_child_and_inherit_globals(m_params, "gpt")); + params.insert("samples", gpt_parameters.m_samples_per_pass); + } + m_pixel_renderer_factory.reset( new UniformPixelRendererFactory( m_frame, m_sample_renderer_factory.get(), - get_child_and_inherit_globals(m_params, "uniform_pixel_renderer"))); + params)); return true; } @@ -421,8 +484,19 @@ bool RendererComponents::create_pixel_renderer_factory() bool RendererComponents::create_shading_result_framebuffer_factory() { const std::string name = m_params.get_optional("shading_result_framebuffer", "ephemeral"); + GPTPassCallback *gpt_pass_callback = dynamic_cast(m_pass_callback.get()); - if (name.empty()) + if (gpt_pass_callback != nullptr) + { + VarianceTrackingShadingResultFrameBufferFactory *framebuffer_factory = + new VarianceTrackingShadingResultFrameBufferFactory(m_frame); + + m_shading_result_framebuffer_factory.reset( + framebuffer_factory); + gpt_pass_callback->set_framebuffer(framebuffer_factory); + return true; + } + else if (name.empty()) { return true; } @@ -520,7 +594,7 @@ bool RendererComponents::create_tile_renderer_factory() } } -bool RendererComponents::create_frame_renderer() +bool RendererComponents::create_frame_renderer_factory() { const std::string name = m_params.get_required("frame_renderer", "generic"); @@ -542,6 +616,15 @@ bool RendererComponents::create_frame_renderer() return false; } + ParamArray params = get_child_and_inherit_globals(m_params, "generic_frame_renderer"); + + const std::string lighting_engine_name = m_params.get_required("lighting_engine", ""); + + if (lighting_engine_name == "gpt") + { + params.insert("passes", 10000000); + } + m_frame_renderer.reset( GenericFrameRendererFactory::create( m_frame, @@ -549,7 +632,7 @@ bool RendererComponents::create_frame_renderer() m_tile_renderer_factory.get(), m_tile_callback_factory, m_pass_callback.get(), - get_child_and_inherit_globals(m_params, "generic_frame_renderer"))); + params)); return true; } diff --git a/src/appleseed/renderer/kernel/rendering/renderercomponents.h b/src/appleseed/renderer/kernel/rendering/renderercomponents.h index c90e167c31..ae18b59a98 100644 --- a/src/appleseed/renderer/kernel/rendering/renderercomponents.h +++ b/src/appleseed/renderer/kernel/rendering/renderercomponents.h @@ -32,6 +32,7 @@ #include "renderer/kernel/lighting/backwardlightsampler.h" #include "renderer/kernel/lighting/forwardlightsampler.h" #include "renderer/kernel/lighting/ilightingengine.h" +#include "renderer/kernel/lighting/sdtree.h" #include "renderer/kernel/rendering/iframerenderer.h" #include "renderer/kernel/rendering/ipasscallback.h" #include "renderer/kernel/rendering/ipixelrenderer.h" @@ -51,6 +52,7 @@ namespace foundation { class IAbortSwitch; } namespace renderer { class Frame; } namespace renderer { class IFrameRenderer; } +namespace renderer { class IRendererController; } namespace renderer { class ITileCallbackFactory; } namespace renderer { class OIIOTextureSystem; } namespace renderer { class OnFrameBeginRecorder; } @@ -122,11 +124,12 @@ class RendererComponents OIIOTextureSystem& m_oiio_texture_system; OSLShadingSystem& m_osl_shading_system; - std::unique_ptr m_shading_result_framebuffer_factory; + std::unique_ptr m_sd_tree; std::unique_ptr m_lighting_engine_factory; std::unique_ptr m_sample_renderer_factory; std::unique_ptr m_sample_generator_factory; std::unique_ptr m_pixel_renderer_factory; + std::unique_ptr m_shading_result_framebuffer_factory; std::unique_ptr m_tile_renderer_factory; std::unique_ptr m_pass_callback; foundation::auto_release_ptr m_frame_renderer; @@ -137,7 +140,7 @@ class RendererComponents bool create_pixel_renderer_factory(); bool create_shading_result_framebuffer_factory(); bool create_tile_renderer_factory(); - bool create_frame_renderer(); + bool create_frame_renderer_factory(); }; diff --git a/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.cpp b/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.cpp index a2e4af5c91..e0b90ced9a 100644 --- a/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.cpp +++ b/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.cpp @@ -41,6 +41,7 @@ // Standard headers. #include +#include using namespace foundation; @@ -106,6 +107,7 @@ void ShadingResultFrameBuffer::merge( const size_t source_y, const float scaling) { + assert(typeid(this) == typeid(source)); assert(m_channel_count == source.m_channel_count); const float* APPLESEED_RESTRICT source_ptr = source.pixel(source_x, source_y); diff --git a/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.h b/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.h index 6b4667257a..3659b78ee0 100644 --- a/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.h +++ b/src/appleseed/renderer/kernel/rendering/shadingresultframebuffer.h @@ -61,9 +61,11 @@ class ShadingResultFrameBuffer const size_t aov_count, const foundation::AABB2u& crop_window); + virtual ~ShadingResultFrameBuffer() {}; + static size_t get_total_channel_count(const size_t aov_count); - void add( + virtual void add( const foundation::Vector2u& pi, const ShadingResult& sample); @@ -75,13 +77,15 @@ class ShadingResultFrameBuffer const size_t source_y, const float scaling); - void develop_to_tile( + virtual void develop_to_tile( foundation::Tile& tile, TileStack& aov_tiles) const; + protected: + std::vector m_scratch; + private: const size_t m_aov_count; - std::vector m_scratch; }; inline size_t ShadingResultFrameBuffer::get_total_channel_count(const size_t aov_count) diff --git a/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebuffer.cpp b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebuffer.cpp new file mode 100644 index 0000000000..6cd440e122 --- /dev/null +++ b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebuffer.cpp @@ -0,0 +1,221 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// Interface header. +#include "variancetrackingshadingresultframebuffer.h" + +// appleseed.renderer headers. +#include "renderer/kernel/aov/tilestack.h" +#include "renderer/kernel/shading/shadingresult.h" + +// appleseed.foundation headers. +#include "foundation/image/color.h" +#include "foundation/image/colorspace.h" +#include "foundation/image/tile.h" +#include "foundation/platform/compiler.h" + +// Standard headers. +#include + +using namespace foundation; +using namespace std; + +namespace renderer +{ + +VarianceTrackingShadingResultFrameBuffer::VarianceTrackingShadingResultFrameBuffer( + const size_t width, + const size_t height, + const size_t aov_count) + : ShadingResultFrameBuffer( + width, + height, + aov_count + 1) + , m_aov_count(aov_count) +{ +} + +VarianceTrackingShadingResultFrameBuffer::VarianceTrackingShadingResultFrameBuffer( + const size_t width, + const size_t height, + const size_t aov_count, + const AABB2u& crop_window) + : ShadingResultFrameBuffer( + width, + height, + aov_count + 1, + crop_window) + , m_aov_count(aov_count) +{ +} + +void VarianceTrackingShadingResultFrameBuffer::add( + const Vector2u& pi, + const ShadingResult& sample) +{ + float* ptr = &m_scratch[0]; + + const float main_0 = sample.m_main[0]; + const float main_1 = sample.m_main[1]; + const float main_2 = sample.m_main[2]; + const float main_3 = sample.m_main[3]; + + *ptr++ = main_0; + *ptr++ = main_1; + *ptr++ = main_2; + *ptr++ = main_3; + + for (size_t i = 0, e = m_aov_count; i < e; ++i) + { + const Color4f& aov = sample.m_aovs[i]; + *ptr++ = aov[0]; + *ptr++ = aov[1]; + *ptr++ = aov[2]; + *ptr++ = aov[3]; + } + + // Put squared samples in the last buffer channels. + *ptr++ = main_0 * main_0; + *ptr++ = main_1 * main_1; + *ptr++ = main_2 * main_2; + *ptr++ = main_3; // do not square alpha + + AccumulatorTile::add(pi, &m_scratch[0]); +} + +void VarianceTrackingShadingResultFrameBuffer::develop_to_tile( + Tile& tile, + TileStack& aov_tiles) const +{ + const float* ptr = pixel(0); + + for (size_t y = 0, h = m_height; y < h; ++y) + { + for (size_t x = 0, w = m_width; x < w; ++x) + { + const float weight = *ptr++; + const float rcp_weight = weight == 0.0f ? 0.0f : 1.0f / weight; + + const Color4f color(ptr[0], ptr[1], ptr[2], ptr[3]); + tile.set_pixel(x, y, color * rcp_weight); + ptr += 4; + + for (size_t i = 0, e = m_aov_count; i < e; ++i) + { + const Color4f aov(ptr[0], ptr[1], ptr[2], ptr[3]); + aov_tiles.set_pixel(x, y, i, aov * rcp_weight); + ptr += 4; + } + + ptr += 4; // Skip sum of squared samples + } + } +} + +float VarianceTrackingShadingResultFrameBuffer::estimator_variance() const +{ + float estimator_variance_sum = 0.0f; + const float* ptr = pixel(0); + + for (size_t y = 0, h = m_height; y < h; ++y) + for (size_t x = 0, w = m_width; x < w; ++x) + { + const float weight = *ptr; + const float rcp_weight = weight == 0.0f ? 0.0f : 1.0f / weight; + const float rcp_weight_minus_one = weight - 1.0f == 0.0f ? 0.0f : 1.0f / (weight - 1.0f); + + const Color3f pixel_value( + ptr[1] * rcp_weight, + ptr[2] * rcp_weight, + ptr[3] * rcp_weight); + + ptr += m_channel_count - 4; // skip to beginning of summed squares + + const Color3f square_sum( + ptr[0], + ptr[1], + ptr[2]); + + // Estimator for the variance of the mean estimator (= pixel value): Sigma² / n. + // Sigma² is estimated as [sum_of_squares - n * pixel_value²] / (n - 1) + const Color3f sigma_2((square_sum - weight * pixel_value * pixel_value) * rcp_weight_minus_one); + + // Clamp sigma² to mitigate the effect of fireflies. + const float estimator_variance = std::min(luminance(sigma_2), 5000.0f) * rcp_weight; + + estimator_variance_sum += estimator_variance; + + ptr += 4; + } + + return estimator_variance_sum / get_pixel_count(); +} + +float VarianceTrackingShadingResultFrameBuffer::estimator_variance_to_tile( + Tile& tile) const +{ + float estimator_variance_sum = 0.0f; + const float* ptr = pixel(0); + + for (size_t y = 0, h = m_height; y < h; ++y) + for (size_t x = 0, w = m_width; x < w; ++x) + { + const float weight = *ptr; + const float rcp_weight = weight == 0.0f ? 0.0f : 1.0f / weight; + const float rcp_weight_minus_one = weight - 1.0f == 0.0f ? 0.0f : 1.0f / (weight - 1.0f); + + // Clamp the values to mitigate the effect of fireflies. + const Color3f pixel_value( + ptr[1] * rcp_weight, + ptr[2] * rcp_weight, + ptr[3] * rcp_weight); + + ptr += m_channel_count - 4; // skip to beginning of summed squares + + const Color3f square_sum( + ptr[0], + ptr[1], + ptr[2]); + + // Estimator for the variance of the mean estimator (= pixel value): Sigma² / n. + // Sigma² is estimated as [sum_of_squares - n * pixel_value²] / (n - 1) + const Color3f sigma_2((square_sum - weight * pixel_value * pixel_value) * rcp_weight_minus_one); + + // Clamp sigma² to mitigate the effect of fireflies. + const float estimator_variance = std::min(luminance(sigma_2), 5000.0f) * rcp_weight; + + tile.set_pixel(x, y, Color3f(estimator_variance)); + estimator_variance_sum += estimator_variance; + + ptr += 4; + } + + return estimator_variance_sum / get_pixel_count(); +} + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebuffer.h b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebuffer.h new file mode 100644 index 0000000000..9d5e3a2c39 --- /dev/null +++ b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebuffer.h @@ -0,0 +1,95 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/kernel/rendering/shadingresultframebuffer.h" + +// appleseed.foundation headers. +#include "foundation/image/image.h" +#include "foundation/image/tile.h" +#include "foundation/math/aabb.h" + +// Standard headers. +#include + +// Forward declarations. +namespace foundation { class Tile; } +namespace renderer { class ShadingResult; } +namespace renderer { class TileStack; } + +namespace renderer +{ + +class VarianceTrackingShadingResultFrameBuffer + : public ShadingResultFrameBuffer +{ + public: + VarianceTrackingShadingResultFrameBuffer( + const size_t width, + const size_t height, + const size_t aov_count); + + VarianceTrackingShadingResultFrameBuffer( + const size_t width, + const size_t height, + const size_t aov_count, + const foundation::AABB2u& crop_window); + + ~VarianceTrackingShadingResultFrameBuffer() override {} + + static size_t get_total_channel_count( + const size_t aov_count); + + void add( + const foundation::Vector2u& pi, + const ShadingResult& sample) override; + + virtual void develop_to_tile( + foundation::Tile& tile, + TileStack& aov_tiles) const override; + + // Return estimate of the mean pixel variance. + float estimator_variance() const; + + // Transfer pixel variance estimates to tile and return estimate of the mean pixel variance. + float estimator_variance_to_tile( + foundation::Tile& tile) const; + + private: + const size_t m_aov_count; +}; + +inline size_t VarianceTrackingShadingResultFrameBuffer::get_total_channel_count(const size_t aov_count) +{ + // The squared sample sum plus all channels of the ShadingResultFramebuffer. + return 4 + ShadingResultFrameBuffer::get_total_channel_count(aov_count); +} + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.cpp b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.cpp new file mode 100644 index 0000000000..104adde24c --- /dev/null +++ b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.cpp @@ -0,0 +1,158 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// Interface header. +#include "variancetrackingshadingresultframebufferfactory.h" + +// appleseed.renderer headers. +#include "renderer/kernel/aov/imagestack.h" +#include "renderer/kernel/rendering/variancetrackingshadingresultframebuffer.h" +#include "renderer/modeling/frame/frame.h" + +// appleseed.foundation headers. +#include "foundation/image/canvasproperties.h" +#include "foundation/image/tile.h" + +using namespace foundation; + +namespace renderer +{ + +void VarianceTrackingShadingResultFrameBufferFactory::release() +{ + delete this; +} + +VarianceTrackingShadingResultFrameBufferFactory::VarianceTrackingShadingResultFrameBufferFactory( + const Frame& frame) +{ + const size_t tile_count_x = frame.image().properties().m_tile_count_x; + const size_t tile_count_y = frame.image().properties().m_tile_count_y; + + m_framebuffers.resize(tile_count_x * tile_count_y, nullptr); +} + +VarianceTrackingShadingResultFrameBufferFactory::~VarianceTrackingShadingResultFrameBufferFactory() +{ + for (size_t i = 0; i < m_framebuffers.size(); ++i) + delete m_framebuffers[i]; +} + +ShadingResultFrameBuffer* VarianceTrackingShadingResultFrameBufferFactory::create( + const Frame& frame, + const size_t tile_x, + const size_t tile_y, + const AABB2u& tile_bbox) +{ + const size_t tile_count_x = frame.image().properties().m_tile_count_x; + const size_t index = tile_y * tile_count_x + tile_x; + + if (m_framebuffers[index] == nullptr) + { + const Tile& tile = frame.image().tile(tile_x, tile_y); + + m_framebuffers[index] = + new VarianceTrackingShadingResultFrameBuffer( + tile.get_width(), + tile.get_height(), + frame.aov_images().size(), + tile_bbox); + + m_framebuffers[index]->clear(); + } + + return m_framebuffers[index]; +} + +void VarianceTrackingShadingResultFrameBufferFactory::destroy( + ShadingResultFrameBuffer* framebuffer) +{ +} + +void VarianceTrackingShadingResultFrameBufferFactory::clear() +{ + for (auto framebuffer : m_framebuffers) + { + if (framebuffer != nullptr) + framebuffer->clear(); + } +} + +size_t VarianceTrackingShadingResultFrameBufferFactory::get_total_channel_count( + const size_t aov_count) const +{ + return VarianceTrackingShadingResultFrameBuffer::get_total_channel_count( + aov_count); +} + +float VarianceTrackingShadingResultFrameBufferFactory::estimator_variance() const +{ + float variance = 0.0f; + size_t buffer_count = 0; + + for (const auto framebuffer : m_framebuffers) + { + if (framebuffer == nullptr) + continue; + + variance += framebuffer->estimator_variance(); + ++buffer_count; + } + + return buffer_count == 0 ? 0 : variance / buffer_count; +} + +float VarianceTrackingShadingResultFrameBufferFactory::estimator_variance_to_image( + Image& image) const +{ + const size_t tile_count_x = image.properties().m_tile_count_x; + const size_t tile_count_y = image.properties().m_tile_count_y; + + float variance = 0.0f; + size_t buffer_count = 0; + + for (size_t tile_y = 0; tile_y < tile_count_y; ++tile_y) + for (size_t tile_x = 0; tile_x < tile_count_x; ++tile_x) + { + Tile &tile = image.tile(tile_x, tile_y); + const size_t index = tile_y * tile_count_x + tile_x; + + const VarianceTrackingShadingResultFrameBuffer* framebuffer = + m_framebuffers[index]; + + if (framebuffer == nullptr) + continue; + + variance += framebuffer->estimator_variance_to_tile(tile); + ++buffer_count; + } + + return buffer_count == 0 ? 0 : variance / buffer_count; +} + +} // namespace renderer diff --git a/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.h b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.h new file mode 100644 index 0000000000..eda8a837b7 --- /dev/null +++ b/src/appleseed/renderer/kernel/rendering/variancetrackingshadingresultframebufferfactory.h @@ -0,0 +1,89 @@ + +// +// This source file is part of appleseed. +// Visit https://appleseedhq.net/ for additional information and resources. +// +// This software is released under the MIT license. +// +// Copyright (c) 2019 Stephen Agyemang, The appleseedhq Organization +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#pragma once + +// appleseed.renderer headers. +#include "renderer/kernel/rendering/ishadingresultframebufferfactory.h" + +// appleseed.foundation headers. +#include "foundation/image/image.h" +#include "foundation/math/aabb.h" +#include "foundation/platform/compiler.h" + +// Standard headers. +#include +#include + +// Forward declarations. +namespace renderer { class Frame; } +namespace renderer { class VarianceTrackingShadingResultFrameBuffer; } + +namespace renderer +{ + +class VarianceTrackingShadingResultFrameBufferFactory + : public IShadingResultFrameBufferFactory +{ + public: + // Constructor. + explicit VarianceTrackingShadingResultFrameBufferFactory( + const Frame& frame); + + // Destructor. + ~VarianceTrackingShadingResultFrameBufferFactory() override; + + // Delete this instance. + void release() override; + + ShadingResultFrameBuffer* create( + const Frame& frame, + const size_t tile_x, + const size_t tile_y, + const foundation::AABB2u& tile_bbox) override; + + void destroy( + ShadingResultFrameBuffer* framebuffer) override; + + size_t get_total_channel_count( + const size_t aov_count) const override; + + void clear(); + + // Return estimate of the mean pixel variance. + float estimator_variance() const; + + // Transfer pixel variance estimates to image and return estimate of the mean pixel variance. + float estimator_variance_to_image( + foundation::Image& image) const; + + private: + std::vector m_framebuffers; +}; + +} // namespace renderer diff --git a/src/appleseed/renderer/modeling/frame/frame.cpp b/src/appleseed/renderer/modeling/frame/frame.cpp index 4ca83df12f..a712571389 100644 --- a/src/appleseed/renderer/modeling/frame/frame.cpp +++ b/src/appleseed/renderer/modeling/frame/frame.cpp @@ -531,10 +531,10 @@ void Frame::denoise( namespace { - size_t get_checkpoint_total_channel_count(const size_t aov_count) + size_t get_checkpoint_total_channel_count(const IShadingResultFrameBufferFactory* buffer_factory, const size_t aov_count) { // The beauty image plus the shading result framebuffer. - return ShadingResultFrameBuffer::get_total_channel_count(aov_count) + 1; + return buffer_factory->get_total_channel_count(aov_count) + 1; } typedef std::vector> CheckpointProperties; @@ -558,7 +558,7 @@ namespace m_frame.image().properties().m_canvas_height, m_frame.image().properties().m_tile_width, m_frame.image().properties().m_tile_height, - get_checkpoint_total_channel_count(m_frame.aov_images().size()), + get_checkpoint_total_channel_count(buffer_factory, m_frame.aov_images().size()), PixelFormatFloat) { assert(buffer_factory); @@ -647,9 +647,10 @@ namespace } bool is_checkpoint_compatible( - const std::string& checkpoint_path, - const Frame& frame, - const CheckpointProperties& checkpoint_props) + const std::string& checkpoint_path, + const Frame& frame, + const IShadingResultFrameBufferFactory* buffer_factory, + const CheckpointProperties& checkpoint_props) { const Image& frame_image = frame.image(); const CanvasProperties& frame_props = frame_image.properties(); @@ -681,7 +682,7 @@ namespace // Check that the shading buffer layer has correct amount of channel. // The checkpoint should contain the beauty image and the shading buffer. - const size_t expect_channel_count = get_checkpoint_total_channel_count(frame.aov_images().size()); + const size_t expect_channel_count = get_checkpoint_total_channel_count(buffer_factory, frame.aov_images().size()); if (std::get<1>(checkpoint_props[1]).m_channel_count != expect_channel_count) { RENDERER_LOG_ERROR("incorrect checkpoint: the shading buffer doesn't contain the correct number of channels."); @@ -827,7 +828,7 @@ bool Frame::load_checkpoint( } // Check checkpoint file's compatibility. - if (!is_checkpoint_compatible(impl->m_checkpoint_resume_path, *this, checkpoint_props)) + if (!is_checkpoint_compatible(impl->m_checkpoint_resume_path, *this, buffer_factory, checkpoint_props)) return false; // Compute the index of the first pass to render. diff --git a/src/appleseed/renderer/modeling/project/configuration.cpp b/src/appleseed/renderer/modeling/project/configuration.cpp index 5ef53046b2..df77731182 100644 --- a/src/appleseed/renderer/modeling/project/configuration.cpp +++ b/src/appleseed/renderer/modeling/project/configuration.cpp @@ -32,6 +32,7 @@ // appleseed.renderer headers. #include "renderer/kernel/lighting/backwardlightsampler.h" +#include "renderer/kernel/lighting/gpt/gptlightingengine.h" #include "renderer/kernel/lighting/pt/ptlightingengine.h" #include "renderer/kernel/lighting/sppm/sppmlightingengine.h" #include "renderer/kernel/rendering/final/adaptivetilerenderer.h" @@ -157,6 +158,11 @@ Dictionary Configuration::get_metadata() Dictionary() .insert("label", "Unidirectional Path Tracer") .insert("help", "Unidirectional path tracing")) + .insert( + "gpt", + Dictionary() + .insert("label", "Guided Path Tracer") + .insert("help", "Guided path tracing")) .insert( "sppm", Dictionary() @@ -215,6 +221,10 @@ Dictionary Configuration::get_metadata() "pt", PTLightingEngineFactory::get_params_metadata()); + metadata.dictionaries().insert( + "gpt", + GPTLightingEngineFactory::get_params_metadata()); + metadata.dictionaries().insert( "sppm", SPPMLightingEngineFactory::get_params_metadata());