diff --git a/apps/tutorials/pe/02_ConfinedGasExtended.cfg b/apps/tutorials/pe/02_ConfinedGasExtended.cfg
index 9c1497e2eb5a87440d020808185970d7c40410e5..06f681aa9a8881d1619ea01b562fcc0c4af86aca 100644
--- a/apps/tutorials/pe/02_ConfinedGasExtended.cfg
+++ b/apps/tutorials/pe/02_ConfinedGasExtended.cfg
@@ -5,15 +5,27 @@ ConfinedGasExtended
    simulationDomain < 20, 20, 20 >;
    blocks < 2, 2, 2 >;
    isPeriodic < 0, 0, 0 >;
-   raytracerSkippedSteps 10;
    setupRun false;
+   simulationSteps 100;
+   visSpacing 10;
+}
+
+Raytracing 
+{
+   image_x 640;
+   image_y 480;
+   fov_vertical 49.13;
+   image_output_directory .;
+   cameraPosition < -25, 10, 10 >;
+   lookAt < -5, 10, 10 >;
+   upVector < 0, 0, 1 >;
+   antiAliasFactor 2;
+   blockAABBIntersectionPadding 0.8;
+   reductionMethod MPI_REDUCE;
+   Lighting {
+      pointLightOrigin < -10, 10, 15 >;
+      ambientColor < 0.3, 0.3, 0.3 >;
+      diffuseColor < 1, 1, 1 >;
+      specularColor < 1, 1, 1 >;
+   }
 }
-Raytracing {
-	image_x 640;
-	image_y 480;
-	fov_vertical 49.13;
-	tbuffer_output_directory /Users/ng/Desktop/walberla;
-	cameraPosition < -25, 10, 10 >;
-	lookAt < -5, 10, 10 >;
-	upVector < 0, 0, 1 >;
-}
\ No newline at end of file
diff --git a/apps/tutorials/pe/02_ConfinedGasExtended.cpp b/apps/tutorials/pe/02_ConfinedGasExtended.cpp
index 4ea95a8379095661e70a853ff79291090966aa83..8415f6946186863fece9913aeec8c1fedb720607 100644
--- a/apps/tutorials/pe/02_ConfinedGasExtended.cpp
+++ b/apps/tutorials/pe/02_ConfinedGasExtended.cpp
@@ -21,6 +21,7 @@
 #include <pe/basic.h>
 #include <pe/statistics/BodyStatistics.h>
 #include <pe/vtk/SphereVtkOutput.h>
+#include <pe/raytracing/Raytracer.h>
 
 #include <core/Abort.h>
 #include <core/Environment.h>
@@ -31,7 +32,6 @@
 #include <core/waLBerlaBuildInfo.h>
 #include <postprocessing/sqlite/SQLite.h>
 #include <vtk/VTKOutput.h>
-#include <pe/raytracing/Raytracer.h>
 
 using namespace walberla;
 using namespace walberla::pe;
@@ -105,10 +105,6 @@ int main( int argc, char ** argv )
    WALBERLA_LOG_INFO_ON_ROOT("visSpacing: " << visSpacing);
    const std::string path = mainConf.getParameter<std::string>("path",  "vtk_out" );
    WALBERLA_LOG_INFO_ON_ROOT("path: " << path);
-   
-   const int raytracerSkippedSteps = mainConf.getParameter<int>("raytracerSkippedSteps",  10 );
-   WALBERLA_LOG_INFO_ON_ROOT("raytracerSkippedSteps: " << raytracerSkippedSteps);
-   integerProperties["raytracerSkippedSteps"] = raytracerSkippedSteps;
 
    WALBERLA_LOG_INFO_ON_ROOT("syncShadowOwners: " << syncShadowOwners);
    integerProperties["syncShadowOwners"] = syncShadowOwners;
@@ -145,24 +141,6 @@ int main( int argc, char ** argv )
    auto ccdID               = forest->addBlockData(ccd::createHashGridsDataHandling( globalBodyStorage, storageID ), "CCD");
    auto fcdID               = forest->addBlockData(fcd::createGenericFCDDataHandling<BodyTuple, fcd::AnalyticCollideFunctor>(), "FCD");
 
-   WALBERLA_LOG_INFO_ON_ROOT("*** RAYTRACER ***");
-   //if (cfg == NULL) {
-   //   WALBERLA_ABORT("raytracer needs a working config");
-   //}
-   //Raytracer raytracer(forest, storageID, globalBodyStorage, ccdID,
-   //                    cfg->getBlock("Raytracing"));
-   Lighting lighting(Vec3(-12, 12, 12),
-                     Color(1, 1, 1),
-                     Color(1, 1, 1),
-                     Color(real_t(0.4), real_t(0.4), real_t(0.4)));
-   Raytracer raytracer(forest, storageID, globalBodyStorage, ccdID,
-                       640, 480,
-                       real_t(49.13), 2,
-                       Vec3(-25, 10, 10), Vec3(-5, 10, 10), Vec3(0, 0, 1),
-                       lighting,
-                       Color(real_t(0.1), real_t(0.1), real_t(0.1)),
-                       radius);
-   
    WALBERLA_LOG_INFO_ON_ROOT("*** INTEGRATOR ***");
    cr::HCSITS cr(globalBodyStorage, forest, storageID, ccdID, fcdID);
    cr.setMaxIterations( 10 );
@@ -190,6 +168,19 @@ int main( int argc, char ** argv )
       syncCallWithoutTT = boost::bind( pe::syncShadowOwners<BodyTuple>, boost::ref(*forest), storageID, static_cast<WcTimingTree*>(NULL), real_c(0.0), false );
    }
    //! [Bind Sync Call]
+   
+   WALBERLA_LOG_INFO_ON_ROOT("*** RAYTRACER ***");
+   //! [Raytracer Init]
+   std::function<ShadingParameters(const BodyID body)> customShadingFunction = [](const BodyID body) {
+      if (body->getTypeID() == Sphere::getStaticTypeID()) {
+         return processRankDependentShadingParams(body).makeGlossy();
+      }
+      return defaultBodyTypeDependentShadingParams(body);
+   };
+   Raytracer raytracer(forest, storageID, globalBodyStorage, ccdID,
+                       cfg->getBlock("Raytracing"),
+                       customShadingFunction);
+   //! [Raytracer Init]
 
    WALBERLA_LOG_INFO_ON_ROOT("*** VTK ***");
    //! [VTK Domain Output]
@@ -266,12 +257,9 @@ int main( int argc, char ** argv )
          vtkDomainOutput->write( );
          vtkSphereOutput->write( );
          //! [VTK Output]
-      }
-      
-      if ( i % raytracerSkippedSteps == 0) {
-         tp["Raytracing"].start();
+         //! [Image Output]
          raytracer.generateImage<BodyTuple>(size_t(i), &tt);
-         tp["Raytracing"].end();
+         //! [Image Output]
       }
    }
    tp["Total"].end();
diff --git a/apps/tutorials/pe/02_ConfinedGasExtended.dox b/apps/tutorials/pe/02_ConfinedGasExtended.dox
index 45c6cc4c9ca90be9cf6bb29f1026ab9df8640be2..952b60429df137020e522d76669b31975873b2ed 100644
--- a/apps/tutorials/pe/02_ConfinedGasExtended.dox
+++ b/apps/tutorials/pe/02_ConfinedGasExtended.dox
@@ -91,6 +91,34 @@ depending on the type of information you want to store.
 You can then dump the information to disc. timing::TimingPool and timing::TimingTree already have predefined save
 functions so you do not have to extract all the information yourself and save it in the property array.
 \snippet 02_ConfinedGasExtended.cpp SQL Save
+
+\section tutorial_pe_02_raytracing Image Output
+Using a raytracer snapshots of the simulation can be generated each timestep and output as PNG files.
+Setting up the Raytracer can be done by reading a config object, as it is done in the tutorial:
+\snippet 02_ConfinedGasExtended.cpp Raytracer Init
+Alternatively it can also be entirely setup in code:
+\code
+Lighting lighting(Vec3(-12, 12, 12),
+                  Color(1, 1, 1),
+                  Color(1, 1, 1),
+                  Color(real_t(0.4), real_t(0.4), real_t(0.4)));
+Raytracer raytracer(forest, storageID, globalBodyStorage, ccdID,
+                  640, 480,
+                  real_t(49.13), 2,
+                  Vec3(-25, 10, 10), Vec3(-5, 10, 10), Vec3(0, 0, 1),
+                  lighting,
+                  Color(real_t(0.1), real_t(0.1), real_t(0.1)),
+                  radius,
+                  customShadingFunction);
+\endcode
+To apply custom coloring to bodies, you can supply a user defined function which returns a ShadingParameters struct for
+a given BodyID. For an overview over predefined shading functions, visit the file ShadingFunctions.h.
+After the configuration is done, images can be generated each timestep by calling Raytracer::generateImage<BodyTuple>()
+which will be output to the specified directory.
+\snippet 02_ConfinedGasExtended.cpp Image Output
+For further information see the documentation for the classes Raytracer and Lighting, the ShadingParameters class and
+ShadingFunctions.h file may also be useful.
+
 */
 
 }