• Create Account

Most Efficient Way for Lighting in Forward Rendering

Old topic!

Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

13 replies to this topic

#1Medo3337  Members

891
Like
1Likes
Like

Posted 13 June 2014 - 05:49 PM

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?

Edited by Medo3337, 13 June 2014 - 06:04 PM.

#2cozzie  Members

4628
Like
5Likes
Like

Posted 14 June 2014 - 05:43 AM

POPULAR

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

#3Jason Z  Members

6416
Like
0Likes
Like

Posted 14 June 2014 - 09:40 AM

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.

Jason Zink :: DirectX MVP

Direct3D 11 engine on CodePlex: Hieroglyph 3

Games: Lunar Rift

#4mhagain  Members

12436
Like
6Likes
Like

Posted 14 June 2014 - 12:43 PM

POPULAR

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
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.

Edited by mhagain, 14 June 2014 - 12:43 PM.

It appears that the gentleman thought C++ was extremely difficult and he was overjoyed that the machine was absorbing it; he understood that good C++ is difficult but the best C++ is well-nigh unintelligible.

#5Medo3337  Members

891
Like
0Likes
Like

Posted 14 June 2014 - 01:53 PM

@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?

Edited by Medo3337, 14 June 2014 - 02:31 PM.

#6mhagain  Members

12436
Like
0Likes
Like

Posted 14 June 2014 - 02:33 PM

@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.

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.

It appears that the gentleman thought C++ was extremely difficult and he was overjoyed that the machine was absorbing it; he understood that good C++ is difficult but the best C++ is well-nigh unintelligible.

#7Medo3337  Members

891
Like
0Likes
Like

Posted 14 June 2014 - 03:42 PM

@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;
}

// Lighting pass
pass Pass1
{
AlphaBlendEnable = TRUE;
DestBlend = ONE;
SrcBlend = ONE;
ZEnable = FALSE;
}
}

#8SeanMiddleditch  Members

16772
Like
0Likes
Like

Posted 14 June 2014 - 04:01 PM

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.

Game Developer, C++ Geek, Dragon Slayer - http://seanmiddleditch.com

C++ SG14 "Games & Low Latency" - Co-chair - public forums

Wargaming Seattle - Lead Server Engineer - We're hiring!

#9mhagain  Members

12436
Like
2Likes
Like

Posted 14 June 2014 - 06:15 PM

@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).

Edited by mhagain, 14 June 2014 - 06:16 PM.

It appears that the gentleman thought C++ was extremely difficult and he was overjoyed that the machine was absorbing it; he understood that good C++ is difficult but the best C++ is well-nigh unintelligible.

#10Medo3337  Members

891
Like
0Likes
Like

Posted 14 June 2014 - 08:43 PM

@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++

#11mhagain  Members

12436
Like
0Likes
Like

Posted 15 June 2014 - 06:22 AM

@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++

On some hardware you may see this if you use a different D3DPT_ in some of the passes.  I got hit by it once; thinking "If I'm only drawing 4 vertices I'll draw it as a strip or fan rather than an indexed triangle list, and save on some buffer locking".  Bad idea and Z-fighting was the result.

So draw everything as an indexed triangle list and it should be fine.

Edited by mhagain, 15 June 2014 - 06:23 AM.

It appears that the gentleman thought C++ was extremely difficult and he was overjoyed that the machine was absorbing it; he understood that good C++ is difficult but the best C++ is well-nigh unintelligible.

#12Medo3337  Members

891
Like
0Likes
Like

Posted 15 June 2014 - 11:37 PM

@mhagain: I'm rendering all the meshes using LPD3DXMESH, so I only use pMesh->DrawSubset() and I still have the Z-Fighting problem when animation is playing

Another problem, when I draw the mesh multiple times I get very bright mesh (even if there is no lighting)

Here is what I have in the Shader:

pass0 = ambient pass (render once)

pass1 = tex2D(ColorMap, IN.UV) + light (repeat rendering according to the number of lights)

    pass pass0
{
AlphaBlendEnable = FALSE;
DestBlend = ONE;
SrcBlend = ZERO;
ZEnable = TRUE;
}

pass pass1
{
AlphaBlendEnable = TRUE;
SRCBLEND = ONE;
DESTBLEND = ONE;
ZWRITEENABLE = TRUE;
ZEnable = TRUE;
}

#13Medo3337  Members

891
Like
0Likes
Like

Posted 16 June 2014 - 03:58 PM

I figured out that the Z-Fighting was due to the animation getting updated every time the mesh is rendered, so I changed the code to only update once every frame no matter how many times I do draw calls.

The problem now is that I the mesh is getting very bright when I render it multiple times as the following:

PS Pass 0: return Ambient;

PS Pass 1: return tex2D(ColorMap, IN.UV) + Light1();

PS Pass 1: return tex2D(ColorMap, IN.UV) + Light2();

PS Pass 1: return tex2D(ColorMap, IN.UV) + Light3();

I believe that it's getting very bright because tex2D(ColorMap, IN.UV) is added multiple times.

How do I fix it?

#14mhagain  Members

12436
Like
3Likes
Like

Posted 16 June 2014 - 05:12 PM

return tex2D(ColorMap, IN.UV) + Light1();

Typically you'd use * not +.

I'm not sure why you're using +; that would be expected to make things excessively bright for sure.  Think about it: if a light value is 0 that means it's fully dark so you want to return black (0); on the other hand if it's 1 that means it's fully bright so you return the texture colour; it should be clear that you use:

return tex2D(ColorMap, IN.UV) * Light1();

And not:

return tex2D(ColorMap, IN.UV) + Light1();

If you go back to my earlier post where I derive the additive blending method from basic mathematics you'll see that it's correct to also read the texture for every pass, because t * (l0 + l1 + l2 + l3) == (t * l0) + (t * l1) + (t * l2) + (t * l3).  This is the same as 2 * (1 + 3) == (2 * 1) + (2 * 3).

It appears that the gentleman thought C++ was extremely difficult and he was overjoyed that the machine was absorbing it; he understood that good C++ is difficult but the best C++ is well-nigh unintelligible.

Old topic!

Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.