[Solved] Content importer processor How to process seperate files into one xnb?

Standard problem i want to take data from a file that describes rectangles in a sheet and load in a texture2d then output that to a single xnb.
Problem is no matter how i try to get this to work to combine them i can’t.
It is like larry mo and curly.

Specifically i don’t know how to get both a Texture2d and the sprite rectangle description file read into my class?
Additionally i can’t seem to just transform the texture data on its own into a texture2D it seems like texture2Dcontent isn’t really a texture2D and i have no clue how to cast it into one? Or am i supposed to read its pixels and use setdata to do it myself ? Still i don’t know how to chain the importer processors together to do it.

    [ContentProcessor(DisplayName = "SpriteSheet Processor")]
    public class SpriteSheetProcessor : ContentProcessor<BinaryReader, SpriteSheet>
    {
        public SpriteSheetProcessor()
        {
        }

        public override SpriteSheet Process(BinaryReader input, ContentProcessorContext context)
        {
            try
            {
                context.Logger.LogMessage("Processing SpriteSheet");
                SpriteSheet ss = new SpriteSheet();

                // ... pull in spr data to instance.

                // P R O B L E M

                // No clue how to get a seperate texture processed file into my instance?
                // Or how to convert it to a Texture2d from Texture2Dcontent ?
                // trying to make a seperate processor or importer and call it just fails.
                
                //ss.textureSheet = ???????????????????

                return ss;
            }
            catch (Exception ex)
            {
                context.Logger.LogMessage("Error {0}", ex);
                throw;
            }
        }
    }

Then i think im supposed to just use WriteRawObject to just drop the texture2d to a xnb as shown below. Then later in the reader ReadRawObject but im stuck on reading in the seperate files from the content folder into my class properly.

    [ContentTypeWriter]
    public class SpriteSheetDataWriter : ContentTypeWriter<SpriteSheet>
    {     
        protected override void Write(ContentWriter output, SpriteSheet ss)
        {

            output.WriteRawObject<Texture2D>(ss.textureSheet);

            output.Write(ss.name);
            output.Write(ss.sheetWidth);
            output.Write(ss.sheetHeight);
            output.Write(ss.sprites.Count);

            for (int i = 0; i < ss.sprites.Count; i++)
            {
                output.Write(ss.sprites[i].nameOfSprite);
                output.Write(ss.sprites[i].sourceRectangle.X);
                output.Write(ss.sprites[i].sourceRectangle.Y);
                output.Write(ss.sprites[i].sourceRectangle.Width);
                output.Write(ss.sprites[i].sourceRectangle.Height);
                // skip texture we only write one and it is already written.
            }
        }
        public override string GetRuntimeType(TargetPlatform targetPlatform)
        {
            return typeof(SpriteSheet).AssemblyQualifiedName;
        }
        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(SpriteSheetReader).AssemblyQualifiedName;
        }
    }
}

I could just write the whole thing out as a bin file including the color data, then read it in and use set data, but then what good is the content pipeline at that point for custom data.

Getting aggrivated as hell been at this on and off all day.

.
.

the class itself is simple.

    public class SpriteSheet
    {
        public string name = "None";
        public int sheetWidth = 0;
        public int sheetHeight = 0;
        public Texture2D textureSheet;
        public List<Sprite> sprites = new List<Sprite>();

        public void Add(string name, Texture2D texture, Rectangle source)
        {
            sprites.Add(new Sprite(name, texture, source));
            //return sprites[sprites.Count - 1];
        }
        public void Remove(Sprite s)
        {
            sprites.Remove(s);
        }
        public Rectangle GetSourceRectangle(Sprite s)
        {
            return s.sourceRectangle;
        }
        public Texture2D GetTexture(Sprite s)
        {
            return s.texture;
        }

        public SpriteSheet() { }

        public class Sprite
        {
            public Sprite() { }
            public Sprite(string name, Texture2D texture, Rectangle source)
            {
                nameOfSprite = name;
                this.texture = texture;
                sourceRectangle = source;
            }
            public string nameOfSprite;
            public Texture2D texture;
            public Rectangle sourceRectangle;
        }
    }

This sounds a lot like what I’ve done with the ‘Atlas’ importer.
Importer
runtime

It takes a file that defines sprites and an image and export an object with a set of sprites.
(currently it reads .tmx but it’s not a tilemap, I am using it as an atlas because it’s convenient… in the future I could expand it to read a list of files and do it;s own sprite packing.)

The pipeline doesn’t know of the .Graphics namespace, (in XNA those where seperate dlls). TextureX becomes TextureXContent. It’s a representation of a texture in raw bytes, (it doesn’t make much sense to create a device , SetData to the GPU and then call GetData() to work with the byte array). The writer serialize the TextureXContent in binary and the reader deserialize it at runtime into a TextureX.

humm i see how you did it in the reader but.

            IGraphicsDeviceService graphicsDeviceService = (IGraphicsDeviceService)input.ContentManager.ServiceProvider.GetService(typeof(IGraphicsDeviceService));
            var device = graphicsDeviceService.GraphicsDevice;

            Texture2D sst = new Texture2D(device, ss.sheetWidth, ss.sheetHeight);
            sst = ReadTexture2D(input, sst); //input.ReadRawObject<Texture2D>();
            ss.textureSheet = sst;

I can’t call in the processor, output = input.ReadRawObject<Texture2D>(existingInstance);
Since i can’t save raw data to then load it to a texture2d in the Processor which would be kinda pointless … so…
Then i cant use WriteRawObject in the Writer as it wants a texture2d. output.WriteRawObject<Texture2D>(ss.textureSheet); ???
Which makes no sense to me as to how this workflow is supposed to go.
if i have no easy way to make raw byte data or a texture2dcontent object into a texture2d which the writer wants??? from the processor or importer.

but maybe im missing something or alot of somethings.

So if i go the other route if i save a description file and i save a SpriteSheet image to disk in game1 using texture.SaveAsPng() then try to add it to the content pipeline using a TextureProcessor and having a seperate bin file for source rectangle data.
i cant see how to get both that Texture2dContent and the BinaryReader into the same processor method to give al the needed data to the Writer to make a single Xnb holding the full SpriteSheet instance data…

or rather this is the part i don’t see how to do.

I see in the writer that the write method takes a byte array am i supposed to combine all the data into a single file to import if so how would the writer know then, that this is a textures color array to do things like compress it when i seperate out the color byte data? Or does that all have to be done manually anyways.

Why?
that is exactly what i do [here].

No, It takes a Texture2DContent [here].

You can call the Texture importer directly which gets you a TextureContent. (1] [2]

You do all the processing (texture compression, etc) in the processor, or rather - the TextureProcessor does. The only thing the writer does is to serializes the Texture2DContent.
When you call .WriteRawObject((Texture2DContent)) the pipeline knows to use the Texture2DWriter.

In addition to that,
Do not reference your runtime class ‘SpriteSheet’ in the processor.
You’d have to make a new ‘SpriteSheetContent’ that will mirror the data structs of your ‘SpriteSheet’ but have ‘Texture2DContent’ types in place of ‘Texture2D’.

For example I have a runtime ‘TextureAtlas
and a ‘TextureAtlasContent’ for use in the importer/processor.

1 Like

Thank you nkast.

The last bit is what i was missing as well as a reference to … pipeline… graphics which had me stumped for hours. Well that was extremely hard to get working.

Might as well post up the whole thing for reference for the next person.

So here they are in sequence if you see anything im doing wrong here let me know.

Importer

Grabs the source rectangles .spr description file for the texture that represents our spritesheet with the same name and reads them.

using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

namespace SpriteSheetPipeLine
{

    ///
    /// ContentImporter how we tell the processor to import files from the content folder.
    /// ContentImporter(".spr",  This tells the Pipeline tool to use this content importer for files with the .spr extension which ill be generating. 
    /// It also tells the Pipeline tool to use SpriteSheetProcessor as the default processor.
    ///
    [ContentImporter(".spr", DefaultProcessor = "SpriteSheetProcessor", DisplayName = "SpriteSheetImporter")]
    public class SpriteSheetImporter : ContentImporter<SpriteSheetContent>
    {
        public override SpriteSheetContent Import(string filename, ContentImporterContext context)
        {
            // Debug line that shows up in the Pipeline tool or your build output window.
            context.Logger.LogMessage("Importing SpriteSheet file: {0}", filename);
            
            SpriteSheetContent ssc = new SpriteSheetContent();

            using (BinaryReader input = new BinaryReader(File.OpenRead(filename)))
            {
                ssc.name = input.ReadString();
                ssc.sheetWidth = input.ReadInt32();
                ssc.sheetHeight = input.ReadInt32();
                int spritesLength = input.ReadInt32();
                for (int i = 0; i < spritesLength; i++)
                {
                    var s = new SpriteContent();
                    s.nameOfSprite = input.ReadString();
                    s.sourceRectangle = new Rectangle(input.ReadInt32(), input.ReadInt32(), input.ReadInt32(), input.ReadInt32());
                    ssc.sprites.Add(s);
                }

                TextureImporter texImporter = new TextureImporter();

                var texfilename = ssc.name;
                //var fullFilePath = Path.Combine(Environment.CurrentDirectory, Path.GetDirectoryName(texfilename));
                var fullFilePath = Path.ChangeExtension(texfilename,".png");
                context.Logger.LogMessage("Importing SpriteSheet file: {0}", fullFilePath);
                var textureContent = (Texture2DContent)texImporter.Import(fullFilePath, context);
                textureContent.Name = Path.GetFileNameWithoutExtension(fullFilePath);

                ssc.textureSheet = textureContent;

                for (int i = 0; i < spritesLength; i++)
                {
                    ssc.sprites[i].texture = ssc.textureSheet;
                }
            }
            return ssc;
        }
    }
}

Processor

handles options and other stuff mine is empty here.

using System;
using Microsoft.Xna.Framework.Content.Pipeline;
// 
// We pass the result the spritesheet to the ContentWriter to make the xnb.
//
namespace SpriteSheetPipeLine
{
    /// <summary>
    /// We process Data from the spr file in the content folder. 
    /// The content processor takes data such as sprite rectangles and textures from the importers i thinks. 
    /// public class SpriteSheetProcessor : ContentProcessor Input, Output
    /// </summary>
    [ContentProcessor(DisplayName = "SpriteSheetProcessor")]
    public class SpriteSheetProcessor : ContentProcessor<SpriteSheetContent, SpriteSheetContent>
    {
        //[DisplayName("Scale")]
        //[DefaultValue(1)]
        //[Description("Set the scale of the model.")]
        //public float Scale { get; set; }
        public SpriteSheetProcessor()
        {
            // Maybe you want to do some default preprocessing things. 
            // For example, 
            // You may add parameters to the Pipeline process. 
            // Optional variables that you can change in the Pipeline tool can be added here:
            // then process things accordingly.
            //..
        }
        public override SpriteSheetContent Process(SpriteSheetContent input, ContentProcessorContext context)
        {
            try
            {
                context.Logger.LogMessage("Processing SpriteSheet");
                return input;
            }
            catch (Exception ex)
            {
                context.Logger.LogMessage("Error {0}", ex);
                throw;
            }
        }
    }
}

Writer

makes the xnb.

using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using SpriteSheetData;

namespace SpriteSheetPipeLine
{
    // We write files as a xnb file.
    [ContentTypeWriter]
    public class SpriteSheetDataWriter : ContentTypeWriter<SpriteSheetContent>
    {  
        protected override void Write(ContentWriter output, SpriteSheetContent ss)
        {
            output.Write(ss.name);
            output.Write(ss.sheetWidth);
            output.Write(ss.sheetHeight);
            output.Write(ss.sprites.Count);
            for (int i = 0; i < ss.sprites.Count; i++)
            {
                output.Write(ss.sprites[i].nameOfSprite);
                output.Write(ss.sprites[i].sourceRectangle.X);
                output.Write(ss.sprites[i].sourceRectangle.Y);
                output.Write(ss.sprites[i].sourceRectangle.Width);
                output.Write(ss.sprites[i].sourceRectangle.Height);
                // skip texture we only write one and it is already written.
            }
            output.WriteRawObject((Texture2DContent)ss.textureSheet);           
        }
        public override string GetRuntimeType(TargetPlatform targetPlatform)
        {
            return typeof(SpriteSheetContent).AssemblyQualifiedName;
        }
        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(SpriteSheetReader).AssemblyQualifiedName;
        }
    }
}

SpriteSheetContent,

mockup SpriteSheet class, this stands in during the (importing processing and writeing) part of the workflow.

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

namespace SpriteSheetPipeLine
{
    public class SpriteSheetContent : Texture2DContent
    {
        public string name;
        public int sheetWidth = 0;
        public int sheetHeight = 0;
        public TextureContent textureSheet;
        public List<SpriteContent> sprites = new List<SpriteContent>();
    }
    public class SpriteContent
    {
        public string nameOfSprite;
        public TextureContent texture;
        public Rectangle sourceRectangle;
    }
}

Reader

when we load content from game1 well be using this to load the xnb data we wrote into the spritesheet class we will be using in game1.

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

namespace SpriteSheetData
{
    // We read in the xnb.
    public class SpriteSheetReader : ContentTypeReader<SpriteSheet>
    {
        protected override SpriteSheet Read(ContentReader input, SpriteSheet existingInstance)
        {
            SpriteSheet ss = new SpriteSheet();

            ss.name = input.ReadString();
            ss.sheetWidth = input.ReadInt32();
            ss.sheetHeight = input.ReadInt32();
            int spritesLength = input.ReadInt32();
            for (int i =0;i< spritesLength; i++)
            {
                var s = new SpriteSheet.Sprite();
                s.nameOfSprite = input.ReadString();
                s.sourceRectangle = new Rectangle(input.ReadInt32(), input.ReadInt32(), input.ReadInt32(), input.ReadInt32());
                ss.sprites.Add(s);
            }
            // from nkasts ex.
            IGraphicsDeviceService graphicsDeviceService = (IGraphicsDeviceService)input.ContentManager.ServiceProvider.GetService(typeof(IGraphicsDeviceService));
            var device = graphicsDeviceService.GraphicsDevice;
            Texture2D sst = new Texture2D(device, ss.sheetWidth, ss.sheetHeight);
            sst = ReadTexture2D(input, sst); //input.ReadRawObject<Texture2D>();
            ss.textureSheet = sst;
            for (int i = 0; i < spritesLength; i++)
            {
                ss.sprites[i].texture = sst;
            }
            return ss;
        }
        // nkasts read method
        private Texture2D ReadTexture2D(ContentReader input, Texture2D existingInstance)
        {
            Texture2D output = null;
            try
            {
                output = input.ReadRawObject<Texture2D>(existingInstance);
            }
            catch (NotSupportedException)
            {
                var assembly = typeof(Microsoft.Xna.Framework.Content.ContentTypeReader).Assembly;
                var texture2DReaderType = assembly.GetType("Microsoft.Xna.Framework.Content.Texture2DReader");
                var texture2DReader = (ContentTypeReader)Activator.CreateInstance(texture2DReaderType, true);
                output = input.ReadRawObject<Texture2D>(texture2DReader, existingInstance);
            }
            return output;
        }
    }
}

SpriteSheet

the class that game1 will use.

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

namespace SpriteSheetData
{
    public class SpriteSheet
    {
        public string name = "None";
        public int sheetWidth = 0;
        public int sheetHeight = 0;
        public Texture2D textureSheet;
        public List<Sprite> sprites = new List<Sprite>();

        public void Add(string name, Texture2D texture, Rectangle source)
        {
            sprites.Add(new Sprite(name, texture, source));
            //return sprites[sprites.Count - 1];
        }
        public void Remove(Sprite s)
        {
            sprites.Remove(s);
        }
        public Rectangle GetSourceRectangle(Sprite s)
        {
            return s.sourceRectangle;
        }
        public Texture2D GetTexture(Sprite s)
        {
            return s.texture;
        }

        public SpriteSheet() { }

        public class Sprite
        {
            public Sprite() { }
            public Sprite(string name, Texture2D texture, Rectangle source)
            {
                nameOfSprite = name;
                this.texture = texture;
                sourceRectangle = source;
            }
            public string nameOfSprite;
            public Texture2D texture;
            public Rectangle sourceRectangle;
        }
    }
}

game1 load

            //MakeSpriteSheet("spriteSheetTest01.spr", 512, 512, UiAssets.Texture2DList, out spriteSheet,true, false, savepath);

            spriteSheet = Content.Load<SpriteSheet>("spriteSheetTest01");

Aadditionally the mgcb file looks like this with the two references added.
though i think i should only need the one for the reader and the spritesheet for loading.

#-------------------------------- References --------------------------------#

/reference:…\SpriteSheetData\bin\Debug\SpriteSheetData.dll
/reference:…\SpriteSheetPipeLine\bin\Debug\SpriteSheetPipeLine.dll

#---------------------------------- Content ---------------------------------#

If anyone wants to see the sprite packing algorithm and methods that generated the .spr and png from multiple images, then ill post em.
Its really pretty simple but the extra stuff that goes with it is kinda big.

I might make a function that just packs all this up as a class file like i did with the font thing i sort of like having a couple default classes around that basically create default images fonts ect from a class, plus its cool and fun.

https://drive.google.com/file/d/1fTDCsNITasKCqlqi9Sa1hODTJDTbIRM7/view?usp=sharing

2 Likes