Ray tracing scenery with POV-Ray

Quick introduction

POV-Ray is an open source tool for ray tracing. There are various guides on rendering randomly generated scenery using POV-Ray but this guide focusses on rendering scenery using real-life elevation data. There are two main challenges in rendering real-life scenery, firstly ensuring there is enough vertical resolution in the scenery data (this applies to randomly generated scenery as well) and secondly adding random noise to make the scenery look more realistic.

Processing elevation data

Whether you are using real-life elevation data or randomly generated elevation data, for use in POV-Ray, we will need to format the data as a grayscale image where the brightness of each pixel gives the height at that point. Normally a grayscale image will use 8 bits per pixel which gives 256 height levels. Say, for example, your scenery has an elevation range from sea level to 3000m then the separation between each height level is nearly 12m. Using this data will generate scenery that looks terraced which, although being an interesting effect, isn't ideal. Fortunately POV-Ray can use 16 bit grayscale images as height maps, then we have 65536 height levels.

If you are handling your elevation data with Python then you can use the pypng library to save 16 bit grayscale images. The following function will save a numpy array as a 16 bit grayscale image.


import numpy as np
import png

def save_img(savename, arr):
    arr -= np.min(arr)
    arr /= np.max(arr)
    arr *= 65535
    arr = arr.astype(np.uint16)
    with open(savename, 'wb') as file:
        writer = png.Writer(width=arr.shape[0], height=arr.shape[1], bitdepth=16, greyscale=True)
        arr2list = arr.tolist()
        writer.write(file, arr2list)

Using elevation data in POV-Ray

Height field

The elevation data can used in POV-Ray with a height field. You could simply use the image file as the data for the height field, however, if you are using real-life elevation data, the surface is likely to look much smoother than reality. To fix this we need to add some random noise to make the scenery look more realistic.

Adding random noise

Let's say our landscape is rocky above a certain altitude and grassy below. We want to modify the height field so it has two different terrains. Firstly, create a pigment function from the elevation image


#declare ele=function { pigment {
     image_map {png "images/ele.png" interpolate 2 } }
}

Now the elevation can be accessed by ele(x,y,0).gray. Secondly, declare another pigment function that changes colour based on altitude


#declare surface = function {
  pigment {
      gradient y
      pigment_map {
        [ 0.5 color rgb 0.0 ]
        [ 0.6 color rgb 1.0 ]
      }
  }
}

The colour of this pigment will determine surface type, in this case 0.0 (black) is grass and 1.0 (white) is rock. Now we choose the noise that will be added to each type of terrain, for example


#include "functions.inc" // This line can go at the top of the file

#declare rocky_terrain = pigment {
  average
  pigment_map {
    [ 0.05 granite colour_map {[0 rgb 0][1 rgb 1]} scale <0.0001, 1, 1> ]
    [ 1 function { f_ridged_mf(100*x, 50*y, 100*z, 0.9, 2, 5, 0.6, 5.0, 0) } ]
  }
}

#declare grassy_terrain = pigment {
  average
  pigment_map {
    [ 0.1 agate colour_map {[0 rgb 0][1 rgb 1]} scale 0.1 ]
    [ 1 function { f_ridged_mf(50*x, 50*y, 50*z, 0.9, 2, 5, 0.6, 3.0, 0) } ]
  }
}

Now we can add these to the elevation data to create the height field


#declare terrain = function {
  pigment {
    function { surface(x,ele(x,y,0).gray,1-y).gray }
    pigment_map {
      [ 0.0 grassy_terrain ]
      [ 1.0 rocky_terrain ]
    }
  }
}

#declare resolution = 1000;

height_field {
  function resolution, resolution { ele(x,y,z).gray * (1 + 0.05*terrain(x,y,z).gray) }
  texture {
    function { surface(x,ele(x,1-z,0).gray,z).gray }
    texture_map {
      [ 0.0 grass_texture ]
      [ 1.0 rock_texture ]
    }
  }
}

We have now added noise to the height field based on the terrain. Using the same idea, we can add textures to the height field based on the terrain (define your own grass_texture and rock_texture). The resolution need not be the same as the resolution of the elevation image.

Dents du Midi

There are many ways you can add to these techniques to enrich your scenery. This is an example using the DHM200 elevation data from the Swiss Federal Office of Topography for the Dents du Midi area in Switzerland. You can download the files to render this yourself.