19
\$\begingroup\$

Is there a way to pass an arbitrary number of light locations (and colors) for the fragment shader, and loop over them in the shader?

If not, then how are multiple lights supposed to be simulated? For example with respect to diffuse directional lighting, you can't just pass a sum of the light weights for the shader.

\$\endgroup\$
4
  • \$\begingroup\$ I haven't been working with WebGL, but in OpenGL, you have maximally 8 light sources. In my opinion, if you want to pass more than that, you have to use for example uniform variables. \$\endgroup\$
    – zacharmarz
    Commented Nov 9, 2011 at 14:36
  • \$\begingroup\$ Old method was to always pass in all the lights, unused lights were set to 0 luminance and therefore wouldn't affect the scene. Probably not used much anymore ;-) \$\endgroup\$ Commented Nov 9, 2011 at 17:26
  • 7
    \$\begingroup\$ When you Google stuff like this, don't use the term 'WebGL' - the technology is too young for people to have even though about approaching these problems. Take this search for example, 'I'm feeling lucky' would have worked. Remember that a WebGL problem should translate nicely to the exact same OpenGL problem. \$\endgroup\$ Commented Nov 9, 2011 at 19:23
  • \$\begingroup\$ For more than 8 lights in forward rendering I generally use a multi-pass shader and give each pass a different group of 8 lights to process, using additive blending. \$\endgroup\$
    – ChrisC
    Commented Nov 9, 2011 at 23:09

3 Answers 3

30
\$\begingroup\$

There are generally two methods for dealing with this. Nowadays, they are called forward rendering and deferred rendering. There is one variation on these two that I will discuss below.

Forward rendering

Render each object once for every light that affects it. This includes the ambient light. You use an additive blend mode (glBlendFunc(GL_ONE, GL_ONE)), so each light's contributions are added to each other. Since the contribution of different lights are additive, the framebuffer eventually gets the valu

You can get HDR by rendering to a floating-point framebuffer. You then take a final pass over the scene to down-scale the HDR lighting values to a visible range; this would also be where you implement bloom and other post-effects.

A common performance enhancement for this technique (if the scene has a lot of objects) is to use a "pre-pass", where you render all of the objects without drawing anything to the color framebuffer (use glColorMask to turn off color writes). This just fills in the depth buffer. This way, if you render an object that is behind another, the GPU can quickly skip those fragments. It still has to run the vertex shader, but it can skip the typically more expensive fragment shader computations.

This is simpler to code and easier to visualize. And on some hardware (mainly mobile and embedded GPUs), it can be more efficient than the alternative. But on higher-end hardware, the alternative generally wins out for scenes with a lot of lights.

Deferred rendering

Deferred rendering is a bit more complicated.

The lighting equation you use to compute the light for a point on a surface uses the following surface parameters:

  • Surface position
  • Surface normals
  • Surface diffuse color
  • Surface specular color
  • Surface specular shininess
  • Possibly other surface parameters (depending on how complex your lighting equation is)

In forward rendering, these parameters get to the fragment shader's lighting function either by being passed directly from the vertex shader, being pulled from textures (usually through texture coordinates passed from the vertex shader), or generated from whole cloth in the fragment shader based on other parameters. The diffuse color may be computed by combining a per-vertex color with a texture, combining multiple textures, whatever.

In deferred rendering, we make this all explicit. In the first pass, we render all of the objects. But we don't render colors. Instead, we render surface parameters. So each pixel on the screen has a set of surface parameters. This is done via rendering to off-screen textures. One texture would store the diffuse color as its RGB, and possibly the specular shininess as the alpha. Another texture would store the specular color. A third would store the normal. And so on.

The position is usually not stored. It is instead reconstituted in the second pass by math that's too complex to get into here. Suffice it to say, we use the depth buffer and the screen-space fragment position as the input to figure out the camera-space position of the point on a surface.

So, now that these textures hold essentially all of the surface information for every visible pixel in the scene, we start rendering full-screen quads. Each light gets a full-screen quad render. We sample from the surface parameter textures (and reconstitute the position), then just use them to compute the contribution of that light. This is added (again glBlendFunc(GL_ONE, GL_ONE)) to the image. We keep doing this until we run out of lights.

HDR again is a post-process step.

The biggest downside to deferred rendering is antialiasing. It requires a bit more work to antialias properly.

The biggest upside, if your GPU has a lot of memory bandwidth, is performance. We only render the actual geometry once (or 1 + 1 per light that has shadows, if we're doing shadow mapping). We never spend any time on hidden pixels or geometry that isn't visible after this. All of the lighting pass time is spent on things that are actually visible.

If your GPU doesn't have lots of memory bandwidth, then the light pass really can start to hurt. Pulling from 3-5 textures per screen pixel isn't fun.

Light Pre-Pass

This is sort of a variation on deferred rendering that has interesting tradeoffs.

Just as in deferred rendering, you render your surface parameters to a set of buffers. However, you have abbreviated surface data; the only surface data you care about this time is the depth buffer value (for reconstructing the position), normal, and the specular shininess.

Then for each light, you compute just the lighting results. No multiplication with surface colors, nothing. Just the dot(N, L), and the specular term, completely without the surface colors. The specular and diffuse terms should be kept in separate buffers. The specular and diffuse terms for each light are summed up within the two buffers.

Then, you re-render the geometry, using the total specular and diffuse lighting computations to do the final combination with the surface color, thus producing the overall reflectance.

The upsides here are that you get multisampling back (at least, easier than with deferred). You do less per-object rendering than forward rendering. But the main thing over deferred that this provides is an easier time to have different lighting equations for different surfaces.

With deferred rendering, you generally draw the entire scene with the same shader per-light. So every object must use the same material parameters. With light pre-pass, you can give each object a different shader, so it can do the final lighting step on its own.

This doesn't provide as much freedom as the forward rendering case. But it is still faster if you have the texture bandwidth to spare.

\$\endgroup\$
9
  • \$\begingroup\$ -1: failure to mention LPP/PPL. -1 deferred: rendering is an instant win on any DX9.0 hardware (yes even on my 'business' laptop) - which is baseline requirements circa 2009. Unless you are targeting DX8.0 (which can't do Deferred/LPP) Deferred/LPP is default. Finally 'a lot of memory bandwidth' is insane - we generally aren't even saturating PCI-X x4 yet, furthermore, LPP drops the memory bandwidth substantially. Finally, -1 for your comment; loops like this OK? You know those loops are happening 2073600 times per frame, right? Even with the parrelism of the graphics card, it's bad. \$\endgroup\$ Commented Nov 9, 2011 at 20:29
  • 1
    \$\begingroup\$ @JonathanDickinson I think his point was that the memory bandwidth for deferred / light pre-pass is typically several times larger than for forward rendering. This doesn't invalidate the deferred approach; it's just something to consider when choosing it. BTW: your deferred buffers should be in video memory, so PCI-X bandwidth is irrelevant; it's the GPU's internal bandwidth that matters. Long pixel shaders, e.g. with an unrolled loop, are nothing to freak out about if they're doing useful work. And there's nothing wrong with the z-buffer prepass trick; it works fine. \$\endgroup\$ Commented Nov 9, 2011 at 20:45
  • 5
    \$\begingroup\$ @JonathanDickinson: This is talking about WebGL, so any discussion of "shader models" is irrelevant. And which kind of rendering to use is not a "religious topic": it is simply a matter of what hardware you're running on. An embedded GPU, where "video memory" is just regular CPU RAM, will work out very badly with deferred rendering. On a mobile tile-based renderer, it is even worse. Deferred rendering is not an "instant win" regardless of hardware; it has its tradeoffs, just like any hardware. \$\endgroup\$ Commented Nov 9, 2011 at 21:10
  • 2
    \$\begingroup\$ @JonathanDickinson: "Also, with the z-buffer pre-pass trick you are going to struggle to eliminate z-fighting with the objects that should be drawn." That's total nonsense. You're rendering the same objects with the same transform matrices and the same vertex shader. Multipass rendering was done in the Voodoo 1 days; this is a solved problem. Accumulating lighting does nothing to change that. \$\endgroup\$ Commented Nov 9, 2011 at 21:12
  • 8
    \$\begingroup\$ @JonathanDickinson: But we're not talking about rendering a wireframe, are we? We're talking about rendering the same triangles as before. OpenGL guarantees invariance for the same object being rendered (as long as you are using the same vertex shader, of course, and even then, there's the invariant keyword to guarantee it for other cases). \$\endgroup\$ Commented Nov 9, 2011 at 21:31
4
\$\begingroup\$

You need to use deferred rendering or pre-pass lighting. Some of the older fixed-function pipelines (read: no shaders) supported up to 16 or 24 lights - but that's it. Deferred rendering eliminates the light limit; but at the cost of a much more complicated rendering system.

Apparently WebGL supports MRT which is absolutely required for any form of deferred rendering - so it might be doable; I am just not sure how plausible it is.

Alternatively you could investigate Unity 5 - which has deferred rendering right out of the box.

Another simple way to deal with this is to simply prioritize lights (maybe, based on the distance from the player and whether they are in the camera frustum) and only enable the top 8. A lot of AAA titles managed to do this without much impact on the quality of the output (for example, Far Cry 1).

You could also look into pre-calculated lightmaps. Games like Quake 1 got a lot of mileage from these - and they can be quite small (bilinear filtering softens up stretched lightmaps quite nicely). Unfortunately pre-calculated excludes the notion of 100% dynamic lights, but it really does look great. You could combine this with your limit of 8 lights, so for example, only rockets or such would have a real light - but lights on the wall or such would be lightmaps.

Side note: You wan't to loop over them in a shader? Say goodbye to your performance. A GPU is not a CPU and isn't designed to work the same way that, for example, JavaScript does. Remember that each pixel you render (if even it gets overwritten) has to perform the loop - so if you take running at 1920x1080 and a simple loop that runs 16 times you are effectively running everything inside that loop 33177600 times. You graphics card will run a lot of those fragments in parallel, but those loops will still eat older hardware.

\$\endgroup\$
2
  • \$\begingroup\$ -1: "You need to use deferred rendering" This is not true at all. Deferred rendering is certainly a way to do it, but it is not the only way. Also loops are not that bad in terms of performance, especially if they are based on uniform values (ie: each fragment doesn't have a different loop length). \$\endgroup\$ Commented Nov 9, 2011 at 19:28
  • 1
    \$\begingroup\$ Please read the 4th paragraph. \$\endgroup\$ Commented Nov 9, 2011 at 20:33
2
\$\begingroup\$

You can use a pixel shader that supports n lights (where n is a smallish number like 4 or 8), and redraw the scene multiple times, passing a new batch of lights each time, and using additive blending to combine them all together.

That's the basic idea. Of course there are lots of optimizations needed to make this fast enough for a reasonable sized scene. Don't draw all the lights, just the visible ones (frustum and occlusion culling); don't actually re-draw the entire scene each pass, just the objects within range of the lights in that pass; have multiple versions of the shader that support different numbers of lights (1, 2, 3, ...) so you don't waste time evaluating more lights than you need to.

Deferred rendering as mentioned in the other answer is a good choice when you have many small lights, but it is not the only way.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .