Normal Mapping

This feature "5.8 Bump Mapping or Normal Mapping" enables the user to fake bumpy surfaces with relatively low computation costs. I decided to implement Normal Mapping, which essentially perturbates the surface normal to create shadows on smooth surfaces and in combination with image textures can produce more realistic results.

preview


Implementation

The basic structure of this class is the same as the checkerboard.cpp implementation. The overall implementation is the same as for Images as Textures. Therefore, the report text will be very similar. Some information will be repeated. First, we again need to be able to load the png image into Nori. For this, I used a preexisting png-loader called lodepng. Lodepng reads the image and saves the content into an unsigned char vector. One pixel is represented by a block of 4 chars with the respective RGBA values. This is implemented in the constructor of this class. Additionally, similar to the image texture feature, I added the ability to scale the normal map with a simple 2D vector. I use the same convention as in Blender for the scaling, e.g., if the scale value is 2, the normal map will be repeated twice for each axis.

In the eval method, the uv coordinates are mapped to the image space. I assume that the uv coordinates are always scaled to [0,1]. We can simply calculate the image-u by multiplying uv.x() with the image width and the scaling term. Image-v is calculated analogously. After u and v have been mapped to the image space, we need to calculate the corresponding index in the image vector: idx = (v * image_width + u) * 4. The index is multiplied by 4 because each pixel consists of 4 chars (RGBA). To ensure the texture can be repeated, the index is mapped to the vector length by using the modulo operator: idx = idx % image_size. Up until here, the implementation is basically the same as the images as texture feature. After obtaining the RGB values which are mapped from 0 to 255, we need to extract the normal. To this end, we again divide the RGB values by 255 to map them to [0,1]. Finally, since normals have to be mapped to [-1,1] we multiply each value by 2 and subtract 1 to get the desired normal. Since Nori has a left-handed coordinate system, we now need to switch the first and second coefficients of the normal and negate them: new_normal = Vector3f(-y,-x,z).

During the validation of this feature, I discovered that there are actually two different format types for bump maps, namely OpenGL and DirectX. For the brick wall example, which is shown below, I thought my implementation was wrong, because I felt like the bricks should stick out instead of in. However, Mitsuba produces the same output, therefore I added a flag "opengl" to my code, where you can switch between the different formats.

preview

BSDFs and Shapes

Next, we have to change the implementation of BSDFs and Shapes to enable the perturbation of the surface normals. I decided to add compatibility for the Mesh objects and Diffuse and Disney BSDFs. Other subclasses can be extended if needed.

BSDFs

For BSDFs, we need to edit the addChild method to allow normal maps to be added. This is analogous to the albedo. Furthermore, we need to be able to know whether the BSDF contains a normal map and if so, get access to the new normals. To this end, I provide the method getNormalMap(Texture<Normal3f>* &normalmap) which returns whether the normal map is set and then fills the normal map parameter. At the moment, normal maps are supported for Diffuse surfaces and Disney BSDFs.

Mesh

The most important and most complicated part is inside the Mesh class, where we have to compute a new shading frame from the normal map. All modifications happen in the method setHitInformation. The essential functionality here is, to first check whether the BSDF contains a normal map and if so, read the corresponding normal and transform the normal to world coordinates and creating a new shading frame: its.shFrame = Frame(its.shFrame.toWorld(normal)); where the previous shFrame already has been initialized with the geoFrame or the interpolated shading frame. However, only doing this does not allow us to rotate the object and get the correct lighting information, since the shading frame tangents are not continuous. Therefore, I compute the shading frame first using the UV coordinates, if they are provided. If we have uv coordinates, we can first approximate the dp/du term and then using this, create our new shading frame where the u-tangent should be continuous.

Usage:

<bsdf type="...">
 <texture type="normal_map" name="normal">
  <string name="filename" value="normal_map.png"/>
  <vector name="scale" value="1,1"/>
 </texture>
</bsdf>

The following files have been modified or added to implement this feature:

  • include/nori/lodepng.h
  • src/lodepng.cpp
  • src/normalmap.cpp
  • include/nori/bsdf.h
  • src/diffuse.cpp
  • src/disney.cpp
  • src/mesh.cpp

Validation

To validate the textures, I rendered normal maps onto different objects, and compared the results to the same scene rendered by Mitsuba and Blender.

The sources of the normal maps: Wikipedia example scene, Brick wall

normal map example scene normal map example scene


Comparison to Mistuba

First, I'm going to show comparisons with Mitsuba to show that the expected output is produced for the mapping and the colors. Similar to the texture as images feature, Mitsuba's version is a little bit sharper in comparison, but the overall result is almost identical.

Example Scene

  • No Rotation
  • Light position: upper left
Mine Mitsuba
  • 90° Rotation
  • Light position: upper left (closer on the y-axis than previous example)
Mine Mitsuba

Sphere

Applying normal maps on spherical objects also produces the same result as rendering it with Mitsuba.

Mine Mitsuba


Additional Results

Scale Effect

  • Light position: top center
  • uv-scale: (2, 1)
Scaled

Image Texture with and without normal map

Here we can see the subtle differences when we apply normal maps in addition to textures. Applying the same scaling term on both texture and normal map produces consistent results. In this last example, I set the "opengl" flag to false, since I wanted the bricks to bump out instead of in.

None Normal Map Texture Normal and Texture