From 202c47e41e203e9c4ecb5a9989c6bbf46f0e4d17 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Tue, 15 Jun 2021 12:22:51 +0200 Subject: [PATCH 01/12] Add hdistant sensor plugin --- src/sensors/CMakeLists.txt | 1 + src/sensors/distant.cpp | 3 +- src/sensors/hdistant.cpp | 316 +++++++++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 src/sensors/hdistant.cpp diff --git a/src/sensors/CMakeLists.txt b/src/sensors/CMakeLists.txt index d247196aef..7215934e1b 100644 --- a/src/sensors/CMakeLists.txt +++ b/src/sensors/CMakeLists.txt @@ -5,3 +5,4 @@ add_plugin(radiancemeter radiancemeter.cpp) add_plugin(thinlens thinlens.cpp) add_plugin(irradiancemeter irradiancemeter.cpp) add_plugin(distant distant.cpp) +add_plugin(hdistant hdistant.cpp) diff --git a/src/sensors/distant.cpp b/src/sensors/distant.cpp index e42cda1408..cd41cc3b4b 100644 --- a/src/sensors/distant.cpp +++ b/src/sensors/distant.cpp @@ -30,8 +30,7 @@ Distant radiancemeter sensor (:monosp:`distant`) - Sensor-to-world transformation matrix. * - direction - |vector| - - Alternative (and exclusive) to ``to_world``. Direction orienting the - sensor's reference hemisphere. + - Alternative (and exclusive) to ``to_world``. Direction orienting the sensor. * - target - |point| or nested :paramtype:`shape` plugin - *Optional.* Define the ray target sampling strategy. diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp new file mode 100644 index 0000000000..22892e3261 --- /dev/null +++ b/src/sensors/hdistant.cpp @@ -0,0 +1,316 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +NAMESPACE_BEGIN(mitsuba) + +enum class RayTargetType { Shape, Point, None }; + +// Forward declaration of specialized DistantSensor +template +class HemisphericalDistantSensorImpl; + +/**! + +.. _sensor-distant: + +Hemispherical distant radiancemeter sensor (:monosp:`hdistant`) +--------------------------------------------------------------- + +.. pluginparameters:: + + * - to_world + - |transform| + - Sensor-to-world transformation matrix. + * - target + - |point| or nested :paramtype:`shape` plugin + - *Optional.* Define the ray target sampling strategy. + If this parameter is unset, ray target points are sampled uniformly on + the cross section of the scene's bounding sphere. + If a |point| is passed, rays will target it. + If a shape plugin is passed, ray target points will be sampled from its + surface. + +This sensor plugin implements a distant directional sensor which records +radiation leaving the scene. It records the spectral radiance leaving the scene +in directions covering a hemisphere defined by its ``to_world`` parameter and +mapped to film coordinates. To some extent, it can be seen as the adjoint to +the ``envmap`` emitter. + +The ``to_world`` transform is best set using a +:py:meth:`~mitsuba.core.Transform4f.look_at`. The default orientation covers a +hemisphere defined by the [0, 0, 1] direction, and the ``up`` film direction is +set to [0, 1, 0]. + +.. code:: xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +By default, ray target points are sampled from the cross section of the scene's +bounding sphere. The ``target`` parameter can be set to restrict ray target +sampling to a specific subregion of the scene. The recorded radiance is averaged +over the targeted geometry. + +Ray origins are positioned outside of the scene's geometry. + +.. warning:: + + If this sensor is used with a targeting strategy leading to rays not hitting + the scene's geometry (*e.g.* default targeting strategy), it will pick up + ambient emitter radiance samples (or zero values if no ambient emitter is + defined). Therefore, it is almost always preferrable to use a nondefault + targeting strategy. +*/ + +template +class HemisphericalDistantSensor final : public Sensor { +public: + MTS_IMPORT_BASE(Sensor, m_to_world, m_film) + MTS_IMPORT_TYPES(Scene, Shape) + + HemisphericalDistantSensor(const Properties &props) : Base(props), m_props(props) { + + // Get target + if (props.has_property("target")) { + if (props.type("target") == Properties::Type::Array3f) { + props.point3f("target"); + m_target_type = RayTargetType::Point; + } else if (props.type("target") == Properties::Type::Object) { + // We assume it's a shape + m_target_type = RayTargetType::Shape; + } else { + Throw("Unsupported 'target' parameter type"); + } + } else { + m_target_type = RayTargetType::None; + } + + props.mark_queried("to_world"); + props.mark_queried("target"); + } + + // This must be implemented. However, it won't be used in practice: + // instead, HemisphericalDistantSensorImpl::bbox() is used when the plugin is + // instantiated. + ScalarBoundingBox3f bbox() const override { return ScalarBoundingBox3f(); } + + template + using Impl = HemisphericalDistantSensorImpl; + + // Recursively expand into an implementation specialized to the target + // specification. + std::vector> expand() const override { + ref result; + switch (m_target_type) { + case RayTargetType::Shape: + result = (Object *) new Impl(m_props); + break; + case RayTargetType::Point: + result = (Object *) new Impl(m_props); + break; + case RayTargetType::None: + result = (Object *) new Impl(m_props); + break; + default: + Throw("Unsupported ray target type!"); + } + return { result }; + } + + MTS_DECLARE_CLASS() + +protected: + Properties m_props; + RayTargetType m_target_type; +}; + +template +class HemisphericalDistantSensorImpl final : public Sensor { +public: + MTS_IMPORT_BASE(Sensor, m_to_world, m_film) + MTS_IMPORT_TYPES(Scene, Shape) + + HemisphericalDistantSensorImpl(const Properties &props) : Base(props) { + // Check reconstruction filter radius + if (m_film->reconstruction_filter()->radius() > + 0.5f + math::RayEpsilon) { + Log(Warn, "This sensor is best used with a reconstruction filter " + "with a radius of 0.5 or lower (e.g. default box)"); + } + + // Set ray target if relevant + if constexpr (TargetType == RayTargetType::Point) { + m_target_point = props.point3f("target"); + } else if constexpr (TargetType == RayTargetType::Shape) { + auto obj = props.object("target"); + m_target_shape = dynamic_cast(obj.get()); + + if (!m_target_shape) + Throw( + "Invalid parameter target, must be a Point3f or a Shape."); + } else { + Log(Debug, "No target specified."); + } + } + + void set_scene(const Scene *scene) override { + m_bsphere = scene->bbox().bounding_sphere(); + m_bsphere.radius = + ek::max(math::RayEpsilon, + m_bsphere.radius * (1.f + math::RayEpsilon) ); + } + + std::pair + sample_ray(Float time, Float wavelength_sample, + const Point2f & film_sample, + const Point2f &aperture_sample, Mask active) const override { + MTS_MASK_ARGUMENT(active); + + Ray3f ray; + ray.time = time; + + // Sample spectrum + auto [wavelengths, wav_weight] = + sample_wavelength(wavelength_sample); + ray.wavelengths = wavelengths; + + // Sample ray origin + Spectrum ray_weight = 0.f; + + // Sample ray direction + ray.d = m_to_world.value().transform_affine( + warp::square_to_uniform_hemisphere(film_sample) + ); + + // Sample target point and position ray origin + if constexpr (TargetType == RayTargetType::Point) { + ray.o = m_target_point - 2.f * ray.d * m_bsphere.radius; + ray_weight = wav_weight; + } else if constexpr (TargetType == RayTargetType::Shape) { + // Use area-based sampling of shape + PositionSample3f ps = + m_target_shape->sample_position(time, aperture_sample, active); + ray.o = ps.p - 2.f * ray.d * m_bsphere.radius; + ray_weight = wav_weight / (ps.pdf * m_target_shape->surface_area()); + } else { // if constexpr (TargetType == RayTargetType::None) { + // Sample target uniformly on bounding sphere cross section + Point2f offset = + warp::square_to_uniform_disk_concentric(aperture_sample); + Vector3f perp_offset = + m_to_world.value().transform_affine(Vector3f(offset.x(), offset.y(), 0.f)); + ray.o = m_bsphere.center + perp_offset * m_bsphere.radius - ray.d * m_bsphere.radius; + ray_weight = wav_weight; + } + + return { ray, ray_weight & active }; + } + + std::pair sample_ray_differential( + Float time, Float wavelength_sample, const Point2f &film_sample, + const Point2f &aperture_sample, Mask active) const override { + MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); + + RayDifferential3f ray; + Spectrum ray_weight; + + std::tie(ray, ray_weight) = sample_ray( + time, wavelength_sample, film_sample, aperture_sample, active); + + // Since the film size is always 1x1, we don't have differentials + ray.has_differentials = false; + + return { ray, ray_weight & active }; + } + + // This sensor does not occupy any particular region of space, return an + // invalid bounding box + ScalarBoundingBox3f bbox() const override { return ScalarBoundingBox3f(); } + + std::string to_string() const override { + std::ostringstream oss; + oss << "HemisphericalDistantSensor[" << std::endl + << " to_world = " << m_to_world << "," << std::endl + << " film = " << m_film << "," << std::endl; + + if constexpr (TargetType == RayTargetType::Point) + oss << " target = " << m_target_point << std::endl; + else if constexpr (TargetType == RayTargetType::Shape) + oss << " target = " << m_target_shape << std::endl; + else // if constexpr (TargetType == RayTargetType::None) + oss << " target = none" << std::endl; + + oss << "]"; + + return oss.str(); + } + + MTS_DECLARE_CLASS() + +protected: + ScalarBoundingSphere3f m_bsphere; + ref m_target_shape; + Point3f m_target_point; +}; + +MTS_IMPLEMENT_CLASS_VARIANT(HemisphericalDistantSensor, Sensor) +MTS_EXPORT_PLUGIN(HemisphericalDistantSensor, "HemisphericalDistantSensor") + +NAMESPACE_BEGIN(detail) +template +constexpr const char *distant_sensor_class_name() { + if constexpr (TargetType == RayTargetType::Shape) { + return "HemisphericalDistantSensor_Shape"; + } else if constexpr (TargetType == RayTargetType::Point) { + return "HemisphericalDistantSensor_Point"; + } else if constexpr (TargetType == RayTargetType::None) { + return "HemisphericalDistantSensor_NoTarget"; + } +} +NAMESPACE_END(detail) + +template +Class *HemisphericalDistantSensorImpl::m_class = new Class( + detail::distant_sensor_class_name(), "Sensor", + ::mitsuba::detail::get_variant(), nullptr, nullptr); + +template +const Class *HemisphericalDistantSensorImpl::class_() const { + return m_class; +} + +NAMESPACE_END(mitsuba) From 7a1aaded4640d851e9895f95d2ebf065eb6b08de Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Thu, 17 Jun 2021 15:03:17 +0200 Subject: [PATCH 02/12] hdistant: Fixed typos --- src/sensors/hdistant.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index 22892e3261..0b4e29ab83 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -12,13 +12,13 @@ NAMESPACE_BEGIN(mitsuba) enum class RayTargetType { Shape, Point, None }; -// Forward declaration of specialized DistantSensor +// Forward declaration of specialized HemisphericalDistantSensor template class HemisphericalDistantSensorImpl; /**! -.. _sensor-distant: +.. _sensor-hdistant: Hemispherical distant radiancemeter sensor (:monosp:`hdistant`) --------------------------------------------------------------- From 86af7f0cd69046ef96a1a5f7cf7f8453e54a77a3 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Thu, 17 Jun 2021 16:27:00 +0200 Subject: [PATCH 03/12] hdistant: Fix string representation --- src/sensors/hdistant.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index 0b4e29ab83..c2d8b9fb5d 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -264,13 +264,13 @@ class HemisphericalDistantSensorImpl final : public Sensor { std::string to_string() const override { std::ostringstream oss; oss << "HemisphericalDistantSensor[" << std::endl - << " to_world = " << m_to_world << "," << std::endl - << " film = " << m_film << "," << std::endl; + << " to_world = " << string::indent(m_to_world) << "," << std::endl + << " film = " << string::indent(m_film) << "," << std::endl; if constexpr (TargetType == RayTargetType::Point) oss << " target = " << m_target_point << std::endl; else if constexpr (TargetType == RayTargetType::Shape) - oss << " target = " << m_target_shape << std::endl; + oss << " target = " << string::indent(m_target_shape) << std::endl; else // if constexpr (TargetType == RayTargetType::None) oss << " target = none" << std::endl; From 6bf813f06483bfd263677d70fd092aa7b510b2a6 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Thu, 17 Jun 2021 20:59:10 +0200 Subject: [PATCH 04/12] hdistant: Flip sampled ray directions --- src/sensors/hdistant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index c2d8b9fb5d..e2774ed3d5 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -213,7 +213,7 @@ class HemisphericalDistantSensorImpl final : public Sensor { Spectrum ray_weight = 0.f; // Sample ray direction - ray.d = m_to_world.value().transform_affine( + ray.d = -m_to_world.value().transform_affine( warp::square_to_uniform_hemisphere(film_sample) ); From 50359cd59b9c4829db1a40e78a2077ebc4928514 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Tue, 22 Jun 2021 13:04:55 +0200 Subject: [PATCH 05/12] hdistant: Add tests --- src/sensors/tests/test_hdistant.py | 330 +++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 src/sensors/tests/test_hdistant.py diff --git a/src/sensors/tests/test_hdistant.py b/src/sensors/tests/test_hdistant.py new file mode 100644 index 0000000000..b300923ea4 --- /dev/null +++ b/src/sensors/tests/test_hdistant.py @@ -0,0 +1,330 @@ +import enoki as ek +import numpy as np +import pytest + + +def sensor_dict(target=None, to_world=None): + result = {"type": "hdistant"} + + if to_world is not None: + result["to_world"] = to_world + + if target == "point": + result.update({"target": [0, 0, 0]}) + + elif target == "shape": + result.update({"target": {"type": "rectangle"}}) + + elif isinstance(target, dict): + result.update({"target": target}) + + return result + + +def make_sensor(d): + from mitsuba.core.xml import load_dict + + return load_dict(d).expand()[0] + + +def test_construct(variant_scalar_rgb): + from mitsuba.core import ScalarTransform4f + + # Construct without parameters + sensor = make_sensor({"type": "hdistant"}) + assert sensor is not None + assert not sensor.bbox().valid() # Degenerate bounding box + + # Construct with transform + sensor = make_sensor( + sensor_dict(to_world=ScalarTransform4f.look_at( + origin=[0, 0, 0], target=[0, 0, 1], up=[1, 0, 0]))) + + # Test different target values + # -- No target, + sensor = make_sensor(sensor_dict()) + assert sensor is not None + + # -- Point target + sensor = make_sensor(sensor_dict(target="point")) + assert sensor is not None + + # -- Shape target + sensor = make_sensor(sensor_dict(target="shape")) + assert sensor is not None + + # -- Random object target (we expect to raise) + with pytest.raises(RuntimeError): + make_sensor(sensor_dict(target={"type": "constant"})) + + +def test_sample_ray_direction(variant_scalar_rgb): + sensor = make_sensor(sensor_dict()) + + # Check that directions are appropriately set + for (sample1, sample2, expected) in [ + [[0.5, 0.5], [0.16, 0.44], [0, 0, -1]], + [[0.0, 0.0], [0.23, 0.40], [0.707107, 0.707107, 0]], + [[1.0, 0.0], [0.22, 0.81], [-0.707107, 0.707107, 0]], + [[0.0, 1.0], [0.99, 0.42], [0.707107, -0.707107, 0]], + [[1.0, 1.0], [0.52, 0.31], [-0.707107, -0.707107, 0]], + ]: + ray, _ = sensor.sample_ray(1.0, 1.0, sample1, sample2, True) + + # Check that ray direction is what is expected + assert ek.allclose(ray.d, expected, atol=1e-7) + + +@pytest.mark.parametrize( + "sensor_setup", + [ + "default", + "target_square", + "target_square_small", + "target_square_large", + "target_disk", + "target_point", + ], +) +@pytest.mark.parametrize("w_e", [[0, 0, -1], [0, 1, -1]]) +@pytest.mark.parametrize("w_o", [[0, 0, 1], [0, 1, 1]]) +def test_sample_target(variant_scalar_rgb, sensor_setup, w_e, w_o): + # This test checks if targeting works as intended by rendering a basic scene + from mitsuba.core import Bitmap, ScalarTransform4f, Struct + from mitsuba.core.xml import load_dict + + # Basic illumination and sensing parameters + l_e = 1.0 # Emitted radiance + w_e = list(w_e / np.linalg.norm(w_e)) # Emitter direction + w_o = list(w_o / np.linalg.norm(w_o)) # Sensor direction + cos_theta_e = abs(np.dot(w_e, [0, 0, 1])) + + # Reflecting surface specification + surface_scale = 1.0 + rho = 1.0 # Surface reflectance + + # Sensor definitions + sensors = { + "default": { # No target, origin projected to bounding sphere + "type": "hdistant", + "sampler": { + "type": "independent", + "sample_count": 100000, + }, + "film": { + "type": "hdrfilm", + "height": 4, + "width": 4, + "rfilter": {"type": "box"}, + }, + }, + "target_square": { # Targeting square, origin projected to bounding sphere + "type": "hdistant", + "target": { + "type": "rectangle", + "to_world": ScalarTransform4f.scale(surface_scale), + }, + "sampler": { + "type": "independent", + "sample_count": 1000, + }, + "film": { + "type": "hdrfilm", + "height": 16, + "width": 16, + "rfilter": {"type": "box"}, + }, + }, + "target_square_small": { # Targeting small square, origin projected to bounding sphere + "type": "hdistant", + "target": { + "type": "rectangle", + "to_world": ScalarTransform4f.scale(0.5 * surface_scale), + }, + "sampler": { + "type": "independent", + "sample_count": 1000, + }, + "film": { + "type": "hdrfilm", + "height": 16, + "width": 16, + "rfilter": {"type": "box"}, + }, + }, + "target_square_large": { # Targeting large square, origin projected to bounding sphere + "type": "hdistant", + "target": { + "type": "rectangle", + "to_world": ScalarTransform4f.scale(2.0 * surface_scale), + }, + "sampler": { + "type": "independent", + "sample_count": 1000000, + }, + "film": { + "type": "hdrfilm", + "height": 4, + "width": 4, + "rfilter": {"type": "box"}, + }, + }, + "target_point": { # Targeting point, origin projected to bounding sphere + "type": "hdistant", + "target": [0, 0, 0], + "sampler": { + "type": "independent", + "sample_count": 1000, + }, + "film": { + "type": "hdrfilm", + "height": 16, + "width": 16, + "rfilter": {"type": "box"}, + }, + }, + "target_disk": { # Targeting disk, origin projected to bounding sphere + "type": "hdistant", + "target": { + "type": "disk", + "to_world": ScalarTransform4f.scale(surface_scale), + }, + "sampler": { + "type": "independent", + "sample_count": 1000, + }, + "film": { + "type": "hdrfilm", + "height": 16, + "width": 16, + "rfilter": {"type": "box"}, + }, + }, + } + + # Scene setup + scene_dict = { + "type": "scene", + "shape": { + "type": "rectangle", + "to_world": ScalarTransform4f.scale(surface_scale), + "bsdf": { + "type": "diffuse", + "reflectance": rho, + }, + }, + "emitter": {"type": "directional", "direction": w_e, "irradiance": l_e}, + "integrator": {"type": "path"}, + } + + scene = load_dict({**scene_dict, "sensor": sensors[sensor_setup]}) + + # Run simulation + sensor = scene.sensors()[0] + scene.integrator().render(scene, sensor) + + # Check result + result = np.array( + sensor.film() + .bitmap() + .convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, False) + ).squeeze() + + surface_area = 4.0 * surface_scale ** 2 # Area of square surface + l_o = l_e * cos_theta_e * rho / np.pi # * cos_theta_o + expected = { # Special expected values for some cases + "default": l_o * 2.0 / ek.Pi, + "target_square_large": l_o * 0.25, + } + expected_value = expected.get(sensor_setup, l_o) + + rtol = { # Special tolerance values for some cases + "default": 1e-2, + "target_square_large": 1e-2, + } + rtol_value = rtol.get(sensor_setup, 5e-3) + + assert np.allclose(result, expected_value, rtol=rtol_value) + + +def test_checkerboard(variants_all_rgb): + """ + Very basic render test with checkerboard texture and square target. + """ + from mitsuba.core import ScalarTransform4f + from mitsuba.core.xml import load_dict + + l_o = 1.0 + rho0 = 0.5 + rho1 = 1.0 + + # Scene setup + scene_dict = { + "type": "scene", + "shape": { + "type": "rectangle", + "bsdf": { + "type": "diffuse", + "reflectance": { + "type": "checkerboard", + "color0": rho0, + "color1": rho1, + "to_uv": ScalarTransform4f.scale(2), + }, + }, + }, + "emitter": { + "type": "directional", + "direction": [0, 0, -1], + "irradiance": 1.0 + }, + "sensor0": { + "type": "hdistant", + "target": { + "type": "rectangle" + }, + "sampler": { + "type": "independent", + "sample_count": 50000, + }, + "film": { + "type": "hdrfilm", + "height": 4, + "width": 4, + "pixel_format": "luminance", + "component_format": "float32", + "rfilter": { + "type": "box" + }, + }, + }, + # "sensor1": { # In case one would like to check what the scene looks like + # "type": "perspective", + # "to_world": ScalarTransform4f.look_at(origin=[0, 0, 5], target=[0, 0, 0], up=[0, 1, 0]), + # "sampler": { + # "type": "independent", + # "sample_count": 10000, + # }, + # "film": { + # "type": "hdrfilm", + # "height": 256, + # "width": 256, + # "pixel_format": "luminance", + # "component_format": "float32", + # "rfilter": {"type": "box"}, + # }, + # }, + "integrator": { + "type": "path" + }, + } + + scene = load_dict(scene_dict) + + sensor = scene.sensors()[0] + scene.integrator().render(scene, sensor) + + data = np.array(sensor.film().bitmap()) + + expected = l_o * 0.5 * (rho0 + rho1) / ek.Pi + assert np.allclose(data, expected, atol=1e-3) From 1fe73388201d6465274bd02e6c8c1fd680528427 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Tue, 22 Jun 2021 16:50:53 +0200 Subject: [PATCH 06/12] hdistant: Update docs Update data submodule --- resources/data | 2 +- src/sensors/hdistant.cpp | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/resources/data b/resources/data index cba1bf767e..c7537606a7 160000 --- a/resources/data +++ b/resources/data @@ -1 +1 @@ -Subproject commit cba1bf767efb6df6d7fcfc9e19270884d0386dd4 +Subproject commit c7537606a7384806b3ae0380b2e956006c0f4361 diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index e2774ed3d5..61ac8b24a6 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -48,6 +48,17 @@ The ``to_world`` transform is best set using a hemisphere defined by the [0, 0, 1] direction, and the ``up`` film direction is set to [0, 1, 0]. +The following XML snippet creates a scene with a ``roughconductor`` +surface illuminated by three ``directional`` emitter, each emitting in +a single RGB channel. A ``hdistant`` plugin with default orientation is +defined. + +.. subfigstart:: +.. subfigure:: ../../resources/data/docs/images/sensor/sensor_hdistant_illumination_optimized.svg + :caption: Example scene illumination setup +.. subfigend:: + :label: fig-hdistant-illumination + .. code:: xml @@ -61,7 +72,7 @@ set to [0, 1, 0]. - + @@ -70,9 +81,15 @@ set to [0, 1, 0]. + + + + + + @@ -80,6 +97,24 @@ set to [0, 1, 0]. +The following figures show the recorded exitant radiance with the default film +orientation (left, ``up = [0,1,0]``) and with a rotated film (right, +``up = [1,1,0]``). Colored dots on the plots materialize emitter directions. +The orange arrow represents the ``up`` direction on the film. +Note that on the plots, the origin of pixel coordinates is taken at the bottom +left. + +.. subfigstart:: +.. subfigure:: ../../resources/data/docs/images/sensor/sensor_hdistant_film_default_optimized.svg + :caption: Default film orientation +.. subfigure:: ../../resources/data/docs/images/sensor/sensor_hdistant_film_rotated_optimized.svg + :caption: Rotated film +.. subfigure:: ../../resources/data/docs/images/sensor/sensor_hdistant_default.svg + :caption: Exitant radiance +.. subfigure:: ../../resources/data/docs/images/sensor/sensor_hdistant_rotated.svg + :caption: Exitant radiance +.. subfigend:: + :label: fig-hdistant-film By default, ray target points are sampled from the cross section of the scene's bounding sphere. The ``target`` parameter can be set to restrict ray target From 717997be64231f1aefb2d783f6e01563bdd400e0 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Tue, 22 Jun 2021 16:50:53 +0200 Subject: [PATCH 07/12] hdistant: Add ray differentials --- src/sensors/hdistant.cpp | 90 +++++++++++++++++++++++------- src/sensors/tests/test_hdistant.py | 85 ++++++++++++++++++---------- 2 files changed, 125 insertions(+), 50 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index 61ac8b24a6..76c73a41d5 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -160,8 +160,8 @@ class HemisphericalDistantSensor final : public Sensor { } // This must be implemented. However, it won't be used in practice: - // instead, HemisphericalDistantSensorImpl::bbox() is used when the plugin is - // instantiated. + // instead, HemisphericalDistantSensorImpl::bbox() is used when the plugin + // is instantiated. ScalarBoundingBox3f bbox() const override { return ScalarBoundingBox3f(); } template @@ -208,6 +208,10 @@ class HemisphericalDistantSensorImpl final : public Sensor { "with a radius of 0.5 or lower (e.g. default box)"); } + // Store film sample location spacing for performance + m_d.x() = 1.0f / m_film->size().x(); + m_d.y() = 1.0f / m_film->size().y(); + // Set ray target if relevant if constexpr (TargetType == RayTargetType::Point) { m_target_point = props.point3f("target"); @@ -230,10 +234,10 @@ class HemisphericalDistantSensorImpl final : public Sensor { m_bsphere.radius * (1.f + math::RayEpsilon) ); } - std::pair - sample_ray(Float time, Float wavelength_sample, - const Point2f & film_sample, - const Point2f &aperture_sample, Mask active) const override { + std::pair sample_ray(Float time, Float wavelength_sample, + const Point2f &film_sample, + const Point2f &aperture_sample, + Mask active) const override { MTS_MASK_ARGUMENT(active); Ray3f ray; @@ -249,32 +253,65 @@ class HemisphericalDistantSensorImpl final : public Sensor { // Sample ray direction ray.d = -m_to_world.value().transform_affine( - warp::square_to_uniform_hemisphere(film_sample) - ); + warp::square_to_uniform_hemisphere(film_sample)); // Sample target point and position ray origin if constexpr (TargetType == RayTargetType::Point) { - ray.o = m_target_point - 2.f * ray.d * m_bsphere.radius; + ray.o = m_target_point - 2.f * ray.d * m_bsphere.radius; ray_weight = wav_weight; } else if constexpr (TargetType == RayTargetType::Shape) { // Use area-based sampling of shape PositionSample3f ps = m_target_shape->sample_position(time, aperture_sample, active); - ray.o = ps.p - 2.f * ray.d * m_bsphere.radius; + ray.o = ps.p - 2.f * ray.d * m_bsphere.radius; ray_weight = wav_weight / (ps.pdf * m_target_shape->surface_area()); } else { // if constexpr (TargetType == RayTargetType::None) { // Sample target uniformly on bounding sphere cross section Point2f offset = warp::square_to_uniform_disk_concentric(aperture_sample); - Vector3f perp_offset = - m_to_world.value().transform_affine(Vector3f(offset.x(), offset.y(), 0.f)); - ray.o = m_bsphere.center + perp_offset * m_bsphere.radius - ray.d * m_bsphere.radius; + Vector3f perp_offset = m_to_world.value().transform_affine( + Vector3f(offset.x(), offset.y(), 0.f)); + ray.o = m_bsphere.center + perp_offset * m_bsphere.radius - + ray.d * m_bsphere.radius; ray_weight = wav_weight; } return { ray, ray_weight & active }; } + // Ray sampling with spectral sampling removed + std::pair + sample_ray_dir_origin(Float time, const Point2f &film_sample, + const Point2f &aperture_sample, Mask active) const { + MTS_MASK_ARGUMENT(active); + + // Sample ray direction + Vector3f direction = -m_to_world.value().transform_affine( + warp::square_to_uniform_hemisphere(film_sample)); + + // Sample target point and position ray origin + Point3f origin; + + if constexpr (TargetType == RayTargetType::Point) { + origin = m_target_point - 2.f * direction * m_bsphere.radius; + } else if constexpr (TargetType == RayTargetType::Shape) { + // Use area-based sampling of shape + PositionSample3f ps = + m_target_shape->sample_position(time, aperture_sample, active); + origin = ps.p - 2.f * direction * m_bsphere.radius; + } else { // if constexpr (TargetType == RayTargetType::None) { + // Sample target uniformly on bounding sphere cross section + Point2f offset = + warp::square_to_uniform_disk_concentric(aperture_sample); + Vector3f perp_offset = m_to_world.value().transform_affine( + Vector3f(offset.x(), offset.y(), 0.f)); + origin = m_bsphere.center + perp_offset * m_bsphere.radius - + direction * m_bsphere.radius; + } + + return { direction, origin }; + } + std::pair sample_ray_differential( Float time, Float wavelength_sample, const Point2f &film_sample, const Point2f &aperture_sample, Mask active) const override { @@ -286,8 +323,16 @@ class HemisphericalDistantSensorImpl final : public Sensor { std::tie(ray, ray_weight) = sample_ray( time, wavelength_sample, film_sample, aperture_sample, active); - // Since the film size is always 1x1, we don't have differentials - ray.has_differentials = false; + // Compute ray differentials + ray.has_differentials = true; + + Point2f film_sample_x{ film_sample.x() + m_d.x(), film_sample.y() }; + std::tie(ray.d_x, ray.o_x) = + sample_ray_dir_origin(time, film_sample_x, aperture_sample, active); + + Point2f film_sample_y{ film_sample.x(), film_sample.y() + m_d.y() }; + std::tie(ray.d_y, ray.o_y) = + sample_ray_dir_origin(time, film_sample_y, aperture_sample, active); return { ray, ray_weight & active }; } @@ -317,9 +362,14 @@ class HemisphericalDistantSensorImpl final : public Sensor { MTS_DECLARE_CLASS() protected: + // Scene bounding sphere ScalarBoundingSphere3f m_bsphere; + // Target shape if any ref m_target_shape; + // Target point if any Point3f m_target_point; + // Spacing between two pixels in film coordinates + ScalarPoint2f m_d; }; MTS_IMPLEMENT_CLASS_VARIANT(HemisphericalDistantSensor, Sensor) @@ -339,12 +389,14 @@ constexpr const char *distant_sensor_class_name() { NAMESPACE_END(detail) template -Class *HemisphericalDistantSensorImpl::m_class = new Class( - detail::distant_sensor_class_name(), "Sensor", - ::mitsuba::detail::get_variant(), nullptr, nullptr); +Class *HemisphericalDistantSensorImpl::m_class = + new Class(detail::distant_sensor_class_name(), "Sensor", + ::mitsuba::detail::get_variant(), nullptr, + nullptr); template -const Class *HemisphericalDistantSensorImpl::class_() const { +const Class * +HemisphericalDistantSensorImpl::class_() const { return m_class; } diff --git a/src/sensors/tests/test_hdistant.py b/src/sensors/tests/test_hdistant.py index b300923ea4..9392bbdc37 100644 --- a/src/sensors/tests/test_hdistant.py +++ b/src/sensors/tests/test_hdistant.py @@ -37,8 +37,12 @@ def test_construct(variant_scalar_rgb): # Construct with transform sensor = make_sensor( - sensor_dict(to_world=ScalarTransform4f.look_at( - origin=[0, 0, 0], target=[0, 0, 1], up=[1, 0, 0]))) + sensor_dict( + to_world=ScalarTransform4f.look_at( + origin=[0, 0, 0], target=[0, 0, 1], up=[1, 0, 0] + ) + ) + ) # Test different target values # -- No target, @@ -220,15 +224,7 @@ def test_sample_target(variant_scalar_rgb, sensor_setup, w_e, w_o): scene = load_dict({**scene_dict, "sensor": sensors[sensor_setup]}) # Run simulation - sensor = scene.sensors()[0] - scene.integrator().render(scene, sensor) - - # Check result - result = np.array( - sensor.film() - .bitmap() - .convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, False) - ).squeeze() + result = np.array(scene.render()).squeeze() surface_area = 4.0 * surface_scale ** 2 # Area of square surface l_o = l_e * cos_theta_e * rho / np.pi # * cos_theta_o @@ -244,7 +240,47 @@ def test_sample_target(variant_scalar_rgb, sensor_setup, w_e, w_o): } rtol_value = rtol.get(sensor_setup, 5e-3) - assert np.allclose(result, expected_value, rtol=rtol_value) + assert np.allclose(expected_value, result, rtol=rtol_value) + + +@pytest.mark.parametrize("target", ("point", "shape")) +def test_sample_ray_differential(variant_scalar_rgb, target): + from mitsuba.core.warp import uniform_hemisphere_to_square + + # We set odd and even values on purpose + n_x = 5 + n_y = 6 + + d = sensor_dict(target=target) + d.update( + { + "film": { + "type": "hdrfilm", + "width": n_x, + "height": n_y, + "rfilter": {"type": "box"}, + } + } + ) + sensor = make_sensor(d) + + sample1 = [0.5, 0.5] + sample2 = [0.5, 0.5] + ray, _ = sensor.sample_ray_differential(1.0, 1.0, sample1, sample2, True) + + # Sampled ray differential directions are expected to map to film + # coordinates shifted by one pixel + sample_dx = [0.5 + 1.0 / n_x, 0.5] + expected_ray_dx, _ = sensor.sample_ray(1.0, 1.0, sample_dx, sample2, True) + assert ek.allclose(sample_dx, uniform_hemisphere_to_square(-ray.d_x)) + assert ek.allclose(ray.d_x, expected_ray_dx.d) + assert ek.allclose(ray.o_x, expected_ray_dx.o) + + sample_dy = [0.5, 0.5 + 1.0 / n_y] + expected_ray_dy, _ = sensor.sample_ray(1.0, 1.0, sample_dy, sample2, True) + assert ek.allclose(sample_dy, uniform_hemisphere_to_square(-ray.d_y)) + assert ek.allclose(ray.d_y, expected_ray_dy.d) + assert ek.allclose(ray.o_y, expected_ray_dy.o) def test_checkerboard(variants_all_rgb): @@ -273,16 +309,10 @@ def test_checkerboard(variants_all_rgb): }, }, }, - "emitter": { - "type": "directional", - "direction": [0, 0, -1], - "irradiance": 1.0 - }, + "emitter": {"type": "directional", "direction": [0, 0, -1], "irradiance": 1.0}, "sensor0": { "type": "hdistant", - "target": { - "type": "rectangle" - }, + "target": {"type": "rectangle"}, "sampler": { "type": "independent", "sample_count": 50000, @@ -293,9 +323,7 @@ def test_checkerboard(variants_all_rgb): "width": 4, "pixel_format": "luminance", "component_format": "float32", - "rfilter": { - "type": "box" - }, + "rfilter": {"type": "box"}, }, }, # "sensor1": { # In case one would like to check what the scene looks like @@ -314,17 +342,12 @@ def test_checkerboard(variants_all_rgb): # "rfilter": {"type": "box"}, # }, # }, - "integrator": { - "type": "path" - }, + "integrator": {"type": "path"}, } scene = load_dict(scene_dict) - sensor = scene.sensors()[0] - scene.integrator().render(scene, sensor) - - data = np.array(sensor.film().bitmap()) + data = np.array(scene.render()) expected = l_o * 0.5 * (rho0 + rho1) / ek.Pi - assert np.allclose(data, expected, atol=1e-3) + assert np.allclose(expected, data, atol=1e-3) From 618109e6235f1ee514a44a4e9250ed2ce839abda Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Tue, 22 Jun 2021 16:50:53 +0200 Subject: [PATCH 08/12] hdistant: Minor test adjustments --- src/sensors/tests/test_hdistant.py | 32 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/sensors/tests/test_hdistant.py b/src/sensors/tests/test_hdistant.py index 9392bbdc37..ce18e3eccf 100644 --- a/src/sensors/tests/test_hdistant.py +++ b/src/sensors/tests/test_hdistant.py @@ -91,16 +91,14 @@ def test_sample_ray_direction(variant_scalar_rgb): ], ) @pytest.mark.parametrize("w_e", [[0, 0, -1], [0, 1, -1]]) -@pytest.mark.parametrize("w_o", [[0, 0, 1], [0, 1, 1]]) -def test_sample_target(variant_scalar_rgb, sensor_setup, w_e, w_o): - # This test checks if targeting works as intended by rendering a basic scene +def test_sample_target(variant_scalar_rgb, sensor_setup, w_e): + # Check if targeting works as intended by rendering a basic scene from mitsuba.core import Bitmap, ScalarTransform4f, Struct from mitsuba.core.xml import load_dict # Basic illumination and sensing parameters l_e = 1.0 # Emitted radiance w_e = list(w_e / np.linalg.norm(w_e)) # Emitter direction - w_o = list(w_o / np.linalg.norm(w_o)) # Sensor direction cos_theta_e = abs(np.dot(w_e, [0, 0, 1])) # Reflecting surface specification @@ -224,10 +222,14 @@ def test_sample_target(variant_scalar_rgb, sensor_setup, w_e, w_o): scene = load_dict({**scene_dict, "sensor": sensors[sensor_setup]}) # Run simulation - result = np.array(scene.render()).squeeze() + scene.render() + result = np.array( + scene.sensors()[0].film().bitmap().convert( + Bitmap.PixelFormat.RGB, Struct.Type.Float32, False + ) + ).squeeze() - surface_area = 4.0 * surface_scale ** 2 # Area of square surface - l_o = l_e * cos_theta_e * rho / np.pi # * cos_theta_o + l_o = l_e * cos_theta_e * rho / np.pi # Outgoing radiance expected = { # Special expected values for some cases "default": l_o * 2.0 / ek.Pi, "target_square_large": l_o * 0.25, @@ -287,10 +289,10 @@ def test_checkerboard(variants_all_rgb): """ Very basic render test with checkerboard texture and square target. """ - from mitsuba.core import ScalarTransform4f + from mitsuba.core import Bitmap, ScalarTransform4f, Struct from mitsuba.core.xml import load_dict - l_o = 1.0 + l_e = 1.0 # Emitted radiance rho0 = 0.5 rho1 = 1.0 @@ -346,8 +348,12 @@ def test_checkerboard(variants_all_rgb): } scene = load_dict(scene_dict) + scene.render() + result = np.array( + scene.sensors()[0].film().bitmap().convert( + Bitmap.PixelFormat.RGB, Struct.Type.Float32, False + ) + ).squeeze() - data = np.array(scene.render()) - - expected = l_o * 0.5 * (rho0 + rho1) / ek.Pi - assert np.allclose(expected, data, atol=1e-3) + expected = l_e * 0.5 * (rho0 + rho1) / ek.Pi + assert np.allclose(expected, result, atol=1e-3) From 00703f738330a8547c58242368c8294030677845 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Tue, 22 Jun 2021 16:50:53 +0200 Subject: [PATCH 09/12] hdistant: De-templatise --- src/sensors/hdistant.cpp | 139 +++++++---------------------- src/sensors/tests/test_hdistant.py | 2 +- 2 files changed, 32 insertions(+), 109 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index 76c73a41d5..1d609ea46c 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -138,69 +138,7 @@ class HemisphericalDistantSensor final : public Sensor { MTS_IMPORT_BASE(Sensor, m_to_world, m_film) MTS_IMPORT_TYPES(Scene, Shape) - HemisphericalDistantSensor(const Properties &props) : Base(props), m_props(props) { - - // Get target - if (props.has_property("target")) { - if (props.type("target") == Properties::Type::Array3f) { - props.point3f("target"); - m_target_type = RayTargetType::Point; - } else if (props.type("target") == Properties::Type::Object) { - // We assume it's a shape - m_target_type = RayTargetType::Shape; - } else { - Throw("Unsupported 'target' parameter type"); - } - } else { - m_target_type = RayTargetType::None; - } - - props.mark_queried("to_world"); - props.mark_queried("target"); - } - - // This must be implemented. However, it won't be used in practice: - // instead, HemisphericalDistantSensorImpl::bbox() is used when the plugin - // is instantiated. - ScalarBoundingBox3f bbox() const override { return ScalarBoundingBox3f(); } - - template - using Impl = HemisphericalDistantSensorImpl; - - // Recursively expand into an implementation specialized to the target - // specification. - std::vector> expand() const override { - ref result; - switch (m_target_type) { - case RayTargetType::Shape: - result = (Object *) new Impl(m_props); - break; - case RayTargetType::Point: - result = (Object *) new Impl(m_props); - break; - case RayTargetType::None: - result = (Object *) new Impl(m_props); - break; - default: - Throw("Unsupported ray target type!"); - } - return { result }; - } - - MTS_DECLARE_CLASS() - -protected: - Properties m_props; - RayTargetType m_target_type; -}; - -template -class HemisphericalDistantSensorImpl final : public Sensor { -public: - MTS_IMPORT_BASE(Sensor, m_to_world, m_film) - MTS_IMPORT_TYPES(Scene, Shape) - - HemisphericalDistantSensorImpl(const Properties &props) : Base(props) { + HemisphericalDistantSensor(const Properties &props) : Base(props) { // Check reconstruction filter radius if (m_film->reconstruction_filter()->radius() > 0.5f + math::RayEpsilon) { @@ -213,16 +151,25 @@ class HemisphericalDistantSensorImpl final : public Sensor { m_d.y() = 1.0f / m_film->size().y(); // Set ray target if relevant - if constexpr (TargetType == RayTargetType::Point) { - m_target_point = props.point3f("target"); - } else if constexpr (TargetType == RayTargetType::Shape) { - auto obj = props.object("target"); - m_target_shape = dynamic_cast(obj.get()); - - if (!m_target_shape) - Throw( - "Invalid parameter target, must be a Point3f or a Shape."); + // Get target + if (props.has_property("target")) { + if (props.type("target") == Properties::Type::Array3f) { + m_target_type = RayTargetType::Point; + m_target_point = props.point3f("target"); + } else if (props.type("target") == Properties::Type::Object) { + // We assume it's a shape + m_target_type = RayTargetType::Shape; + auto obj = props.object("target"); + m_target_shape = dynamic_cast(obj.get()); + + if (!m_target_shape) + Throw( + "Invalid parameter target, must be a Point3f or a Shape."); + } else { + Throw("Unsupported 'target' parameter type"); + } } else { + m_target_type = RayTargetType::None; Log(Debug, "No target specified."); } } @@ -256,16 +203,16 @@ class HemisphericalDistantSensorImpl final : public Sensor { warp::square_to_uniform_hemisphere(film_sample)); // Sample target point and position ray origin - if constexpr (TargetType == RayTargetType::Point) { + if (m_target_type == RayTargetType::Point) { ray.o = m_target_point - 2.f * ray.d * m_bsphere.radius; ray_weight = wav_weight; - } else if constexpr (TargetType == RayTargetType::Shape) { + } else if (m_target_type == RayTargetType::Shape) { // Use area-based sampling of shape PositionSample3f ps = m_target_shape->sample_position(time, aperture_sample, active); ray.o = ps.p - 2.f * ray.d * m_bsphere.radius; ray_weight = wav_weight / (ps.pdf * m_target_shape->surface_area()); - } else { // if constexpr (TargetType == RayTargetType::None) { + } else { // if (m_target_type == RayTargetType::None) { // Sample target uniformly on bounding sphere cross section Point2f offset = warp::square_to_uniform_disk_concentric(aperture_sample); @@ -292,14 +239,14 @@ class HemisphericalDistantSensorImpl final : public Sensor { // Sample target point and position ray origin Point3f origin; - if constexpr (TargetType == RayTargetType::Point) { + if (m_target_type == RayTargetType::Point) { origin = m_target_point - 2.f * direction * m_bsphere.radius; - } else if constexpr (TargetType == RayTargetType::Shape) { + } else if (m_target_type == RayTargetType::Shape) { // Use area-based sampling of shape PositionSample3f ps = m_target_shape->sample_position(time, aperture_sample, active); origin = ps.p - 2.f * direction * m_bsphere.radius; - } else { // if constexpr (TargetType == RayTargetType::None) { + } else { // if (m_target_type == RayTargetType::None) { // Sample target uniformly on bounding sphere cross section Point2f offset = warp::square_to_uniform_disk_concentric(aperture_sample); @@ -347,12 +294,12 @@ class HemisphericalDistantSensorImpl final : public Sensor { << " to_world = " << string::indent(m_to_world) << "," << std::endl << " film = " << string::indent(m_film) << "," << std::endl; - if constexpr (TargetType == RayTargetType::Point) + if (m_target_type == RayTargetType::Point) oss << " target = " << m_target_point << std::endl; - else if constexpr (TargetType == RayTargetType::Shape) + else if (m_target_type == RayTargetType::Shape) oss << " target = " << string::indent(m_target_shape) << std::endl; - else // if constexpr (TargetType == RayTargetType::None) - oss << " target = none" << std::endl; + else // if (m_target_type == RayTargetType::None) + oss << " target = None" << std::endl; oss << "]"; @@ -364,6 +311,8 @@ class HemisphericalDistantSensorImpl final : public Sensor { protected: // Scene bounding sphere ScalarBoundingSphere3f m_bsphere; + // Ray target type + RayTargetType m_target_type; // Target shape if any ref m_target_shape; // Target point if any @@ -374,30 +323,4 @@ class HemisphericalDistantSensorImpl final : public Sensor { MTS_IMPLEMENT_CLASS_VARIANT(HemisphericalDistantSensor, Sensor) MTS_EXPORT_PLUGIN(HemisphericalDistantSensor, "HemisphericalDistantSensor") - -NAMESPACE_BEGIN(detail) -template -constexpr const char *distant_sensor_class_name() { - if constexpr (TargetType == RayTargetType::Shape) { - return "HemisphericalDistantSensor_Shape"; - } else if constexpr (TargetType == RayTargetType::Point) { - return "HemisphericalDistantSensor_Point"; - } else if constexpr (TargetType == RayTargetType::None) { - return "HemisphericalDistantSensor_NoTarget"; - } -} -NAMESPACE_END(detail) - -template -Class *HemisphericalDistantSensorImpl::m_class = - new Class(detail::distant_sensor_class_name(), "Sensor", - ::mitsuba::detail::get_variant(), nullptr, - nullptr); - -template -const Class * -HemisphericalDistantSensorImpl::class_() const { - return m_class; -} - NAMESPACE_END(mitsuba) diff --git a/src/sensors/tests/test_hdistant.py b/src/sensors/tests/test_hdistant.py index ce18e3eccf..0d9bd82ed0 100644 --- a/src/sensors/tests/test_hdistant.py +++ b/src/sensors/tests/test_hdistant.py @@ -24,7 +24,7 @@ def sensor_dict(target=None, to_world=None): def make_sensor(d): from mitsuba.core.xml import load_dict - return load_dict(d).expand()[0] + return load_dict(d) def test_construct(variant_scalar_rgb): From 798ef44de93bbfc07ed80643970ca5d1a0ef8ec6 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Fri, 19 Nov 2021 18:53:08 +0100 Subject: [PATCH 10/12] hdistant: Explain in doc why only flat surfaces should be used as targets --- src/sensors/hdistant.cpp | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index 1d609ea46c..6956ad7c77 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -121,15 +121,34 @@ bounding sphere. The ``target`` parameter can be set to restrict ray target sampling to a specific subregion of the scene. The recorded radiance is averaged over the targeted geometry. -Ray origins are positioned outside of the scene's geometry. +Ray origins are positioned outside of the scene's geometry, such that it is +as if the sensor would be located at an infinite distance from the scene. + +By default, ray target points are sampled from the cross section of the scene's +bounding sphere. The ``target`` parameter should be set to restrict ray target +sampling to a specific subregion of the scene using a flat surface. The recorded +radiance is averaged over the targeted geometry. .. warning:: - If this sensor is used with a targeting strategy leading to rays not hitting - the scene's geometry (*e.g.* default targeting strategy), it will pick up - ambient emitter radiance samples (or zero values if no ambient emitter is - defined). Therefore, it is almost always preferrable to use a nondefault - targeting strategy. + * While setting ``target`` using any shape plugin is possible, only specific + configurations will produce meaningful results. This is due to ray sampling + method: when ``target`` is a shape, a point is sampled at its surface, + then shifted along the ``-direction`` vector by the diameter of the scene's + bounding sphere, effectively positioning the ray origin outside of the + geometry. The ray's weight is set to :math:`\frac{1}{A \, p}`, where + :math:`A` is the shape's surface area and :math:`p` is the shape's position + sampling PDF value. This weight definition is irrelevant when the sampled + origin may corresponds to multiple points on the shape, *i.e.* when the + sampled ray can intersect the target shape multiple times. From this + follows that only flat surfaces should be used to set the ``target`` + parameter. Typically, one will rather use a ``rectangle`` or ``disk`` + shape. + * If this sensor is used with a targeting strategy leading to rays not + hitting the scene's geometry (*e.g.* default targeting strategy), it will + pick up ambient emitter radiance samples (or zero values if no ambient + emitter is defined). Therefore, it is almost always preferrable to use a + nondefault targeting strategy. */ template From 09d111e18e3c3113407e80a6562c68aea2fa5cff Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Fri, 19 Nov 2021 19:42:11 +0100 Subject: [PATCH 11/12] hdistant: Update to new Properties interface --- src/sensors/hdistant.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index 6956ad7c77..cd407e8786 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -173,17 +173,17 @@ class HemisphericalDistantSensor final : public Sensor { // Get target if (props.has_property("target")) { if (props.type("target") == Properties::Type::Array3f) { - m_target_type = RayTargetType::Point; - m_target_point = props.point3f("target"); + m_target_type = RayTargetType::Point; + m_target_point = props.get("target"); } else if (props.type("target") == Properties::Type::Object) { // We assume it's a shape - m_target_type = RayTargetType::Shape; + m_target_type = RayTargetType::Shape; auto obj = props.object("target"); m_target_shape = dynamic_cast(obj.get()); if (!m_target_shape) - Throw( - "Invalid parameter target, must be a Point3f or a Shape."); + Throw("Invalid parameter target, must be a Point3f or a " + "Shape."); } else { Throw("Unsupported 'target' parameter type"); } From 788bccbf5659187be9de25b0e8dcb1f176019046 Mon Sep 17 00:00:00 2001 From: Vincent Leroy Date: Fri, 19 Nov 2021 19:43:05 +0100 Subject: [PATCH 12/12] hdistant: Optimize sample_ray_differential() --- src/sensors/hdistant.cpp | 77 ++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/src/sensors/hdistant.cpp b/src/sensors/hdistant.cpp index cd407e8786..f78ef4c62f 100644 --- a/src/sensors/hdistant.cpp +++ b/src/sensors/hdistant.cpp @@ -204,7 +204,7 @@ class HemisphericalDistantSensor final : public Sensor { const Point2f &film_sample, const Point2f &aperture_sample, Mask active) const override { - MTS_MASK_ARGUMENT(active); + MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); Ray3f ray; ray.time = time; @@ -245,61 +245,62 @@ class HemisphericalDistantSensor final : public Sensor { return { ray, ray_weight & active }; } - // Ray sampling with spectral sampling removed - std::pair - sample_ray_dir_origin(Float time, const Point2f &film_sample, - const Point2f &aperture_sample, Mask active) const { - MTS_MASK_ARGUMENT(active); + std::pair sample_ray_differential( + Float time, Float wavelength_sample, const Point2f &film_sample, + const Point2f &aperture_sample, Mask active) const override { + MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); + + RayDifferential3f ray; + ray.has_differentials = true; + ray.time = time; + + // Sample spectrum + auto [wavelengths, wav_weight] = + sample_wavelength(wavelength_sample); + ray.wavelengths = wavelengths; + + // Sample ray origin + Spectrum ray_weight = 0.f; // Sample ray direction - Vector3f direction = -m_to_world.value().transform_affine( + ray.d = -m_to_world.value().transform_affine( warp::square_to_uniform_hemisphere(film_sample)); + ray.d_x = -m_to_world.value().transform_affine( + warp::square_to_uniform_hemisphere( + Point2f{ film_sample.x() + m_d.x(), film_sample.y() })); + ray.d_y = -m_to_world.value().transform_affine( + warp::square_to_uniform_hemisphere( + Point2f{ film_sample.x(), film_sample.y() + m_d.y() })); // Sample target point and position ray origin - Point3f origin; - if (m_target_type == RayTargetType::Point) { - origin = m_target_point - 2.f * direction * m_bsphere.radius; + ray.o = m_target_point - 2.f * ray.d * m_bsphere.radius; + ray.o_x = m_target_point - 2.f * ray.d_x * m_bsphere.radius; + ray.o_y = m_target_point - 2.f * ray.d_y * m_bsphere.radius; + ray_weight = wav_weight; } else if (m_target_type == RayTargetType::Shape) { // Use area-based sampling of shape PositionSample3f ps = m_target_shape->sample_position(time, aperture_sample, active); - origin = ps.p - 2.f * direction * m_bsphere.radius; + ray.o = ps.p - 2.f * ray.d * m_bsphere.radius; + ray.o_x = ps.p - 2.f * ray.d_x * m_bsphere.radius; + ray.o_y = ps.p - 2.f * ray.d_y * m_bsphere.radius; + ray_weight = wav_weight / (ps.pdf * m_target_shape->surface_area()); } else { // if (m_target_type == RayTargetType::None) { // Sample target uniformly on bounding sphere cross section Point2f offset = warp::square_to_uniform_disk_concentric(aperture_sample); Vector3f perp_offset = m_to_world.value().transform_affine( Vector3f(offset.x(), offset.y(), 0.f)); - origin = m_bsphere.center + perp_offset * m_bsphere.radius - - direction * m_bsphere.radius; + ray.o = m_bsphere.center + perp_offset * m_bsphere.radius - + ray.d * m_bsphere.radius; + ray.o_x = m_bsphere.center + perp_offset * m_bsphere.radius - + ray.d_x * m_bsphere.radius; + ray.o_y = m_bsphere.center + perp_offset * m_bsphere.radius - + ray.d_y * m_bsphere.radius; + ray_weight = wav_weight; } - return { direction, origin }; - } - - std::pair sample_ray_differential( - Float time, Float wavelength_sample, const Point2f &film_sample, - const Point2f &aperture_sample, Mask active) const override { - MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); - - RayDifferential3f ray; - Spectrum ray_weight; - - std::tie(ray, ray_weight) = sample_ray( - time, wavelength_sample, film_sample, aperture_sample, active); - - // Compute ray differentials - ray.has_differentials = true; - - Point2f film_sample_x{ film_sample.x() + m_d.x(), film_sample.y() }; - std::tie(ray.d_x, ray.o_x) = - sample_ray_dir_origin(time, film_sample_x, aperture_sample, active); - - Point2f film_sample_y{ film_sample.x(), film_sample.y() + m_d.y() }; - std::tie(ray.d_y, ray.o_y) = - sample_ray_dir_origin(time, film_sample_y, aperture_sample, active); - return { ray, ray_weight & active }; }