Skip to content

Commit b19985b

Browse files
schunkesSpeierers
authored andcommitted
Added the irradiancemeter plugin and adapted shapes to support attaching sensors
1 parent a168f29 commit b19985b

File tree

11 files changed

+359
-11
lines changed

11 files changed

+359
-11
lines changed

src/librender/shape.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ NAMESPACE_BEGIN(mitsuba)
3333
MTS_VARIANT Shape<Float, Spectrum>::Shape(const Properties &props) : m_id(props.id()) {
3434
for (auto &kv : props.objects()) {
3535
Emitter *emitter = dynamic_cast<Emitter *>(kv.second.get());
36+
Sensor *sensor = dynamic_cast<Sensor *>(kv.second.get());
3637
BSDF *bsdf = dynamic_cast<BSDF *>(kv.second.get());
3738
Medium *medium = dynamic_cast<Medium *>(kv.second.get());
3839

@@ -54,6 +55,10 @@ MTS_VARIANT Shape<Float, Spectrum>::Shape(const Properties &props) : m_id(props.
5455
Throw("Only a single exterior medium can be specified per shape.");
5556
m_exterior_medium = medium;
5657
}
58+
} else if (sensor) {
59+
if (m_sensor)
60+
Throw("Only a single Sensor child object can be specified per shape.");
61+
m_sensor = sensor;
5762
} else {
5863
Throw("Tried to add an unsupported object of type \"%s\"", kv.second);
5964
}

src/sensors/CMakeLists.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
set(MTS_PLUGIN_PREFIX "sensors")
22

3-
add_plugin(perspective perspective.cpp)
4-
add_plugin(radiancemeter radiancemeter.cpp)
5-
add_plugin(thinlens thinlens.cpp)
3+
add_plugin(perspective perspective.cpp)
4+
add_plugin(radiancemeter radiancemeter.cpp)
5+
add_plugin(thinlens thinlens.cpp)
6+
add_plugin(irradiancemeter irradiancemeter.cpp)
67

78
# Register the test directory
89
add_tests(${CMAKE_CURRENT_SOURCE_DIR}/tests)

src/sensors/irradiancemeter.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#include <mitsuba/core/fwd.h>
2+
#include <mitsuba/core/properties.h>
3+
#include <mitsuba/core/transform.h>
4+
#include <mitsuba/core/warp.h>
5+
#include <mitsuba/render/fwd.h>
6+
#include <mitsuba/render/sensor.h>
7+
8+
NAMESPACE_BEGIN(mitsuba)
9+
10+
/**!
11+
12+
.. _sensor-irradiancemeter:
13+
14+
Irradiance meter (:monosp:`irradiancemeter`)
15+
--------------------------------------------
16+
17+
.. pluginparameters::
18+
19+
* - none
20+
21+
This sensor plugin implements an irradiance meter, which measures
22+
the incident power per unit area over a shape which it is attached to.
23+
This sensor is used with films of 1 by 1 pixels.
24+
25+
If the irradiance meter is attached to a mesh-type shape, it will measure the
26+
irradiance over all triangles in the mesh.
27+
28+
This sensor is not instantiated on its own but must be defined as a child
29+
object to a shape in a scene. To create an irradiance meter,
30+
simply instantiate the desired sensor shape and specify an
31+
:monosp:`irradiancemeter` instance as its child:
32+
33+
.. code-block:: xml
34+
:name: sphere-meter
35+
36+
<shape type="sphere">
37+
<sensor type="irradiancemeter">
38+
<!-- film -->
39+
</sensor>
40+
</shape>
41+
*/
42+
43+
MTS_VARIANT class IrradianceMeter final : public Sensor<Float, Spectrum> {
44+
public:
45+
MTS_IMPORT_BASE(Sensor, m_film, m_world_transform, m_shape)
46+
MTS_IMPORT_TYPES(Shape)
47+
48+
IrradianceMeter(const Properties &props) : Base(props) {
49+
if (props.has_property("to_world"))
50+
Throw("Found a 'to_world' transformation -- this is not allowed. "
51+
"The irradiance meter inherits this transformation from its parent "
52+
"shape.");
53+
54+
if (m_film->size() != ScalarPoint2i(1, 1))
55+
Throw("This sensor only supports films of size 1x1 Pixels!");
56+
57+
if (m_film->reconstruction_filter()->radius() >
58+
0.5f + math::RayEpsilon<Float>)
59+
Log(Warn, "This sensor should only be used with a reconstruction filter"
60+
"of radius 0.5 or lower(e.g. default box)");
61+
}
62+
63+
void set_shape(Shape *shape) override {
64+
if (m_shape)
65+
Throw("An irradiance meter can be only be attached to a single shape.");
66+
67+
Base::set_shape(shape);
68+
}
69+
70+
std::pair<RayDifferential3f, Spectrum>
71+
sample_ray_differential(Float time, Float wavelength_sample,
72+
const Point2f & sample2,
73+
const Point2f & sample3,
74+
Mask active) const override {
75+
76+
MTS_MASKED_FUNCTION(ProfilerPhase::EndpointSampleRay, active);
77+
78+
// 1. Sample spatial component
79+
PositionSample3f ps = m_shape->sample_position(time, sample2, active);
80+
81+
// 2. Sample directional component
82+
Vector3f local = warp::square_to_cosine_hemisphere(sample3);
83+
84+
// 3. Sample spectrum
85+
auto [wavelengths, wav_weight] = sample_wavelength<Float, Spectrum>(wavelength_sample);
86+
87+
return std::make_pair(
88+
RayDifferential3f(ps.p, Frame3f(ps.n).to_world(local), time, wavelengths),
89+
unpolarized<Spectrum>(wav_weight) * math::Pi<ScalarFloat>
90+
);
91+
}
92+
93+
std::pair<DirectionSample3f, Spectrum>
94+
sample_direction(const Interaction3f &it, const Point2f &sample, Mask active) const override {
95+
return std::make_pair(m_shape->sample_direction(it, sample, active), math::Pi<ScalarFloat>);
96+
}
97+
98+
Float pdf_direction(const Interaction3f &it, const DirectionSample3f &ds,
99+
Mask active) const override {
100+
return m_shape->pdf_direction(it, ds, active);
101+
}
102+
103+
Spectrum eval(const SurfaceInteraction3f &/*si*/, Mask /*active*/) const override {
104+
return math::Pi<ScalarFloat> / m_shape->surface_area();
105+
}
106+
107+
ScalarBoundingBox3f bbox() const override { return m_shape->bbox(); }
108+
109+
std::string to_string() const override {
110+
std::ostringstream oss;
111+
oss << "IrradianceMeter[" << std::endl
112+
<< " shape = " << m_shape << "," << std::endl
113+
<< " film = " << m_film << "," << std::endl
114+
<< "]";
115+
return oss.str();
116+
}
117+
118+
MTS_DECLARE_CLASS()
119+
};
120+
121+
MTS_IMPLEMENT_CLASS_VARIANT(IrradianceMeter, Sensor)
122+
MTS_EXPORT_PLUGIN(IrradianceMeter, "IrradianceMeter");
123+
NAMESPACE_END(mitsuba)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import numpy as np
2+
import pytest
3+
4+
import enoki as ek
5+
import mitsuba
6+
7+
8+
def example_shape(radius, center):
9+
from mitsuba.core.xml import load_string
10+
11+
xml = f"""
12+
<shape version='0.1.0' type="sphere">
13+
<float name="radius" value="{radius}"/>
14+
<transform name="to_world">
15+
<translate x="{center.x}" y="{center.y}" z="{center.z}"/>
16+
</transform>
17+
<sensor type="irradiancemeter">
18+
<film type="hdrfilm">
19+
<integer name="width" value="1"/>
20+
<integer name="height" value="1"/>
21+
</film>
22+
</sensor>
23+
</shape>
24+
"""
25+
return load_string(xml)
26+
27+
def test_construct(variant_scalar_rgb):
28+
"""
29+
We construct an irradiance meter attached to a sphere and assert that the
30+
following parameters get set correctly:
31+
- associated shape
32+
- film
33+
"""
34+
from mitsuba.core import Vector3f
35+
center_v = Vector3f(0.0)
36+
radius = 1.0
37+
sphere = example_shape(radius, center_v)
38+
sensor = sphere.sensor()
39+
40+
assert sensor.shape() == sphere
41+
assert ek.allclose(sensor.film().size(), [1, 1])
42+
43+
44+
@pytest.mark.parametrize(("center", "radius"), [([2.0, 5.0, 8.3], 2.0), ([0.0, 0.0, 0.0], 1.0), ([1.0, 4.0, 0.0], 5.0)])
45+
def test_sampling(variant_scalar_rgb, center, radius):
46+
"""
47+
We construct an irradiance meter attached to a sphere and assert that sampled
48+
rays originate at the sphere's surface
49+
"""
50+
from mitsuba.core import Vector3f
51+
52+
center_v = Vector3f(center)
53+
sphere = example_shape(radius, center_v)
54+
sensor = sphere.sensor()
55+
num_samples = 100
56+
57+
wav_samples = np.random.rand(num_samples)
58+
pos_samples = np.random.rand(num_samples, 2)
59+
dir_samples = np.random.rand(num_samples, 2)
60+
61+
for i in range(100):
62+
ray = sensor.sample_ray_differential(0.0, wav_samples[i], pos_samples[i], dir_samples[i])[0]
63+
64+
# assert that the ray starts at the sphere surface
65+
assert ek.allclose(ek.norm(center_v - ray.o), radius)
66+
# assert that all rays point away from the sphere center
67+
assert ek.dot(ek.normalize(ray.o - center_v), ray.d) > 0.0
68+
69+
@pytest.mark.parametrize("radiance", [2.04, 1.0, 0.0])
70+
def test_incoming_flux(variant_scalar_rgb, radiance):
71+
"""
72+
We test the recorded power density of the irradiance meter, by creating a simple scene:
73+
The irradiance meter is attached to a sphere with unit radius at the coordinate origin
74+
surrounded by a constant environment emitter.
75+
We sample a number of rays and average their contribution to the cumulative power
76+
density.
77+
We expect the average value to be \\pi * L with L the radiance of the constant
78+
emitter.
79+
"""
80+
from mitsuba.core import Spectrum
81+
from mitsuba.core.xml import load_string
82+
83+
sensor_xml = f"""
84+
<shape version='0.1.0' type="sphere">
85+
<float name="radius" value="1"/>
86+
<transform name="to_world">
87+
<translate x="0" y="0" z="0"/>
88+
</transform>
89+
<sensor type="irradiancemeter">
90+
<film type="hdrfilm">
91+
<integer name="width" value="1"/>
92+
<integer name="height" value="1"/>
93+
</film>
94+
</sensor>
95+
</shape>
96+
"""
97+
98+
emitter_xml = f"""
99+
<emitter type="constant">
100+
<spectrum name="radiance" type='uniform'>
101+
<float name="value" value="{radiance}"/>
102+
</spectrum>
103+
</emitter>
104+
"""
105+
106+
scene_xml = f"""
107+
<scene version="0.1.0">
108+
{sensor_xml}
109+
{emitter_xml}
110+
</scene>
111+
"""
112+
scene = load_string(scene_xml)
113+
sensor = scene.sensors()[0]
114+
115+
power_density_cum = 0.0
116+
num_samples = 100
117+
118+
wav_samples = np.random.rand(num_samples)
119+
pos_samples = np.random.rand(num_samples, 2)
120+
dir_samples = np.random.rand(num_samples, 2)
121+
122+
for i in range(100):
123+
ray, weight = sensor.sample_ray_differential(0.0, wav_samples[i], pos_samples[i], dir_samples[i])
124+
125+
intersection = scene.ray_intersect(ray)
126+
power_density_cum += weight * intersection.emitter(scene).eval(intersection)
127+
power_density_avg = power_density_cum / float(num_samples)
128+
129+
assert ek.allclose(power_density_avg, Spectrum(ek.pi * radiance))
130+
131+
@pytest.mark.parametrize("radiance", [2.04, 1.0, 0.0])
132+
def test_incoming_flux_integrator(variant_scalar_rgb, radiance):
133+
"""
134+
We test the recorded power density of the irradiance meter, by creating a simple scene:
135+
The irradiance meter is attached to a sphere with unit radius at the coordinate origin
136+
surrounded by a constant environment emitter.
137+
We render the scene with the path tracer integrator and compare the recorded power
138+
density with our theoretical expectation.
139+
We expect the average value to be \\pi * L with L the radiance of the constant
140+
emitter.
141+
"""
142+
143+
from mitsuba.core import Spectrum, Bitmap, Struct
144+
from mitsuba.core.xml import load_string
145+
146+
sensor_xml = f"""
147+
<shape version='0.1.0' type="sphere">
148+
<float name="radius" value="1"/>
149+
<transform name="to_world">
150+
<translate x="0" y="0" z="0"/>
151+
</transform>
152+
<sensor type="irradiancemeter">
153+
<film type="hdrfilm">
154+
<integer name="width" value="1"/>
155+
<integer name="height" value="1"/>
156+
</film>
157+
</sensor>
158+
</shape>
159+
"""
160+
161+
emitter_xml = f"""
162+
<emitter type="constant">
163+
<spectrum name="radiance" type='uniform'>
164+
<float name="value" value="{radiance}"/>
165+
</spectrum>
166+
</emitter>
167+
"""
168+
169+
integrator_xml = f"""
170+
<integrator type="path">
171+
172+
<integer name="max_depth" value="-1"/>
173+
</integrator>
174+
"""
175+
176+
sampler_xml = f"""
177+
<sampler type="independent">
178+
<integer name="sample_count" value="100"/>
179+
</sampler>
180+
"""
181+
scene_xml = f"""
182+
<scene version="0.1.0">
183+
{integrator_xml}
184+
{sensor_xml}
185+
{emitter_xml}
186+
{sampler_xml}
187+
</scene>
188+
"""
189+
scene = load_string(scene_xml)
190+
sensor = scene.sensors()[0]
191+
192+
scene.integrator().render(scene, sensor)
193+
film = sensor.film()
194+
195+
img = film.bitmap(raw=True).convert(Bitmap.PixelFormat.Y, Struct.Type.Float32, srgb_gamma=False)
196+
image_np = np.array(img)
197+
198+
ek.allclose(image_np, (radiance*ek.pi))

src/shapes/cylinder.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ A simple example for instantiating a cylinder, whose interior is visible:
7575
template <typename Float, typename Spectrum>
7676
class Cylinder final : public Shape<Float, Spectrum> {
7777
public:
78-
MTS_IMPORT_BASE(Shape, bsdf, emitter, is_emitter)
78+
MTS_IMPORT_BASE(Shape, bsdf, emitter, is_emitter, sensor, is_sensor)
7979
MTS_IMPORT_TYPES()
8080

8181
using typename Base::ScalarIndex;
@@ -116,6 +116,8 @@ class Cylinder final : public Shape<Float, Spectrum> {
116116
}
117117
if (is_emitter())
118118
emitter()->set_shape(this);
119+
if (is_sensor())
120+
sensor()->set_shape(this);
119121
}
120122

121123
ScalarBoundingBox3f bbox() const override {

src/shapes/disk.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <mitsuba/render/emitter.h>
1010
#include <mitsuba/render/fwd.h>
1111
#include <mitsuba/render/interaction.h>
12+
#include <mitsuba/render/sensor.h>
1213
#include <mitsuba/render/shape.h>
1314

1415
NAMESPACE_BEGIN(mitsuba)
@@ -67,7 +68,7 @@ The following XML snippet instantiates an example of a textured disk shape:
6768
template <typename Float, typename Spectrum>
6869
class Disk final : public Shape<Float, Spectrum> {
6970
public:
70-
MTS_IMPORT_BASE(Shape, bsdf, emitter, is_emitter)
71+
MTS_IMPORT_BASE(Shape, bsdf, emitter, is_emitter, sensor, is_sensor)
7172
MTS_IMPORT_TYPES()
7273

7374
using typename Base::ScalarSize;
@@ -97,6 +98,8 @@ class Disk final : public Shape<Float, Spectrum> {
9798

9899
if (is_emitter())
99100
emitter()->set_shape(this);
101+
if (is_sensor())
102+
sensor()->set_shape(this);
100103
}
101104

102105
ScalarBoundingBox3f bbox() const override {

0 commit comments

Comments
 (0)