Creating bump-mapped sprites in Mongame

Work-in-progress as I'm creating the blog site while adding the content

TODO: Write an example of using shaders to use a normal map for sprites in Monogame

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;

public class ShadedTexture2D
{
    static Dictionary lights = null;
    static bool firstRun = true;

    static int MAX_LIGHTS = 3;
    public struct LightSource
    {
        public Vector2 position;
        public Color color;
    }

    public struct VertexPositionTextureNormal
    {
        public Vector3 Position;
        public Vector2 TextureCoordinate;
        public Vector3 Normal;

        public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration(
            new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
            new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
            new VertexElement(sizeof(float) * 5, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0)
        );

        public VertexPositionTextureNormal(Vector3 position, Vector2 textureCoordinate, Vector3 normal)
        {
            Position = position;
            TextureCoordinate = textureCoordinate;
            Normal = normal;
        }
    }

    static GraphicsDevice gfx;
    static Effect effect;
    static VertexBuffer vertexBuffer;
    static IndexBuffer indexBuffer;
    private Texture2D spaceshipTexture;
    private Texture2D spaceshipNormalMap;
    static VertexPositionTextureNormal[] vertices;
    //static short[] indices = new short[] { 0, 1, 2, 2, 1, 3 };
    static short[] indices = new short[] { 0, 2, 1, 1, 2, 3 };

    public ShadedTexture2D(Texture2D spaceshipTexture, Texture2D spaceshipNormalMap)
    {
        this.spaceshipTexture = spaceshipTexture;
        this.spaceshipNormalMap = spaceshipNormalMap;
    }

    internal static void Init(ContentManager content, GraphicsDevice graphicsDevice)
    {
        gfx = graphicsDevice;
        effect = content.Load("BumpMapping");
        lights = new Dictionary();
        //gfx.SamplerStates[0] = new SamplerState { Filter = TextureFilter.Linear };
        //gfx.SamplerStates[1] = new SamplerState { Filter = TextureFilter.Linear };

        vertices = new VertexPositionTextureNormal[]
        {
            new VertexPositionTextureNormal(new Vector3(-0.5f, 0.5f, 0), new Vector2(0, 0), Vector3.UnitZ),
            new VertexPositionTextureNormal(new Vector3(0.5f, 0.5f, 0), new Vector2(1, 0), Vector3.UnitZ),
            new VertexPositionTextureNormal(new Vector3(-0.5f, -0.5f, 0), new Vector2(0, 1), Vector3.UnitZ),
            new VertexPositionTextureNormal(new Vector3(0.5f, -0.5f, 0), new Vector2(1, 1), Vector3.UnitZ)
        };

        vertexBuffer = new VertexBuffer(gfx, VertexPositionTextureNormal.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly);
        vertexBuffer.SetData(vertices);

        indexBuffer = new IndexBuffer(gfx, IndexElementSize.SixteenBits, indices.Length, BufferUsage.WriteOnly);
        indexBuffer.SetData(indices);
    }
    public void Draw(Vector2 position, float rotation, float scale)
    {
        // Bind the vertex buffer to the graphics device
        gfx.SetVertexBuffer(vertexBuffer);
        gfx.Indices = indexBuffer;

        Matrix world = Matrix.CreateTranslation(new Vector3(position, 0)) * Matrix.CreateRotationZ(rotation) * Matrix.CreateScale(scale);

        effect.Parameters["World"].SetValue(world);
        effect.Parameters["View"].SetValue(Matrix.Identity);
        effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, gfx.Viewport.Width, gfx.Viewport.Height, 0, 0, 1));
        effect.Parameters["Texture"].SetValue(spaceshipTexture);
        effect.Parameters["NormalMap"].SetValue(spaceshipNormalMap);

        // Create arrays to hold the light positions and colors
        Vector3[] lightPositionsArray = new Vector3[MAX_LIGHTS];
        Vector3[] lightColorsArray = new Vector3[MAX_LIGHTS];

        int lightIndex = 0;
        foreach (var light in lights.Values)
        {
            // Pass light position and color to the arrays
            lightPositionsArray[lightIndex] = new Vector3(light.position, 0);
            lightColorsArray[lightIndex] = light.color.ToVector3();

            lightIndex++;
        }

        // Set the arrays of light positions and colors to the shader parameters
        effect.Parameters["LightPositions"].SetValue(lightPositionsArray);
        effect.Parameters["LightColors"].SetValue(lightColorsArray);

        int passCount = 0;
        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            if (firstRun)
            {
                System.Diagnostics.Debug.WriteLine($"Pass {passCount++}");
            }
            pass.Apply();
            gfx.DrawIndexedPrimitives(
                Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleList,
                0,
                0,
                2);
        }
        if (firstRun)
        {
            // Log some debug info
            foreach (var light in lights.Values)
            {
                System.Diagnostics.Debug.WriteLine("Light position: " + light.position);
                System.Diagnostics.Debug.WriteLine("Light color: " + light.color);
            }
            System.Diagnostics.Debug.WriteLine("World matrix: " + world);
        }
        firstRun = false;
    }


    public void Draw2(Vector2 position, float rotation, float scale)
    {
        BasicEffect basicEffect = new BasicEffect(gfx)
        {
            TextureEnabled = false,
            VertexColorEnabled = true
        };
        VertexPositionColor[] vertices = new VertexPositionColor[]
        {
                    new VertexPositionColor(new Vector3(-100, -100, 0), Color.Red),
                    new VertexPositionColor(new Vector3(100, -100, 0), Color.Green),
                    new VertexPositionColor(new Vector3(-100, 100, 0), Color.Blue),
                    new VertexPositionColor(new Vector3(100, 100, 0), Color.Yellow),
        };
        short[] indices = new short[] { 0, 1, 2, 2, 1, 3 };

        Matrix world = Matrix.CreateTranslation(new Vector3(position, 0)) * Matrix.CreateRotationZ(rotation) * Matrix.CreateScale(scale);

        basicEffect.World = world;
        basicEffect.View = Matrix.Identity; // Set identity matrix for view
        basicEffect.Projection = Matrix.CreateOrthographicOffCenter(0, gfx.Viewport.Width, gfx.Viewport.Height, 0, 0, 1); // Set an orthographic projection matrix

        foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
        {
            pass.Apply();

            gfx.DrawUserIndexedPrimitives(
                Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleList,
                vertices,
                0,
                vertices.Length,
                indices,
                0,
                2
            );
        }
    }

    internal static void AddLightSource(string id, Vector2 position, Color color)
    {
        if (lights == null)
            lights = new Dictionary();

        if (!lights.ContainsKey(id))
        {
            LightSource light = new LightSource
            {
                position = position,
                color = color
            };
            lights.Add(id, light);
        }
    }
}