diff --git a/src/sensors/irradiancemeter.cpp b/src/sensors/irradiancemeter.cpp index 398c845e8..20515fef2 100644 --- a/src/sensors/irradiancemeter.cpp +++ b/src/sensors/irradiancemeter.cpp @@ -4,6 +4,7 @@ #include #include #include +#include NAMESPACE_BEGIN(mitsuba) @@ -16,7 +17,10 @@ Irradiance meter (:monosp:`irradiancemeter`) .. pluginparameters:: - * - none + * - srf + - |spectrum| + - If set, sensor response function used to sample wavelengths from. This + parameter is ignored if used with nonspectral variants. This sensor plugin implements an irradiance meter, which measures the incident power per unit area over a shape which it is attached to. @@ -43,9 +47,18 @@ simply instantiate the desired sensor shape and specify an MTS_VARIANT class IrradianceMeter final : public Sensor { public: MTS_IMPORT_BASE(Sensor, m_film, m_world_transform, m_shape) - MTS_IMPORT_TYPES(Shape) - - IrradianceMeter(const Properties &props) : Base(props) { + MTS_IMPORT_TYPES(Shape, Texture) + + IrradianceMeter(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)"); + } + } + if (props.has_property("to_world")) Throw("Found a 'to_world' transformation -- this is not allowed. " "The irradiance meter inherits this transformation from its parent " @@ -75,7 +88,19 @@ MTS_VARIANT class IrradianceMeter final : public Sensor { Vector3f local = warp::square_to_cosine_hemisphere(sample3); // 3. Sample spectrum - auto [wavelengths, wav_weight] = sample_wavelength(wavelength_sample); + 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) + ); + } return std::make_pair( RayDifferential3f(ps.p, Frame3f(ps.n).to_world(local), time, wavelengths), @@ -109,6 +134,8 @@ MTS_VARIANT class IrradianceMeter final : public Sensor { } MTS_DECLARE_CLASS() +private: + ref m_srf; }; MTS_IMPLEMENT_CLASS_VARIANT(IrradianceMeter, Sensor) diff --git a/src/sensors/perspective.cpp b/src/sensors/perspective.cpp index 8f0ede4c3..a97037e37 100644 --- a/src/sensors/perspective.cpp +++ b/src/sensors/perspective.cpp @@ -2,6 +2,7 @@ #include #include #include +#include NAMESPACE_BEGIN(mitsuba) @@ -47,6 +48,10 @@ Perspective pinhole camera (:monosp:`perspective`) - |float| - Distance to the near/far clip planes. (Default: :monosp:`near_clip=1e-2` (i.e. :monosp:`0.01`) and :monosp:`far_clip=1e4` (i.e. :monosp:`10000`)) + * - srf + - |spectrum| + - If set, sensor response function used to sample wavelengths from. This parameter is ignored if + used with nonspectral variants. .. subfigstart:: .. subfigure:: ../../resources/data/docs/images/render/sensor_perspective.jpg @@ -56,7 +61,7 @@ Perspective pinhole camera (:monosp:`perspective`) .. subfigend:: :label: fig-perspective -This plugin implements a simple idealizied perspective camera model, which +This plugin implements a simple idealized perspective camera model, which has an infinitely small aperture. This creates an infinite depth of field, i.e. no optical blurring occurs. @@ -89,19 +94,27 @@ class PerspectiveCamera final : public ProjectiveCamera { MTS_IMPORT_BASE(ProjectiveCamera, m_world_transform, m_needs_sample_3, m_film, m_sampler, m_resolution, m_shutter_open, m_shutter_open_time, m_near_clip, m_far_clip) - MTS_IMPORT_TYPES() + MTS_IMPORT_TYPES(Texture) // ============================================================= //! @{ \name Constructors // ============================================================= - PerspectiveCamera(const Properties &props) : Base(props) { + PerspectiveCamera(const Properties &props) : Base(props), m_srf(nullptr) { ScalarVector2i size = m_film->size(); m_x_fov = parse_fov(props, size.x() / (float) size.y()); if (m_world_transform->has_scale()) Throw("Scale factors in the camera-to-world transformation are not allowed!"); + 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)"); + } + } + update_camera_transforms(); } @@ -143,7 +156,21 @@ class PerspectiveCamera final : public ProjectiveCamera { Mask active) const override { MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); - auto [wavelengths, wav_weight] = sample_wavelength(wavelength_sample); + // 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) + ); + } + Ray3f ray; ray.time = time; ray.wavelengths = wavelengths; @@ -171,8 +198,22 @@ class PerspectiveCamera final : public ProjectiveCamera { sample_ray_differential(Float time, Float wavelength_sample, const Point2f &position_sample, const Point2f & /*aperture_sample*/, Mask active) const override { MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active); - - auto [wavelengths, wav_weight] = sample_wavelength(wavelength_sample); + + // 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) + ); + } + RayDifferential3f ray; ray.time = time; ray.wavelengths = wavelengths; @@ -291,6 +332,7 @@ class PerspectiveCamera final : public ProjectiveCamera { << " shutter_open = " << m_shutter_open << "," << std::endl << " shutter_open_time = " << m_shutter_open_time << "," << std::endl << " world_transform = " << indent(m_world_transform) << std::endl + << " srf = " << indent(m_srf) << std::endl << "]"; return oss.str(); } @@ -303,6 +345,7 @@ class PerspectiveCamera final : public ProjectiveCamera { ScalarFloat m_normalization; ScalarFloat m_x_fov; ScalarVector3f m_dx, m_dy; + ref m_srf; }; MTS_IMPLEMENT_CLASS_VARIANT(PerspectiveCamera, ProjectiveCamera) diff --git a/src/sensors/radiancemeter.cpp b/src/sensors/radiancemeter.cpp index 9fab03aaa..db44499d0 100644 --- a/src/sensors/radiancemeter.cpp +++ b/src/sensors/radiancemeter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include NAMESPACE_BEGIN(mitsuba) @@ -27,6 +28,10 @@ Radiance meter (:monosp:`radiancemeter`) - |vector| - Alternative (and exclusive) to `to_world`. Direction in which the sensor is pointing in world coordinates. Must be used with `origin`. + * - srf + - |spectrum| + - If set, sensor response function used to sample wavelengths from. This + parameter is ignored if used with nonspectral variants. This sensor plugin implements a simple radiance meter, which measures the incident power per unit area per unit solid angle along a @@ -46,13 +51,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 +108,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 +144,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 +174,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 +183,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_irradiancemeter.py b/src/sensors/tests/test_irradiancemeter.py index 18178a30c..6824c2a01 100644 --- a/src/sensors/tests/test_irradiancemeter.py +++ b/src/sensors/tests/test_irradiancemeter.py @@ -5,7 +5,7 @@ import mitsuba -def sensor_shape_dict(radius, center): +def sensor_shape_dict(radius, center, srf=None): from mitsuba.core import ScalarTransform4f d = { @@ -23,6 +23,9 @@ def sensor_shape_dict(radius, center): } } + if srf is not None: + d["sensor"]["srf"] = srf + return d @@ -156,3 +159,48 @@ def test_incoming_flux_integrator(variant_scalar_rgb, radiance): image_np = np.array(img) ek.allclose(image_np, (radiance * ek.pi)) + + +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("srf", list(srf_dict.keys())) +def test_srf(variant_scalar_spectral, srf): + # Test the spectral response function specification feature + from mitsuba.core.xml import load_dict + from mitsuba.core import sample_shifted, ScalarVector3f + from mitsuba.render import SurfaceInteraction3f + + origin = [0, 0, 0] + direction = [0, 0, 1] + srf = srf_dict[srf] + + shape = load_dict(sensor_shape_dict(1, ScalarVector3f(0, 0, 0), srf=srf)) + sensor = shape.sensor() + srf = load_dict(srf) + time = 0.5 + wav_sample = 0.5 + pos_sample = [0.2, 0.6] + + ray, spec_weight = sensor.sample_ray_differential(time, wav_sample, pos_sample, 0) + + # Importance sample wavelength and weight + wav, wav_weight = srf.sample_spectrum( + SurfaceInteraction3f(), sample_shifted(wav_sample)) + wav_weight *= ek.pi + + assert ek.allclose(ray.wavelengths, wav) + assert ek.allclose(spec_weight, wav_weight) diff --git a/src/sensors/tests/test_perspective.py b/src/sensors/tests/test_perspective.py index 86eafafd2..26a71d662 100644 --- a/src/sensors/tests/test_perspective.py +++ b/src/sensors/tests/test_perspective.py @@ -1,9 +1,10 @@ -import mitsuba import pytest + import enoki as ek +import mitsuba -def create_camera(o, d, fov=34, fov_axis="x", s_open=1.5, s_close=5): +def create_camera(o, d, fov=34, fov_axis="x", s_open=1.5, s_close=5, srf={}): from mitsuba.core.xml import load_dict from mitsuba.core import ScalarTransform4f, ScalarVector3f t = [o[0] + d[0], o[1] + d[1], o[2] + d[2]] @@ -29,6 +30,9 @@ def create_camera(o, d, fov=34, fov_axis="x", s_open=1.5, s_close=5): } } + if srf: + camera_dict["srf"] = srf + return load_dict(camera_dict) @@ -85,7 +89,6 @@ def test02_sample_ray(variant_packet_spectral, origin, direction): assert ek.allclose(ray.d, direction, atol=1e-7) - @pytest.mark.parametrize("origin", origins) @pytest.mark.parametrize("direction", directions) def test03_sample_ray_differential(variant_packet_spectral, origin, direction): @@ -154,11 +157,53 @@ def check_fov(camera, sample): for fov_axis in ['y', 'smaller']: camera = create_camera(origin, direction, fov=fov, fov_axis=fov_axis) for sample in [[0.5, 0.0], [0.5, 1.0]]: - check_fov(camera, sample) + check_fov(camera, sample) # Check the 4 corners for the `diagonal` case camera = create_camera(origin, direction, fov=fov, fov_axis='diagonal') for sample in [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]]: - check_fov(camera, sample) + check_fov(camera, sample) + + +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": 1.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] + + camera = create_camera(origin, direction, srf=srf) + srf = load_dict(srf) + time = 0.5 + wav_sample = 0.5 + pos_sample = [0.2, 0.6] + + sample_func = camera.sample_ray_differential if ray_differential else camera.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) diff --git a/src/sensors/tests/test_radiancemeter.py b/src/sensors/tests/test_radiancemeter.py index 1ee23eba7..2422b12aa 100644 --- a/src/sensors/tests/test_radiancemeter.py +++ b/src/sensors/tests/test_radiancemeter.py @@ -4,7 +4,7 @@ import mitsuba -def make_sensor(origin=None, direction=None, to_world=None, pixels=1): +def make_sensor(origin=None, direction=None, to_world=None, pixels=1, srf=None): from mitsuba.core.xml import load_dict d = { @@ -23,6 +23,8 @@ def make_sensor(origin=None, direction=None, to_world=None, pixels=1): 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) @@ -151,3 +153,48 @@ def test_render(variant_scalar_rgb, radiance): 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) diff --git a/src/sensors/thinlens.cpp b/src/sensors/thinlens.cpp index 4465a39d9..dcee3c889 100644 --- a/src/sensors/thinlens.cpp +++ b/src/sensors/thinlens.cpp @@ -52,6 +52,10 @@ Perspective camera with a thin lens (:monosp:`thinlens`) - |float| - Distance to the near/far clip planes. (Default: :monosp:`near_clip=1e-2` (i.e. :monosp:`0.01`) and :monosp:`far_clip=1e4` (i.e. :monosp:`10000`)) + * - srf + - |spectrum| + - If set, sensor response function used to sample wavelengths from. This parameter is ignored if + used with nonspectral variants. .. subfigstart:: .. subfigure:: ../../resources/data/docs/images/render/sensor_thinlens_small_aperture.jpg diff --git a/src/spectra/blackbody.cpp b/src/spectra/blackbody.cpp index 9d3920c82..98dc79ba3 100644 --- a/src/spectra/blackbody.cpp +++ b/src/spectra/blackbody.cpp @@ -8,8 +8,8 @@ NAMESPACE_BEGIN(mitsuba) .. _spectrum-blackbody: -sRGB D65 spectrum (:monosp:`blackbody`) ---------------------------------------- +Blackbody spectrum (:monosp:`blackbody`) +---------------------------------------- This is a black body radiation spectrum for a specified temperature And therefore takes a single :monosp:`float`-valued parameter :paramtype:`temperature` (in Kelvins). diff --git a/src/spectra/tests/test_uniform.py b/src/spectra/tests/test_uniform.py new file mode 100644 index 000000000..e7bc602b6 --- /dev/null +++ b/src/spectra/tests/test_uniform.py @@ -0,0 +1,67 @@ +import pytest + +import enoki as ek +import mitsuba + + +def make_spectrum(value=None, lambda_min=None, lambda_max=None): + from mitsuba.core.xml import load_dict + + spectrum_dict = {"type": "uniform"} + + if value is not None: + spectrum_dict["value"] = value + if lambda_min is not None: + spectrum_dict["lambda_min"] = lambda_min + if lambda_max is not None: + spectrum_dict["lambda_max"] = lambda_max + return load_dict(spectrum_dict) + + +def test_construct(variant_scalar_spectral): + assert make_spectrum() is not None + assert make_spectrum(value=2.) is not None + assert make_spectrum(value=2., lambda_min=400., lambda_max=500.) is not None + + with pytest.raises(RuntimeError): + make_spectrum(lambda_min=500., lambda_max=400.) + + +def test_eval(variant_scalar_spectral): + from mitsuba.render import SurfaceInteraction3f + si = SurfaceInteraction3f() + + value = 2. + lambda_min = 400. + lambda_max = 500. + dlambda = lambda_max - lambda_min + + s = make_spectrum(value, lambda_min, lambda_max) + wavelengths = [390., 400., 450., 510.] + values = [0, value, value, 0] + + si.wavelengths = wavelengths + assert ek.allclose(s.eval(si), values) + assert ek.allclose( + s.pdf_spectrum(si), [1. / dlambda if value else 0. for value in values]) + + assert ek.allclose(s.eval_1(si), value) + + with pytest.raises(RuntimeError) as excinfo: + s.eval_3(si) + assert 'not implemented' in str(excinfo.value) + + +def test_sample_spectrum(variant_scalar_spectral): + from mitsuba.render import SurfaceInteraction3f + from mitsuba.core import MTS_WAVELENGTH_MIN, MTS_WAVELENGTH_MAX + dlambda = MTS_WAVELENGTH_MAX - MTS_WAVELENGTH_MIN + + value = 0.5 + s = make_spectrum(value) + + si = SurfaceInteraction3f() + assert ek.allclose(s.sample_spectrum(si, 0), [MTS_WAVELENGTH_MIN, value * dlambda]) + assert ek.allclose(s.sample_spectrum(si, .5), [ + MTS_WAVELENGTH_MIN + .5 * dlambda, value * dlambda]) + assert ek.allclose(s.sample_spectrum(si, 1), [MTS_WAVELENGTH_MAX, value * dlambda]) diff --git a/src/spectra/uniform.cpp b/src/spectra/uniform.cpp index 54414e4c3..36d9e6b54 100644 --- a/src/spectra/uniform.cpp +++ b/src/spectra/uniform.cpp @@ -1,6 +1,6 @@ -#include -#include #include +#include +#include NAMESPACE_BEGIN(mitsuba) @@ -11,8 +11,22 @@ NAMESPACE_BEGIN(mitsuba) Uniform spectrum (:monosp:`uniform`) ------------------------------------ -This spectrum returns a constant reflectance or emission value between 360 and 830nm. - +In its default form, this spectrum returns a constant reflectance or emission +value between 360 and 830nm. When using spectral variants, the covered spectral +interval can be specified tuned using its full XML specification; the plugin +will return 0 outside of the covered spectral range. + +.. pluginparameters:: + + * - value + - |float| + - Returned value + * - lambda_min + - |float| + - Lower bound of the covered spectral interval. Default: MTS_WAVELENGTH_MIN + * - lambda_max + - |float| + - Upper bound of the covered spectral interval. Default: MTS_WAVELENGTH_MAX */ template @@ -20,8 +34,21 @@ class UniformSpectrum final : public Texture { public: MTS_IMPORT_TYPES(Texture) - UniformSpectrum(const Properties &props) : Texture(props) { - m_value = props.float_("value"); + UniformSpectrum(const Properties &props) + : Texture(props), m_value(1.f), m_lambda_min(MTS_WAVELENGTH_MIN), + m_lambda_max(MTS_WAVELENGTH_MAX) { + if (props.has_property("value")) + m_value = props.float_("value"); + + if (props.has_property("lambda_min")) + m_lambda_min = max(props.float_("lambda_min"), MTS_WAVELENGTH_MIN); + + if (props.has_property("lambda_max")) + m_lambda_max = min(props.float_("lambda_max"), MTS_WAVELENGTH_MAX); + + if (!(m_lambda_min < m_lambda_max)) + Throw( + "UniformSpectrum: 'lambda_min' must be less than 'lambda_max'"); } UnpolarizedSpectrum eval(const SurfaceInteraction3f &si, @@ -29,17 +56,18 @@ class UniformSpectrum final : public Texture { MTS_MASKED_FUNCTION(ProfilerPhase::TextureEvaluate, active); if constexpr (is_spectral_v) { - auto active_w = (si.wavelengths >= MTS_WAVELENGTH_MIN) && - (si.wavelengths <= MTS_WAVELENGTH_MAX); + auto active_w = (si.wavelengths >= m_lambda_min) && + (si.wavelengths <= m_lambda_max); return select(active_w, UnpolarizedSpectrum(m_value), - UnpolarizedSpectrum(0.f)); + UnpolarizedSpectrum(0.f)); } else { return m_value; } } - Float eval_1(const SurfaceInteraction3f & /* it */, Mask active) const override { + Float eval_1(const SurfaceInteraction3f & /* it */, + Mask active) const override { MTS_MASKED_FUNCTION(ProfilerPhase::TextureEvaluate, active); return m_value; } @@ -48,13 +76,14 @@ class UniformSpectrum final : public Texture { MTS_MASKED_FUNCTION(ProfilerPhase::TextureEvaluate, active); if constexpr (is_spectral_v) { - auto active_w = (si.wavelengths >= MTS_WAVELENGTH_MIN) && - (si.wavelengths <= MTS_WAVELENGTH_MAX); + auto active_w = (si.wavelengths >= m_lambda_min) && + (si.wavelengths <= m_lambda_max); return select(active_w, - Wavelength(1.f / (MTS_WAVELENGTH_MAX - MTS_WAVELENGTH_MIN)), Wavelength(0.f)); + Wavelength(1.f / (m_lambda_max - m_lambda_min)), + Wavelength(0.f)); } else { - NotImplementedError("pdf"); + NotImplementedError("pdf_spectrum"); } } @@ -64,11 +93,11 @@ class UniformSpectrum final : public Texture { MTS_MASKED_FUNCTION(ProfilerPhase::TextureSample, active); if constexpr (is_spectral_v) { - return { MTS_WAVELENGTH_MIN + (MTS_WAVELENGTH_MAX - MTS_WAVELENGTH_MIN) * sample, - m_value * (MTS_WAVELENGTH_MAX - MTS_WAVELENGTH_MIN) }; + return { m_lambda_min + (m_lambda_max - m_lambda_min) * sample, + m_value * (m_lambda_max - m_lambda_min) }; } else { ENOKI_MARK_USED(sample); - NotImplementedError("sample"); + NotImplementedError("sample_spectrum"); } } @@ -76,15 +105,25 @@ class UniformSpectrum final : public Texture { void traverse(TraversalCallback *callback) override { callback->put_parameter("value", m_value); + callback->put_parameter("lambda_min", m_lambda_min); + callback->put_parameter("lambda_max", m_lambda_max); } std::string to_string() const override { - return tfm::format("UniformSpectrum[value=%f]", m_value); + std::ostringstream oss; + oss << "UniformSpectrum[" << std::endl + << " value = " << m_value << std::endl + << " lambda_min = " << m_lambda_min << std::endl + << " lambda_max = " << m_lambda_max << std::endl + << "]"; + return oss.str(); } MTS_DECLARE_CLASS() private: Float m_value; + ScalarFloat m_lambda_min; + ScalarFloat m_lambda_max; }; MTS_IMPLEMENT_CLASS_VARIANT(UniformSpectrum, Texture)