Load Texture as 16 color grayscale image?

is it possible to load a texture from file and then manually set the image palette data?

I need to do two things.

  • Limit image palette to 16 colors
  • Set all 16 colors to the correct grayscale values.

then I would run it through a palette swap shader to draw.

What I’d do would be:

  1. convert the grayscale image to a RGB image that the R component is a number between 0 to 15 (or the index of the grayscale color)
  2. Create a shader which has an array with 16 slots for each color. This is the LUT.
  3. When you want to render the palletized sprite, upload the LUT first.
  4. Inside the shader, when sampling the texture (point sampling, linear sample will mess everything), it will be a number between 0/256 to 15/256
    Multiply that value by 256 to get the index of the array, and read the LUT array which contains the color. Output that color.

remarks:

  • To do 1) you’ll probably have to write your own converter, or create a loader which reads a palletized file format and they you can convert it in runtime to your desired RGB texture.

  • Instead of uploading the LUT and accessing as an array, you could use a second texture and sample the LUT colors from that texture
    -> Or you could even, have a second LUT texture which contains multiple LUTs, and specify the LUT to use for each vertex. That texture would look a LOT like the original PSX VRAM, ahhh, good old times :_)

how would I create a custom converter?
Is there a tutorial on something like this?

People in Monogame usually uses the content pipeline and a custom content importer to do this kind of things, but I don’t use it so I can’t offer much help.

In my case I’d write a standalone EXE which loads the texture and writes it back in my desired format, or better for quick prototyping, loading the image with a library that returns you both the color data and the palette data.

First option is probably not for you, second option, depending on the library you use, you’ll have big problems if you have to port to other platforms.

However, in your case, if your textures are grayscale, if the grayscale is “homogeneous” (color 0 i 0,0,0 , color 1 i 16,16,16, color 2 i 32,32,32 … color 15 is 255,255,255) you can deduce directly the index from the color

What kind of file is the image you are trying load i.e. the extension is it .pal or is it a .dat ect. ?

Well, as I see it there are two ways to approach this.

  1. The Automated way, in run-time.
  2. The Asset/Resource way, ahead-of-time.

Option one is where you take the built-in tools for manipulating image data, and dynamically generate a texture for use with your game. I’m not sure how this would affect performance, but there should be a way to handle it that won’t be too bad. The big downside of this is that you won’t have a chance to fine-tune your results.

Option two is where you basically do the same thing, but you do it ahead of time, and export your finished results as an image that you will then use as the basis for your image in the actual game execution. This approach is the one I would recommend. This approach would allow you to “preview” your results and perform adjustments before exporting your graphic.

In both scenarios, your best bet is to take the input image, run through all of the pixels, and average out the value of each individual pixel. This is the usual approach for converting an image to greyscale. For your purposes, you’re also going to want to define a series of 16 greyscale color values. When converting your image to greyscale, you perform a secondary check after the average calculation to see which of the 16 values your average falls within, and assign them to that value. Adjusting the ranges for each of the 16 values will allow you to tweak the results, while also keeping them within the palette your’e trying to define.

Also, don’t forget how to choose your colors for the “palette” shader that you use later. Everything has to be floats with shaders, so your grey values will need to target the U values that will give you the center of sampled pixels. Here’s a little snippet of code I’ve been using to determine the proper colors.

int totalPixelNumber = 16;
float singlePixelWidth = 1.0f / (float)totalPixelNumber;
float halfSinglePixelWidth = singlePixelWidth / 2.0f;
for (int i = (totalPixelNumber - 1); i >= 0; i–) {
float uPosition = (singlePixelWidth * i) + halfSinglePixelWidth;
int uColor = (int)Math.Round((double)uPosition * 255.0d);
Console.WriteLine(“Color #” + (totalPixelNumber - i).ToString() + " - U Value: " + uPosition.ToString() + ", Byte Value: " + uColor.ToString());
}

This will return you a list of the colors to target given a specific number of different colors desired.

@KakCAT that’s too bad neither one seems reasonable
@willmotil I am trying to load png, bmp, and jpeg formats
@Will_Hawthorne I am using a editor that will do this before the game is ran. so the textures in the game will all be grayscale.
Also thank you for the shader advice. I think my palette is just going to be a array of colors instead of a texture so its easier to change them in the editor.

Where or how is your color palette data stored ?
Is this hard coded data.
e.g.
index[0] = new Color(255,255,255,255);
index[1] = new Color(0,255,0,255);

or
Is it stored in a file what is the file extension if so i.e. .dat is it in a .txt file
Is it in indexed pixels in a second texture ect.

@willmotil

it is a hardcoded array of 16 colors basically.
I use a color wrapper but it’s basically just a struct with int R,G,B,A properties

    public struct Col
    {
        public int R { get; set; }
        public int G { get; set; }
        public int B { get; set; }
        public int A { get; set; }

        internal Col(int r=0, int g=0, int b=0, int a=255)
        {
            R = r;
            G = g;
            B = b;
            A = a;
        }

        public static Col Neutral     => new Col(191, 191, 191);
        public static Col NeutralDark => new Col(000, 000, 000);
        public static Col NeutralLite => new Col(255, 255, 255);
        public static Col Red         => new Col(255, 000, 000);
        public static Col RedDark     => new Col(191, 000, 000);
        public static Col RedLite     => new Col(255, 191, 191);
        public static Col Yellow      => new Col(255, 255, 000);
        public static Col YellowDark  => new Col(191, 191, 000);
        public static Col YellowLite  => new Col(255, 255, 191);
        public static Col Green       => new Col(000, 255, 000);
        public static Col GreenDark   => new Col(000, 191, 000);
        public static Col GreenLite   => new Col(191, 255, 191);
        public static Col Cyan        => new Col(000, 255, 255);
        public static Col CyanDark    => new Col(000, 191, 191);
        public static Col CyanLite    => new Col(191, 255, 255);
        public static Col Blue        => new Col(000, 000, 255);
        public static Col BlueDark    => new Col(000, 000, 191);
        public static Col BlueLite    => new Col(191, 191, 255);
        public static Col Magenta     => new Col(255, 000, 255);
        public static Col MagentaDark => new Col(191, 000, 191);
        public static Col MagentaLite => new Col(255, 191, 255);

        public static implicit operator Microsoft.Xna.Framework.Color(Col col)
        {
            return new Microsoft.Xna.Framework.Color(col.R, col.G, col.B, col.A);
        }

    }```

public class Pal
{      
    private Col[] Cols = new Col[16];
    public  Col this[int index]
    {
        get => Cols[index];
        set => Cols[index] = value;
    }

    public int Count => Cols.Length;

    internal Pal()
    {

    }
}

One more piece of information.

The data in your pngs jpegs ect… is in greyscale and relates to those indexs in your pal class.
e.g.
So that the data in the png, for example, at row and column x=1 y=1 in the image, has a red pixel value that corresponds to a… color index already, within the index color array in your pal class?

Or

Is the data in your images not yet in greyscale but just a regular image and the intent is that you will load the image process it to grayscale then later be matching those pixels to a color in your pal class manually or programatically ?

@willmotil
I will load the image in the editor and then convert it to grayscale for the game to draw with palettes.
saving the image would just be me drawing it with the shader to a surface and saving the surface.

This is how you can do it without using a pixel shader or the content reader importer. I did this just using get set data. Regardless most of these steps are needed to do it in the other ways.

You can load your images via the pipeline tool shown at the bottom of the post if you don’t know how to do so already.

In the below resulting image i picked a random texture and did the following.

* 1) loaded it and drawn it as it is.
* 2) colored it to grayscale.
* 3) reduced the grayscale image to a set of values matching the 21 colors in your array but in no specific order. then drew the image data that contains grayscale values from 0 to 21 hence it is nearly black.
* 4) reassigned the colors from your palatte array to the corresponding indexs within a reduced grayscaled image. The color table can be swapped.
* 5) the function can be called on the texture again with a different color palatte table at run time.

Here is the actual .Cs File in full.

Please see the two primary GrayScale functions especially the parameters they take.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace GLCrossPlatDesktop_GrayScale_Ex01
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Texture2D texture, textureAsGrayScale, textureAsPalette, textureAsPaletteImage;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 800;
        }

        protected override void Initialize()
        {

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            texture = Content.Load<Texture2D>("QuadDraw"); // <<< load any image of your own here that you have added thru the content pipeline tool.
            textureAsGrayScale = ToGrayScaledPalettable(texture, false, 16);
            textureAsPalette = ToGrayScaledPalettable(texture, true, 16);
            textureAsPaletteImage = ToGrayScaledPaletteSet(texture, palColorArray);
        }

        protected override void UnloadContent()
        {
            textureAsGrayScale.Dispose();
            textureAsPalette.Dispose();
            textureAsPaletteImage.Dispose();
        }

        public static Color[] palColorArray = new Color[]
            {
                    new Color(191, 191, 191),
                    new Color(000, 000, 000),
                    new Color(255, 255, 255),
                    new Color(255, 000, 000),
                    new Color(191, 000, 000),
                    new Color(255, 191, 191),
                    new Color(255, 255, 000),
                    new Color(191, 191, 000),
                    new Color(255, 255, 191),
                    new Color(000, 255, 000),
                    new Color(000, 191, 000),
                    new Color(191, 255, 191),
                    new Color(000, 255, 255),
                    new Color(000, 191, 191),
                    new Color(191, 255, 255),
                    new Color(000, 000, 255),
                    new Color(000, 000, 191),
                    new Color(191, 191, 255),
                    new Color(255, 000, 255),
                    new Color(191, 000, 191),
                    new Color(255, 191, 255)
            };

        public Texture2D ToGrayScaledPalettable(Texture2D original, bool makePaletted, int numberOfPalColors)
        {
            //make an empty bitmap the same size as original
            Color[] colors = new Color[original.Width * original.Height];
            original.GetData<Color>(colors);
            Color[] destColors = new Color[original.Width * original.Height];
            Texture2D newTexture = new Texture2D(GraphicsDevice, original.Width, original.Height);

            for (int i = 0; i < original.Width; i++)
            {
                for (int j = 0; j < original.Height; j++)
                {
                    //get the pixel from the original image
                    int index = i + j * original.Width;
                    Color originalColor = colors[index];

                    //create the grayscale version of the pixel
                    float maxval = .3f + .59f + .11f + .79f;
                    float grayScale = (((originalColor.R / 255f) * .3f) + ((originalColor.G / 255f) * .59f) + ((originalColor.B / 255f) * .11f) + ((originalColor.A / 255f) * .79f));
                    grayScale = grayScale / maxval;

                    if (makePaletted)
                    {
                        var val = (int)((grayScale -.0001f) * numberOfPalColors);
                        destColors[index] = new Color(val, val, val, 255);
                    }
                    else
                    {
                        destColors[index] = new Color(grayScale, grayScale, grayScale, 1f);
                    }
                }
            }
            newTexture.SetData<Color>(destColors);
            return newTexture;
        }

        public Texture2D ToGrayScaledPaletteSet(Texture2D original, Color[] palColorArray)
        {
            var numberOfPalColors = palColorArray.Length;

            //make an empty bitmap the same size as original
            Color[] colors = new Color[original.Width * original.Height];
            original.GetData<Color>(colors);
            Color[] destColors = new Color[original.Width * original.Height];
            Texture2D newTexture = new Texture2D(GraphicsDevice, original.Width, original.Height);

            for (int i = 0; i < original.Width; i++)
            {
                for (int j = 0; j < original.Height; j++)
                {
                    //get the pixel from the original image
                    int index = i + j * original.Width;
                    Color originalColor = colors[index];

                    //create the grayscale version of the pixel
                    float maxval = .3f + .59f + .11f + .79f;
                    float grayScale = (((originalColor.R / 255f) * .3f) + ((originalColor.G / 255f) * .59f) + ((originalColor.B / 255f) * .11f) + ((originalColor.A / 255f) * .79f));
                    grayScale = grayScale / maxval;

                    var val = (int)((grayScale - .0001f) * numberOfPalColors);
                    destColors[index] = palColorArray[val];
                }
            }
            newTexture.SetData<Color>(destColors);
            return newTexture;
        }


        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();


            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            // the original texture
            spriteBatch.Draw(texture, new Rectangle(20, 5, 400, 140), Color.White);
            // the original texture in grayscale.
            spriteBatch.Draw(textureAsGrayScale, new Rectangle(20, 150, 400, 140), Color.White);
            // the original texture data compressed to palatted index relative values.
            spriteBatch.Draw(textureAsPalette, new Rectangle(20, 300, 400, 140), Color.White);
            // the original texture thru all the above steps and then recolored to the palette.
            spriteBatch.Draw(textureAsPaletteImage, new Rectangle(20, 450, 400, 140), Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Here is the resulting output.

Loading your images into the content pipeline bmp i don’t think will work.
png jpeg will.

Steps 1 is valid below then use add “existing item” for step 2 browse to your images add them save the content build the content. Go back to game1 and load them as i have with the first (texture) in the above game1 project within the load method.

If you wish to change the palette texture on the shader at runtime.
Though you really shouldn’t have to unless this is to change dynamically and a lot at runtime.

You should make a pixel shader that takes the palatte array of colors. On load you should turn your textures into the 3rd image type and send them to spritebatch using your own shader when you draw them.

The below is not the best examples to start with that while not directly related it is related. You should be able to infer how to create and use a shader with spritebatch via study of the above and the related parts of the below.

There maybe better examples if you search.


@willmotil thank you this is exactly what I wanted.

why won’t bmp work to add to the content pipeline?
also do I need to add these image to the content pipeline for it to be cross platform? if so how would I do it at runtime?

I was thinking of just having a serializable class that contains all the game data called quest that would be ran in the engine.
this would hold tilesets, sounds, input bindings, and game object data.

Edit

Ah i forgot to mention earlier in that algorithm i used the alpha value you may not want to use that.
The values i used are supposedly good values to use to get a good spectrum for a monochrome image.

why won’t bmp work to add to the content pipeline?
also do I need to add these image to the content pipeline for it to be cross platform?

licensing issues and platform capability’s in combination limit things. Currently png and jpeg use open source librarys to load images and turn them into xnb’s which are then in a cross platform format. The xnb’s of your content will be found in the bin directory’s after you build your project.

Set and Get data will work on most platforms not sure if every platform supports it.

if so how would I do it at runtime?

If you really need to load a image at runtime you can use Texture2D.FromStream(…). For that you need to set up some file io and you may have to do that separately for a project depending on the platform you are targeting. Specifying the path to the file is also on you.

        protected override void LoadContent()
        {
            // you need then to specify the path as well in some way at runtime.
            //string path = // Path.Combine( directoryPath , "someImage.png");
            loadedTexture2D = LoadToTextureFromFilePath(GraphicsDevice, path);
            // ....
        }

        public Texture2D LoadToTextureFromFilePath(GraphicsDevice gd, string path)
        {
            Texture2D t;
            if (File.Exists(path))
            {
                FileStream fs = File.Open(path, FileMode.Open);
                t = Texture2D.FromStream(gd, fs);
            }
            else
            {
                throw new Exception("File not found: " + path);
            }
            return t;
        }

You would also need to manually dispose this texture in the UnLoad method.

I was thinking of just having a serializable class that contains all the game data called quest that would be ran in the engine.
this would hold tilesets, sounds, input bindings, and game object data.

This is the entire idea behind the content pipeline in order for it to work in a stable cross platform manner.

You can find tutorials for creating your own custom content reader writers importers and exporters for your own types by googling. Such as this one here.

you can find a overview of it here.
https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb447756(v%3Dxnagamestudio.41)

I might just stick with alpha being the lookup index.

I want to have everything in one file. not multipule xnb’s
this way will make it easier to load quests and make it more difficult to modify outside of the editor.

do you think just serializing the object to a binary format would work cross platform?
I don’t really want to have to use the content pipeline.

The algorithm uses the alpha in the original image to calculate the index it shouldn’t make a difference if you keep it as is. Other then that i don’t like the way it makes the grayscale image not shade black to black. But you don’t use the alpha to read the index in the resulting 3rd image, instead the r g or b elements hold the index.

do you think just serializing the object to a binary format would work cross platform?
I don’t really want to have to use the content pipeline.

Serializing the object would have to occur prior to runtime saved added to your project then, deserializing can occur whenever. Pure c# code works anywere, however image loading is platform specific.
However because you are using a image and those are encoded png and jpeg are encoded file types you wouldn’t serialize that or would you want to.
You can’t actually serialize them without loading them to a color array.
In that case you have the same problems if you want to use them dynamically it would be pointless to serialize them. Even if you did it would have to be done in the same manner as just calling FromStream.

Anyways you need to use set and get data in either case to convert them to grayscale then to the indexed pal if they are to be loaded dynamically.
If not convert them first then just add them to the pipeline or to your project in the indexed forms.

The rest of the class or game data can be serialized and deserialized but you wouldn’t need to dynamically load that you would do that at compile time and just add them to your project directly and set in the properties to copy if newer for the file type or do it all thru the pipeline.

The pipeline is there to help make a very complicated set of tasks with many different things you probably never considered a lot easier especially if you are new. The pipeline tool makes most of them really easy.
Dynamic loading of textures is one exception unless you write a content importer reader. Which is all about reading and writing serialized data that can also be compressed to a xnb.
But the alternatives are far less easy in that case anyways.

You really should read some of those links at least the second one.
https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb447756(v%3Dxnagamestudio.41)

Except
There are other circumstances, however, when it helps to understand how the Content Pipeline works.
A third party may provide custom XNA Game Studio Content Pipeline components that support additional art assets and formats.
You may need to write your own custom XNA Game Studio Content Pipeline components to support a new type of art asset or format from a DCC.
You may wish to write your own custom XNA Game Studio Content Pipeline components to derive special-purpose content from another piece of content at the time the game is built.
Content Pipeline Components
The processes that comprise the XNA Game Studio Content Pipeline fall into two types, depending on when they execute: build-time components and run-time components.

.

Securing assets against modification is a whole other topic beyond the scope of this post and you might be hard pressed to get a answer to that depending on your level of expectations and willingness to do some heavy lifting.