Rendering tutorial

Cornell Box

This post looks into how to turn a 3D scene into an image. The objects of a scene are modeled with triangular primitives connected to a so-called mesh. Every mesh has a material which controls its appearance, currently there is support for a diffuse (i.e. matte) material only. After the scene is set up, one needs to define a camera: two points from where one looks at what and the angle of view expressed by focal length. For simulating how the light propagates inside the scene, Flat uses path-tracing, a very accurate (and extremely computationally intensive) method. The simulation may result in light intensities way beyond what a monitor can display, therefore it is needed to employ "tone mapping" to reduce its dynamic range.

from flat import diffuse, mesh, scene

gray = diffuse((0.7, 0.7, 0.7))
red = diffuse((0.7, 0.2, 0.2))
green = diffuse((0.2, 0.7, 0.2))
lamp = diffuse((0.7, 0.7, 0.7), (1000.0, 1000.0, 1000.0))

s = scene()
# Scene by Harrison Ainsworth / HXA7241
# http://www.hxa.name/minilight/
s.camera((0.278, 0.275, -0.789), (0.278, 0.275, 0), 50)
s.environment((0.0906, 0.0943, 0.1151), (0.1, 0.09, 0.07))
s.add(mesh(
    ((0.556, 0.000, 0.000), (0.006, 0.000, 0.559), (0.556, 0.000, 0.559)),
    ((0.006, 0.000, 0.559), (0.556, 0.000, 0.000), (0.003, 0.000, 0.000)),
    ((0.556, 0.000, 0.559), (0.000, 0.549, 0.559), (0.556, 0.549, 0.559)),
    ((0.000, 0.549, 0.559), (0.556, 0.000, 0.559), (0.006, 0.000, 0.559))), gray)
s.add(mesh(
    ((0.006, 0.000, 0.559), (0.000, 0.549, 0.000), (0.000, 0.549, 0.559)),
    ((0.000, 0.549, 0.000), (0.006, 0.000, 0.559), (0.003, 0.000, 0.000))), red)
s.add(mesh(
    ((0.556, 0.000, 0.000), (0.556, 0.549, 0.559), (0.556, 0.549, 0.000)),
    ((0.556, 0.549, 0.559), (0.556, 0.000, 0.000), (0.556, 0.000, 0.559))), green)
s.add(mesh(
    ((0.556, 0.549, 0.559), (0.000, 0.549, 0.000), (0.556, 0.549, 0.000)),
    ((0.000, 0.549, 0.000), (0.556, 0.549, 0.559), (0.000, 0.549, 0.559))), gray)
s.add(mesh(
    ((0.343, 0.545, 0.332), (0.213, 0.545, 0.227), (0.343, 0.545, 0.227)),
    ((0.213, 0.545, 0.227), (0.343, 0.545, 0.332), (0.213, 0.545, 0.332))), lamp)
s.add(mesh(
    ((0.474, 0.165, 0.225), (0.426, 0.165, 0.065), (0.316, 0.165, 0.272)),
    ((0.266, 0.165, 0.114), (0.316, 0.165, 0.272), (0.426, 0.165, 0.065)),
    ((0.266, 0.000, 0.114), (0.266, 0.165, 0.114), (0.316, 0.165, 0.272)),
    ((0.316, 0.000, 0.272), (0.266, 0.000, 0.114), (0.316, 0.165, 0.272)),
    ((0.316, 0.000, 0.272), (0.316, 0.165, 0.272), (0.474, 0.165, 0.225)),
    ((0.474, 0.165, 0.225), (0.316, 0.000, 0.272), (0.474, 0.000, 0.225)),
    ((0.474, 0.000, 0.225), (0.474, 0.165, 0.225), (0.426, 0.165, 0.065)),
    ((0.426, 0.165, 0.065), (0.426, 0.000, 0.065), (0.474, 0.000, 0.225)),
    ((0.426, 0.000, 0.065), (0.426, 0.165, 0.065), (0.266, 0.165, 0.114)),
    ((0.266, 0.165, 0.114), (0.266, 0.000, 0.114), (0.426, 0.000, 0.065))), gray)
s.add(mesh(
    ((0.133, 0.330, 0.247), (0.291, 0.330, 0.296), (0.242, 0.330, 0.456)),
    ((0.242, 0.330, 0.456), (0.084, 0.330, 0.406), (0.133, 0.330, 0.247)),
    ((0.133, 0.000, 0.247), (0.133, 0.330, 0.247), (0.084, 0.330, 0.406)),
    ((0.084, 0.330, 0.406), (0.084, 0.000, 0.406), (0.133, 0.000, 0.247)),
    ((0.084, 0.000, 0.406), (0.084, 0.330, 0.406), (0.242, 0.330, 0.456)),
    ((0.242, 0.330, 0.456), (0.242, 0.000, 0.456), (0.084, 0.000, 0.406)),
    ((0.242, 0.000, 0.456), (0.242, 0.330, 0.456), (0.291, 0.330, 0.296)),
    ((0.291, 0.330, 0.296), (0.291, 0.000, 0.296), (0.242, 0.000, 0.456)),
    ((0.291, 0.000, 0.296), (0.291, 0.330, 0.296), (0.133, 0.330, 0.247)),
    ((0.133, 0.330, 0.247), (0.133, 0.000, 0.247), (0.291, 0.000, 0.296))), gray)

s.render(200, 200, 10).tonemapped().png('cornellbox.png')

The above runs on a dual-core 1.7 GHz i5 under PyPy 2.1 in 37 seconds and with multiprocessing turned off it took 56 seconds.

Out of curiosity, I tried to render the same scene (cornellbox.ml.txt) in MiniLight 1.6, both of its C and Python versions, and the times were 10 and 117 seconds, respectively. The comparison is not entirely fair: MiniLight calculates the tone-mapped image every while and Flat uses stratified sampling.

When using CPython 2.7 instead of PyPy, the times in seconds become: 724 for Flat with multiprocessing, 1535 without and 3188 for MiniLight. In other words, PyPy decreases the rendering time by around 20x for multiprocessing and 27x for single-threaded execution.

Rendering times

And here is the code used to draw the chart:

from flat import rgb, font, shape, strike, document, view

def chart(width, height, padding, data):
    black = rgb(0, 0, 0)
    blue = rgb(25, 51, 229)
    # Montserrat by Julieta Ulanovsky
    # http://www.google.com/fonts/specimen/Montserrat
    regular = font.open('Montserrat-Regular.ttf')
    drawing = shape().stroke(blue).width(2.8)
    caption = strike(regular).size(11, 14).color(black)
    page = document(width, height, 'pt').addpage()

    values, keys = zip(*data)
    maxvalue, maxkey = max(values), max(map(caption.width, keys))
    scale = (width - padding * 9.0 - maxkey) / maxvalue
    step = (height - padding * (len(data) + 1.0)) / len(data)
    half = caption.ascender() * 0.7
    x, y = padding * 4, padding
    for value, key in data:
        dx, dy = x + value * scale, y + 0.5 * step
        page.place(drawing.line(x, dy, dx, dy))
        page.place(caption.text(key)).position(dx + padding, dy - half)
        y += step + padding

    return page.image(kind='rgb').png()

view(chart(800, 140, 8, [
    (10, 'MiniLight 1.6, C'),
    (37, 'Flat 0.1, PyPy 2.1, MP'),
    (56, 'Flat 0.1, PyPy 2.1'),
    (117, 'MiniLight 1.6, PyPy 2.1'),
    (724, 'Flat 0.1, CPython 2.7, MP'),
    (1535, 'Flat 0.1, CPython 2.7'),
    (3188, 'MiniLight 1.6, CPython 2.7')]))

— 10. 11. 2013