diff --git a/src/sensors/radiancemeter.cpp b/src/sensors/radiancemeter.cpp index 9fab03aaa..7f017ffeb 100644 --- a/src/sensors/radiancemeter.cpp +++ b/src/sensors/radiancemeter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include NAMESPACE_BEGIN(mitsuba) @@ -46,13 +47,23 @@ priority. */ -MTS_VARIANT class RadianceMeter final : public Sensor { +template +class RadianceMeter final : public Sensor { public: MTS_IMPORT_BASE(Sensor, m_film, m_world_transform, m_needs_sample_2, m_needs_sample_3) - MTS_IMPORT_TYPES() + MTS_IMPORT_TYPES(Texture) + + RadianceMeter(const Properties &props) : Base(props), m_srf(nullptr) { + if (props.has_property("srf")) { + if constexpr(is_spectral_v) { + m_srf = props.texture("srf"); + } else { + Log(Warn, "Ignoring spectral response function " + "(not supported for non-spectral variants)"); + } + } - RadianceMeter(const Properties &props) : Base(props) { if (props.has_property("to_world")) { // if direction and origin are present but overridden by // to_world, they must still be marked as queried @@ -93,22 +104,34 @@ MTS_VARIANT class RadianceMeter final : public Sensor { const Point2f & /*aperture_sample*/, Mask active) const override { MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); + + // 1. Sample spectrum + Wavelength wavelengths; + Spectrum wav_weight; + + if (m_srf == nullptr) { + std::tie(wavelengths, wav_weight) = + sample_wavelength(wavelength_sample); + } else { + std::tie(wavelengths, wav_weight) = + m_srf->sample_spectrum( + zero(), + math::sample_shifted(wavelength_sample) + ); + } + + // 2. Set ray origin and direction Ray3f ray; ray.time = time; - - // 1. Sample spectrum - auto [wavelengths, wav_weight] = - sample_wavelength(wavelength_sample); ray.wavelengths = wavelengths; - // 2. Set ray origin and direction auto trafo = m_world_transform->eval(time, active); ray.o = trafo.transform_affine(Point3f{ 0.f, 0.f, 0.f }); ray.d = trafo.transform_affine(Vector3f{ 0.f, 0.f, 1.f }); ray.update(); - return std::make_pair(ray, wav_weight); + return { ray, wav_weight }; } std::pair @@ -117,15 +140,26 @@ MTS_VARIANT class RadianceMeter final : public Sensor { const Point2f & /*aperture_sample*/, Mask active) const override { MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); + // 1. Sample spectrum + Wavelength wavelengths; + Spectrum wav_weight; + + if (m_srf == nullptr) { + std::tie(wavelengths, wav_weight) = + sample_wavelength(wavelength_sample); + } else { + std::tie(wavelengths, wav_weight) = + m_srf->sample_spectrum( + zero(), + math::sample_shifted(wavelength_sample) + ); + } + + // 2. Set ray origin and direction RayDifferential3f ray; ray.time = time; - - // 1. Sample spectrum - auto [wavelengths, wav_weight] = - sample_wavelength(wavelength_sample); ray.wavelengths = wavelengths; - // 2. Set ray origin and direction auto trafo = m_world_transform->eval(time, active); ray.o = trafo.transform_affine(Point3f{ 0.f, 0.f, 0.f }); ray.d = trafo.transform_affine(Vector3f{ 0.f, 0.f, 1.f }); @@ -136,7 +170,7 @@ MTS_VARIANT class RadianceMeter final : public Sensor { ray.update(); - return std::make_pair(ray, wav_weight); + return { ray, wav_weight }; } ScalarBoundingBox3f bbox() const override { @@ -145,15 +179,20 @@ MTS_VARIANT class RadianceMeter final : public Sensor { } std::string to_string() const override { + using string::indent; + std::ostringstream oss; oss << "RadianceMeter[" << std::endl << " world_transform = " << m_world_transform << "," << std::endl << " film = " << m_film << "," << std::endl + << " srf = " << indent(m_srf) << std::endl << "]"; return oss.str(); } MTS_DECLARE_CLASS() +private: + ref m_srf; }; MTS_IMPLEMENT_CLASS_VARIANT(RadianceMeter, Sensor) diff --git a/src/sensors/tests/test_radiancemeter.py b/src/sensors/tests/test_radiancemeter.py index c1a30de08..e24834966 100644 --- a/src/sensors/tests/test_radiancemeter.py +++ b/src/sensors/tests/test_radiancemeter.py @@ -3,51 +3,41 @@ import pytest -def xml_sensor(params="", pixels="1"): - xml = f""" - - - - - - - {params} - - """ - return xml - - -def xml_lookat(origin, target, up="0,0,1"): - xml = f""" - - - - """ - return xml - - -def xml_origin(value): - return f"""""" - - -def xml_direction(value): - return f"""""" - - -def example_sensor(params="", pixels="1"): - from mitsuba.core.xml import load_string - xml = xml_sensor(params, pixels) - return load_string(xml) +def make_sensor(origin=None, direction=None, to_world=None, srf=None, pixels=1): + from mitsuba.core.xml import load_dict + + d = { + "type": "radiancemeter", + "film": { + "type": "hdrfilm", + "width": pixels, + "height": pixels, + "rfilter": {"type": "box"} + } + } + + if origin is not None: + d["origin"] = origin + if direction is not None: + d["direction"] = direction + if to_world is not None: + d["to_world"] = to_world + if srf is not None: + d["srf"] = srf + + return load_dict(d) def test_construct(variant_scalar_rgb): - from mitsuba.core.xml import load_string + from mitsuba.core.xml import load_dict + from mitsuba.core import ScalarTransform4f # Test construct from to_world - only_to_world = xml_sensor( - params=xml_lookat(origin="0,0,0", target="0,1,0") - ) - sensor = load_string(only_to_world) + sensor = make_sensor(to_world=ScalarTransform4f.look_at( + origin=[0, 0, 0], + target=[0, 1, 0], + up=[0, 0, 1] + )) assert not sensor.bbox().valid() # Degenerate bounding box assert ek.allclose( sensor.world_transform().eval(0.).matrix, @@ -58,10 +48,7 @@ def test_construct(variant_scalar_rgb): ) # Test construct from origin and direction - origin_direction = xml_sensor( - params=xml_origin("0,0,0") + xml_direction("0,1,0") - ) - sensor = load_string(origin_direction) + sensor = make_sensor(origin=[0, 0, 0], direction=[0, 1, 0]) assert not sensor.bbox().valid() # Degenerate bounding box assert ek.allclose( sensor.world_transform().eval(0.).matrix, @@ -72,11 +59,15 @@ def test_construct(variant_scalar_rgb): ) # Test to_world overriding direction + origin - to_world_origin_direction = xml_sensor( - params=xml_lookat(origin="0,0,0", target="0,1,0") + - xml_origin("1,0,0") + xml_direction("4,1,0") + sensor = make_sensor( + to_world=ScalarTransform4f.look_at( + origin=[0, 0, 0], + target=[0, 1, 0], + up=[0, 0, 1] + ), + origin=[1, 0, 0], + direction=[4, 1, 0] ) - sensor = load_string(to_world_origin_direction) assert not sensor.bbox().valid() # Degenerate bounding box assert ek.allclose( sensor.world_transform().eval(0.).matrix, @@ -87,20 +78,15 @@ def test_construct(variant_scalar_rgb): ) # Test raise on missing direction or origin - only_direction = xml_sensor(params=xml_direction("0,1,0")) with pytest.raises(RuntimeError): - sensor = load_string(only_direction) + sensor = make_sensor(direction=[0, 1, 0]) - only_origin = xml_sensor(params=xml_origin("0,1,0")) with pytest.raises(RuntimeError): - sensor = load_string(only_origin) + sensor = make_sensor(origin=[0, 1, 0]) # Test raise on wrong film size with pytest.raises(RuntimeError): - sensor = example_sensor( - params=xml_lookat(origin="0,0,-2", target="0,0,0"), - pixels=2 - ) + sensor = make_sensor(pixels=2) @pytest.mark.parametrize("direction", [[0.0, 0.0, 1.0], [-1.0, -1.0, 0.0], [2.0, 0.0, 0.0]]) @@ -108,11 +94,8 @@ def test_construct(variant_scalar_rgb): def test_sample_ray(variant_scalar_rgb, direction, origin): sample1 = [0.32, 0.87] sample2 = [0.16, 0.44] - direction_str = ",".join([str(x) for x in direction]) - origin_str = ",".join([str(x) for x in origin]) - sensor = example_sensor( - params=xml_direction(direction_str) + xml_origin(origin_str) - ) + + sensor = make_sensor(direction=direction, origin=origin) # Test regular ray sampling ray = sensor.sample_ray(1., 1., sample1, sample2, True) @@ -129,37 +112,88 @@ def test_sample_ray(variant_scalar_rgb, direction, origin): @pytest.mark.parametrize("radiance", [10**x for x in range(-3, 4)]) def test_render(variant_scalar_rgb, radiance): # Test render results with a simple scene - from mitsuba.core.xml import load_string + from mitsuba.core.xml import load_dict import numpy as np - - scene_xml = """ - - - - - - - - - - - - - - - - - - - - - - - - """ - - scene = load_string(scene_xml, spp=1, radiance=radiance) + + spp = 1 + + scene_dict = { + "type": "scene", + "integrator": { + "type": "path" + }, + "sensor": { + "type": "radiancemeter", + "film": { + "type": "hdrfilm", + "width": 1, + "height": 1, + "pixel_format": "rgb", + "rfilter": { + "type": "box" + } + }, + "sampler": { + "type": "independent", + "sample_count": spp + } + }, + "emitter": { + "type": "constant", + "radiance": { + "type": "uniform", + "value": radiance + } + } + } + + scene = load_dict(scene_dict) sensor = scene.sensors()[0] scene.integrator().render(scene, sensor) img = sensor.film().bitmap() assert np.allclose(np.array(img), radiance) + + +srf_dict = { + # Uniform SRF covering full spectral range + "uniform_full": { + "type": "uniform", + "value": 1.0 + }, + # Uniform SRF covering full the [400, 700] nm spectral range + "uniform_restricted": { + "type": "uniform", + "value": 2.0, + "lambda_min": 400.0, + "lambda_max": 700.0, + } +} + + +@pytest.mark.parametrize("ray_differential", [False, True]) +@pytest.mark.parametrize("srf", list(srf_dict.keys())) +def test_srf(variant_scalar_spectral, ray_differential, srf): + # Test the spectral response function specification feature + from mitsuba.core.xml import load_dict + from mitsuba.core import sample_shifted + from mitsuba.render import SurfaceInteraction3f + + origin = [0, 0, 0] + direction = [0, 0, 1] + srf = srf_dict[srf] + + sensor = make_sensor(origin=origin, direction=direction, srf=srf) + srf = load_dict(srf) + time = 0.5 + wav_sample = 0.5 + pos_sample = [0.2, 0.6] + + sample_func = sensor.sample_ray_differential if ray_differential else sensor.sample_ray + ray, spec_weight = sample_func(time, wav_sample, pos_sample, 0) + + # Importance sample wavelength and weight + wav, wav_weight = srf.sample_spectrum( + SurfaceInteraction3f(), sample_shifted(wav_sample)) + + assert ek.allclose(ray.wavelengths, wav) + assert ek.allclose(spec_weight, wav_weight)