public static (Image <Rgba32>, DeviceBuffer[]) CreateSpriteAtlas( SpriteFile spriteFile, GraphicsDevice gd, ResourceFactory disposeFactory, TextureLoader textureLoader) { //Merge all of the sprite frames together into one texture //The sprite's bounds are the maximum size that a frame can be //Since most sprites have identical frame sizes, this lets us optimize pretty well //Determine how many frames to put on one line //This helps optimize texture size var framesPerLine = (int)Math.Ceiling(Math.Sqrt(spriteFile.Frames.Count)); //Account for the change in size when converting textures (var maximumWith, var maximumHeight) = textureLoader.ComputeScaledSize(spriteFile.MaximumWidth, spriteFile.MaximumHeight); var totalWidth = maximumWith * framesPerLine; var totalHeight = maximumHeight * framesPerLine; var atlasImage = new Image <Rgba32>(totalWidth, totalHeight); var graphicsOptions = GraphicsOptions.Default; graphicsOptions.BlenderMode = PixelBlenderMode.Src; var nextFramePosition = new Point(); //Determine which texture format it is TextureFormat textureFormat; switch (spriteFile.TextureFormat) { case SpriteTextureFormat.Normal: case SpriteTextureFormat.Additive: textureFormat = TextureFormat.Normal; break; case SpriteTextureFormat.AlphaTest: textureFormat = TextureFormat.AlphaTest; break; default: textureFormat = TextureFormat.IndexAlpha; break; } var vertexBuffers = new List <DeviceBuffer>(); foreach (var frame in spriteFile.Frames) { //Each individual texture is converted before being added to the atlas to avoid bleeding effects between frames var frameImage = textureLoader.ConvertTexture( new IndexedColor256Image(spriteFile.Palette, frame.TextureData, frame.Area.Width, frame.Area.Height), textureFormat); atlasImage.Mutate(context => context.DrawImage(graphicsOptions, frameImage, nextFramePosition)); //Note: The frame origin does not apply to texture coordinates, only to vertex offsets //Important! these are float types PointF frameOrigin = nextFramePosition; var frameSize = new SizeF(frameImage.Width, frameImage.Height); //Convert to texture coordinates frameOrigin.X /= totalWidth; frameOrigin.Y /= totalHeight; frameSize.Width /= totalWidth; frameSize.Height /= totalHeight; //The vertices should be scaled to match the frame size //These don't need to be modified to account for texture scale! var scale = new Vector3(0, frame.Area.Width, frame.Area.Height); var translation = new Vector3(0, frame.Area.X, frame.Area.Y); //Construct the vertices var vertices = new WorldTextureCoordinate[] { new WorldTextureCoordinate { Vertex = (Vertices[0] * scale) + translation, Texture = new Vector2(frameOrigin.X, frameOrigin.Y) }, new WorldTextureCoordinate { Vertex = (Vertices[1] * scale) + translation, Texture = new Vector2(frameOrigin.X + frameSize.Width, frameOrigin.Y) }, new WorldTextureCoordinate { Vertex = (Vertices[2] * scale) + translation, Texture = new Vector2(frameOrigin.X + frameSize.Width, frameOrigin.Y + frameSize.Height) } , new WorldTextureCoordinate { Vertex = (Vertices[3] * scale) + translation, Texture = new Vector2(frameOrigin.X, frameOrigin.Y + frameSize.Height) } }; var vb = disposeFactory.CreateBuffer(new BufferDescription(vertices.SizeInBytes(), BufferUsage.VertexBuffer)); gd.UpdateBuffer(vb, 0, vertices); //TODO: could refactor this by having one vertex and one index buffer, and returning the start & count of indices for each frame vertexBuffers.Add(vb); nextFramePosition.X += maximumWith; //Wrap to next line if (nextFramePosition.X >= totalWidth) { nextFramePosition.X = 0; nextFramePosition.Y += maximumHeight; } } return(atlasImage, vertexBuffers.ToArray()); }