Skip to content

Commit 043b0b6

Browse files
loubetgSpeierers
andcommitted
Add pathreparam and smootharea emitter
Co-authored-by: Sebastien Speierer <[email protected]>
1 parent ce56284 commit 043b0b6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3811
-1360
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ macro (add_file DST SRC)
572572
endmacro()
573573

574574
add_file(data/srgb.coeff ${CMAKE_BINARY_DIR}/ext_build/rgb2spec/srgb.coeff rgb2spec_opt_run)
575+
add_file(data/vmf-hemisphere.data ${CMAKE_CURRENT_SOURCE_DIR}/src/integrators/vmf-hemisphere.data)
575576

576577
file(COPY resources/data/ior DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/dist/data)
577578

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import os
2+
import time
3+
import enoki as ek
4+
5+
import mitsuba
6+
mitsuba.set_variant('gpu_autodiff_rgb')
7+
8+
from mitsuba.core import UInt32, Float, Thread, xml, Vector2f, Vector3f, Transform4f, ScalarTransform4f
9+
from mitsuba.render import SurfaceInteraction3f
10+
from mitsuba.python.util import traverse
11+
from mitsuba.python.autodiff import render, write_bitmap, Adam, SGD
12+
13+
# Return contiguous flattened array (will be included in next enoki release)
14+
def ravel(buf, dim = 3):
15+
idx = dim * UInt32.arange(int(len(buf) / dim))
16+
if dim == 2:
17+
return Vector2f(ek.gather(buf, idx), ek.gather(buf, idx + 1))
18+
elif dim == 3:
19+
return Vector3f(ek.gather(buf, idx), ek.gather(buf, idx + 1), ek.gather(buf, idx + 2))
20+
21+
# Convert flat array into a vector of arrays (will be included in next enoki release)
22+
def unravel(source, target, dim = 3):
23+
idx = UInt32.arange(ek.slices(source))
24+
for i in range(dim):
25+
ek.scatter(target, source[i], dim * idx + i)
26+
27+
# Prepare output folder
28+
output_path = "output/invert_heightfield/"
29+
if not os.path.isdir(output_path):
30+
os.makedirs(output_path)
31+
32+
# Load example scene
33+
scene_folder = '../../../resources/data/docs/examples/invert_heightfield/'
34+
Thread.thread().file_resolver().append(scene_folder)
35+
scene = xml.load_file(scene_folder + 'heightfield.xml')
36+
37+
params = traverse(scene)
38+
positions_buf = params['grid_mesh.vertex_positions_buf']
39+
positions_initial = ravel(positions_buf)
40+
normals_initial = ravel(params['grid_mesh.vertex_normals_buf'])
41+
vertex_count = ek.slices(positions_initial)
42+
43+
# Create a texture with the reference displacement map
44+
disp_tex = xml.load_dict({
45+
"type" : "bitmap",
46+
"filename" : "mitsuba_coin.jpg",
47+
"to_uv" : ScalarTransform4f.scale([1, -1, 1]) # texture is upside-down
48+
}).expand()[0]
49+
50+
# Create a fake surface interaction with an entry per vertex on the mesh
51+
mesh_si = SurfaceInteraction3f.zero(vertex_count)
52+
mesh_si.uv = ravel(params['grid_mesh.vertex_texcoords_buf'], dim=2)
53+
54+
# Evaluate the displacement map for the entire mesh
55+
disp_tex_data_ref = disp_tex.eval_1(mesh_si)
56+
57+
# Apply displacement to mesh vertex positions and update scene (e.g. OptiX BVH)
58+
def apply_displacement(amplitude = 0.05):
59+
new_positions = disp_tex.eval_1(mesh_si) * normals_initial * amplitude + positions_initial
60+
unravel(new_positions, positions_buf)
61+
params['grid_mesh.vertex_positions_buf'] = positions_buf
62+
params.update()
63+
64+
# Apply displacement before generating reference image
65+
apply_displacement()
66+
67+
# Render a reference image (no derivatives used yet)
68+
image_ref = render(scene, spp=32)
69+
crop_size = scene.sensors()[0].film().crop_size()
70+
write_bitmap(output_path + 'out_ref.exr', image_ref, crop_size)
71+
print("Write " + output_path + "out_ref.exr")
72+
73+
# Reset texture data to a constant
74+
disp_tex_params = traverse(disp_tex)
75+
disp_tex_params.keep(['data'])
76+
disp_tex_params['data'] = ek.full(Float, 0.25, len(disp_tex_params['data']))
77+
disp_tex_params.update()
78+
79+
# Construct an Adam optimizer that will adjust the texture parameters
80+
opt = Adam(disp_tex_params, lr=0.005)
81+
82+
time_a = time.time()
83+
84+
iterations = 100
85+
for it in range(iterations):
86+
# Perform a differentiable rendering of the scene
87+
image = render(scene,
88+
optimizer=opt,
89+
spp=4,
90+
unbiased=True,
91+
pre_render_callback=apply_displacement)
92+
93+
write_bitmap(output_path + 'out_%03i.exr' % it, image, crop_size)
94+
95+
# Objective: MSE between 'image' and 'image_ref'
96+
ob_val = ek.hsum(ek.sqr(image - image_ref)) / len(image)
97+
98+
# Back-propagate errors to input parameters
99+
ek.backward(ob_val)
100+
101+
# Optimizer: take a gradient step -> update displacement map
102+
opt.step()
103+
104+
# Compare iterate against ground-truth value
105+
err_ref = ek.hsum(ek.sqr(disp_tex_data_ref - disp_tex.eval_1(mesh_si)))
106+
print('Iteration %03i: error=%g' % (it, err_ref[0]), end='\r')
107+
108+
time_b = time.time()
109+
110+
print()
111+
print('%f ms per iteration' % (((time_b - time_a) * 1000) / iterations))

docs/src/inverse_rendering/advanced.rst

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,172 @@ The solution we found optimizes the objective well (i.e. the rendered image
126126
matches the target), but the reconstructed texture may not match our
127127
expectation. In such a case, it may be advisable to introduce further
128128
regularization (non-negativity, smoothness, etc.).
129+
130+
.. note::
131+
132+
The full Python script of this tutorial can be found in the file:
133+
:file:`docs/examples/10_inverse_rendering/invert_bunny.py`.
134+
135+
136+
Heightfield optimization
137+
------------------------
138+
139+
This advanced example demonstrates how to optimize a displacement map texture, which implies the
140+
differentiation of mesh parameters, such as vertex positions. Computing derivatives for parameters
141+
that affect visibility is a complex problem as it would normally make the integrants of the rendering
142+
equation non-differentiable. For this reason, this example requires the use of the specialized
143+
:ref:`pathreparam <integrator-pathreparam>` integrator, described in this
144+
`article <https://rgl.epfl.ch/publications/Loubet2019Reparameterizing>`_.
145+
146+
The example scene can be found in ``resource/data/docs/examples/invert_heightfield/`` and contains a
147+
simple grid mesh illuminated by a rectangular light source. To avoid discontinuities around the
148+
area light, we use the :ref:`smootharea <emitter-smootharea>` plugin.
149+
150+
First, we define two helper functions that we will use to transform the mesh
151+
parameter buffers (flatten arrays) into ``VectorXf`` type (and the other way around).
152+
Note that those functions will be natively supported by ``enoki`` in a futur release.
153+
154+
.. code-block:: python
155+
156+
# Return contiguous flattened array (will be included in next enoki release)
157+
def ravel(buf, dim = 3):
158+
idx = dim * UInt32.arange(int(len(buf) / dim))
159+
if dim == 2:
160+
return Vector2f(ek.gather(buf, idx), ek.gather(buf, idx + 1))
161+
elif dim == 3:
162+
return Vector3f(ek.gather(buf, idx), ek.gather(buf, idx + 1), ek.gather(buf, idx + 2))
163+
164+
# Convert flat array into a vector of arrays (will be included in next enoki release)
165+
def unravel(source, target, dim = 3):
166+
idx = UInt32.arange(ek.slices(source))
167+
for i in range(dim):
168+
ek.scatter(target, source[i], dim * idx + i)
169+
170+
171+
Using those, we can now load the scene and read the initial grid mesh parameters (vertex positions, normals and texture coordinates), which we will use
172+
later in the script.
173+
174+
.. code-block:: python
175+
176+
import enoki as ek
177+
import mitsuba
178+
mitsuba.set_variant('gpu_autodiff_rgb')
179+
180+
from mitsuba.core import UInt32, Float, Thread, xml, Vector2f, Vector3f, Transform4f
181+
from mitsuba.render import SurfaceInteraction3f
182+
from mitsuba.python.util import traverse
183+
from mitsuba.python.autodiff import render, write_bitmap, Adam
184+
185+
# Load example scene
186+
scene_folder = '../../../resources/data/docs/examples/invert_heightfield/'
187+
Thread.thread().file_resolver().append(scene_folder)
188+
scene = xml.load_file(scene_folder + 'heightfield.xml')
189+
190+
params = traverse(scene)
191+
positions_buf = params['grid_mesh.vertex_positions_buf']
192+
positions_initial = ravel(positions_buf)
193+
normals_initial = ravel(params['grid_mesh.vertex_normals_buf'])
194+
vertex_count = ek.slices(positions_initial)
195+
196+
197+
In this example, we implement displacement mapping directly in Python instead of using a C++ plugin.
198+
This showcases the flexibility of the framework, and the ability to fully control the optimization
199+
process. For instance, one could want to add constraints on the displacement values range, ...
200+
201+
We first create a :ref:`Bitmap <texture-bitmap>` texture instance using
202+
:py:func:`mitsuba.core.xml.load_dict`, which will load the displacement map image file from disk.
203+
We also create a :py:class:`mitsuba.render.SurfaceInteraction3f` with one entry per vertex on the
204+
mesh. By properly setting the texture coordinates on this surface interaction, we can now evaluate
205+
the displacement map for the entire mesh in one line of code.
206+
207+
.. code-block:: python
208+
209+
# Create a texture with the reference displacement map
210+
disp_tex = xml.load_dict({
211+
"type" : "bitmap",
212+
"filename" : "mitsuba_coin.jpg"
213+
}).expand()[0]
214+
215+
# Create a fake surface interaction with an entry per vertex on the mesh
216+
mesh_si = SurfaceInteraction3f.zero(vertex_count)
217+
mesh_si.uv = ravel(params['grid_mesh.vertex_texcoords_buf'], dim=2)
218+
219+
# Evaluate the displacement map for the entire mesh
220+
disp_tex_data_ref = disp_tex.eval_1(mesh_si)
221+
222+
Finally, we define a function that applies the displacement map onto the original mesh. This will
223+
be called at every iteration of the optimization loop to update the mesh data everytime the
224+
displacement map is refined.
225+
226+
.. code-block:: python
227+
228+
# Apply displacement to mesh vertex positions and call update scene
229+
def apply_displacement(amplitude = 0.05):
230+
new_positions = disp_tex.eval_1(mesh_si) * normals_initial * amplitude + positions_initial
231+
unravel(new_positions, positions_buf)
232+
params['grid_mesh.vertex_positions_buf'] = positions_buf
233+
params.update()
234+
235+
We can now generate a reference image.
236+
237+
.. code-block:: python
238+
239+
# Apply displacement before generating reference image
240+
apply_displacement()
241+
242+
# Render a reference image (no derivatives used yet)
243+
image_ref = render(scene, spp=32)
244+
crop_size = scene.sensors()[0].film().crop_size()
245+
write_bitmap('out_ref.exr', image_ref, crop_size)
246+
print("Write out_ref.exr")
247+
248+
Before runing the optimization loop, we need to change the displacement data to a constant value
249+
(here ``0.25``). This can be done using the :py:func:`mitsuba.python.util.traverse` function
250+
on the texture object directly. We can then create an optimizer that will adjust those texture
251+
parameters during the optimization process.
252+
253+
.. code-block:: python
254+
255+
# Reset texture data to a constant
256+
disp_tex_params = traverse(disp_tex)
257+
disp_tex_params['data'] = ek.full(Float, 0.25, len(disp_tex_params['data']))
258+
disp_tex_params.update()
259+
260+
# Construct an Adam optimizer that will adjust the texture parameters
261+
disp_tex_params.keep(['data'])
262+
opt = Adam(disp_tex_params, lr=0.005)
263+
264+
The optimization loop is very similar to the previous example, to the exception that it needs to
265+
manually apply the displacement mapping to the mesh at every iteration.
266+
267+
.. code-block:: python
268+
269+
iterations = 100
270+
for it in range(iterations):
271+
# Apply displacement to mesh and update scene (e.g. OptiX BVH)
272+
apply_displacement()
273+
274+
# Perform a differentiable rendering of the scene
275+
image = render(scene, optimizer=opt, spp=4)
276+
write_bitmap('out_%03i.exr' % it, image, crop_size)
277+
278+
# Objective: MSE between 'image' and 'image_ref'
279+
ob_val = ek.hsum(ek.sqr(image - image_ref)) / len(image)
280+
281+
# Back-propagate errors to input parameters
282+
ek.backward(ob_val)
283+
284+
# Optimizer: take a gradient step -> update displacement map
285+
opt.step()
286+
287+
# Compare iterate against ground-truth value
288+
err_ref = ek.hsum(ek.sqr(disp_tex_data_ref - disp_tex.eval_1(mesh_si)))
289+
print('Iteration %03i: error=%g' % (it, err_ref[0]), end='\r')
290+
291+
292+
Here we can see the result of the heightfield optimization:
293+
294+
.. note::
295+
296+
The full Python script of this tutorial can be found in the file:
297+
:file:`docs/examples/10_inverse_rendering/invert_heightfield.py`.

docs/src/inverse_rendering/diff_render.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ overly large value.
322322
.. note::
323323

324324
The full Python script of this tutorial can be found in the file:
325-
:file:`docs/examples/10_diff_render/invert_cbox.py`.
325+
:file:`docs/examples/10_inverse_rendering/invert_cbox.py`.
326326

327327

328328
Forward-mode differentiation
@@ -395,4 +395,4 @@ respect to albedo, the red color disappears.
395395
.. note::
396396

397397
The full Python script of this tutorial can be found in the file:
398-
:file:`docs/examples/10_diff_render/forward_diff.py`.
398+
:file:`docs/examples/10_inverse_rendering/forward_diff.py`.

include/mitsuba/core/warp.h

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -554,9 +554,9 @@ MTS_INLINE Value square_to_beckmann_pdf(const Vector<Value, 3> &m,
554554
// =======================================================================
555555

556556
/// Warp a uniformly distributed square sample to a von Mises Fisher distribution
557-
template <typename Value>
557+
template <typename Value, typename KappaValue = scalar_t<Value>>
558558
MTS_INLINE Vector<Value, 3> square_to_von_mises_fisher(const Point<Value, 2> &sample,
559-
const scalar_t<Value> &kappa) {
559+
const KappaValue &kappa) {
560560
#if 0
561561
// Approach 1: warping method based on standard disk mapping
562562

@@ -595,8 +595,8 @@ MTS_INLINE Vector<Value, 3> square_to_von_mises_fisher(const Point<Value, 2> &sa
595595
}
596596

597597
/// Inverse of the mapping \ref von_mises_fisher_to_square
598-
template <typename Value>
599-
MTS_INLINE Point<Value, 2> von_mises_fisher_to_square(const Vector<Value, 3> &v, scalar_t<Value> kappa) {
598+
template <typename Value, typename KappaValue = scalar_t<Value>>
599+
MTS_INLINE Point<Value, 2> von_mises_fisher_to_square(const Vector<Value, 3> &v, KappaValue kappa) {
600600
Value expm2k = exp(-2.f * kappa),
601601
t = exp((v.z() - 1.f) * kappa),
602602
sy = (expm2k - t) / (expm2k - 1.f),
@@ -607,17 +607,15 @@ MTS_INLINE Point<Value, 2> von_mises_fisher_to_square(const Vector<Value, 3> &v,
607607
}
608608

609609
/// Probability density of \ref square_to_von_mises_fisher()
610-
template <typename Value>
611-
MTS_INLINE Value square_to_von_mises_fisher_pdf(const Vector<Value, 3> &v, scalar_t<Value> kappa) {
610+
template <typename Value, typename KappaValue = scalar_t<Value>>
611+
MTS_INLINE Value square_to_von_mises_fisher_pdf(const Vector<Value, 3> &v, KappaValue kappa) {
612612
/* Stable algorithm for evaluating the von Mises Fisher distribution
613613
https://www.mitsuba-renderer.org/~wenzel/files/vmf.pdf */
614614

615-
assert(kappa >= 0);
616-
if (unlikely(kappa == 0))
617-
return math::InvFourPi<Value>;
618-
else
619-
return exp(kappa * (v.z() - 1.f)) * (kappa * math::InvTwoPi<Value>) /
615+
Value result = exp(kappa * (v.z() - 1.f)) * (kappa * math::InvTwoPi<Value>) /
620616
(1.f - exp(-2.f * kappa));
617+
masked(result, eq(kappa, 0.f)) = math::InvFourPi<Value>;
618+
return result;
621619
}
622620

623621
// =======================================================================

include/mitsuba/render/bsdf.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ template <typename Float, typename Spectrum> struct BSDFSample3 {
220220
/// Stores the component index that was sampled by \ref BSDF::sample()
221221
UInt32 sampled_component;
222222

223+
/// Roughness of the sampled material (used in \ref DiffPathIntegrator)
224+
Float sampled_roughness;
225+
223226
//! @}
224227
// =============================================================
225228

@@ -242,13 +245,13 @@ template <typename Float, typename Spectrum> struct BSDFSample3 {
242245
*/
243246
BSDFSample3(const Vector3f &wo)
244247
: wo(wo), pdf(0.f), eta(1.f), sampled_type(0),
245-
sampled_component(uint32_t(-1)) { }
248+
sampled_component(uint32_t(-1)), sampled_roughness(math::Infinity<Float>) { }
246249

247250

248251
//! @}
249252
// =============================================================
250253

251-
ENOKI_STRUCT(BSDFSample3, wo, pdf, eta, sampled_type, sampled_component);
254+
ENOKI_STRUCT(BSDFSample3, wo, pdf, eta, sampled_type, sampled_component, sampled_roughness);
252255
};
253256

254257

@@ -509,7 +512,7 @@ NAMESPACE_END(mitsuba)
509512
// -----------------------------------------------------------------------
510513

511514
ENOKI_STRUCT_SUPPORT(mitsuba::BSDFSample3, wo, pdf, eta,
512-
sampled_type, sampled_component)
515+
sampled_type, sampled_component, sampled_roughness)
513516

514517
//! @}
515518
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)