Quick Start

Overview

This guide provides a simple example demonstrating the following:

  • How to setup a scene and render it

  • How to update scene parameters

  • How to perform differentiable rendering

We’ll be using psdr_jit renderer for rendering, so ensure you have it installed:

pip install psdr-jit

Other connectors can also be used but modifications may be needed.

Scene Setup

We will start by importing the necessary modules.

from irtk.scene import (
    Scene,
    Mesh,
    EnvironmentLight,
    DiffuseBRDF,
    PerspectiveCamera,
    HDRFilm,
    Integrator,
)  # Scene components
from irtk.renderer import Renderer  # Renderer
from irtk import get_connector_list
from irtk.io import to_torch_f, to_numpy, to_srgb

import matplotlib.pyplot as plt  # For displaying images
from pathlib import Path  # For handling file paths

data_path = Path("data")  # Path to the data folder

# Define a helper function of displaying images
def display_image(image):
    plt.imshow(to_numpy(to_srgb(image)))
    plt.axis('off')
    plt.show()

Constructing a scene is easy. Let’s start by add adding a blue armadillo. You can print out the scene to see what’s inside. Numerical values are converted and stored as pytorch tensors.

scene = Scene()
scene.set('armadillo', Mesh.from_file(data_path / 'meshes' / 'armadillo.obj', mat_id='armadillo_mat'))
scene.set('armadillo_mat', DiffuseBRDF((0.2, 0.2, 0.9)))
print(scene)
armadillo:
----
v:
	description: mesh vertex positions
	is differentiable: True
	requires grad: False
	value: 	tensor([[ 0.0414, -0.1924, -0.1188],
        [ 0.0353, -0.1864, -0.1199],
        [ 0.0409, -0.1873, -0.1259],
        ...,
        [-0.4123,  0.3096,  0.2094],
        [-0.4167,  0.2992,  0.2206],
        [-0.4152,  0.3048,  0.2151]], device='cuda:0')
f:
	description: mesh face indices
	is differentiable: False
	value: 	tensor([[    0,     1,     2],
        [    0,     3,     1],
        [    1,     4,     2],
        ...,
        [26444, 26442, 26441],
        [26444, 26436, 26442],
        [26444, 26443, 26437]], device='cuda:0', dtype=torch.int32)
uv:
	description: mesh uv coordinates
	is differentiable: False
	value: 	tensor([[0.0864, 0.1027],
        [0.0831, 0.1033],
        [0.0862, 0.1072],
        ...,
        [0.7044, 0.1020],
        [0.6640, 0.0828],
        [0.8925, 0.6453]], device='cuda:0')
fuv:
	description: mesh uv face indices
	is differentiable: False
	value: 	tensor([[    0,     1,     2],
        [    0,     3,     1],
        [    1,     4,     2],
        ...,
        [31289, 31281, 31280],
        [31289, 31272, 31281],
        [31290, 31285, 31273]], device='cuda:0', dtype=torch.int32)
to_world:
	description: mesh to world matrix
	is differentiable: True
	requires grad: False
	value: 	tensor([[1., 0., 0., 0.],
        [0., 1., 0., 0.],
        [0., 0., 1., 0.],
        [0., 0., 0., 1.]], device='cuda:0')
use_face_normal:
	description: whether to use face normal
	is differentiable: False
	value: 	True
can_change_topology:
	description: whether to the topology can be chagned
	is differentiable: False
	value: 	False
mat_id:
	description: name of the material of the mesh if used as an non-emitter
	is differentiable: False
	value: 	armadillo_mat
----


armadillo_mat:
----
d:
	description: diffuse reflectance
	is differentiable: True
	requires grad: False
	value: 	tensor([0.2000, 0.2000, 0.9000], device='cuda:0')
----

To access a scene component and its parameters, you can use scene[{component_name}] and scene[{component_name}.{parameter_name}], respectively.

scene['armadillo.v'] # Return the underlying attribute, in this case, a pytorch tensor representing the vertex positions
tensor([[ 0.0414, -0.1924, -0.1188],
        [ 0.0353, -0.1864, -0.1199],
        [ 0.0409, -0.1873, -0.1259],
        ...,
        [-0.4123,  0.3096,  0.2094],
        [-0.4167,  0.2992,  0.2206],
        [-0.4152,  0.3048,  0.2151]], device='cuda:0')

In order to render the scene, we still need a light source, a camera, a film to store the image, and an integrator to perform the rendering.

The integrator requires special attention, as the integrator type and its configuration are dependent on the renderer. Refer to the Connector Reference for more details. It is always a good idea to check it before using any connector.

scene.set('envlight', EnvironmentLight.from_file(data_path / 'envmaps' / 'factory.exr'))
scene.set('sensor', PerspectiveCamera.from_lookat(fov=40, origin=(1.5, 0, 1.5), target=(0, 0, 0), up=(0, 1, 0)))
scene.set('film', HDRFilm(width=512, height=512))
scene.set('integrator', Integrator(type='path', config={
    'max_depth': 3,
    'hide_emitters': False
}))

Rendering the scene

We are ready to render the scene. Before doing so, we can check the available connectors. Since we only have one renderer installed, the connector list will only have one entry.

Connectors are responsible for interfacing with different differentiable renderers for rendering the scene and obtaining the gradients with respect to the scene parameters.

get_connector_list()
['psdr_jit']

Let’s define a renderer that uses the psdr_jit connector and render the scene.

Again, the renderer is backend-specific, so make sure to check the documentation for the renderer you are using.

# The render_options are specific to the renderer
render = Renderer('psdr_jit', render_options={
    'spp': 128,
    'sppe': 0,
    'sppse': 0,
    'log_level': 0,
    'npass': 1
})
image_0 = render(scene, sensor_ids=[0], integrator_id='integrator')[0] # Image is a pytorch tensor of shape (height, width, 3)

display_image(image_0)
../_images/accf11e362e6aa0c8b8fd5da5e28c1cdb0bf2d4e145b4aa61aada6336a4c631b.png

Updating scene parameters

Let’s change the color of the armadillo to demonstrate how to update scene parameters.

scene['armadillo_mat.d'] = (0.7, 0.5, 0.3) # The value can be a tuple, list, numpy array, or pytorch tensor
image_1 = render(scene, sensor_ids=[0], integrator_id='integrator')[0]

display_image(image_1)
../_images/24cdbe48a5d0e85cf729a62e3959cf14e59bdab34ddc02bfaddf62ea5474c1d2.png

The connector need to know which parameters are being updated. When you do scene[{param_name}] = ..., the parameter is automatically tracked. However, if the paramter is updated inplace, for instance, when it is updated by a pytorch optimizer, the change might not be tracked. In such a case, you can manually mark the parameter as updated:

# An example
scene['armadillo_mat'].mark_updated('d')

Differentiable Rendering

To do differentiable rendering, we first need to mark the components that requires gradients, and then use scene.configure() to identify components that requires gradients.

# The parameter is a pytorch tensor. Set requires_grad as usual.
# You can have multiple parameters requiring gradients.
scene['armadillo_mat.d'].requires_grad = True

# We only need to call this once unless parameters that require gradients are changed
scene.configure()

# This will return a tuple of the names of the components that requires gradients
scene.requiring_grad
('armadillo_mat.d',)

We will render the updated scene again and obtain the gradient with respect to the armadillo’s diffuse reflectance. irtk.renderer.Renderer is a torch.nn.Module, so we can perform standard backpropagation with standard pytorch operations.

image_1 = render(scene, sensor_ids=[0], integrator_id='integrator')[0]

loss = (image_1 - image_0).pow(2).mean() # a differentiable loss function
loss.backward() 

# We can print out the gradient to see what it is
scene['armadillo_mat.d'].grad
tensor([ 0.0102,  0.0050, -0.0097], device='cuda:0')

We can even use a non-leaf tensor to update the scene parameter, enabling the use of latent variables. For instance, we can use the output from a neural network to update the scene parameter, and the gradient will be correctly backpropagated to the neural network. Below, we show a simpler example by using a single scalar to control the RGB color of the armadillo.

x = to_torch_f(0.5)
x.requires_grad = True

abledo = x * to_torch_f((0.4, 0.8, 0.6))
scene['armadillo_mat.d'] = abledo
scene['armadillo_mat.d'].requires_grad
True
image_1 = render(scene, sensor_ids=[0], integrator_id='integrator')[0]

display_image(image_1)
../_images/ad9711f0632958994a35fbb11d0c959a07272179f2e53c657e28c790004c32c4.png
loss = (image_1 - image_0).pow(2).mean()
loss.backward() 

# Print the gradient of the latent variable
x.grad
tensor(-0.0032, device='cuda:0')

Given the gradient, we can construct an inverse rendering pipeline by updating the scene parameters with gradient descent. irtk provides other useful scene components and utilities. Make sure to check them out.