Interactive RayTracer 2: Wrapping with SWIG

To ease profiling and testing, I have wrapped the library with SWIG.

Creation of a wrapper

The module’s name will be IRT. For the vctor, I’ll use Numpy and its C API.

Here is the main file:

%{
#define SWIG_FILE_WITH_INIT
#define PY_ARRAY_UNIQUE_SYMBOL PyArray_API
%}
%include "numpy.i"
%init %{
import_array();
%}

%include "constraints.i"

%module(package="IRT", docstring="Python interface to the Interactive RayTracer") IRT

%nodefaultdtor;
%nodefaultctor;

%include "primitives.i"
%include "simple_scene.i"
%include "raytracer.i"

Nothing out of the ordinary, the only trick is not to generate a default constructor or destructor so that nothing bad can happen.

Handling primitives (and lights in the future) is tricky: when creating an object, Python will handle its life, but in IRT’s case, the life time is the scene responsibility. Without special instructions, if the Python object is deleted, so is the undelying C++ object and then we look at memory corruption, double frees, …. I’ll use a SWIG function to split this.

%{
#include "primitives.h"
%}

%typemap(in)
    (IRT::Vector3df&)
    (PyArrayObject* array=NULL, int is_new_object=0)
{
  array = obj_to_array_contiguous_allow_conversion($input, NPY_FLOAT, &is_new_object);
  if (!array || !require_dimensions(array, 1) || (array->dimensions[0] != 3)) SWIG_fail;

  $1 = new IRT::Vector3df((float*) array->data);
}
%typemap(freearg)
    (IRT::Vector3df&)
{
  if (is_new_object$argnum && array$argnum) Py_DECREF(array$argnum);
}

%typemap(in)
    (IRT::Color& color)
    (PyArrayObject* array=NULL, int is_new_object=0)
{
  array = obj_to_array_contiguous_allow_conversion($input, NPY_FLOAT, &is_new_object);
  if (!array || !require_dimensions(array, 1) || (array->dimensions[0] != IRT::nbColors)) SWIG_fail;

  $1 = new IRT::Color((float*) array->data);
}
%typemap(freearg)
    (IRT::Color&)
{
  if (is_new_object$argnum && array$argnum) Py_DECREF(array$argnum);
}

I use here the constant IRT::nbColors. This indicates the number of colors (which means that I could use more that RGB, or it can be useful in other contexts, as acoustics). These typemaps are freely inspired by Numpy’s.

The following typemaps handle giving or taking life responsibility from Python to C++ when interacting with the scene.

%typemap(in) IRT::Primitive*
{
  if ((SWIG_ConvertPtr($input,(void **)(&$1),$1_descriptor, SWIG_POINTER_EXCEPTION | SWIG_POINTER_DISOWN)) == -1) SWIG_fail;
}

%typemap(out) IRT::Primitive*
{
  $result = SWIG_NewPointerObj($1, $1_descriptor, SWIG_POINTER_OWN);
}

namespace IRT
{
  class Primitive
  {
  public:
    ~Primitive();
  };

  class Sphere: public Primitive
  {
  public:
    Sphere(IRT::Vector3df& ray, float dist);
    ~Sphere();
    void setColor(IRT::Color& color);
  };
}

I didn’t expose the whole interface to Python. I don’t want everyone to do fancy stuff with the raytracer’s inner code.

Now, for the scene, there is not much to say:

%{
#include "simple_scene.h"
%}

%apply Pointer NONNULL{IRT::Primitive*};

namespace IRT
{
  class SimpleScene
  {
  public:
    SimpleScene();
    ~SimpleScene();
    IRT::Primitive* removePrimitive(unsigned long index);
    bool addPrimitive(IRT::Primitive* primitive);
  };
}

If a NULL pointer is passed to the function, SWIG throws itself an exception, I don’t have to handle this case myself. This is done in constraints.i.

Finally, this is the raytracer wrapper:

%{
#include "raytracer.h"
%}

%typecheck(SWIG_TYPECHECK_DOUBLE_ARRAY)
  (float* INPLACE_ARRAY)
{
  $1 = is_array($input) && PyArray_EquivTypenums(array_type($input),NPY_FLOAT);
}
%typemap(in)
  (float* INPLACE_ARRAY)
  (PyArrayObject* array=NULL, int is_new_object=0)
{
  array = obj_to_array_contiguous_allow_conversion($input, NPY_FLOAT, &is_new_object);
  $1 = ($1_ltype) array->data;
}

namespace IRT
{
  class Raytracer
  {
  public:
    Raytracer(unsigned long pixelWidth, unsigned long pixelHeight, float width, float height, float depth);
    ~Raytracer();

    void draw(float* INPLACE_ARRAY);
    void setResolution(unsigned long pixelWidth, unsigned long pixelHeight);
    void setScene(IRT::SimpleScene* scene);
  };
}

The raytracer wrapper must be able to set its scene, draw it and change the actual resolution. The main issue here is the image where the scene will be drawn on, implemented here as a Numpy array in Python.

Python scene and display

To generate profiles, I’ve created a small sample scene:

import IRT
import numpy

class Sample(object):
  def __init__(self):
    self.raytracer = IRT.Raytracer(800, 600, 8., 6., 40.)
    self.scene = IRT.SimpleScene()

    sphere = IRT.Sphere(numpy.array((0, 0, 80.), dtype=numpy.float32), 2.)
    sphere.setColor(numpy.array((0., 0., 1.), dtype=numpy.float32))
    self.scene.addPrimitive(sphere)
    sphere = IRT.Sphere(numpy.array((2., 1., 60.), dtype=numpy.float32), 1.)
    sphere.setColor(numpy.array((1., 0., 0.), dtype=numpy.float32))
    self.scene.addPrimitive(sphere)
    sphere = IRT.Sphere(numpy.array((-2., -1., 60.), dtype=numpy.float32), 1.)
    sphere.setColor(numpy.array((0., 1., 0.), dtype=numpy.float32))
    self.scene.addPrimitive(sphere)

    self.raytracer.setScene(self.scene)

  def __call__(self, screen):
    self.raytracer.draw(screen)

The screen has a size of (800,600) with a resolution of 0.1. Three spheres will be drawn, one blue, one green and one red. __call__() is where the time can be measured or the profile is relevent.

import timeit

setup="""import numpy
import sample
import pylab

s = sample.Sample()

screen = numpy.zeros((600, 800, 3), dtype=numpy.float32)
"""

timer = timeit.Timer('s(screen)', setup=setup)

print timer.timeit(10)

Here is the result with Matplotlib:

A simple scene with 3 spheres (Matplotlib)

The next post on IRT will expose lights and shadows.

Buy Me a Coffee!
Other Amount:
Your Email Address:

Leave a Reply