glsl area light (implementation)

Started by
18 comments, last by ArKano22 14 years, 5 months ago
I´ve implemented rectangular area lights in glsl (I missed them from Maya and 3dsMax :__ ). They´re not the real thing but an approximation, however they work quite well. I´ve used a spotlight as a basis, so they´re very simple and fast, also. You can orient them and change their width and height. They cast no shadow, that will be a different thread :D (a very blurred pcss shadowmap should do the trick). I´m very excited about them because (at least for me) they open a new world of illumination schemes! Screenies: Code: (there is room for optimization but it´s usable as it is)

vec3 projectOnPlane(in vec3 p, in vec3 pc, in vec3 pn)
{
    float distance = dot(pn, p-pc);
    return p - distance*pn;
}
int sideOfPlane(in vec3 p, in vec3 pc, in vec3 pn){
   if (dot(p-pc,pn)>=0.0) return 1; else return 0;
}
vec3 linePlaneIntersect(in vec3 lp, in vec3 lv, in vec3 pc, in vec3 pn){
   return lp+lv*(dot(pn,pc-lp)/dot(pn,lv));
}
void areaLight(in int i, in vec3 N, in vec3 V, in float shininess,
                inout vec4 ambient, inout vec4 diffuse, inout vec4 specular)
{
    vec3 right = normalize(vec3(gl_ModelViewMatrix*gl_LightSource.ambient));
    vec3 pnormal = normalize(gl_LightSource.spotDirection);
    vec3 up = normalize(cross(right,pnormal));

    //width and height of the area light:
    float width = 1.0; 
    float height = 4.0;

    //project onto plane and calculate direction from center to the projection.
    vec3 projection = projectOnPlane(V,vec3(gl_LightSource.position.xyz),pnormal);// projection in plane
    vec3 dir = projection-vec3(gl_LightSource.position.xyz);

    //calculate distance from area:
    vec2 diagonal = vec2(dot(dir,right),dot(dir,up));
    vec2 nearest2D = vec2(clamp( diagonal.x,-width,width  ),clamp(  diagonal.y,-height,height));
    vec3 nearestPointInside = vec3(gl_LightSource.position.xyz)+(right*nearest2D.x+up*nearest2D.y);

    float dist = distance(V,nearestPointInside);//real distance to area rectangle

    vec3 L = normalize(nearestPointInside - V);
    float attenuation = calculateAttenuation(i, dist);

    float nDotL = dot(pnormal,-L);

    if (nDotL > 0.0 && sideOfPlane(V,vec3(gl_LightSource.position.xyz),pnormal) == 1) //looking at the plane
    {   
        //shoot a ray to calculate specular:
        vec3 R = reflect(normalize(-V), N);
        vec3 E = linePlaneIntersect(V,R,vec3(gl_LightSource.position.xyz),pnormal);

        float specAngle = dot(R,pnormal);
        if (specAngle > 0.0){
	    vec3 dirSpec = E-vec3(gl_LightSource.position.xyz);
    	    vec2 dirSpec2D = vec2(dot(dirSpec,right),dot(dirSpec,up));
          vec2 nearestSpec2D = vec2(clamp( dirSpec2D.x,-width,width  ),clamp(  dirSpec2D.y,-height,height));
    	    float specFactor = 1.0-clamp(length(nearestSpec2D-dirSpec2D)*shininess,0.0,1.0);
          specular += gl_LightSource.specular * attenuation * specFactor * specAngle;   
        }
        diffuse  += gl_LightSource.diffuse  * attenuation * nDotL;  
    }
   
    ambient  += gl_LightSource.ambient * attenuation;
}


Specular is correctly calculated as you can see, along with diffuse and ambient. I see lots of potential uses for this in any graphics application, for example: -fluorescent bulbs (spot, omni or directional can´t even approximate this) -windows -glowing panels -lava pits ... You can even fake GI (fake because you are emitting light, not reflecting it) using some well-positioned area lights: in a room that has bright green floor, placing a greenish area light pointing upwards there can give quite a boost to the scene at a negligible cost. [Edited by - ArKano22 on November 6, 2009 6:23:14 PM]
Advertisement
Nicely done. Haven't taken the time to understand the code. Some drawings and a more verbose write up would be nice. But nicely done!
Quote:Original post by bzroom
Nicely done. Haven't taken the time to understand the code. Some drawings and a more verbose write up would be nice. But nicely done!


Thank you!

Well, here goes an explanation about the shader code:

Basically what i´m doing is shading each pixel proportionally to its distance to an oriented rectangle. For this, my algorithm goes like this:

Step 1.- Project the soon-to-be-shaded point on the plane that contains the area light.
Step 2.- Calculate distance from the center of the rectangle to the projected point.
Step 3.- Obtain 2D coordinates of the projected point on the plane (how do you call this? the area´s object space?) using the distance calculated in (2) along with area height and width.
Step 4.- Clamp the projected point in 2D so that it lies inside the area.
Step 5.- This is the area point that lies nearest to our point. Retrieve distance between both points, using the width and height of the area to take back the nearest point to eye-space. This is used to calculate attenuation as usual.
Step 6.- In an area light each point is shaded from several directions at once, to simulate this multiply the usual dot product between normal and light direction by a factor and clamp it, so that you get a range of normals that get maximum illumination, then a smooth decay as they turn away from the light.

Specular:
As usual, reflecting the light direction calculated previously. Clamp the specular so that it doesn´t appear outside the area rectangle.

I´ll add some drawings later to illustrate this.

EDIT: Here´s a video showing them in action:


[Edited by - ArKano22 on November 3, 2009 7:45:32 PM]
It seems like the line lights elsewhere on this forum could benefit from step 6.

I'm assuming that step makes a considerable difference.

Thanks for the writeup.
This work looks fantastic, and I'll probably try and implement an HLSL version myself sometime over the weekend.

Could I get a definition of what i and V represent please? I'm guessing V is view space position, as for i I'm not really sure since your calculateAttenuation() function is not shown in the code in your post.
Looks very nice! About shadows... I think we can adjust PCF filter accordingly to distance to the rectangle.
Quote:Original post by adt7
This work looks fantastic, and I'll probably try and implement an HLSL version myself sometime over the weekend.

Could I get a definition of what i and V represent please? I'm guessing V is view space position, as for i I'm not really sure since your calculateAttenuation() function is not shown in the code in your post.


Hello! I´m glad you like it :)

i is the light index. In my engine i use forward shading and for that i iterate over all lights adding their contribution. So usually i goes from 0 to GL_MAX_LIGHTS.

V is the view space position, calculated this way in the vertex shader:
// Calculate the normal (N)normal = normalize(gl_NormalMatrix * gl_Normal);   // Transform the vertex position to eye space (V)vertex = vec3(gl_ModelViewMatrix * gl_Vertex);


The calculateAttenuation function is just like the fixed function pipeline:
float calculateAttenuation(in int i, in float dist){    return(1.0 / (gl_LightSource.constantAttenuation +                  gl_LightSource.linearAttenuation * dist +                  gl_LightSource.quadraticAttenuation * dist * dist));}
Quote:Original post by KRIGSSVIN
Looks very nice! About shadows... I think we can adjust PCF filter accordingly to distance to the rectangle.


That´s just what i was thinking of doing with pcss. However getting the light view point can be tricky because of the light´s variable size, depending on that you must render the shadowmap from a different perspective.
As promised here are two drawings explaining the method:

First of all found the nearest point inside the area: for that project V on the plane that contains the area. Then take it to object space and clamp its position to the width and height of the area to find the nearest point. Calculate the distance between V and the nearest point:



After calculating the attenuation using that distance, we have to simulate somehow that in a rectangular area light does not come from a single point. I chose to use a simple approximation, so that the highest possible illumination is not only for normals directly facing the area but for a wider range of normals.



And that´s basically it. The specular is pretty simple and does not need to be explained, it´s the usual thing.
an orthographic projection wouldn't have been simpler ?

This topic is closed to new replies.

Advertisement