Rendering a scene

In the previous section, we learned how to load a scene from an XML file. Once a scene has been loaded, it can be rendered as follows:

...

# Load the scene for an XML file
scene = load_file(filename)

# Get the scene's sensor (if many, can pick one by specifying the index)
sensor = scene.sensors()[0]

# Call the scene's integrator to render the loaded scene with the desired sensor
scene.integrator().render(scene, sensor)

After rendering, it is possible to write out the rendered data as an HDR OpenEXR file like this:

# The rendered data is stored in the film
film = sensor.film()

# Write out data as high dynamic range OpenEXR file
film.set_destination_file('/path/to/output.exr')
film.develop()

One can also write out a gamma tone-mapped JPEG file of the same rendering using the mitsuba.core.Bitmap class:

# Write out a tone-mapped JPG of the same rendering
from mitsuba.core import Bitmap, Struct
img = film.bitmap(raw=True).convert(Bitmap.PixelFormat.RGB, Struct.Type.UInt8, srgb_gamma=True)
img.write('/path/to/output.jpg')

The raw=True argument in film.bitmap() specifies that we are interested in the raw film contents to be able to perform a conversion into the desired output format ourselves.

See mitsuba.core.Bitmap.convert() for more information regarding the bitmap convertion routine.

The data stored in the Bitmap object can also be cast into a NumPy array for further processing in Python:

# Get linear pixel values as a NumPy array for further processing
img = img.convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, srgb_gamma=False)
import numpy as np
image_np = np.array(img)
print(image_np.shape)

Note

The full Python script of this tutorial can be found in the file: docs/examples/01_render_scene/render_scene.py

Custom rendering pipeline in Python

In the following section, we show how to use the Python bindings to write a simple depth map renderer, including ray generation and pixel value splatting, purely in Python. While this is of course much more work than simply calling the integrator’s render(), this fine-grained level of control can be useful in certain applications. Please also refer to the related section on developing custom plugins in Python.

Similar to before, we import a number of modules and load the scene from disk:

import os
import enoki as ek
import numpy as np
import mitsuba

# Set the desired mitsuba variant
mitsuba.set_variant('packet_rgb')

from mitsuba.core import Float, UInt32, UInt64, Vector2f, Vector3f
from mitsuba.core import Bitmap, Struct, Thread
from mitsuba.core.xml import load_file
from mitsuba.render import ImageBlock

# Absolute or relative path to the XML file
filename = 'path/to/my/scene.xml'

# Add the scene directory to the FileResolver's search path
Thread.thread().file_resolver().append(os.path.dirname(filename))

# Load the scene
scene = load_file(filename)

In this example we use the packet variant of Mitsuba. This means all calls to Mitsuba functions will be vectorized and we avoid expensive for-loops in Python. The same code will work for gpu variants of the renderer as well.

Instead of calling the scene’s existing integrator as before, we will now manually trace rays through each pixel of the image:

# Instead of calling the scene's integrator, we build our own small integrator
# This integrator simply computes the depth values per pixel
sensor = scene.sensors()[0]
film = sensor.film()
sampler = sensor.sampler()
film_size = film.crop_size()
spp = 32

# Seed the sampler
total_sample_count = ek.hprod(film_size) * spp

if sampler.wavefront_size() != total_sample_count:
    sampler.seed(0, total_sample_count)

# Enumerate discrete sample & pixel indices, and uniformly sample
# positions within each pixel.
pos = ek.arange(UInt32, total_sample_count)

pos //= spp
scale = Vector2f(1.0 / film_size[0], 1.0 / film_size[1])
pos = Vector2f(Float(pos  % int(film_size[0])),
               Float(pos // int(film_size[0])))

pos += sampler.next_2d()

# Sample rays starting from the camera sensor
rays, weights = sensor.sample_ray_differential(
    time=0,
    sample1=sampler.next_1d(),
    sample2=pos * scale,
    sample3=0
)

# Intersect rays with the scene geometry
surface_interaction = scene.ray_intersect(rays)

After computing the surface intersections for all the rays, we then extract the depth values

# Given intersection, compute the final pixel values as the depth t
# of the sampled surface interaction
result = surface_interaction.t

# Set to zero if no intersection was found
result[~surface_interaction.is_valid()] = 0

We then splat these depth values to an ImageBlock, which is an image data structure that handles averaging over samples and accounts for the pixel filter. The ImageBlock is then converted to a Bitmap object and the resulting image saved to disk.

block = ImageBlock(
    film.crop_size(),
    channel_count=5,
    filter=film.reconstruction_filter(),
    border=False
)
block.clear()
# ImageBlock expects RGB values (Array of size (n, 3))
block.put(pos, rays.wavelengths, Vector3f(result, result, result), 1)

# Write out the result from the ImageBlock
# Internally, ImageBlock stores values in XYZAW format
# (color XYZ, alpha value A and weight W)
xyzaw_np = np.array(block.data()).reshape([film_size[1], film_size[0], 5])

# We then create a Bitmap from these values and save it out as EXR file
bmp = Bitmap(xyzaw_np, Bitmap.PixelFormat.XYZAW)
bmp = bmp.convert(Bitmap.PixelFormat.Y, Struct.Type.Float32, srgb_gamma=False)
bmp.write('depth.exr')

Note

The code for this example can be found in docs/examples/02_depth_integrator/depth_integrator.py