A quick question comparing FF pipeline lighting to programmable shader pipeline...

Started by
16 comments, last by Moe 18 years, 8 months ago
Hey all, I have been thinking about this a little. I haven't really tried out using shaders for myself, but I am wondering how hard it is to implement lighting using shaders compared to using the fixed function pipeline. By this I mean, how hard is it to add/remove lights using shaders as compared to the fixed function pipeline? With the FF pipeline it seemed like a piece of cake - simply declare a new D3DLIGHT9, fill out the parameters, and turn it on. I imagine shaders are a bit more difficult when trying to accomplish something similar. Am I correct on this?
Advertisement
Hi there Moe,
How are you doing?

[The problem]
When working with shaders, how easy is it to incorporate light?

[The theory/Solution]
In shaders it's not as hard as you would think given that you know the theory. A normal light equation:

diffuse = (n • l)
n : Normal vector
l : Light vector

So you would give the shader your light position, then calculate the light in your shader and multiply it with your color.

e.g.
float 3 light = normalize(IN.lightVec);
float diffuse = saturate(dot(normal, light));
OUT.color = color * diffuse;

probably not the best example but it should put you on the right track.
It's got alot to do with maths.

I hope this helps,
Keep cool.
Yeah, I am considering the move to using shaders (when I actually get some programming done, one of these months. From what I am hearing about the future of graphics hardware/programming, the fixed function pipeline is being phased out entirely.

From what I have heard, shader model 3 supports looping and branching, doesn't it? Wouldn't that make things simpler by doing a loop for the number of active lights in a scene? I would like to be able to add and remove lights without having to write a bunch of different shaders. I would prefer to have one general shader that could handle whatever I throw at it (nothing too complicated, albeit).
Hi there Moe,
How are you doing?

[The questions]
From what I have heard, shader model 3 supports looping and branching, doesn't it?
Shader model 3.0 features

Wouldn't that make things simpler by doing a loop for the number of active lights in a scene?
I would normally do a pass for each active light in the shader. So if you just want 1 light, you do 1 pass in the shader... etc. That might get tedious and messy and I'm sure there are other more efficient methods to do it.

I would like to be able to add and remove lights without having to write a bunch of different shaders. I would prefer to have one general shader that could handle whatever I throw at it (nothing too complicated, albeit).

I refer to shaders as a "coloring book" The shader you use is what you want to color your object in with. Some of the objects you want to color in with the same coloring pens etc... Others you want a different coloring/shading scheme.

I hope this helps a bit.
Keep cool.
Quote:By this I mean, how hard is it to add/remove lights using shaders as compared to the fixed function pipeline? With the FF pipeline it seemed like a piece of cake - simply declare a new D3DLIGHT9, fill out the parameters, and turn it on. I imagine shaders are a bit more difficult when trying to accomplish something similar. Am I correct on this?


Once you have the shader up and running, yes it is just as easy as declaring a new D3DLIGHT9. You set some new floats, and *BAM* you've got a new light. In fact, translating your light data from D3DLIGHT9 to floats in shader should be trivial.

Quote:From what I have heard, shader model 3 supports looping and branching, doesn't it? Wouldn't that make things simpler by doing a loop for the number of active lights in a scene? I would like to be able to add and remove lights without having to write a bunch of different shaders. I would prefer to have one general shader that could handle whatever I throw at it (nothing too complicated, albeit).


SM3 does support looping, but it's essentially useless. All array-esque data that can be used as part of loop iterations is vertex input registers, of which you're limited to ten. A better, simpler, solution would be to, in your render function, have a loop where you render the scene for each light onscreen, doing additive blends on each render. Correct me if I'm wrong, but I THINK the RenderState is for SRCBlend to be SRCBLEND_ONE and destblend to be DESTBLEND_ONE. This way, you have one shader for all of the lights in your scene. Is it the fastest given what we can do with hardware nowadays? Not even close, but you want simplicity at this point, so it's the best solution. It is also friendly to older hardware (Doom3 does one light at a time, for example).

Later on, when you have more experience, you'll want to set up seperate shaders that can stuff together two, three, four or more lights to reduce the number of passes, and therefore geometry processing you have to do.
Look at shaders, they are a hard first step, but you will get used to them.

The appeal of the fixed function pipeline as it comes to lighting is that you can keep adding lights to a scene and you never really see any difference in how you do your drawing. One light? Two lights? Sure there is a performance issue but you do not really have to worry about anything else.

When you have shaders that are custom written, it becomes a bit of a hassle. As you metioned, you do not want to write a shader for each combination of lights and such. You also just want to be able to add a new light structure. Well, it is never going to be that easy unless you do the setup right. Some people have suggested doing multiple passes for each light in the scene - while this works and really is flexible you do burn horse power by having to transform all the vertices for each light. It does give you a flexible system for doing the lighting - one pass for each light. One light, one pass. Five lights? Five passes.

What we have done is written our own little interface that manages lighting. Objects in the scene can be lit by, at most, three lights at a time. We have a standardized shader constant set for lights 0, 1 and 2. And we wrote our HLSL shader code to use the same lighting code, included from a global file. All the lighting uses the same lighting code, so it is pretty easy to manage changes to the lighting.

Our light manager collects the lights active in the scene, determines which lights an object is lit by and, based on priority of the lights, puts the first three lights into the shader constants. If there are less then three lights, it simply puts an "Empty Light" which is a light of zero intensity. The object is then rendered using the shader.

This worked pretty well but we realized there really were cases when we were wasting a lot of cycles rendering objects with the three light system when it did not need it. To solve this, we compiled our shaders three times, each time defining a different preprocessor constant: #define ONE_LIGHT, #TWO_LIGHTS, etc. The compiler would generate three shaders outputs for each shader input. Since, at runtime, the light manager knows how many lights are active for an object, it can provide a hint to the rendering system to upload a program with the correct number of lights, of available.

This solved our performance issue, but it also resulted in a lot of thrashing of program uploads, so we went a step further and sorted geometry to be done first by shader type & number of lights, then by the lights themselves. Changing shader constants is cheaper than changing shaders, so this was the sort logic we used.

All in all it works pretty well - using HLSL with a common lighting block made it very easy to provide different lighting code based on ONE_LIGHT, TWO_LIGHT defines without having to change code everywhere else.


Just a point about multipassing, would you not want to do a single pass for each light, but with that light all at once, and then draw the objects again a second time for the next light? I am not familiar with the exact process for this, but it is just a thought.

As for branching, yes you could loop and branch but you will run out of code and constant space in the shader. Also, looping and branching always results in a performance hit compared to a non looped shader.

Best of luck.

-S
Quote:Just a point about multipassing, would you not want to do a single pass for each light, but with that light all at once, and then draw the objects again a second time for the next light? I am not familiar with the exact process for this, but it is just a thought.


That's basically how it works, actually.
Quote:Original post by Cypher19
Quote:Just a point about multipassing, would you not want to do a single pass for each light, but with that light all at once, and then draw the objects again a second time for the next light? I am not familiar with the exact process for this, but it is just a thought.


That's basically how it works, actually.


So the renderer is storing all the geometry used by a light and rendering everything one light at a time, instead of one model at a time like some other rendering schemes I have seen?
Quote:Original post by Sphet
So the renderer is storing all the geometry used by a light and rendering everything one light at a time, instead of one model at a time like some other rendering schemes I have seen?

Yes, it's handled like that. First all lights are rendered to framebuffer using additive blending and then, finally, a diffuse pass is modulated on top of that. Usually there is also a z-fill/ambient pass at the beginning of the frame, but you may have known this already. [smile]

Just out of curiosity, how did the FF pipeline handle multiple lights? Did it do multiple passes (albeit, in hardware)?

This topic is closed to new replies.

Advertisement