5

I want to be able to append multiple light sources to each node of my scene graph, but I have no clue how to do it!

From the tutorials on learningwebgl.com I learned to either use directional or position lighting, but I couldn't find a good explanation of how to implement multiple light sources.

So, the objective should be, to have the option to append an arbitrary number of light sources to each node, which type can be either directional or position lighting, and if possible and advisable, this should be realized by using only one shader-program (if this isn't the only possibility to do it anyway), because I automatically create the program for each node depending on their specific needs (unless there is already a program on the stack with equal settings).

Based on the tutorials on learningwebgl.com, my fragment-shader source for a node object using lighting without preset binding to one of the lighting-types could look like this...

precision highp float;

uniform bool uUsePositionLighting;
uniform bool uUseDirectionalLighting;

uniform vec3 uLightPosition;
uniform vec3 uLightDirection;

uniform vec3 uAmbientColor;
uniform vec3 uDirectionalColor;

uniform float uAlpha;

varying vec4 vPosition;
varying vec3 vTransformedNormal;
varying vec3 vColor;


void main (void) {

  float directionalLightWeighting;

  if (uUseDirectionalLighting) {

    directionalLightWeighting = max(dot(vTransformedNormal, uLightDirection), 0.0);

  else if (uUsePositionLighting) {

    vec3 lightDirection = normalize(uLightPosition, vPosition.xyz);

    directionalLightWeighting = max(dot(normalize(vTransformedNormal, lightDirection), 0.0);

  }

  vec3 lightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;

  gl_FragColor = vec4(vColor * lightWeighting, uAlpha);

}

...so, that's basically my poor state of knowledge concerning this subject.

I also ask myself, how adding more light sources would affect the lighting-colors:

I mean, do uAmbientColor and uDirectionalColor have to sum up to 1.0? In this case (and particularly when using more than one light source) it surely would be good to precalculate these values before passing them to the shader, wouldn't it?

2 Answers 2

6

Put your lights into an array and loop over them for each fragment. Start with a fixed array of light sources, unbounded arrays are not supported until OpenGL 4.3 and are more complicated to work with.

Something along the lines of:

uniform vec3 uLightPosition[16];
uniform vec3 uLightColor[16];
uniform vec3 uLightDirection[16];
uniform bool uLightIsDirectional[16];

 ....

 void main(void) {
   vec3 reflectedLightColor;

   // Calculate incoming light for all light sources
   for(int i = 0; i < 16; i++) {
     vec3 lightDirection = normalize(uLightPosition[i], vPosition.xyz);
     if (lightIsDirectional[i]) {
       reflectedLightColor += max(dot(vTransformedNormal, uLightDirection[i]), 0.0) * uLightColor[i];
     }
     else  {
       reflectedLightColor += max(dot(normalize(vTransformedNormal, lightDirection), 0.0) * uLightColor[i];
     }
   }

   glFragColor = vec4(uAmbientColor + reflectedLightColor * vColor, uAlpha);
 }

Then you can enable/disable the light sources by setting uLightColor to (0,0,0) for the entries you don't use.

Ambient and directional don't have to sum up to 1, actually the light source can have a strength much stronger than 1.0, but then you will need to do tonemapping to get back to a range of values that can be displayed on a screen, I would suggest playing around to get a feel for what is happening (e.g. what happens when a light source have negative colors, or colors above 1.0?).

uAmbientColor is just a (poor) way to simulate light that has bounced several times in the scene. Otherwise things in shadow becomes completely black Which looks unrealistic.

Reflectance should typically be between 0 and 1 (in this example it would be the parts returned by the 'max' computations), otherwise a lightsource will get stronger when looked at via the material.

Sign up to request clarification or add additional context in comments.

9 Comments

Thank you very much for your answer Erik Man! But I have one question on this: Is it really a good idea to use a for-loop inside a shader - I mean, couldn't that cause some performance-issues?
Yes the loop will cost some performance, but it is the only way to handle multiple light sources. The GLSL compiler will probably unroll the loop since the size is fixed, so replace the ´16´ by as low number as possible.
On desktop you shouldn't notice any performance problems until you have lots of light sources, mobile is more limited and there it might be better to move some lights to the vertex shader instead if becomes to slow.
Ok, thanks! As for creating the shader-programs automatically for each node anyway, depending on their individual requirements, I'm just going to set the number of array-entries equal to the number of light sources bound to the node to avoid unnecessary loops.
On the fly is up to you. Sure it's common to generate the shaders you need. Whether you generate them at init time or as needed and cache them is really up to you and also how much to generate vs put options in the shader. For example a point light is really just a spotlight with its limits set so everything passes so maybe no need for different shaders for point vs spot. There isn't much to tutor. Do some string substitution. newShaderSource = originalShaderSource.replace(/NUM_LIGHTS/g, numLights) etc... Most games limit dynamic lights to 1-4 per model drawn.
|
1

@ErikMan's answer is great, but may involve a lot of extra work on the part of the GPU since you're checking every light per fragment, which isn't strictly necessary.

Rather than an array, I'd suggest building a clip-space quadtree. (You can do this in a compute shader if it's supported by your target platform / GL version.)

A node might have a structure such as (pseudocode as my JS is rusty):

typedef struct
{
    uint LightMask; /// bitmask - each light has a bit indicating whether it is active for this node. uint will allow for 32 lights. 

    bool IsLeaf;

} Node;

const uint maxLevels = 4;
const uint maxLeafCount = pow(4,maxLevels);      
const uint maxNodeCount = (4 * maLeafCount - 1) / 3;

/// linear quadtree - node offset = 4 * parentIndex + childIndex;
Node tree[maxNodeCount];

When building the tree, just check each light's clip-space bounding box against the implicit node bounds. (Root goes from (-1,-1) to (+1,+1). Each child is half that size on each dimension. So, you don't really need to store node bounds.)

If the light touches the node, set a bit in Node.LightMask corresponding to the light. If the light completely contains the node, stop recursing. If it intersects the node, subdivide and continue.

In your fragment shader, find which leaf node contains your fragment, and apply all lights whose bit is set in the leaf node's mask.

You could also store your tree in a mipmap pyramid if you expect it to be dense.

Keep your tiles to a size that is a multiple of 32, preferably square.

vec2 minNodeSize = vec2(2.f / 32);

Now, if you have a small number of lights, this may be overkill. You would probably have to have a lot of lights to see any real performance benefit. Also, a normal loop may help reduce data divergence in your shader, and makes it easier to eliminate branching.

This is one way to implement a simple tiled renderer, and opens the door to having hundreds of lights.

4 Comments

Thank you very much for your contribution, David! Maybe I'll appreciate your concept even more, as soon as I understand it. ;-) Until then, I can just say, that in WebGL, there is no such thing as a compute-shader, only vertex-shader and fragment-shader, but that would probably be the least hurdle for me, when putting this into action...
@var you can do it in the vertex or fragment shaders. It's just a little easier in compute. Good luck!
Is this the same idea as "Forward Plus" ? If so seems like there is some public reference code at github.com/shrekshao/WebGL-Tile-Based-Forward-Plus-Renderer
@davidkomer yes, though there’s a bit more to forward+, like handling translucency, occlusion, etc. the reference code I’ve seen requires compute shaders, though admittedly I haven’t done an exhaustive search. Thanks for the link!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.