Como criar uma textura XNA a partir de uma imagem GDI com retângulo de origem?

8

Um Texture2D no XNA pode suportar tanto. Dependendo de qual perfil eu uso, esse limite tem 2048 ou 4096 pixels de largura. Tanto quanto pude perceber na minha pesquisa (alguém me corrija se eu estiver errado), as Imagens GDI não têm outra limitação além de não exceder a memória do usuário.

Estou procurando a maneira mais eficiente de obter uma imagem GDI e um retângulo de origem (dentro das limitações de tamanho do XNA) e criar um novo Texture2D a partir desses dados em tempo de execução. O processo também deve levar em conta o alfa pré-multiplicado do XNA 4.0!

OU...

Uma maneira de pegar o Texture2D.FromStream e adaptá-lo para que ele também use um retângulo de origem, pois isso seria ainda melhor para minhas necessidades.

Os requisitos são:

  • Sem pipeline de conteúdo, preciso poder carregá-los em tempo de execução.
  • Utilizando apenas o perfil Reach, se possível!
  • Se preocupe apenas com o Windows. Não se preocupe com portabilidade.

TL; DR:

Eu preciso de um método como:

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

Ou

Texture2D Texture2DHelper.FromImage(Image, SourceRectangle)

De preferência o primeiro.


Quadro geral:

Ignorando o retângulo de origem por um momento (para facilitar a visualização), tentei carregar uma textura inteira no GDI e passar os dados para um Texture2D usando uma abordagem de força bruta:

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);
}

E o resultado foi muito lento. Demorou muitas vezes mais do que simplesmente fazer:

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

Também pensei em usar o Bitmap.LockBits e uma cópia de bloco, mas isso deixaria de fora a correção alfa pré-multiplicada e não sei como aplicá-la após a cópia.

Estou tentado a pensar que uma solução melhor seria contornar o Texture2D.FromStream diretamente ... Não estou muito familiarizado com o funcionamento do Stream, mas vi alguns métodos no .NET que usam um Stream e um Retângulo. Como eu conseguiria algo assim? Um método FromStream inteiramente novo ou um invólucro em torno de qualquer Stream que forneça a funcionalidade "Retângulo".

David Gouveia
fonte

Respostas:

4

OK, consegui encontrar a solução para isso, no estilo GDI! Eu o envolvi em uma classe auxiliar estática:

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;
        }
    }
}

Caso você não tenha notado que o Texture2D.FromFile foi removido do XNA 4.0, aproveitei a chance para replicar o comportamento antigo quando não passava um retângulo de origem.

E a melhor parte é que, como ainda depende do Texture2D.FromStream, toda a fase alfa pré-multiplicada é automatizada! Também é executado relativamente rápido. Ok, isso não é verdade! Você ainda precisa fazer os cálculos alfa pré-multiplicados. Eu simplesmente não me lembro bem, já que eu já tinha código para fazer isso no lugar .

Mas ainda me pergunto se existe uma maneira de fazer isso diretamente do Stream sem precisar passar pelo GDI. A mente vagou para decorar o córrego de alguma maneira, mas não chegou a lugar algum. :)

E, navegando pelo meu código antigo, encontrei um comentário sobre o Texture2D.FromStream estar com erros e não conseguir ler todos os formatos que a documentação diz, então estou usando o GDI de qualquer maneira. Então, eu vou ficar com esse método por enquanto.

David Gouveia
fonte