Most Efficient Way for Lighting in Forward Rendering

Started by
12 comments, last by 21st Century Moose 9 years, 10 months ago

I'm currently using Forward Lighting and trying to do my best to make sure that the FPS game will always render all the lights

1. In Pixel Shader 2.0, I have


DirectionalLight dirLights[maxLights];
PointLight pointLights[maxLights];
SpotLight spotLights[maxLights];

What is the appropriate number for the variable maxLights?

2. Since Forward Lighting is limited to a certain amount of lights, I have been thinking that I could:

- Detect collision between the light and the camera bounding frustum and only render the light if the camera can see it

- Arrange the lights from closest to farthest to the camera and remove any light if it's index is over 'maxLights' variable

What do you think about my idea? What is the most efficient way to work with forward lighting?

Advertisement
I think you're on the right way, compared to what I do myself.
In pseudo:

- cull lights against frustum and enabled yes/no
- cull objects and their renderables (subsets)
- for each visible renderable check which visible positional lights affects it
- render per renderable using the found lights

You can get away with this using max 4 or 8 positional lights per renderable, depending on the scene. For terrain I currently cut the large mesh in chunks and it's all good. You might want to find another solution for handling large renderables/subsets.

As far as I know there's no max number of lights in forward rendering (from shader model 3 onwards), of course there will be if performance drops. I would first try the above and see how it goes.

In practice I also didn't have more then 2 or 3 directional lights in a scene.

Another hint which might help, compile different version of your effect with different number of max lights, so you can choose the effect with less performance waste. You can do this with compiling using "defines", for example the effect has MAX_PLIGHTS arrays, where you define MAX_PLIGHTS as a parameter when compiling the effect. For example one for 2, 4 and one for 8 lights (effectively per renderable). Make sure you sort your renderables in some sort of renderqueue, to prevent switching effects for each renderable. To make sure this really improves performance (versus filling not used lights and looping through them) you could do some profiling.

Good luck

Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me


What is the appropriate number for the variable maxLights?

That all depends on the scene you are rendering. If you have a total of 4 lights in your scene, then of course it makes no sense to all for 8 of them in your shader. I would recommend to take the advice that cozzie provided, and just have one combination for each number of lights so you can choose the appropriate one when you need to.

Forward rendering doesn't necesssarily have this limit. You could do a multipass setup and handle one light per pass, which looks something like:

  1. Draw all scene geometry with your ambient light colour
  2. Switch to additive blending
  3. For each light:
    1. Draw all geometry that the light hits

A trick, which would work if you're not concerned about shadows, is to use instancing for step (3) above; each light would make very good per-instance data.

That would give you an arbitrary maximum number of lights in a forward rendering setup.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

@mhagain: Should I handle only one light per pass or more than one light?

Maybe I could set the maxLights to 4 and If the lights on a mesh exceed 4 lights, I can render it multiple times.

Which is better?

@mhagain: Should I handle only one light per pass or more than one light?

Maybe I could set the maxLights to 4 and If the lights on a mesh exceed 4 lights, I can render it multiple times.

What's your suggestion?

Profile and find out.

This is an "it depends" answer. How many lights does a typical scene have? Of those, how many lights do you typically have per object? How many per pixel? Does having a 4 light shader hurt in cases where an object has fewer than 4 lights on it? And are those cases common enough that you need to care? What's your target hardware? How does it perform with multipass versus how does it perform with more complex shaders? And don't forget how you're going to (or not going to) handle shadows.

In other words I can't make a suggestion here.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

@mhagain: I'm having a problem with blending diffuse color with light using multipass, here is the Shader:

(I'm getting white meshes)


// ....
PS_OUTPUT PS_Diffuse( VS_OUTPUT IN )
{
         // ...
         OUT.Color = tex2D(diffuseMap, IN.UV).rgb;
}

PS_OUTPUT PS_Lighting( VS_OUTPUT IN )
{
         OUT.Color = CalcLighting();
}

//-----------------------------------------------------------------------------
// Techniques.
//-----------------------------------------------------------------------------
technique Lighting
{
    // Diffuse pass
    pass Pass0
    {
        AlphaBlendEnable = TRUE;
        DestBlend = INVSRCALPHA;
        SrcBlend = SRCALPHA;
        ZEnable = TRUE;
        VertexShader = compile vs_3_0 VS();
        PixelShader = compile ps_3_0 PS_Diffuse();
    }


    // Lighting pass
    pass Pass1
    {
        AlphaBlendEnable = TRUE;
        DestBlend = ONE;
        SrcBlend = ONE;
        ZEnable = FALSE;
        VertexShader = compile vs_3_0 VS();
        PixelShader = compile ps_3_0 PS_Lighting();
    }
}
If you want to use forward shading and lots of lights, you almost certainly want Forward+ shading (which a lot of AAA games are moving to).

The basic gist of the idea is to use a compute shader to bin all lights into the scene into a 2D grid indicating which parts of the screen they intersect with and then using the appropriate bin for all lighting calculating for a particular fragmnet. The binning is done by breaking the screen into a grid, generated a frustrum for each part of the grid, then doing an intersection test against each light volume and the frustrum for a particular grid location. If the light intersects, add it to that bin. More advanced versions of Forward+ deal with depth more intelligently.

It's a more advanced technique than either regular forward shading or deferred shading due to the use of compute shader, but it isn't that much more complex. It's all the same math you already have to do anyway (frustrum generation, intersection tests, etc.) just now it's in a computer shader instead of CPU code.

Sean Middleditch – Game Systems Engineer – Join my team!

@mhagain: I'm having a problem with blending diffuse color with light using multipass, here is the Shader:

(I'm getting white meshes)

I don't see what's happening in your CalcLighting function, but you almost certainly don't want what I think you have.

The first thing is that the first pass isn't a diffuse pass, it's an ambient pass. This can be black (just output 0 from your PS) if you don't have any ambient light, but it's critically important to do it even if so as it's laying down a baseline for the additive passes. Alpha blending should be disabled, enable depth writing, do a standard depth test.

The second and subsequent passes are your additive passes. In order to understand how to do each of these, you first need to look at what a single pass/multiple light setup would look like:

output = ambient + diffuse * (light0 + light1 + light2 + light3); // assume 4 lights for the purpose of this example

Using some basic mathematics, let's break that into 5 passes, ambient plus 1 for each light, and we get:

output = ambient; // initial ambient pass - I've already covered this above

output += diffuse * light0; // first additive pass

output += diffuse * light1; // second additive pass

output += diffuse * light2; // third additive pass

output += diffuse * light3; // fourth additive pass

So each additive pass needs to sample the diffuse texture as well as calculate the lighting. The additive blend adds it to the result from the previous pass(es), enable depth testing but set the depth test to equal, disable depth writing, and you'll get the (mostly) correct result.

I put in "mostly" above because the one caveat is that if your lighting goes above 1.0 each pass will clamp, so it's not going to look 100% identical to a single pass/multiple light setup. You can of course draw to a floating point rendertarget if this is a problem for you, but I'd encourage experimentation before making that decision - it may be just fine as is.

If you want to use forward shading and lots of lights, you almost certainly want Forward+ shading (which a lot of AAA games are moving to).

The basic gist of the idea is to use a compute shader to bin all lights into the scene into a 2D grid indicating which parts of the screen they intersect with and then using the appropriate bin for all lighting calculating for a particular fragmnet. The binning is done by breaking the screen into a grid, generated a frustrum for each part of the grid, then doing an intersection test against each light volume and the frustrum for a particular grid location. If the light intersects, add it to that bin. More advanced versions of Forward+ deal with depth more intelligently.

It's a more advanced technique than either regular forward shading or deferred shading due to the use of compute shader, but it isn't that much more complex. It's all the same math you already have to do anyway (frustrum generation, intersection tests, etc.) just now it's in a computer shader instead of CPU code.

I suspect that the OP is using D3D9 (PS2.0 in the first post, SM3 in the effect file code).

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

@mhagain: It's now working well, however I see Z-Fighting in the animated character (only when the animation is playing)

I'm using D3D9, C++

This topic is closed to new replies.

Advertisement