Search code examples
c#xnarenderinglightning

Light Rendering Results in Low FPS


I am currently developing a class for my XNA game whose rendering the lights on the image. At the time, i have made the source to draw my lightmap, however, the FPS is very low in my source. I know that it is brutally reduced upon looping through each pixel, however, I do not know any other way to get & set each pixel on my Texture in XNA but using the "For" statement?

Current Source:

 public struct Light
    {
        public int Range;
        public int Intensity;
        public Color LightColor;
        public Vector2 LightLocation;

        public Light(int _Range, int _Intensity, Color _LightColor, Vector2 _LightLocation)
        {
            Range = _Range;
            Intensity = _Intensity;
            LightLocation = _LightLocation;
            LightColor = _LightColor;
        }
    }
    public class RenderClass
    {
        [System.Runtime.InteropServices.DllImport("User32.dll")]
        public static extern bool MessageBox(IntPtr h, string S, string C, int a);

        public static Texture2D RenderImage(Light[] LightLocations, Texture2D ScreenImage, Viewport v, bool ShadowBack = false)
        {
            Texture2D[] Images = new Texture2D[LightLocations.Count()];
            int curCount = 0;

            /*LOOP THROUGHT EACH LIGHT*/
            foreach (Light LightLocation in LightLocations)
            {
                /*VARIABLES*/
                Color LightColor = LightLocation.LightColor;
                int Range = LightLocation.Range;
                int Intensity = LightLocation.Intensity;

                /*GET COLORS*/
                int Width = v.Width;
                int Height = v.Height;
                Color[] Data = new Color[Width * Height];
                ScreenImage.GetData<Color>(Data);

                /*VARIABLES TO SET COLOR*/
                Color[] SetColorData = new Color[Width * Height];

                /*CIRCEL*/
                int Radius = 15 / 2; // Define range to middle [Radius]
                int Area = (int)Math.PI * (Radius * Radius);

                for (int X = 0; X < Width; X++)
                {
                    for (int Y = 0; Y < Height; Y++)
                    {
                        int Destination = X + Y * Width;

                        #region Light
                        /*GET COLOR*/
                        Color nColor = Data[Destination];

                        /*CREATE NEW COLOR*/
                        Vector2 MiddlePos = new Vector2(LightLocation.LightLocation.X + Radius, LightLocation.LightLocation.Y + Radius);
                        Vector2 CurrentLocation = new Vector2(X, Y);

                        float Distance;
                        Distance = Vector2.Distance(MiddlePos, CurrentLocation);
                        Distance *= 100;
                        Distance /= MathHelper.Clamp(Range, 0, 100);

                        Vector3 newColors = nColor.ToVector3();

                        nColor = new Color(
                            newColors.X,
                            newColors.Y,
                            newColors.Z, 
                            Distance / 100);


                        /*SET COLOR*/
                        SetColorData[Destination] = nColor; // Add to array
                        #endregion
                        #region Shadow
                        #endregion
                    }
                }


                ScreenImage.SetData<Color>(SetColorData);
                Images[curCount] = ScreenImage;
                curCount++;
            }

            return Images[0]; // Temporarily returning the first image of the array.
        }
    }

As you can see, this is a slow and bad method. So I was wondering, is there a better way to get & set each pixel?

Thanks in advance, dotTutorials! =)


Solution

  • I think that job would be best done in a pixel shader.

    You could create an Effect file that operates over one light at a time.
    XNA uses DX9 so you'll be limited to 128 constant registers, which I think you can use to squeeze up to three lights.

    So you set your lightmap as a render target, loop through all the lights, set the constant data on your effect, render a render-target-sized quad and in your pixel shader compute your lighting equation.

    In essence something like that:

        // In LoadContent
    RenderTarget2D lightmapRT = new RenderTarget2D(graphics.GraphicsDevice,
                                                     128,
                                                     128,
                                                     false, //No mip-mapping
                                                     SurfaceFormat.Color,
                                                     DepthFormat.Depth24);
    
    // We now render to the lightmap in Render method
    graphics.GraphicsDevice.SetRenderTarget(lightmapRT);
    
    // Lightmap is black by default
    graphics.GraphicsDevice.Clear(Color.Black);
    
    // Use the sprite batch to draw quads with custom shader
    spriteBatch.Begin(0, BlendState.Opaque, null, null, null, lightmapFx);
    
    foreach (var light in lights)
    {
        // Pass the light parameters to the shader
        lightmapFx.Parameters["Viewport"].SetValue(new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height));
        lightmapFx.Parameters["Range"].SetValue(light.Range);
        lightmapFx.Parameters["Intensity"].SetValue(light.Intensity);
        lightmapFx.Parameters["LightColor"].SetValue(light.LightColor);
        lightmapFx.Parameters["LightLocation"].SetValue(light.LightLocation);
    
        // Render quad
        spriteBatch.Draw(...);
    }
    spriteBatch.End();
    

    And the FX file would look something like that:

    float Range;
    float Intensity;
    float3 LightColor;
    float2 LightLocation;
    float2 Viewport;
    
    struct VStoPS
    {
        float4 Position : POSITION0;
        float2 TexCoord : TEXCOORD0;
    };
    
    VStoPS VS(in float4 color    : COLOR0,
              in float2 texCoord : TEXCOORD0,
              in float4 position : POSITION0)
    {
        VStoPS vsout = (VStoPS)0;
    
        // Half pixel offset for correct texel centering.
        vsout.Position.xy -= 0.5;
    
        // Viewport adjustment.
        vsout.Position.xy = position.xy / Viewport;
        vsout.Position.xy *= float2(2, -2);
        vsout.Position.xy -= float2(1, -1);
    
        // Pass texcoords as is
        vsout.TexCoord = texCoord;
    
        return vsout;
    }
    
    float4 PS(VStoPS psin)
    {
        // Do calculations here
        // Here I just set it to white
        return float4(1.0f, 1.0f, 1.0f, 1.0f);
    }
    
    technique Main
    {
        pass p0
        {
            VertexShader = compile vs_3_0 VS();
            PixelShader = compile ps_3_0 PS();
        }
    }
    

    Note that this non-tested code and probably full of errors. I leave it up to you to figure out what needs to go in the pixel shader.