@@ -126,3 +126,172 @@ The solution we found optimizes the objective well (i.e. the rendered image
126126matches the target), but the reconstructed texture may not match our
127127expectation. In such a case, it may be advisable to introduce further
128128regularization (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 `.
0 commit comments