Chris The Avatar's picture

Lighting

I am using GL.LightModel/GL.Light to replace the lighting engine in my isometric game. Its working wonderfully other than the fact the number of lights that can be rendered seems extremely low (only 8 active lights rendered on the screen at a time?). Is there a recommended alternative or method to simulate or do lighting when you need to support more lights? Previously I was using images as light masks and using color blending to add/subtract lighted areas, while this method works well it certainly is not as nice quality as the GL Light method. Is there a reason the light limit is so well, my video card is supposed to be pretty decent (Nvidia Gforce GTS 240).

Thanks for your help in advance

Chris


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
the Fiddler's picture

Read this: http://www.opengl.org/sdk/docs/man/xhtml/glLight.xml

Quote:

The number of lights depends on the implementation, but at least eight lights are supported. They are identified by symbolic names of the form GL_LIGHT i, where i ranges from 0 to the value of GL_MAX_LIGHTS - 1.

You can query max lights using GL.GetInteger(GetPName.MaxLights). Symbolic constants only go up to 8 (the guaranteed number), so use Light0 + i where to access all lights.

Note that you can use multi-pass rendering to increase your light count beyond max lights. Or you can use shaders which are not subject to the limits of fixed-function hardware.

Chris The Avatar's picture
the Fiddler wrote:

Note that you can use multi-pass rendering to increase your light count beyond max lights. Or you can use shaders which are not subject to the limits of fixed-function hardware.

I used GL.GetInteger... which returned 8 as the integer. so above you mention two other methods, do you have examples of using multi-pass rendering of how I could get that to work? Or a shader example.

Thanks I appreciate the help

Chris

the Fiddler's picture

Multi-pass rendering is quite simple. It works like this:

  1. For pass #1, enable depth writes and disable blending & textures. Setup your first 8 lights and render your geometry.
  2. For pass #2, enable blending and disable depth writes. Setup your next 8 lights and render your geometry.
  3. Repeat #2 until all lights are exhausted.
  4. Make a final pass with textures enabled.

In other words, pass #1 lays down depth information and renders the first 8 lights. Passes #2-#n use existing depth information and render the next sets of lights. Final pass lays down textures. Depth testing should be enabled at all times.

You can toggle depth writes with GL.DepthMask. Depth testing, textures and blending are controlled via GL.Enable/Disable.

As an optimization, you can render all static lights into a texture.

Note that modern games typically perform something like 4-8 passes per frame.

Edit: shaders are orthogonal to multi-pass rendering. Their main advantage is versatility and image quality. Unless you use something like deferred shading, which is overkill for an isometric game, you may still need to perform multiple passes (so implement this first). If you wish to improve IQ at some later point, then look into shaders.

The Example Browser contains several examples (including per-pixel lighting).

Edit 2: Blend function for passes #2-#n should be GL.BlendFunc(DstColor, Zero), i.e. modulation.

Chris The Avatar's picture

Ok I am obviously getting something wrong, Ihave a bunch of rendered squares with orange light blobs on some of them (the lights obviously). I am fairly newbish at 3d concepts, so I interepted what you said in code as the following.... note that I realize the example below would only support 24 lights but I am just trying to do a proof of concept at this point.

I have posted the code below that I am utilizing lighting for. obviously the minute details are kind of irrelvant so I think the code works to go over this concept. All objects in my game are rendered as textured quads... So ShowObjects essentially will loop through the map objects and render textured quads for them. I might be misunderstanding your geometry statement, I have no "geometry" other than the textured quads so I assumed to render them in each pass correct?

                     Lighting.Lights.Clear();
                      AddMapLights();
                        for (int pass = 0; pass < 4; pass++)
                        {
                            if (pass == 0)
                            {
                                GL.Disable(EnableCap.Texture2D);
                                GL.Disable(EnableCap.Blend);
                                GL.DepthMask(true);
                                 Lighting.Start();
 
                            }
                            else if (pass < 3)
                            {
                                GL.Enable(EnableCap.Blend);
                                GL.DepthMask(false);
                                Lighting.Start();
 
                            }
                            else if (pass == 3)
                            {
                                GL.Enable(EnableCap.Texture2D);
                            }
                            ShowObjects(nextEvents);
 
 
                        }
                        if (AllMapObjectsDrawn != null)
                            AllMapObjectsDrawn(this);
 
                        Lighting.Finish();
 
                        ShowText();
                        if (AllTextDrawn != null)
                            AllTextDrawn(this);

Below my code for lighting:
each time Lighting.Start is called the function below is called, in short it loops through the first 8 lights in the collection and applies the light to opengl, then removes the light from the collection :

     float[] array = new float[4];
 
            GL.Enable(EnableCap.Lighting);
 
            ToColorArray(array, Ambient);
            GL.LightModel(LightModelParameter.LightModelAmbient, array);
 
            GL.Enable(EnableCap.ColorMaterial);
            GL.ColorMaterial(MaterialFace.FrontAndBack,
                             ColorMaterialParameter.AmbientAndDiffuse);
 
       for (int i = 0; i < 8 && i < Lights.Count; i++)
                {
                    EnableCap lightID = (EnableCap)((int)EnableCap.Light0 + i);
                    LightName lightName = (LightName)((int)LightName.Light0 + i);
                    if (Lights[0].Enabled == false)
                    {
                        GL.Disable(lightID);
                        continue;
                    }
 
                    GL.Enable(lightID);
 
                    ToColorArray(array, Lights[0].Diffuse);
                    GL.Light(lightName, LightParameter.Diffuse, array);
 
                    ToColorArray(array, Lights[0].Ambient);
                    GL.Light(lightName, LightParameter.Ambient, array);
 
                    ToVectorArray(array, Lights[0].Position);
                    GL.Light(lightName, LightParameter.Position, array);
 
                    GL.Light(lightName, LightParameter.ConstantAttenuation, Lights[0].AttenuationConstant);
                    GL.Light(lightName, LightParameter.LinearAttenuation, Lights[0].AttenuationLinear);
                    GL.Light(lightName, LightParameter.QuadraticAttenuation, Lights[0].AttenuationQuadratic);
                    Lights.RemoveAt(0);
 
                }

I also did some other playing around, and I am slightly confused on when the lights are actually applied to the underlying scene, it seems like with no "flushing" mechanism the lights overwrite each other each loop. Any clarification and continuing help would be fantastic. Lighting seems pretty simple other than this multipass mechanism.

Thanks

Chris

the Fiddler's picture

Your code looks almost entirely correct.

Quote:

I might be misunderstanding your geometry statement, I have no "geometry" other than the textured quads so I assumed to render them in each pass correct?

Indeed.

Conceptually, you can skip quads that are not affected by any light during the current pass but this optimization is generally not worth it (unless your scene is so complex that you are geometry-limited).

Quote:

I also did some other playing around, and I am slightly confused on when the lights are actually applied to the underlying scene, it seems like with no "flushing" mechanism the lights overwrite each other each loop.

Make sure you are setting the blending function to (DstColor, Zero):

else if (pass < 3)
{
    GL.Enable(EnableCap.Blend);
    GL.BlendFunc(BlendingFactorSrc.DstColor, BlendingFactorDest.Zero); // Add this
    GL.DepthMask(false);
    Lighting.Start();
}

Without this, every pass will simply overwrite the results of the previous one.

Chris The Avatar's picture

I added the blending as you recommended... Let me give you a visual of my problem... I think it might help to go a long with the code... Ill also post some more verbose code.

When I call ShowObjects (render textured quads) each pass I endup with a screen full of shaded rectangles because we disable Textures on the first pass... Unforunately there is no way for me to know which objects will effected by light because there doesnt seem to be a good way to measure light in terms of pixels (although it doesnt really matter at this point). Note that the textures are loaded on demand and cache simply because any texture in my game could be used at any time see picture

So here is the code for that result:

 Lighting.Lights.Clear();
                        AddMapLights();
                        for (int pass = 0; pass < 4; pass++)
                        {
                            if (pass == 0)
                            {
                                GL.Disable(EnableCap.Texture2D);
                                GL.Disable(EnableCap.Blend);
                                GL.DepthMask(true);
                                Lighting.Start();
 
 
                            }
                            else if (pass < 3)
                            {
 
                                GL.Enable(EnableCap.Blend);
                                GL.BlendFunc(BlendingFactorSrc.DstColor, BlendingFactorDest.Zero);
                                GL.DepthMask(false);
                                Lighting.Render();
 
                            }
                            else if (pass == 3)
                            {
                                GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);
                                GL.Enable(EnableCap.Texture2D);
 
 
 
                            }
 
                            ShowObjects(nextEvents);
 
                        }
                        if (AllMapObjectsDrawn != null)
                            AllMapObjectsDrawn(this);
 
                        Lighting.Finish();

Now if I move ShowObjects into the pass == 3 if block everything renders properly but I still end up with the same issue with the light only 8 are rendered. The result for that is below, I wont repost the code for that because I think it is pretty obvious what I am doing:

I also adjusted my lighting code a bit to disable all prior lights and the one time stuff in start() and the stuff that needs to be done in each pass in render(), finish simply disables lighting for the rest of the scene, see below:

 public void Start()
        {
            float[] array = new float[4];
 
            GL.Enable(EnableCap.Lighting);
            ToColorArray(array, Ambient);
            GL.LightModel(LightModelParameter.LightModelAmbient, array);
 
            GL.Enable(EnableCap.ColorMaterial);
            GL.ColorMaterial(MaterialFace.FrontAndBack,
                             ColorMaterialParameter.AmbientAndDiffuse);
            for (int i = 0; i < 8; i++)
            {
 
                EnableCap lightID = (EnableCap)((int)EnableCap.Light0 + i);
                LightName lightName = (LightName)((int)LightName.Light0 + i);
                GL.Disable(lightID);
            }
            Enabled = true;
            Render();
        }
        public void Finish()
        {
            Enabled = false;
            GL.Disable(EnableCap.Lighting);
        }
        public void Render()
        {
            float[] array = new float[4];
 
                for (int i = 0; i < 8 && i < Lights.Count; i++)
                {
                    EnableCap lightID = (EnableCap)((int)EnableCap.Light0 + i);
                    LightName lightName = (LightName)((int)LightName.Light0 + i);
                    if (Lights[0].Enabled == false)
                    {
                        GL.Disable(lightID);
                        continue;
                    }
 
                    GL.Enable(lightID);
 
                    ToColorArray(array, Lights[0].Diffuse);
                    GL.Light(lightName, LightParameter.Diffuse, array);
 
                    ToColorArray(array, Lights[0].Ambient);
                    GL.Light(lightName, LightParameter.Ambient, array);
 
                    ToVectorArray(array, Lights[0].Position);
                    GL.Light(lightName, LightParameter.Position, array);
 
                    GL.Light(lightName, LightParameter.ConstantAttenuation, Lights[0].AttenuationConstant);
                    GL.Light(lightName, LightParameter.LinearAttenuation, Lights[0].AttenuationLinear);
                    GL.Light(lightName, LightParameter.QuadraticAttenuation, Lights[0].AttenuationQuadratic);
                    Lights.RemoveAt(0);
 
                }
 
        }

My code for drawing textured quads is below, there are essentially two functions in the whole code base that handle all of the drawing, this is the main one, the other is used for rotations... Just wanted you to see something if I am missing something with the gray blocks, note that the surfaces are loaded right before they are drawn, I do not know if this is an issue with disabling textures:

        private void DrawImage(Surface Surf, System.Drawing.Rectangle SourceRect, System.Drawing.Rectangle DestRect, System.Drawing.Color Blender)
        {
            if (Surf != null)
            {
                GL.BindTexture(TextureTarget.Texture2D, Surf.GlTextureID);
                //GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)All.Nearest);
                //GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)All.Nearest);
 
                GL.TexParameter(TextureTarget.Texture2D,
           TextureParameterName.TextureMinFilter,
           (int)TextureMinFilter.Linear);
                GL.TexParameter(TextureTarget.Texture2D,
                                TextureParameterName.TextureMagFilter,
                                (int)TextureMagFilter.Linear);
 
 
                Point[] dstVertex = new Point[4];
                PointF[] srcVertex = new PointF[4];
                RectangleF srcCoords = new RectangleF(
                    SourceRect.X / (float)Surf.Width,
                    SourceRect.Y / (float)Surf.Height,
                    SourceRect.Right / (float)Surf.Width,
                    SourceRect.Bottom / (float)Surf.Height);
 
                srcVertex[0] = new PointF(srcCoords.Left, srcCoords.Top);
                srcVertex[1] = new PointF(srcCoords.Width, srcCoords.Top);
                srcVertex[2] = new PointF(srcCoords.Width, srcCoords.Height);
                srcVertex[3] = new PointF(srcCoords.Left, srcCoords.Height);
 
                dstVertex[0] = new Point(DestRect.X, DestRect.Y);
                dstVertex[1] = new Point(DestRect.X + DestRect.Width, DestRect.Y);
                dstVertex[2] = new Point(DestRect.X + DestRect.Width, DestRect.Y + DestRect.Height);
                dstVertex[3] = new Point(DestRect.X, DestRect.Y + DestRect.Height);
 
                float colorA = Blender.A / 255.0f;
                float colorR = Blender.R / 255.0f;
                float colorG = Blender.G / 255.0f;
                float colorB = Blender.B / 255.0f;
                GL.Begin(BeginMode.Quads);
 
 
                for (int i = 0; i < 4; i++)
                {
                    GL.Color4(colorR, colorG, colorB, colorA);
                    GL.TexCoord2(srcVertex[i].X, srcVertex[i].Y);
                    GL.Vertex2(dstVertex[i].X, dstVertex[i].Y);
                }
                GL.End();
            }
        }

Any further recommendations would be appreciated I am getting frustrated :-)

the Fiddler's picture

Ok, it seems I forgot a pretty important detail:

GL.DepthFunc(DepthFunc.Lequal); // Add to pass 0
GL.DepthFunc(DepthFunc.Equal); // Add to every other pass

Sorry about that!

Chris The Avatar's picture

Alright still having problems, but I discovered quite a bit while playing around. I am going to take a break on this issue for a couple weeks and tackle some others, I will probably be back with some questions..

I have had some success with doing the following but not quite the effect I am looking for:

RenderObjects()

Turn Lighting on
while (lights > 0)
{
Turn on/off flags you recommended above
Render 8 lights at a time
Render textureless surfaces in the same size as my tiles over the map surface.
Remove 8 lights
}

ResetFlags

using blendfunc as zero just gave me a black screen, changing it to one resulted in all lights being rendered but the depending on the pass they were in resulted in brighter and brighter lights... I think I am close but not quite there yet, my brain needs a break from this problem. if you have some other recommendations I am happy to listen to them and I appreciate your help very much.

Thank You

Inertia's picture

This has not been mentioned yet, but Deferred Shading is a good solution if you want alot of dynamic lights in your scene with neglectible cost (i.e. the user can drop torches at will).

In forward shading your (unoptimized) steps should be:
1. clear everything.
2. enable lighting and light0, setup light0, disable blending.
3. turn on texture mapping, enable depth writes, enable depth test, depth comparison to default.
4. render all geometry once to lay down color and depth in the framebuffer.
5. turn off texture mapping, disable depth writes, change depth comparison to equal.
6. enable additive blending.
7. for each light (except the light0 from step 2), set it up properly and draw all relevant geometry again. (This will add more and more brightness (light) to the framebuffer)

Key Observation: the geometry is only drawn 1x with texture mapping and depth writes enabled, but many times with different lighting ontop of that first pass.