From f982427f7ef0b528b27a844b8f21906c34d44046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20G=C3=BCnther?= Date: Thu, 27 Jun 2024 09:48:19 -0400 Subject: [PATCH] Better ShadowCatcher - regard lightSamples - reuse NEE code - correct accumulation within spp (e.g. with colored shadows) - demonstrate shadowCatcher via ospExamples - documentation and tests Co-authored-by: David E. DeMarle --- CHANGELOG.md | 2 + README.md | 73 +++++++++-- apps/ospExamples/GLFWOSPRayWindow.cpp | 7 + apps/ospTestSuite/CMakeLists.txt | 1 + apps/ospTestSuite/test_shadowcatcher.cpp | 89 +++++++++++++ doc/Makefile | 4 +- doc/api.md | 38 +++++- doc/images.md | 3 + doc/links.md | 2 + modules/cpu/CMakeLists.txt | 1 - modules/cpu/common/FeatureFlagsEnum.h | 5 + modules/cpu/render/ScreenSample.ih | 21 ++- .../cpu/render/pathtracer/GeometryLight.ispc | 4 +- .../render/pathtracer/NextEventEstimation.ih | 1 + .../pathtracer/NextEventEstimation.ispc | 10 ++ .../cpu/render/pathtracer/PathSampler.ispc | 101 +++++++++++---- modules/cpu/render/pathtracer/PathStructs.ih | 4 +- modules/cpu/render/pathtracer/PathTracer.cpp | 2 + modules/cpu/render/pathtracer/PathTracer.ispc | 17 ++- .../cpu/render/pathtracer/PathTracerUtil.ih | 6 +- .../cpu/render/pathtracer/ShadowCatcher.ih | 24 ---- .../cpu/render/pathtracer/ShadowCatcher.ispc | 120 ------------------ .../cpu/render/pathtracer/VirtualLight.ispc | 10 +- scripts/tests/run_tests.sh | 12 +- ...her_ShadowCatcher.multipleLights_0.png.md5 | 1 + ...her_ShadowCatcher.multipleLights_1.png.md5 | 1 + 26 files changed, 356 insertions(+), 203 deletions(-) create mode 100644 apps/ospTestSuite/test_shadowcatcher.cpp delete mode 100644 modules/cpu/render/pathtracer/ShadowCatcher.ih delete mode 100644 modules/cpu/render/pathtracer/ShadowCatcher.ispc create mode 100644 test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_0.png.md5 create mode 100644 test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_1.png.md5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 86faef336..ca6af4e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Version History - New parameter `specularMetallic` for the Principled material to optionally disable the incluence of `specular` to metallicness, improving compatibility with glTF `KHR_materials_specular` +- Improvements to and documentation of the pathtracer's Shadow Catcher + feature (enabled via parameter `shadowCatcherPlane`) ### Changes in v3.2.0: diff --git a/README.md b/README.md index 549b8839a..dd13419a0 100644 --- a/README.md +++ b/README.md @@ -2076,6 +2076,7 @@ table below. | float | metallic | 0 | mix between dielectric (diffuse and/or specular) and metallic (specular only with complex IOR) in \[0–1\] | | float | diffuse | 1 | diffuse reflection weight in \[0–1\] | | float | specular | 1 | specular reflection/transmission weight in \[0–1\] | +| bool | specularMetallic | true | whether specular influences metallic | | float | ior | 1 | dielectric index of refraction | | float | transmission | 0 | specular transmission weight in \[0–1\] | | vec3f | transmissionColor | white | attenuated color due to transmission (Beer’s law, linear RGB) | @@ -2870,8 +2871,8 @@ variance below the `varianceThreshold`. This feature requires a Per default the background of the rendered image will be transparent black, i.e., the alpha channel holds the opacity of the rendered objects. This eases transparency-aware blending of the image with an -arbitrary background image by the application (via -$ospray.rgb + appBackground.rgb⋅(1-ospray.alpha)$). The parameter +arbitrary background image by the application (via the “over” operator, +i.e., $ospray.rgb + appBackground.rgb⋅(1-ospray.alpha)$). The parameter `backgroundColor` or `map_backplate` can be used to already blend with a constant background color or backplate texture, respectively, (and alpha) during rendering. @@ -2960,14 +2961,15 @@ realistic materials. This renderer is created by passing the type string parameters](#renderer) understood by all renderers the path tracer supports the following special parameters: -| Type | Name | Default | Description | -|:------|:--------------------------|--------:|:------------------------------------------------------------------------------------------------------------------------------------------| -| int | lightSamples | all | number of random light samples per path vertex, best results when a power of 2; per default all light sources are sampled | -| bool | limitIndirectLightSamples | true | after the first non-specular (i.e., diffuse and glossy) path vertex take (at most) a single light sample (instead of `lightSamples` many) | -| int | roulettePathLength | 5 | ray recursion depth at which to start Russian roulette termination | -| int | maxScatteringEvents | 20 | maximum number of non-specular (i.e., diffuse and glossy) bounces | -| float | maxContribution | ∞ | samples are clamped to this value before they are accumulated into the framebuffer | -| bool | backgroundRefraction | false | allow for alpha blending even if background is seen through refractive objects like glass | +| Type | Name | Default | Description | +|:------|:--------------------------|---------:|:------------------------------------------------------------------------------------------------------------------------------------------| +| int | lightSamples | all | number of random light samples per path vertex, best results when a power of 2; per default all light sources are sampled | +| bool | limitIndirectLightSamples | true | after the first non-specular (i.e., diffuse and glossy) path vertex take (at most) a single light sample (instead of `lightSamples` many) | +| int | roulettePathLength | 5 | ray recursion depth at which to start Russian roulette termination | +| int | maxScatteringEvents | 20 | maximum number of non-specular (i.e., diffuse and glossy) bounces | +| float | maxContribution | ∞ | samples are clamped to this value before they are accumulated into the framebuffer | +| bool | backgroundRefraction | false | allow for alpha blending even if background is seen through refractive objects like glass | +| vec4f | shadowCatcherPlane | disabled | components of an invisible [plane](#planes) that captures shadow attentuations, which are blended with the background | Special parameters understood by the path tracer. @@ -2980,6 +2982,57 @@ The scattering albedo can be specified using the [transfer function](#transfer-function). Extinction is assumed to be spectrally constant. +Rendering an object with an [HDRI light](#hdri-light) results in +realistic illumination, but the object looks detached and floats in +space. As remedy a so-called Shadow Catcher can be used by setting the +coefficients of a [plane](#planes) via `shadowCatcherPlane`, which will +ground the object by computing matching (contact) shadows. The accuracy +of the shadow estimation depends on the number of light samples per +frame (via `lightSamples` and/or `pixelSamples`), in particular when +using a high-contrast HDRI or multiple (colored) light sources. With +just a single light sample the intensity, gradient and color of the +shadows will potentially be quite off. + +
+ + +
+ + + +
+ + +
+ + + +Applications can also perform their own compositing of the captured +shadow, because the shadow is also baked into the alpha channel (which +however means loosing the color tint of the shadow). To do so, the +`backgroundColor` must be transparent (alpha=0) and should be black +(otherwise the background color will bleed into the shadow); likewise, +light sources should be set to `visible=false` to avoid them being +visible in the background. When using the [denoiser](#denoiser) best +enable `denoiseAlpha` as well. + +
+ + +
+ + + Framebuffer ----------- diff --git a/apps/ospExamples/GLFWOSPRayWindow.cpp b/apps/ospExamples/GLFWOSPRayWindow.cpp index a0faea9e7..4e4ef1ab7 100644 --- a/apps/ospExamples/GLFWOSPRayWindow.cpp +++ b/apps/ospExamples/GLFWOSPRayWindow.cpp @@ -875,6 +875,13 @@ void GLFWOSPRayWindow::buildUI() addObjectToCommit(renderer->handle()); } + static vec4f shadowCatcherPlane{0.0f}; + if (ImGui::DragFloat4( + "shadowCatcherPlane", &shadowCatcherPlane[0], 0.05f, -1.f, 1.0f)) { + renderer->setParam("shadowCatcherPlane", shadowCatcherPlane); + addObjectToCommit(renderer->handle()); + } + if (ImGui::SliderFloat("camera motion blur", &cameraMotionBlur, 0.f, 1.f)) { updateCamera(); } diff --git a/apps/ospTestSuite/CMakeLists.txt b/apps/ospTestSuite/CMakeLists.txt index 9486610c0..3680f04ef 100644 --- a/apps/ospTestSuite/CMakeLists.txt +++ b/apps/ospTestSuite/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(ospTestSuite test_framebuffer.cpp test_interpolation.cpp test_imageop.cpp + test_shadowcatcher.cpp ospTestSuite.cpp ) diff --git a/apps/ospTestSuite/test_shadowcatcher.cpp b/apps/ospTestSuite/test_shadowcatcher.cpp new file mode 100644 index 000000000..639040400 --- /dev/null +++ b/apps/ospTestSuite/test_shadowcatcher.cpp @@ -0,0 +1,89 @@ +// Copyright 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +#include "test_fixture.h" + +namespace OSPRayTestScenes { + +// Fixture class for a unit test of the pathtracer's shadowCatcherPlane feature +class ShadowCatcher : public Base, + public ::testing::TestWithParam +{ + public: + ShadowCatcher(); + void SetUp() override; + + protected: +}; + +ShadowCatcher::ShadowCatcher() +{ + rendererType = "pathtracer"; + samplesPerPixel = 32; +} + +void ShadowCatcher::SetUp() +{ + Base::SetUp(); + camera.setParam("position", vec3f(-0.2f, 2.5f, -3.f)); + camera.setParam("direction", vec3f(0.f, -0.3f, 1.0f)); + + cpp::Geometry boxGeometry("box"); + boxGeometry.setParam("box", cpp::CopiedData(box3f(vec3f(0.f), vec3f(0.5f, 2.f, 1.0f)))); + boxGeometry.commit(); + cpp::GeometricModel model(boxGeometry); + + cpp::Material material("obj"); + material.setParam("kd", vec3f(0.1f)); + material.commit(); + model.setParam("material", material); + AddModel(model); + + // floor, to be hidden by ShadowCatcher + cpp::Geometry planeGeometry("plane"); + planeGeometry.setParam("plane.coefficients", cpp::CopiedData(vec4f(0.0, 1.0, 0.0, 0.f))); + planeGeometry.commit(); + cpp::GeometricModel planeModel(planeGeometry); + + cpp::Material planeMaterial("obj"); + planeModel.setParam("material", planeMaterial); + AddModel(planeModel); + + if (GetParam()) { + cpp::Light distant("distant"); + distant.setParam("direction", vec3f(5.f, -2.0f, 5.8f)); + distant.setParam("color", vec3f(15.0f, 3.0f, 3.0f)); + distant.setParam("angularDiameter", 3.0f); + AddLight(distant); + + cpp::Light sphere("sphere"); + sphere.setParam("position", vec3f(2.f, 2.4f, -1.0f)); + sphere.setParam("color", vec3f(3.f, 150.f, 3.f)); + sphere.setParam("radius", 0.02f); + AddLight(sphere); + + cpp::Light ambient("ambient"); + ambient.setParam("visible", false); + ambient.setParam("color", vec3f(0.0f, 0.0f, 3.0f)); + AddLight(ambient); + } else { + cpp::Light light("spot"); + light.setParam("position", vec3f(-1.f, 2.4f, 1.0f)); + light.setParam("direction", vec3f(1.5f, -1.0f, 0.0f)); + light.setParam("radius", 0.2f); + AddLight(light); + } + + renderer.setParam("shadowCatcherPlane", vec4f(0.0, 1.0, 0.0, 0.1f)); + renderer.setParam("lightSamples", 1); + renderer.setParam("backgroundColor", vec4f(0.3f, 0.3f, 0.3f, 0.0f)); +} + +TEST_P(ShadowCatcher, multipleLights) +{ + PerformRenderTest(); +}; + +INSTANTIATE_TEST_SUITE_P(TestShadowCatcher, ShadowCatcher, ::testing::Bool()); + +} // namespace OSPRayTestScenes diff --git a/doc/Makefile b/doc/Makefile index 1e2044626..908d9eae1 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,8 +3,8 @@ SHELL := /bin/bash webpages := $(addprefix www/, $(addsuffix .html, index tutorials documentation gallery downloads related_projects displaywall ospray2_porting_guide legal)) process_version := $(addprefix tmp/, $(addsuffix .md, tutorials getting_ospray readme_head)) tmptexfiles := $(addprefix tmp/, $(addsuffix .tex, overview changelog compilation api tutorials)) -images_jpg := $(addprefix images/, $(addsuffix .jpg, exampleViewer $(addprefix camera_, perspective architectural stereo orthographic panoramic) $(addprefix material_, OBJ Principled CarPaint Metal Alloy Glass ThinGlass MetallicPaint Luminous) ColoredWindow $(addprefix ospMPIDistribTutorial, Volume Spheres _firstFrame _accumulatedFrame))) -images_png := $(addprefix images/, $(addsuffix .png, diffuse_rooms normalmap_frustum tutorial_accumulatedframe tutorial_firstframe ospExamples renderSunSky)) +images_jpg := $(addprefix images/, $(addsuffix .jpg, exampleViewer $(addprefix camera_, perspective architectural stereo orthographic panoramic) $(addprefix material_, OBJ Principled CarPaint Metal Alloy Glass ThinGlass MetallicPaint Luminous) ColoredWindow $(addprefix ospMPIDistribTutorial, Volume Spheres _firstFrame _accumulatedFrame) shadowcatcher shadowcatcher_envonly)) +images_png := $(addprefix images/, $(addsuffix .png, diffuse_rooms normalmap_frustum tutorial_accumulatedframe tutorial_firstframe ospExamples renderSunSky shadowcatcher_alpha)) images_fig := spot_light c-gamma_coords quad_light hdri_light images_svg := structured_spherical_coords vdb_structure diff --git a/doc/api.md b/doc/api.md index 6e98822ab..5df718c2f 100644 --- a/doc/api.md +++ b/doc/api.md @@ -2845,8 +2845,8 @@ refinement of image regions that have an estimated variance below the Per default the background of the rendered image will be transparent black, i.e., the alpha channel holds the opacity of the rendered objects. This eases transparency-aware blending of the image with an -arbitrary background image by the application (via $ospray.rgb + -appBackground.rgb⋅(1-ospray.alpha)$). The parameter +arbitrary background image by the application (via the "over" operator, +i.e., $ospray.rgb + appBackground.rgb⋅(1-ospray.alpha)$). The parameter `backgroundColor` or `map_backplate` can be used to already blend with a constant background color or backplate texture, respectively, (and alpha) during rendering. @@ -2988,6 +2988,11 @@ supports the following special parameters: bool backgroundRefraction false allow for alpha blending even if background is seen through refractive objects like glass + + vec4f shadowCatcherPlane disabled components of an invisible [plane] + that captures shadow + attentuations, which are blended + with the background ------ -------------------------- -------- ---------------------------------- : Special parameters understood by the path tracer. @@ -2998,6 +3003,35 @@ The path tracer supports [volumes](#volumes) with multiple scattering. The scattering albedo can be specified using the [transfer function]. Extinction is assumed to be spectrally constant. +Rendering an object with an [HDRI light] results in realistic +illumination, but the object looks detached and floats in space. As +remedy a so-called Shadow Catcher can be used by setting the +coefficients of a [plane] via `shadowCatcherPlane`, which will ground +the object by computing matching (contact) shadows. The accuracy of the +shadow estimation depends on the number of light samples per frame +(via `lightSamples` and/or `pixelSamples`), in particular when using a +high-contrast HDRI or multiple (colored) light sources. With just a +single light sample the intensity, gradient and color of the shadows +will potentially be quite off. + +![Chair rendered in an interior HDRI environment.][imgNoShadowCatcher] + +![Same scene with enabled Shadow Catcher plane.][imgShadowCatcher] + +Applications can also perform their own compositing of the captured +shadow, because the shadow is also baked into the alpha channel (which +however means loosing the color tint of the shadow). To do so, the +`backgroundColor` must be transparent (alpha=0) and should be black +(otherwise the background color will bleed into the shadow); likewise, light +sources should be set to `visible=false` to avoid them being visible in +the background. When using the [denoiser] best enable `denoiseAlpha` as +well. + +![The Shadow Catcher also bakes the (monochrome) shadow into the alpha +channel, which can be used for further +compositing.][imgShadowCatcherAlpha] + + Framebuffer ----------- diff --git a/doc/images.md b/doc/images.md index 2aa60a6d5..7d1a5e68d 100644 --- a/doc/images.md +++ b/doc/images.md @@ -29,4 +29,7 @@ [imgColoredWindow]: ColoredWindow.jpg { width=60% } [imgStructuredSphericalCoords]: structured_spherical_coords.svg { width=60% } [imgVdbStructure]: vdb_structure.svg { width=80% } +[imgShadowCatcher]: shadowcatcher.jpg { width=80% } +[imgNoShadowCatcher]: shadowcatcher_envonly.jpg { width=80% } +[imgShadowCatcherAlpha]: shadowcatcher_alpha.png { width=80% } diff --git a/doc/links.md b/doc/links.md index dade07281..8f47597ef 100644 --- a/doc/links.md +++ b/doc/links.md @@ -12,6 +12,7 @@ [group]: documentation.html#groups [instance]: documentation.html#instances [world]: documentation.html#world +[plane]: documentation.html#planes [data]: documentation.html#data [GeometricModel]: documentation.html#geometricmodels [VolumetricModel]: documentation.html#volumetricmodels @@ -22,6 +23,7 @@ [material]: documentation.html#materials [OBJ material]: documentation.html#obj-material [point light]: documentation.html#point-light-sphere-light +[HDRI light]: documentation.html#hdri-light [example applications]: tutorials.html#tutorials [tutorial]: tutorials.html#osptutorial diff --git a/modules/cpu/CMakeLists.txt b/modules/cpu/CMakeLists.txt index 77a073f70..0fe4cb08b 100644 --- a/modules/cpu/CMakeLists.txt +++ b/modules/cpu/CMakeLists.txt @@ -181,7 +181,6 @@ set(OSPRAY_KERNEL_SOURCES render/pathtracer/GeometryLight.ispc render/pathtracer/VirtualLight.ispc render/pathtracer/TransparentShadow.ispc - render/pathtracer/ShadowCatcher.ispc render/pathtracer/NextEventEstimation.ispc render/bsdfs/BSDF.ispc diff --git a/modules/cpu/common/FeatureFlagsEnum.h b/modules/cpu/common/FeatureFlagsEnum.h index cf1182ab7..0bd788482 100644 --- a/modules/cpu/common/FeatureFlagsEnum.h +++ b/modules/cpu/common/FeatureFlagsEnum.h @@ -140,6 +140,8 @@ enum FeatureFlagsOther FFO_CAMERA_MOTION_BLUR = 1 << 29, + FFO_RENDERER_SHADOWCATCHER = 1 << 30, + FFO_ALL = 0xffffffff }; @@ -367,6 +369,9 @@ inline void printFfo(uint32_t flags) if (flags & FFO_CAMERA_MOTION_BLUR) std::cout << "FFO_CAMERA_MOTION_BLUR" << std::endl; + + if (flags & FFO_RENDERER_SHADOWCATCHER) + std::cout << "FFO_RENDERER_SHADOWCATCHER" << std::endl; } #endif diff --git a/modules/cpu/render/ScreenSample.ih b/modules/cpu/render/ScreenSample.ih index af2c30873..012325a18 100644 --- a/modules/cpu/render/ScreenSample.ih +++ b/modules/cpu/render/ScreenSample.ih @@ -30,6 +30,11 @@ struct ScreenSample unsigned int primID; unsigned int geomID; unsigned int instID; + + // for shadow catcher + vec4f unshaded; + vec4f shadow; // includes blended background in rgb, but not in alpha + unsigned int cnt; }; inline ScreenSample make_ScreenSample_zero() @@ -42,6 +47,9 @@ inline ScreenSample make_ScreenSample_zero() screenSample.firstNormal = make_vec3f(0.0f); screenSample.normal = make_vec3f(0.0f); screenSample.albedo = make_vec3f(0.0f); + screenSample.unshaded = make_vec4f(0.0f); + screenSample.shadow = make_vec4f(0.0f); + screenSample.cnt = 0; screenSample.primID = RTC_INVALID_GEOMETRY_ID; screenSample.geomID = RTC_INVALID_GEOMETRY_ID; screenSample.instID = RTC_INVALID_GEOMETRY_ID; @@ -57,6 +65,9 @@ inline void ScreenSample_accumulate( accum.firstNormal = accum.firstNormal + sample.firstNormal; accum.normal = accum.normal + sample.normal; accum.albedo = accum.albedo + sample.albedo; + accum.unshaded = accum.unshaded + sample.unshaded; + accum.shadow = accum.shadow + sample.shadow; + accum.cnt += sample.cnt; if (absf(sample.z) < absf(accum.z)) { accum.z = sample.z; accum.primID = sample.primID; @@ -65,15 +76,19 @@ inline void ScreenSample_accumulate( } } -inline void ScreenSample_normalize(ScreenSample &sample, uniform int32 spp) +inline void ScreenSample_normalize(ScreenSample &sample, uniform float rspp) { - const uniform float rspp = rcpf(spp); sample.rgb = sample.rgb * rspp; - sample.alpha = sample.alpha * rspp; + sample.alpha *= rspp; sample.position = sample.position * rspp; sample.firstNormal = sample.firstNormal * rspp; sample.normal = sample.normal * rspp; sample.albedo = sample.albedo * rspp; } +inline void ScreenSample_normalize(ScreenSample &sample, uniform int32 spp) +{ + ScreenSample_normalize(sample, rcpf(spp)); +} + OSPRAY_END_ISPC_NAMESPACE diff --git a/modules/cpu/render/pathtracer/GeometryLight.ispc b/modules/cpu/render/pathtracer/GeometryLight.ispc index 8d9447a4f..7a857f64b 100644 --- a/modules/cpu/render/pathtracer/GeometryLight.ispc +++ b/modules/cpu/render/pathtracer/GeometryLight.ispc @@ -30,8 +30,10 @@ SYCL_EXTERNAL vec3f evaluateGeometryLights(const PathContext &pathContext, Material *material = pathVertex.dg.material; foreach_unique (m in material) { + if (m == NULL) + continue; vec3f emission = Material_dispatch_getEmission(m, pathVertex.dg, ffh); - if (m != NULL && reduce_max(emission) > 0.f) { + if (reduce_max(emission) > 0.f) { float misWeight = 1.0f; if (pathContext.numGeoLights > 0 && lastVertex.numLightSamples > 0) { // at the moment we only support uniform light selection diff --git a/modules/cpu/render/pathtracer/NextEventEstimation.ih b/modules/cpu/render/pathtracer/NextEventEstimation.ih index 02fc7c123..893db8ce6 100644 --- a/modules/cpu/render/pathtracer/NextEventEstimation.ih +++ b/modules/cpu/render/pathtracer/NextEventEstimation.ih @@ -16,6 +16,7 @@ SYCL_EXTERNAL vec3f nextEventEstimation(const PathContext &pathContext, PathState &pathState, PathVertex &pathVertex, float rayConeWidth, + vec3f &unshaded, const uniform FeatureFlagsHandler &ffh); OSPRAY_END_ISPC_NAMESPACE diff --git a/modules/cpu/render/pathtracer/NextEventEstimation.ispc b/modules/cpu/render/pathtracer/NextEventEstimation.ispc index 31775b1b4..a85179d4e 100644 --- a/modules/cpu/render/pathtracer/NextEventEstimation.ispc +++ b/modules/cpu/render/pathtracer/NextEventEstimation.ispc @@ -26,12 +26,14 @@ SYCL_EXTERNAL vec3f nextEventEstimation(const PathContext &pathContext, PathState &pathState, PathVertex &pathVertex, float rayConeWidth, + vec3f &unshaded, const uniform FeatureFlagsHandler &ffh) { const uniform FeatureFlags ff = getFeatureFlags(ffh); // direct lighting including shadows and MIS vec3f L = make_vec3f(0.f); + unshaded = make_vec3f(0.f); // illumination without occluders const uint32 numLightSamples = pathState.firstBounceLight ? pathContext.numFirstBounceLightSamples @@ -66,10 +68,16 @@ SYCL_EXTERNAL vec3f nextEventEstimation(const PathContext &pathContext, // evaluate BSDF Scattering_EvalRes fe; + fe.value = make_vec3f(0.f); if (ff.geometry && pathVertex.type == SURFACE) { foreach_unique (f in pathVertex.bsdf) if (f != NULL) fe = BSDF_dispatch_eval(f, pathVertex.wo, ls.dir, ffh); + } else if (pathVertex.type == SHADOWCATCHER) { + // evaluate a white diffuse BRDF + const float cosThetaI = max(dot(ls.dir, pathVertex.dg.Ns), 0.f); + fe.value = make_vec3f(cosThetaI); // * one_over_pi cancels anyway + fe.pdf = 0.0f; // shadow catcher does not sample FWD } else { #ifdef OSPRAY_ENABLE_VOLUMES if (ff.other & FFO_VOLUME_IN_SCENE) { @@ -104,6 +112,8 @@ SYCL_EXTERNAL vec3f nextEventEstimation(const PathContext &pathContext, const vec3f throughput = pathState.throughput * fe.value; const vec3f unshadedLightContrib = throughput * ls.weight; + unshaded = unshaded + + unshadedLightContrib * misHeuristic(pathContext, ls.pdf, fe.pdf); const vec3f lightContrib = transparentShadow(pathContext.context, pathContext.world, unshadedLightContrib, diff --git a/modules/cpu/render/pathtracer/PathSampler.ispc b/modules/cpu/render/pathtracer/PathSampler.ispc index 9a71b80d3..b7cdfa4a7 100644 --- a/modules/cpu/render/pathtracer/PathSampler.ispc +++ b/modules/cpu/render/pathtracer/PathSampler.ispc @@ -7,7 +7,6 @@ #include "render/pathtracer/PathStructs.ih" #include "render/pathtracer/PathTracerDefines.ih" #include "render/pathtracer/PathTracerUtil.ih" -#include "render/pathtracer/ShadowCatcher.ih" #include "render/pathtracer/TransparentShadow.ih" #include "render/pathtracer/VirtualLight.ih" @@ -38,7 +37,8 @@ inline void postIntersect(const PathContext &pathContext, { const PathTracer *uniform pt = pathContext.context; const uniform FeatureFlags ff = getFeatureFlags(ffh); - if (ff.geometry && pathVertex.type == SURFACE) { + if (pathVertex.type == SHADOWCATCHER + || (ff.geometry && pathVertex.type == SURFACE)) { postIntersect(pathContext.world, &pt->super, pathVertex.dg, @@ -132,16 +132,35 @@ SYCL_EXTERNAL void samplePath(const PathContext &pathContext, sample.alpha = 1.0f; - if (pathContext.context->shadowCatcher) { + // TODO consider real (flagged) geometries with material + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) + && pathContext.context->shadowCatcher) { const Hit hit = intersectPlane( ray.org, ray.dir, pathContext.context->shadowCatcherPlane); if (hit.hit) pathState.shadowCatcherDist = hit.t; } do { - if (pathState.shadowCatcherDist - > ray.t0) // valid hit can hide other geometry - ray.t = min(pathState.shadowCatcherDist, ray.t); + PathVertex pathVertex; + pathVertex.bsdf = NULL; + pathVertex.pdf_w = inf; + pathVertex.type = ENVIRONMENT; +#ifdef OSPRAY_ENABLE_VOLUMES + pathVertex.volume = NULL; +#endif + + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) + && pathState.shadowCatcherDist > ray.t0 + && pathState.shadowCatcherDist <= ray.t + && (pathContext.context->backgroundRefraction + ? pathState.specularTransmissionPath + : pathState.straightPath)) { + pathVertex.type = SHADOWCATCHER; + pathVertex.albedo = make_vec3f(0.8f); + ray.t = pathState.shadowCatcherDist; + ray.Ng = make_vec3f(pathContext.context->shadowCatcherPlane); + ray.instID = RTC_INVALID_GEOMETRY_ID; // to safely call postIntersect + } // Trace ray in clipping geometries scene, fill array with ray intervals RayIntervals rayIntervals; @@ -150,18 +169,8 @@ SYCL_EXTERNAL void samplePath(const PathContext &pathContext, if (ff.geometry) { // Trace ray intervals in geometry traceGeometryRayIntervals(pathContext.world, ray, rayIntervals, ffh); - } - - PathVertex pathVertex; - pathVertex.bsdf = NULL; - pathVertex.pdf_w = inf; -#ifdef OSPRAY_ENABLE_VOLUMES - pathVertex.volume = NULL; -#endif - pathVertex.type = noHit(ray) || !ff.geometry ? ENVIRONMENT : SURFACE; - - if (shadowCatcher(pathContext, pathState, pathVertex, ray, rayCone, ffh)) { - pathVertex.type = ENVIRONMENT; + if (hadHit(ray)) + pathVertex.type = SURFACE; } pathVertex.wo = neg(ray.dir); @@ -191,16 +200,18 @@ SYCL_EXTERNAL void samplePath(const PathContext &pathContext, : ray.t; // background handling - if (pathVertex.type == ENVIRONMENT + if ((pathVertex.type == ENVIRONMENT || pathVertex.type == SHADOWCATCHER) && (pathContext.context->backgroundRefraction ? pathState.specularTransmissionPath : pathState.straightPath)) { - vec4f bg = Renderer_getBackground( + const vec4f bg = Renderer_getBackground( &pathContext.context->super, pathContext.screen, ffh); - pathState.contribution = - pathState.contribution + pathState.throughput * make_vec3f(bg); - sample.alpha = 1.0f - luminance(pathState.throughput); - sample.alpha += (1.f - sample.alpha) * bg.w; + sample.alpha -= luminance(pathState.throughput) * (1.0f - bg.w); + const vec3f bg3 = pathState.throughput * make_vec3f(bg); + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) + && pathVertex.type == SHADOWCATCHER) + sample.shadow = make_vec4f(bg3); + pathState.contribution = pathState.contribution + bg3; } #ifdef OSPRAY_PATHTRACER_DEBUG @@ -213,6 +224,22 @@ SYCL_EXTERNAL void samplePath(const PathContext &pathContext, pathState.contribution = pathState.contribution + evaluateVirtualLights( pathContext, pathState, lastVertex, pathVertex, ray, ffh); + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) + && pathVertex.type == SHADOWCATCHER) { + // gather background light from environment behind the shadow catcher + pathVertex.type = ENVIRONMENT; + const vec3f ray_org = ray.org; + ray.org = ray.org + ray.t * ray.dir; + // FIXME: to avoid two calls (two almost identical loops), the + // Light::eval interface needs to change + const vec3f bg = evaluateVirtualLights( + pathContext, pathState, lastVertex, pathVertex, ray, ffh); + sample.shadow = sample.shadow + make_vec4f(bg); + pathState.contribution = pathState.contribution + bg; + // restore + pathVertex.type = SHADOWCATCHER; + ray.org = ray_org; + } } if (pathVertex.type == ENVIRONMENT) { @@ -288,13 +315,30 @@ SYCL_EXTERNAL void samplePath(const PathContext &pathContext, if (!pathContext.disableNEE) #endif { - pathState.contribution = pathState.contribution - + nextEventEstimation( - pathContext, pathState, pathVertex, sample.rayCone.width, ffh); + vec3f unshaded; // illumination without occlusion + const vec3f shaded = nextEventEstimation(pathContext, + pathState, + pathVertex, + sample.rayCone.width, + unshaded, + ffh); + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) + && pathVertex.type == SHADOWCATCHER) { + sample.cnt++; + sample.unshaded = make_vec4f(unshaded, luminance(unshaded)); + const vec3f shadow = unshaded - shaded; + sample.shadow = // multiply already gathered background + make_vec4f(make_vec3f(sample.shadow) * shadow, luminance(shadow)); + } else + pathState.contribution = pathState.contribution + shaded; pathState.firstBounceLight = false; } } + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) + && pathVertex.type == SHADOWCATCHER) + break; + float ss; uint32 uu3; const vec2f s = LDSampler_getNext4Samples(pathState.ldSampler, ss, uu3); @@ -351,6 +395,9 @@ SYCL_EXTERNAL void samplePath(const PathContext &pathContext, pathState.straightPath = false; if (!(fs.type & SCATTERING_SPECULAR_TRANSMISSION)) pathState.specularTransmissionPath = false; + // update dist for potential next intersection (if transparent) + if (ff.other & FFO_RENDERER_SHADOWCATCHER) + pathState.shadowCatcherDist -= ray.t; setRay(ray, ray_org, fs.wi, pathState.time); pathState.depth++; diff --git a/modules/cpu/render/pathtracer/PathStructs.ih b/modules/cpu/render/pathtracer/PathStructs.ih index ee988c2fe..3ec8a2224 100644 --- a/modules/cpu/render/pathtracer/PathStructs.ih +++ b/modules/cpu/render/pathtracer/PathStructs.ih @@ -12,7 +12,6 @@ #endif // c++ shared #include "PathTracerShared.h" - OSPRAY_BEGIN_ISPC_NAMESPACE struct World; @@ -24,7 +23,8 @@ enum PathVertexType VOLUME, CAMERA, LIGHT, - ENVIRONMENT + ENVIRONMENT, + SHADOWCATCHER }; struct PathVertex diff --git a/modules/cpu/render/pathtracer/PathTracer.cpp b/modules/cpu/render/pathtracer/PathTracer.cpp index eee79a509..5e952e4c2 100644 --- a/modules/cpu/render/pathtracer/PathTracer.cpp +++ b/modules/cpu/render/pathtracer/PathTracer.cpp @@ -80,6 +80,8 @@ devicert::AsyncEvent PathTracer::renderTasks(FrameBuffer *fb, FeatureFlags ff = world->getFeatureFlags(); if (world->pathtracerData->getSh()->numGeoLights) ff.other |= FFO_LIGHT_GEOMETRY; + if (getSh()->shadowCatcher) + ff.other |= FFO_RENDERER_SHADOWCATCHER; ff |= featureFlags; ff |= fb->getFeatureFlags(); ff |= camera->getFeatureFlags(); diff --git a/modules/cpu/render/pathtracer/PathTracer.ispc b/modules/cpu/render/pathtracer/PathTracer.ispc index 3ff2f6a3e..4788e6173 100644 --- a/modules/cpu/render/pathtracer/PathTracer.ispc +++ b/modules/cpu/render/pathtracer/PathTracer.ispc @@ -54,7 +54,9 @@ static ScreenSample PathTraceIntegrator_Li(PathContext &pathContext, pathState.contribution = make_vec3f(0.f); pathState.time = ray.time; pathState.currentMedium = make_Medium_vacuum(); - pathState.shadowCatcherDist = -(float)inf; + const uniform FeatureFlags ff = getFeatureFlags(ffh); + if (ff.other & FFO_RENDERER_SHADOWCATCHER) + pathState.shadowCatcherDist = -(float)inf; samplePath(pathContext, pathState, ray, rayCone, sample, ffh); @@ -197,7 +199,18 @@ static ScreenSample PathTracer_renderPixel(PathTracer *uniform self, ScreenSample_accumulate(screenSample, sample); } - ScreenSample_normalize(screenSample, spp); + const uniform float rspp = rcpf(spp); + ScreenSample_normalize(screenSample, rspp); + + const uniform FeatureFlags ff = getFeatureFlags(ffh); + if ((ff.other & FFO_RENDERER_SHADOWCATCHER) && screenSample.cnt > 0) { + const float coverage = rspp * screenSample.cnt; + // filter 0/0=NaN: define unlit areas as unshadowed 0.0f + const vec4f ratio = + max(screenSample.shadow * rcp(screenSample.unshaded), make_vec4f(0.f)); + screenSample.rgb = screenSample.rgb - coverage * make_vec3f(ratio); + screenSample.alpha += (1.0f - screenSample.alpha) * ratio.w; + } return screenSample; } diff --git a/modules/cpu/render/pathtracer/PathTracerUtil.ih b/modules/cpu/render/pathtracer/PathTracerUtil.ih index 32cc3ffa2..dc71f14bd 100644 --- a/modules/cpu/render/pathtracer/PathTracerUtil.ih +++ b/modules/cpu/render/pathtracer/PathTracerUtil.ih @@ -22,6 +22,8 @@ inline bool isSmooth(const PathVertex &pathVertex) && isSmoothVolumeVertex(pathVertex)) return true; #endif + if (pathVertex.type == SHADOWCATCHER) + return true; return false; } @@ -47,14 +49,14 @@ inline void updateAuxilliaryData( PathState &pathState, const PathVertex &pathVertex, ScreenSample &sample) { sample.albedo = pathState.throughput * pathVertex.albedo; + vec3f Ns = pathVertex.dg.Ns; if (pathVertex.type == SURFACE) { - vec3f Ns = pathVertex.dg.Ns; foreach_unique (f in pathVertex.bsdf) { if (f != NULL && f->frame != NULL) Ns = getN(f); } - sample.normal = pathState.throughput * Ns; } + sample.normal = pathState.throughput * Ns; } OSPRAY_END_ISPC_NAMESPACE diff --git a/modules/cpu/render/pathtracer/ShadowCatcher.ih b/modules/cpu/render/pathtracer/ShadowCatcher.ih deleted file mode 100644 index 4a6e55e3a..000000000 --- a/modules/cpu/render/pathtracer/ShadowCatcher.ih +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2009 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "common/FeatureFlags.ih" - -OSPRAY_BEGIN_ISPC_NAMESPACE - -struct PathContext; -struct PathState; -struct PathVertex; -struct Ray; -struct RayCone; -struct ScreenSample; - -SYCL_EXTERNAL bool shadowCatcher(const PathContext &pathContext, - PathState &pathState, - PathVertex &pathVertex, - const Ray &ray, - const RayCone &rayCone, - const uniform FeatureFlagsHandler &ffh); - -OSPRAY_END_ISPC_NAMESPACE diff --git a/modules/cpu/render/pathtracer/ShadowCatcher.ispc b/modules/cpu/render/pathtracer/ShadowCatcher.ispc deleted file mode 100644 index c5be766e8..000000000 --- a/modules/cpu/render/pathtracer/ShadowCatcher.ispc +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2009 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 - -#include "render/pathtracer/PathStructs.ih" -#include "render/pathtracer/PathTracerDefines.ih" -#include "render/pathtracer/ShadowCatcher.ih" -#include "render/pathtracer/TransparentShadow.ih" - -#include "render/Renderer.ih" - -#include "common/Ray.ih" -#include "common/World.ih" -#include "math/random.ih" -// c++ shared -#include "PathTracerDataShared.h" -#include "lights/LightDispatch.ih" -#include "lights/LightShared.h" - -OSPRAY_BEGIN_ISPC_NAMESPACE - -SYCL_EXTERNAL bool shadowCatcher(const PathContext &pathContext, - PathState &pathState, - PathVertex &pathVertex, - const Ray &ray, - const RayCone &rayCone, - const uniform FeatureFlagsHandler &ffh) - -{ - if (!(pathContext.context->backgroundRefraction - ? pathState.specularTransmissionPath - : pathState.straightPath)) - return false; - - // TODO use MIS as well - // consider real (flagged) geometries with material and move into light loop - // (will also handle MIS) - if (pathState.shadowCatcherDist <= ray.t - && pathState.shadowCatcherDist > ray.t0) { - // "postIntersect" of shadowCatcher plane - pathVertex.dg.Ns = pathVertex.dg.Ng = - make_vec3f(pathContext.context->shadowCatcherPlane); - if (dot(ray.dir, pathVertex.dg.Ng) >= 0.f) - pathVertex.dg.Ns = pathVertex.dg.Ng = neg(pathVertex.dg.Ng); - - pathVertex.dg.renderer = &pathContext.context->super; - pathVertex.dg.P = ray.org + pathState.shadowCatcherDist * ray.dir; - const float rayConeWidth = rayCone.width - + rayCone.dwdt * pathState.shadowCatcherDist; // propagate to hit - const float eps = - calcEpsilon(pathVertex.dg.P, ray.dir, pathState.shadowCatcherDist); - pathVertex.dg.P = pathVertex.dg.P + eps * pathVertex.dg.Ng; - - vec3f unshaded = make_vec3f(0.f); // illumination without occluders - vec3f shaded = make_vec3f(0.f); // illumination including shadows - const World *uniform world = pathContext.world; - const uniform PathTracerData &pathtracerData = - *((const uniform PathTracerData *)world->pathtracerData); - // TODO use lightsSamples - for (uniform uint32 i = 0; i < pathContext.numLights; i++) { - const Light *uniform light = pathtracerData.lights[i]; - float dummy; - const vec2f s = - LDSampler_get3LightSamples(pathState.ldSampler, i, true, dummy); - Light_SampleRes ls = - Light_dispatch_sample(light, pathVertex.dg, s, pathState.time, ffh); - - // skip when zero contribution from light - if (reduce_max(ls.weight) <= 0.0f | ls.pdf <= PDF_CULLING) - continue; - - // evaluate a white diffuse BRDF - const float brdf = - clamp(dot(ls.dir, pathVertex.dg.Ns)); // * one_over_pi cancels anyway - - // skip when zero contribution from material - if (brdf <= 0.0f) - continue; - - // test for shadows - Ray shadowRay; - setRay(shadowRay, pathVertex.dg.P, ls.dir, 0.0f, ls.dist, pathState.time); - // connect ray cone to point on light - RayCone shadowCone; - shadowCone.width = rayConeWidth; - shadowCone.dwdt = -rayConeWidth * rcp(ls.dist); - - // Trace ray in clipping geometries scene, fill array with ray intervals - RayIntervals rayIntervals; - traceClippingRay(pathContext.world, shadowRay, rayIntervals, ffh); - - const vec3f unshadedLightContrib = pathState.throughput * ls.weight - * brdf; // * misHeuristic(pathContext, ls.pdf, brdf); - unshaded = unshaded + unshadedLightContrib; - shaded = shaded - + transparentShadow(pathContext.context, - pathContext.world, - unshadedLightContrib, - shadowRay, - shadowCone, - rayIntervals, - pathState.currentMedium, - ffh); - } - // order of args important to filter NaNs (in case unshaded.X is zero) - const vec3f ratio = min( - pathState.throughput * shaded * rcp(unshaded), pathState.throughput); - - // alpha blend-in black shadow - pathState.throughput = ratio; - pathState.contribution = make_vec3f(0.f); - - return true; - } - - // update dist for potential next intersection (if transparent) - pathState.shadowCatcherDist -= ray.t; - return false; -} - -OSPRAY_END_ISPC_NAMESPACE diff --git a/modules/cpu/render/pathtracer/VirtualLight.ispc b/modules/cpu/render/pathtracer/VirtualLight.ispc index e2729cbbc..001ddec19 100644 --- a/modules/cpu/render/pathtracer/VirtualLight.ispc +++ b/modules/cpu/render/pathtracer/VirtualLight.ispc @@ -23,13 +23,13 @@ inline box1f getMinMaxDistForVirtualLights( // transmission) interval.lower = distance(lastVertex.dg.P, ray.org); - // virtual lights are occluded by hit geometry - // because lastVertex.dg.P can be different from ray.org (when previously - // sampled a Dirac transmission) we cannot just use ray.t as maximum distance - interval.upper = distance(lastVertex.dg.P, ray.org + ray.t * ray.dir); - if (pathVertex.type == ENVIRONMENT) interval.upper = inf; + else + // virtual lights are occluded by hit geometry because lastVertex.dg.P can + // be different from ray.org (when previously sampled a Dirac transmission) + // we cannot just use ray.t as maximum distance + interval.upper = distance(lastVertex.dg.P, ray.org + ray.t * ray.dir); return interval; } diff --git a/scripts/tests/run_tests.sh b/scripts/tests/run_tests.sh index cca251ea7..932afa353 100755 --- a/scripts/tests/run_tests.sh +++ b/scripts/tests/run_tests.sh @@ -55,11 +55,19 @@ export CMAKE_BUILD_PARALLEL_LEVEL=32 cmake --build . --target ospray_test_data let exitCode+=$? +### Excluded tests +################## +# due to IEEE 754 uncompliant NaN handling on ARM NEON, +# see https://github.com/ispc/ispc/issues/3048 +if [[ `uname -m` =~ arm|aarch ]] ; then + test_filters="TestShadowCatcher/ShadowCatcher.multipleLights/0" +fi + export OIDN_VERBOSE=2 if [ $TEST_CPU ]; then mkdir failed - ospTestSuite --gtest_output=xml:tests.xml --baseline-dir=regression_test_baseline/ --failed-dir=failed + ospTestSuite --gtest_output=xml:tests.xml --baseline-dir=regression_test_baseline/ --failed-dir=failed --gtest_filter="-$test_filters" let exitCode+=$? fi @@ -71,7 +79,7 @@ fi if [ $TEST_MPI ]; then mkdir failed-mpi - test_filters="TestScenesVariance/*" + test_filters+=":TestScenesVariance/*" mpiexec $MPI_ROOT_CONFIG ospTestSuite --gtest_output=xml:tests-mpi-offload.xml --baseline-dir=regression_test_baseline/ --failed-dir=failed-mpi --gtest_filter="-$test_filters" --osp:load-modules=mpi_offload --osp:device=mpiOffload : $MPI_WORKER_CONFIG ospray_mpi_worker let exitCode+=$? diff --git a/test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_0.png.md5 b/test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_0.png.md5 new file mode 100644 index 000000000..810f9bd4b --- /dev/null +++ b/test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_0.png.md5 @@ -0,0 +1 @@ +cd7379e4acca07d6965ef527b39d72f4 diff --git a/test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_1.png.md5 b/test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_1.png.md5 new file mode 100644 index 000000000..45c8c181e --- /dev/null +++ b/test_image_data/baseline/TestShadowCatcher_ShadowCatcher.multipleLights_1.png.md5 @@ -0,0 +1 @@ +cdc991bfd581705ebb5c891af4a96320