Real Time Graphics assignment

This project was the ACW from the C++ and Design module and the Real Time Graphics module. We needed to develop an application on top of GXbase framework (based on OpenGL), that will implement different rendering effects. 


The world is given by a box and all the objects are drawn inside it. Inside the box, platonic solids can be added (they are 3D models, loaded from obj files). The box has 8 spot lights, one in each corner, which are able to cast shadows. A glow sphere with 6 spot lights on its surface can be added in the scene. The app also includes particle balls, which follow the same concept as the glow sphere, but have a particle systems instead of lights. The particle systems emit particles that stick to the walls as soon as they hit them, leaving scorch marks. The colour and the transparency of the particles are changed over time. All the objects can be animated using a given path (of form Acos(Bx+C)+Dsin(Ex+F)+G). The objects follow the trajectory and are aligned to the tangent of the curve. If the objects try to get out of the box, their position on the trajectory is projected on the walls of the box. There is a blooming effect that can be applied on all the light sources. Fog has been introduced, and while this display mode is toggled, light beams become visible (from each light source).

The application has several display modes: Flat shaded, Gouraud and Phong. The first 2 had to use standard OpenGL calls, while the last one needed the use of shaders (i used GLSL, shader model 4.0).
So, I will describe some algorithms that might be of interest: implementing Phong shaders, implementing shadows, implementing the particle systems and making the blooming effect.

1. Phong shaders

The fragment shader receives as input the lights using OpenGl structures. The lights’ positions and directions are already transformed in world space, so the vertex shader does not modify light’s parameters.


The fragment shader computes the color of the fragment adding the ambient, diffuse, specular and emissive components:
Where K is the diffuse/emissive material coefficient, T1=(r,g,b,a) is the color of the first texture and T2=(r2,g2,b2,a2) is the color of the second texture. If the fragment is not textured T1=T2=1. “diff” and “spec” for each light are elements (in R^4) that are computed by the fragment shader for each light (see Figure):

f is the attenuation factor, Ldiff/spec is the light’s colour, res is a scalar value (0 or 1) used for shadows and with I denoted the dot product between v and u. Intensity is given by the spot’s cone of light:
Where x=cosθ and theta is the angle between D and l as shown in Figure 1. It is obvious from the above equation that if theta is in the inner cone, the fragments gets the full intensity of the light, if it is outside the outer cone, the fragment gets no light. If theta is in the transition area, the fragment gets only a fraction of the light’s intensity, which is the Hermite interpolation between cosInner and cosOuter. s and d are computed using glsl function “lit”:      (1,d,s,1)=lit(1,N.L,N.H,shininess) ,   and Intensity=smoothstep(cousOuter, cosInner, cosine).
For inserting shadows in the scene we compute res:       res=shadow2DProj(shadowMap, shadowCoords).
If res is zero it means that the fragment is in shadow, thus its diff and spec must be zero. Otherwise, they are lightened. In this equation, shadowmap is the depth texture that has been rendered from the position of the light, and shadowCoords are the corresponding shadow coordinates, as described in the shadowmapping algorithm.

2. Shadows


Shadows are implemented using the shadowmapping algorithm. There are two versions of the algorithm – one for the standard pipeline and one using shaders. First, the scene is drawn from the position of the light. The depth buffer is then copied into a texture. When the scene is drawn using phong shaders, the phong shader decides if a fragment is in shadow or not, using the GLSL function shadow2DProj. This compares the depth from the shadowmap with the corresponding depth of the fragment, returning 0(fragment is in shadow) or 1(lightened). For addressing the shadowmap, the algorithm uses the texture coordinates (s,t,r,q) computed in the vertex shader:

Where (s/q, t/q) are the texture coordinates needed to address the shadowmap, and r/q is the depth of the fragment in the light’s modelview space. This algorithm takes place for each light, so each light has its own depth texture and own texture coordinates. Thus, the shadowmaps are sent using separate multitexture layers.
For implementing shadows in the standard pipeline, I used automatic texture coordinates generation for computing (s,t,r,q). The matrices above are multiplied, and the rows are used to generate GL_S, GL_T, GL_R and GL_Q. Alternatively, we could have generated the coordinates using the identity matrix, and use the texture matrix stack to make the multiplication on GPU. However, shadowmapping using the standard pipeline has a lot of problems. Firstly, it cannot address more than 4 textures simultaneously, using the current version of OpenGL. As I use one texture for materials, only 3 lights can cast shadows in the standard mode. I could have implemented more passes to have more lights casting shadows, but it would have decreased the fps. Other bugs come from using the depth texture. The pipeline has troubles at depth fighting, the shadow having undesired effects while applied on the shadow casters, at the border between shadow and light. More than that, if a section is in shadow from one light, it is drawn black, irrespectively if it is or not lightened by other lights. Another bug comes from the texture wrapping in standard pipeline. The depth textures are currently set to “clampToEdge”. OpenGL prolongs the shadow outside the cone of light, as the shadow coordinates get outside the texture. If I change the wrapping mode to GL_CLAMP the area outside the first cone of light will be in shadow, so all the scene appears black. Problems also occur when the glow ball is casting shadows in the standard pipeline. I suppose the wrapping mode is to blame here as well. All these problems do not appear when I use shaders. More than that, glsl shaders support more types of shadowmaps, like GL_COMPARE_R_TO_TEXTURE_ARB. Standard pipeline worked only with GL_TEXTURE_COMPARE_SGIX.

3. Particle systems

The particle sphere owns 6 particle systems, gathers the required information from them and displays them on the screen. If the particles are point sprites, they are drawn using the standard pipeline. If they are quads, a shader is used for drawing, and additional information is required from the particle systems. I will describe the case of quad particles, as point particles use the same algorithm but simplified.

The particle sphere allocates 3 buffers: a vertex buffer, a colour buffer and a texture one. The sizes of the buffers are calculated for the maximum number of particles. Each particle system can compute the maximum number of living particles it can have, which is (10*m_lifeMax+1)*m_emissionRate. (10 because life decreases with 0.1). It is a memory waste, as the buffer will probably never be filled, but it will not need to be reallocated during drawing, thus it will be fast.

Each particle system contains a list of active particles and a list of “scorch” particles. At each frame the partycle systems are emitting new particles, processing old ones, turning the corresponding particles into scorch marks and killing the particles with no life remained. The active particles are treated just like point particles (one vertex representing the centre of the quad is processed). The scorch particles, however, need to store 4 vertices, so they are properly oriented when they hit the wall. In the update pass, the lists of particles are modified and the information is collected by the particle sphere, as shown in the following diagram:
 

It is notable that the lengths of the lists are modifying at each frame, that is the reason why the particles are processed by the CPU and not by the shaders. The sphere owns now several arrays which are used to update the allocated GPU buffers. The buffers are dynamic and are updated at each frame. However, there is one static buffer, the texture buffer. Each quad will have the same texture coordinates, so the texture buffer will be allocated at the beginning on the graphics board, and never modified. The information collected is stored as in the following diagram:

The colour buffer contains for each vertex the size of the particle (sx and sy), the current life of the particle (l) and the total life of the particle (tl).

The vertex shader takes the centre of the particle and has to compute 4 vertices that will make the oriented quad. That is the reason why the centre is sent 4 times, so each copy may be translated in the correct position. For computing the position of the vertices the shader needs to compute
 V_i=C±s_x/2*(right)±s_y/2*(v_up ) 

as shown in the diagram:



The centre  of  the particle  is  first  transformed  in  the  modelview space. Now we  know  that the right direction of the camera is (1,0,0) and v_up is (0,1,0) so the formula above becomes:

V_i=C+(±s_x/2,±s_y/2,0)=C+(TX,TY,0)


TX and TY are computed from the texture coordinates:

TX=s_x*(t_x-1/2) , TY=s_y*(t_y-1/2).

The scorch particles are already computed by the CPU, because it had all the information needed (i.e. the wall the particle hit, the needed orientation), so the shader just applies the modelview transform on them.

During their life the particles fade and change their colour. The vertex shader uses the total life and current life of a particle to determine a parameter in [0,1] used for interpolation: alpha=(current_life)/(total_life).
The colour is computed using a 1D texture: myColor=texture2D(colorTexture,vec2(alpha,0.0));
The fragment shader textures the quad and applies particle’s colour. The quad particles are thus drawn without lighting computations.
//GLSL VS:
uniform int drawScorch;
uniform sampler2D colorTexture;
varying vec2 textureCoords;
varying float alpha; //is computed according to the amount of life left

varying vec4 myColor;


void main(void){
        alpha=gl_Color.z/gl_Color.w;

//this if is slow, scorch particles and normal particles should have
//used separated shaders
if (drawScorch==0){
        vec4 worldVertex = gl_ModelViewMatrix * gl_Vertex;
        vec2 texCoords =gl_Color.xy*(gl_MultiTexCoord0.xy -  vec2(0.5,0.5));
        worldVertex.xyz=worldVertex.xyz+vec3(texCoords.x,texCoords.y,0);
        //this is equivalent to:
        //  worldVertex.xyz=worldVertex.xyz+
        //    cameraRight*(gl_MultiTexCoord0.x-0.5)+
        //    cameraVup*(gl_MultiTexCoord0.y-0.5);
        //where cameraRight=(1,0,0); cameraVup=(0,1,0);
        //after the modelview transform, camera will always have these values
        gl_Position=gl_ProjectionMatrix*worldVertex;
    }else{
        gl_Position = ftransform();
    }
  
myColor=texture2D(colorTexture,vec2(alpha,0.0));
    textureCoords=gl_MultiTexCoord0.xy;

       
}

//--------------------------------------------------------------------------------

//GLSL PS

uniform sampler2D diffuseTexture;
varying vec2 textureCoords;
varying float alpha;
varying vec4 myColor;

void main(void){
    vec4 color=texture2D(diffuseTexture, textureCoords);
    gl_FragColor=vec4(myColor.xyz*color.xyz,color.w*alpha);
}



4. Blooming





If the blooming effect is active, the scene is drawn into a texture. I’ll call it “sceneTexture”. Then the lights are drawn for obtaining the part of the scene that needs to be blurred. There are 2 modes of implementing blooming: using mipmaps and using several textures:


Mipmap blooming


The lights are drawn into a texture that has the same size with the sceneTexture. Mipmap levels are then created directly on the graphics board. The mipmapped texture goes through a blurring stage, where a Gaussian filter is applied. The mipmap levels are added together using a shader, and the final image is combined with the sceneTexture: sceneColor+=0.6*blurColor.


Blooming using multiple textures


This type of blooming does not generate mipmap levels for the texture as previously, but it draws the glowing objects several times, using different viewports. The main problem that occurs here is the depth buffer. The scene has been drawn at a particular resolution, and if we want to draw using another viewport we must recalculate the depth buffer. The algorithm uses 4 textures, having the dimensions (w/2,h/2), (w/4,h/4), (w/8.h/8), (w/16,h/16), where w and h are the dimensions of the original framebuffer. For each texture, the scene is drawn for storing the depth buffer. It is drawn without applying shaders or materials, and without writing the color buffer.  Then the glowing objects are rendered into textures. The Gaussian filter is applied on each texture, and the final colour adds the colours from the blurred textures with the sceneTexture. It is more accurate than the first method, but slower (depending of the complexity of the scene).


Gaussian blur


The Gaussian filter applied is actually an approximation of a Gaussian kernel, using only the middle vertical and horizontal line from a 15x15 kernel. Because the kernel is symmetric, only 15 Gaussian coefficients are computed using the normal distribution: G(x,y)=1/√(2πσ^2 ) e^(-(x^2+y^2)/(2σ^2 )) with sigma=3. The textures go through a horizontal filter, which blures a texel using the neighbours from the same line. Then a vertical filter is applied on the semi-processed texture, blurring with the neighbours from the same column. The blurring is done using shaders: a quad of the size of the framebuffer is drawn, and every texel becomes a fragment. Thus the fragment shader can apply transformations on each texel of the image. If the scene is drawn at a very detailed resolution, the blurring effect can become slow, having more and more fragments to process.