October 25, 2013

Deferred Lighting: The backdoor, Part 1


Today, I was asked about the theory behind the Deferred Lighting procedure, of which, to be honest, I was starting to forget (too much Functional programming lately); but what is well learned, is never forgotten.

The main idea behind this technique is to delay as much as possible the calculations of light over the objects on the scene, only those that deserve to be rendered will be performed, otherwise will be discarded. This also relies on "several" passes, which also means, the first time an scene is going to be rendered as normal: vertex positions, its normals and the material for each surface and instead of sending it to the screen it is transformed into a "texture" with information of the depth, normals and specular. This last texture becomes the input for the next step, where lighting takes place using the information from the variables from the previous phase. And then one "last" pass calculates all objects in the scene once again but this time it combines the color from the texture with the emissive and ambient light. The image below shows graphically how they are all added:



I would like to publish the whole project, but due to NDA constraints that I signed with the university in order to be able to access the SCE SDK, I will only release the main and renderer classes that I used on the PS3 SDK. Those files can be found in here: https://github.com/rhvall/PS3DeferredLighting.

I am going to explain some parts of the code that are important and that could be useful for others, if there are questions, that what comments are for :).

       SceneNode *root = renderer.getRootNode();
#ifdef DEFERRED_LIGHTING
        Physics *physx = new Physics(root);
#endif
        HeightMap *heightMap = new HeightMap(HEIGHTMAP_RAW);
        heightMap->setDefaultTexture(HEIGHTMAP_TEXTURE);
        heightMap->setBumpTexture(HEIGHTMAP_BUMPMAP);
        SceneNode *heightMapNode = new SceneNode(heightMap, 
                Vector3(1), Vector3(1), HEIGHTMAP);
        heightMapNode->invInertia = Matrix4(0);
        heightMapNode->mass = 1000.0f;
        root->AddChild(*heightMapNode);
        renderer.height = heightMapNode;

General initialization is performed, first the root node, which is the one that is going to help us manage any object on the scene as well as make the respective calls to be rasterized when appropriate. The Physics is the part of the engine in charge of any collision detection as well as the forces calculating repulsion, mostly with the HeightMap, which also has textures, a position and physics variables. As any object in the scene, is referenced to the root node.

        OBJMesh *aSheep = new OBJMesh(SHEEP_OBJ);
        aSheep->setDefaultTexture(SHEEP_TEXTURE);
        aSheep->setBumpTexture(SHEEP_BUMPMAP);

        static unsigned short NUMBER_OF_SHEEP = 3;
        SceneNode *sheepsArray[NUMBER_OF_SHEEP];

        for(int i = 0; i < NUMBER_OF_SHEEP; i++)
        {
                SceneNode *sphereNode = 
                        new SceneNode(aSheep, 
          HEIGHTMAP_MIDDLE + Vector3(i * 100.0f, 200.0f, i * 50.0f),
          Vector3(100.0f, 100.0f, 100.0f), SPHERE);
                sphereNode->collisionable = true;
                sheepsArray[i] = sphereNode;
                root->AddChild(*sphereNode);
        }

        Input::SetPadFunction(INPUT_CROSS, crossButton, 
                static_cast(&(sheepsArray[0]->force)));

        OBJMesh *aCube = new OBJMesh(CUBE_OBJ);
        //aCube->setDefaultTexture("/Resources/CubeTexture.tga");
        SceneNode *cubeNode = new SceneNode(aCube, 
                HEIGHTMAP_MIDDLE, CUBE_SCALE, CUBE);
        cubeNode->invInertia = Matrix4(0);
        cubeNode->mass = 1000.0f;
        cubeNode->boundingRadius = 1;
        root->AddChild(*cubeNode);

This section create two things: the sheeps on the screen that interact with the user, and the classic transparent wall that 99.99% of the games use to prevent a character to fall from within allowed limits. Also, we send function pointers to the Input class to perform an action when a push on a button is detected (coff coff Delegates coff coff).

                Input::UpdateJoypad();        
                float msec = (float)gameTime.GetTimedMS();
                renderer.rotation = msec * 0.005f;
                physx->update(msec);
                root->Update(msec);
                renderer.RenderScene();

Everything before this piece of code is less relevant, because it takes charge of playing a sound and initialize a threaded network server to receive notifications from the desktop version. Later on, there are stages where the main screen is shown and waits for the user to continue the execution. But what is really interesting is the loop which repeats every frame, and that one is above: first, receive any notification from the joypad and execute the functions that we set previously. Get the time from the system, perform a little rotation on the lights (which will matter later on), update the physics engine according to the passed mili seconds, update the objects in the scene according to their position and lastly render everything once more.

Up until now, nothing had to be with the deferred lighting technique, everything was about initialization and later on the render loop. The real magic is built in the Renderer class, the one in charge of setting the puzzle altogether, lights and shaders calls, calculating everything per pixel/vertex. It seems like a good time to go for a chocolate and then come back to the number 2 of these articles; meanwhile, you can appreciate the video here:



To know more about this technique:

Deferred Lighting Rendering Path
Deferred shading



No comments:

Post a Comment