Tutorials
Deferred Rendering
Tutorial Tip: How to do deferred rendering - Part 2
   
 
Survey
Would you prefer this tutorial to be unrelated from the Psyche Engine or do you find it useful as it is?




English Italian

Tutorial - Deferred Rendering (Part 2)

In this second part of our Deferred Rendering's tutorial we will change the previous example reading the light's parameters from a texture. With this solution we can load any amount of light's data into our scene. Moreover I've changed the scene itself defining a more articulated room with more elements.

It is possible to work on the finished version of this example. It can be found in the folder that contains all the main classes (Psyche engine is required).


  OpenGL direct rendering The three data textures and the light's data texture Deferred rendering results Deferred rendering results using warmer lights  

Since we've changed the scene the initialization function changes consequently; the main modification is about objects' loading calls.

try{
   CheckAssignment(milosStatue = DataObjectManager::getInstance()->
      createGLSLRenderObjectFromFile("./models/milosStatue.exo",
      "shaders/DeferredRendering/Milo/deferredShading.vert",
      "shaders/DeferredRendering/Milo/deferredShading.frag") );

   CheckAssignment(milosColumns = DataObjectManager::getInstance()->
      createGLSLRenderObjectFromFile("./models/milosColumns.exo",
      "shaders/DeferredRendering/Milo/deferredShading.vert",
      "shaders/DeferredRendering/Milo/deferredShading_bump_specular.frag") );

   CheckAssignment(milosRoom = DataObjectManager::getInstance()->
      createGLSLRenderObjectFromFile("./models/milosRoom.exo",
      "shaders/DeferredRendering/Milo/deferredShading.vert",
      "shaders/DeferredRendering/Milo/deferredShading_bump_specular.frag") );

Once we created the objects we initialize FBO's buffer, as we've done in the previous tutorial, and then we initialize a new texture which uses PBO (Pixel Buffer Object). This last texture is obtained from a specific class that Psyche provides, and allows to read/write every single pixel on the texture's surface.

   lightsTexture = new PBOFloatTexture(2, LIGHTS_COUNT, GL_RGB,
                                       GL_RGB32F_ARB);
   defBuffer = new FBODeferredShadingBuffer(shWIDTH,shHEIGHT);

   for(int i=0; i<LIGHTS_COUNT; i++)
      impostor[i] = new Impostor(30,
         "shaders/DeferredRendering/Milo/deferredShading.vert",
         "shaders/DeferredRendering/Milo/deferredShading_impostor.frag",
         "./textures/lighticon.png");

The first two values from constructor PBOFloatTexture specifies the texture's width and height. Since we are going to save, for every light, both position and color information, we will define the texture two pixel wide (exactely one for position and the other one for color) and so many pixel as lights amount high. The contructor's remaining parameters specify that the texture is RGB and uses an internal format of 32bit per channel, that is an HDR (High Dynamic Range) texture.

The following for cycle is used to initialize impostors (also known as billboards); we use impostors to render a simple icon in the very place where the light is positioned. Impostors are loaded using a specific fragment shader that we are going to analyze later in this tutorial.

The initialization function ends setting some remaining parameters almost in the same way we did in the previous tutorial. The only noticeable difference is that we pass the light's data texture to the deferred rendering filter.

   vector <Texture*> filterTextures;
   filterTextures.push_back(defBuffer);
   filterTextures.push_back(lightsTexture);

   ImageFilterBuilder builder;
   builder.addAcquireFromTextureOperator(filterTextures);
   builder.addDeferredShadingFilter(
   "./shaders/DeferredRendering/Milo/deferredRendering.vert",
   "./shaders/DeferredRendering/Milo/deferredRendering.frag");
   builder.addRenderToScreenOperator();
   screenFilter = builder.getImageFilter();

   agConsole.hide();
}
catch(PException* e){
   LogMex("Error",e->sException);
   return P_FAIL;
}

The other function we analyze is doRender(). In this function we set the light's data to the texture and we perform the rendering itself. First of all, let's check the first rows where we create the texture that contains the light's data:

void DeferredRenderingMilo::doRender(){
   Camera::setCurrentCamera(&camera);
   glDisable(GL_LIGHTING);

   float pixels[2*LIGHTS_COUNT*3];
   int index = 0;
   for (int row=0; row<3; row++)
      for (int col=0; col<LIGHTS_COUNT/3; col++){
         //Light Position
         pixels[index + 0] = col * 150.0f - 5.0f +
                   FastCos(Time::time()*(col-row+2)*0.008f)*200.0f+210;
         pixels[index + 1] =
                   FastCos(Time::time()*(col-row+2)*0.01f)*200.0f+210;
         pixels[index + 2] = row * 500.0f -500.0f -
                   FastCos(Time::time()*(col-row+2)*0.009f)*200.0f+210;
         //Light Color
         pixels[index + 3] = 1;
         pixels[index + 4] = 1-row/3.0f;
         pixels[index + 5] = col/(LIGHTS_COUNT/3.0f);

         impostor[index/6]->setPosition(pixels[index + 0],
                                        pixels[index + 1],
                                        pixels[index + 2]);

         index += 6;
      }
   lightsTexture->updatePixels(pixels);

Pixel's array is built 2 pixels wide and "light's amount" pixels high taking into account every channel. This means that the first three array's elements are the RGB values of the top left pixel in the texture.

The following for cycle simply assigns positions' and colors' values in function of row, column and time. Notice that we also update every impostor's position to match lights' motion. The remaining rows of this function are analogous to the same portion of code in the first part of this tutorial. The main difference is that we render the impostors too.

   defBuffer->start();
      milosStatue->render();
      milosColumns->render();
      milosRoom->render();

      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA,GL_ONE);
      for(int i=0; i<LIGHTS_COUNT; i++)
            impostor[i]->render();
      glDisable(GL_BLEND);

   defBuffer->stop();

   screenFilter->executeFiltering();
};

Anyway, the most interesting part is the deferrend rendering's shader, updated to receive the lights texture and to use it. Let's analyze its code and comment it.

uniform sampler2D tImage0;
uniform sampler2D tImage1;
uniform sampler2D tImage2;
uniform sampler2D lightTexture;
uniform vec3 cameraPosition;

void main( void )
{
   vec3 diffuse = vec3(0,0,0);
   vec3 specular = vec3(0,0,0);
   vec3 light = vec3(0,0,0);
   vec3 lightDir = vec3(0,0,0);
   vec3 vHalfVector = vec3(0,0,0);
   vec4 image0 = texture2D( tImage0, gl_TexCoord[0].xy );
   vec4 position = texture2D( tImage1, gl_TexCoord[0].xy );
   vec4 normal = texture2D( tImage2, gl_TexCoord[0].xy );
   vec3 eyeDir = normalize(cameraPosition-position.xyz);

   float lightIntensity = 0;
   float specularIntensity = image0.a;
   float selfLighting = position.a;
   normal.w = 0;
   normal = normalize(normal);

The code so far just generates all the useful variables. Diffuse's and specular's vectors store RGB values for the respective components while light's, lightDir's and vHalfVector's vectors are going to be used for lighting. The three vectors image0, position and normal store the three textures and the correspondend data as previously described. Lastly, the view vector eyeDir represent a normal vector from the camera's position to the current point's position.

LightIntensity scalar is used to compute the lighting's intensity for the current light. The two following scalars are different from the previous tutorials and show how we are using the diffusive texture's alpha channel to store the specular intensity value, moved inside specularIntesity, while selfLighting reads from position texture's alpha channel how much self-illumination the pixel has.
The very last rows are just a normalization safety pass.

Let's analyze the lighting section of the shader.

   // Diffusive
   float lightCount = 30;
   for(int i=0; i<lightCount; i++){
      light = texture2D( lightTexture,vec2(0.01,i*0.99/lightCount) ).xyz;
      lightDir = light - position.xyz ;
      float lightDistance = length(lightDir);
      lightDir /= lightDistance;
      lightIntensity = 1 / ( 1.0 + 0.00005 * lightDistance +
                                   0.00009 * pow(lightDistance,2));

      vec3 lightColor = texture2D( lightTexture,
                                   vec2(0.99,i*0.99/lightCount) ).xyz;
      vHalfVector = normalize(lightDir+eyeDir);

      float localDiffuse = max(dot(normal,lightDir),0);
      diffuse += lightColor * localDiffuse * lightIntensity;

      if(localDiffuse>0)
         specular += lightColor * pow(max(dot(normal,vHalfVector),0.0),10) *
                     lightIntensity * 2;
   }

   gl_FragColor = vec4(diffuse,1) * image0 +
                  vec4(specular * specularIntensity * 20, 1) +
                  selfLighting * (image0);
}

Variable lightCount is manually set to 30 since we have no particular needings about managing the lights' number dinamically; obviously was possible to code in any of the lights texture's pixels so we would have been able to read the light's number at run-time. Similarly any fixed parameter, as light's radius, can be read from the light's texture adding some pixels to it.

Variable light is loaded reading the position of the current light from the light's texture; the computation I've performed to read the first pixel is not very accurate, I've just read from position 0.1 where I know there is the left pixel. Vertical movement on the texture works similarly.

Variable lightColor is set reading from the light's texture again, but this time from the right pixel; I search for this information in position 0.99, therefore I just read the value using the specified coordinate. Anyway, if one wants to use more parameters, it would be undoubtly more elegant to implement a specific function for this task.

The remaining computations are the same of the previous tutorial, and anyway they are widely documented (Blinn-Phong lighting model).

The last shader shader is worth to analyze is the impostor's one, even if it is very simple.

varying vec4 position;
varying vec4 normals;
varying mat4 TBN;
uniform sampler2D tDiffuse;

void main( void )
{
   gl_FragData[0] = texture2D(tDiffuse,gl_TexCoord[0].st);
   gl_FragData[1] = vec4(0,0,0,gl_FragData[0].a);
   gl_FragData[2] = vec4(0,0,0,0);
}

The interesting thing shown in this shader is that the position's texture alpha channel (gl_FragData[1]) contains self-illumination data. In practice if it is set as 1, the pixel is fully self-illuminated, if it is set on 0, it just receive the normal shading, else we will use an interpolation of this two ways. Notice how this parameter is read from the alpha channel of the diffusion texture: this allows us to have the impostor that self-illuminate itself where the texture has alpha at 1, and we have the impostor to be transparent where alpha is null.


I hope this tutorial helped you! I wrote it presuming that the reader is, at least, an intermediate graphic programmer therefore I omitted to describe the code row by row. There is no doubt that Psyche engine simplify a lot the code, therefore I really ask you to download the code and to put your hands inside it to get how it works.

As always, for anything you may need, comments, errors' notification, requestes, etc., don't esitate to get in touch with me writing at mail

  Home
About
Curriculum Vitae
Compact Portfolio
Bio
portfolio
Engine section division bar
Psyche Engine
Zenith Engine
Game section division bar
Orbital
Arkam
PoolOMatic
Formula
Other Works section division bar
Cloth Simulation
Cloth Simulation
Papers button
Simple OpenGL Deferred Rendering tutorial
Memory Pools tutorial
Setup Psyche tutorial
Deferred Shading Part 1 tutorial
Deferred Shading Part 2 tutorial
Connections button
     

admin | home | about | psyche | zenith | links