简体   繁体   中英

Creating a rectangular light source in OpenGL?

I am trying to create a rectangular, sharp-edge light source in OpenGL for one application. My idea is to create a spot light and somehow mask the shape of the shade into a rectangle, the mask of course has to be invisible through camera. When I was trying to implement this idea, it turns out that OpenGL will just skip rendering objects outside the camera, although lighting source outside camera is still valid. This has prevented me from creating the effect I wanted and I am wondering if any of you have come across similar problems before.

To make my question more specific, consider the following case of my question:

spot light at 0,0,5

target object at 0,0,0

mask object (a simple quad parallel to x-axis) at 0,0,3.

When camera is at 0,0,4, light passes through mask object and leaves a rectangular shape on the target object (which is what I wanted), but I can also see the mask object!(while I need the mask object to be invisible)

When I move the camera closer to the target object, say 0,0,2. The mask object is behind the camera and therefore invisible. However, since it's invisible, OpenGL stopped rendering it and therefore the mask object does not have any effect on the target object, and the light shade is still round!

My guess would be to start from a spot light, but separating the angle calculation: * Project the L vector on the YZ plane to calculate the angle on the X axis * Project the L vector on the XZ plane to calculate the angle on the Y axis

A very naive implementation of this could be (GLSL):

varying vec3 v_V; // World-space position
varying vec3 v_N; // World-space normal

uniform float time; // global time in seconds since shaderprogram link
uniform vec2 uSpotSize; // Spot size, on X and Y axes

vec3 lp = vec3(0.0, 0.0, 7.0 + cos(time) * 5.0); // Light world-space position
vec3 lz = vec3(0.0, 0.0, -1.0); // Light direction (Z vector)

// Light radius (for attenuation calculation)
float lr = 3.0;

void main()
{
    // Calculate L, the vector from model surface to light
    vec3 L = lp - v_V;

    // Project L on the YZ / XZ plane
    vec3 LX = normalize(vec3(L.x, 0.0, L.z));
    vec3 LY = normalize(vec3(0.0, L.y, L.z));

    // Calculate the angle on X and Y axis using projected vectors just above
    float ax = dot(LX, -lz);
    float ay = dot(LY, -lz);

    // Light attenuation
    float d = distance(lp, v_V);
    float attenuation = 1.0 / (1.0 + (2.0/lr)*d + (1.0/(lr*lr))*d*d);

    float shaded = max(0.0, dot(v_N, L)) * attenuation;

    if(ax > cos(uSpotSize.x) && ay > cos(uSpotSize.y))
        gl_FragColor = vec4(shaded); // Inside the light influence zone, light it up !
    else
        gl_FragColor = vec4(0.1); // Outside the light influence zone.
}

Again, this is very naive. For instance, the X/Y projection is done in world-space. If you want to be able to rotate the light rectangle, you might have to introduce a vector pointing to the right of the light. Thus, you'll be able to get the fragment coordinate in the light's coordinate frame, and with this, you can decide whether to shade the fragment or not.

One solution might be adapting the calculations used for projective texture lookups to simulate a rectangular light source. You did not specify which OpenGL version you're using, but projective texture lookups can even be achieved with the fixed function pipeline - although they're arguably easier to do in a shader.

在此处输入图片说明

Of course, this would not simulate a rectangular area light source, just a point light source that is constrained to a rectangular region.

Using this approach, you'd have to specify view & projection matrices for the light source; where the view matrix is essentially generated by a 'look at' with the light position & it's direction; the projection matrix encodes a perspective projection with your desired horizontal & vertical 'field of view'.

If you just want a rectangular area, you don't even need a texture; A simple vertex/ fragment shader pair could look like this: ( the vertex shader basically transforms the position to the light's clip space, the fragment shader performs the clipping & computes a lambert shading if the fragment is inside the light frustum )

#version 330 core

layout ( location = 0 ) in vec3 vertexPosition;
layout ( location = 1 ) in vec3 vertexNormal;
layout ( location = 3 ) in vec3 vertexDiffuse;

uniform mat4 modelTf;
uniform mat3 normalTf;
uniform mat4 viewTf;                    // view matrix for render camera
uniform mat4 projectiveTf;              // projection matrix for render camera
uniform mat4 viewTf_lightCam;           // view matrix of light source
uniform mat4 projectiveTf_lightCam;     // projective matrix of light source

uniform vec4 lightPosition_worldSpace;

out vec3 diffuseColor;
out vec3 normal_worldSpace;
out vec3 toLight_worldSpace;
out vec4 position_lightClipSpace;

void main()
{
    diffuseColor = vertexDiffuse;
    vec4 vertexPosition_worldSpace = modelTf * vec4( vertexPosition, 1.0 );
    normal_worldSpace = normalTf * vertexNormal;
    toLight_worldSpace = normalize( lightPosition_worldSpace - vertexPosition_worldSpace ).xyz;
    position_lightClipSpace = projectiveTf_lightCam * viewTf_lightCam * vertexPosition_worldSpace;
    gl_Position = projectiveTf * viewTf * vertexPosition_worldSpace;
}

#version 330 core

layout ( location=0 ) out vec4 fragColor;

in vec3 diffuseColor;
in vec3 normal_worldSpace;
in vec3 toLight_worldSpace;
in vec4 position_lightClipSpace;

uniform vec3 ambientLight;

void main()
{
    // clipping against the light frustum
    bool isInsideX = ( position_lightClipSpace.x <= position_lightClipSpace.w && position_lightClipSpace.x >= -position_lightClipSpace.w );
    bool isInsideY = ( position_lightClipSpace.y <= position_lightClipSpace.w && position_lightClipSpace.y >= -position_lightClipSpace.w );
    bool isInsideZ = ( position_lightClipSpace.z <= position_lightClipSpace.w && position_lightClipSpace.z >= -position_lightClipSpace.w );
    bool isInside = isInsideX && isInsideY && isInsideZ;

    vec3 N = normalize( normal_worldSpace );
    vec3 L = normalize( toLight_worldSpace );
    vec3 lightColor = isInside ? max( dot( N, L ), 0.0 ) * vec3( 0.99, 0.66, 0.33 ) : vec3( 0.0 );

    fragColor = vec4( clamp( ( ambientLight + lightColor ) * diffuseColor, vec3( 0.0 ), vec3( 1.0 ) ), 1.0 );
}

There are a lot of good papers on this, Brian Karis wrote about it in 2013 (in regards to UE4) here:

https://de45xmedrsdbp.cloudfront.net/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf

And more recently Michal Drobot wrote an article about area lights in GPU Pro 5.

If you are using a metalness workflow you can also crank up the roughness as an approximation to area lighting, a technique introduced by Tri-Ace:

http://www.fxguide.com/featured/game-environments-parta-remember-me-rendering/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM