Comment créer une texture XNA à partir d'une image GDI avec un rectangle source?

8

Un Texture2D dans XNA ne peut contenir qu'une telle quantité. Selon le profil que j'utilise, cette limite est de 2048 ou 4096 pixels de large. Autant que je sache d'après mes recherches (quelqu'un me corrige si je me trompe), les images GDI n'ont aucune limitation autre que de ne pas dépasser la mémoire de l'utilisateur.

Je cherche le moyen le plus efficace de prendre une image GDI et un rectangle source (dans les limites de taille de XNA) et de créer un nouveau Texture2D à partir de ces données lors de l'exécution. Le processus doit également prendre en compte l'alpha pré-multiplié de XNA 4.0!

OU...

Une façon de prendre Texture2D.FromStream et de l'adapter pour qu'il prenne aussi un rectangle source, car ce serait encore mieux pour mes besoins.

Les exigences sont les suivantes:

  • Pas de pipeline de contenu, je dois pouvoir les charger lors de l'exécution.
  • Utiliser uniquement le profil Reach si possible!
  • Ne vous souciez que de Windows. Ne vous inquiétez pas de la portabilité.

TL; DR:

J'ai besoin d'une méthode telle que:

Texture2D Texture2DHelper.FromStream(GraphicsDevice, Stream, SourceRectangle);

Ou

Texture2D Texture2DHelper.FromImage(Image, SourceRectangle)

De préférence le premier.


Grande image:

Ignorant le rectangle source pendant un moment (pour rendre les choses plus faciles à visualiser), j'ai essayé de charger une texture entière dans GDI et de passer les données à un Texture2D en utilisant une approche complètement brute:

using (System.Drawing.Image image = System.Drawing.Image.FromFile(path))
{
    int w = image.Width;
    int h = image.Height;
    System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(image);
    uint[] data = new uint[w * h];
    for (int i=0; i!=bitmap.Width; ++i)
    {
        for (int j=0; j!=bitmap.Height; ++j)
        {
            System.Drawing.Color pixel = bitmap.GetPixel(i, j);
            Microsoft.Xna.Framework.Color color = Color.FromNonPremultiplied(pixel.R, pixel.G, pixel.B, pixel.A);
            data[i + j * w] = color.PackedValue;
        }
    }
    _texture = new Texture2D(device, w, h);
    _texture.SetData(data);
}

Et le résultat a été vraiment lent. Cela a pris plusieurs fois plus de temps que de le faire:

Texture2D.FromStream(GraphicsDevice, new FileStream(path, FileMode.Open));

J'ai également pensé à utiliser Bitmap.LockBits et une copie de bloc, mais cela laisserait de côté la correction alpha pré-multipliée, et je ne sais pas comment l'appliquer après la copie.

Je suis tenté de penser qu'une meilleure solution serait de contourner directement Texture2D.FromStream ... Je ne suis pas trop familier avec le fonctionnement de Stream mais j'ai vu quelques méthodes dans .NET qui prennent un Stream et un Rectangle. Comment pourrais-je réaliser quelque chose comme ça? Soit une méthode FromStream entièrement nouvelle, soit un wrapper autour de n'importe quel Stream qui fournit la fonctionnalité "Rectangle".

David Gouveia
la source

Réponses:

4

OK, j'ai réussi à trouver la solution à cela, à la manière de GDI! Je l'ai enveloppé dans une classe d'assistance statique:

public static class TextureLoader
{
    public static Texture2D FromFile(GraphicsDevice device, string path, Microsoft.Xna.Framework.Rectangle? sourceRectangle = null)
    {
        // XNA 4.0 removed FromFile in favor of FromStream
        // So if we don't pass a source rectangle just delegate to FromStream
        if(sourceRectangle == null)
        {
            return Texture2D.FromStream(device, new FileStream(path, FileMode.Open));
        }
        else
        {
            // If we passed in a source rectangle convert it to System.Drawing.Rectangle
            Microsoft.Xna.Framework.Rectangle xnaRectangle = sourceRectangle.Value;
            System.Drawing.Rectangle gdiRectangle = new System.Drawing.Rectangle(xnaRectangle.X, xnaRectangle.Y, xnaRectangle.Width, xnaRectangle.Height);

            // Start by loading the entire image
            Image image = Image.FromFile(path);

            // Create an empty bitmap to contain our crop
            Bitmap bitmap = new Bitmap(gdiRectangle.Width, gdiRectangle.Height, image.PixelFormat);

            // Draw the cropped image region to the bitmap
            Graphics graphics = Graphics.FromImage(bitmap);
            graphics.DrawImage(image, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), gdiRectangle.X, gdiRectangle.Y, gdiRectangle.Width, gdiRectangle.Height, GraphicsUnit.Pixel);

            // Save the bitmap into a memory stream
            MemoryStream stream = new MemoryStream();
            bitmap.Save(stream, ImageFormat.Png);

            // Finally create the Texture2D from stream
            Texture2D texture = Texture2D.FromStream(device, stream);

            // Clean up
            stream.Dispose();
            graphics.Dispose();
            bitmap.Dispose();
            image.Dispose();

            return texture;
        }
    }
}

Dans le cas où vous n'avez pas remarqué que Texture2D.FromFile a été supprimé de XNA 4.0, j'ai donc profité de l'occasion pour répliquer l'ancien comportement lorsqu'il ne passait pas un rectangle source.

Et la meilleure partie est que puisqu'à la fin il repose toujours sur Texture2D.FromStream, toute la phase alpha prémultipliée est automatisée! Fonctionne relativement vite aussi. D'accord, ce n'est pas vrai! Vous devez toujours faire les calculs alpha prémultipliés. Je ne me souvenais pas bien car j'avais déjà du code pour le faire .

Mais je me demande toujours s'il existe un moyen de le faire directement à partir du Stream sans avoir à passer par GDI. L'esprit a erré vers la décoration du Stream d'une manière ou d'une autre mais n'est arrivé nulle part. :)

Et en parcourant mon ancien code, j'ai trouvé un commentaire sur Texture2D.FromStream étant buggé et ne pas pouvoir lire tous les formats que la documentation dit, donc j'ai utilisé GDI de toute façon. Je vais donc rester avec cette méthode pour l'instant.

David Gouveia
la source