1
\$\begingroup\$

I am using a 1D array of pixels for rendering in 2D in java. I am interested in rotation (in radians) and mirroring across the x and y axes. The desired result looks something like this:

Public void render(Sprite sprite, int x, int y, double rotation, boolean xFlip, boolean yFlip) {}

I am not using any libraries for the current project. The information that I have gleaned is that I must somehow multiply the points of my pixels with a transformation matrix. I am familiar with matrices (outside of a programming context) and am ok with rigorous mathematics.

How do I rotate and flip 2D sprites with a 1D array of pixels?

EDIT The sprite class is almost exclusively a wrapped array of pixel data. In my Screen class:

image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
int[] pixels  = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();

and then

g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
\$\endgroup\$
14
  • \$\begingroup\$ Dealing with 1D array is trivial, each time you access [y*width+x] instead of [x,y] \$\endgroup\$
    – Kromster
    Sep 4 '14 at 5:27
  • \$\begingroup\$ Just to make sure we're talking about the same thing, could you clarify what you mean with "1D array of pixels"? \$\endgroup\$ Sep 4 '14 at 5:55
  • \$\begingroup\$ @KromStern indeed! I enjoy the sleekness of the 1d array. My confusion, though, is not necessarily how to implement this technique with the 1d array, but implementing this technique at all. \$\endgroup\$
    – MikeJava
    Sep 4 '14 at 5:56
  • \$\begingroup\$ @PandaPajama sure! All of my pixel data is stored in a 1d array (int[]) as opposed to a 2d array (int[][]). Its then fed into the canvas in this form. It's a trivial detail but I thought it may be necessary for proper implementation. \$\endgroup\$
    – MikeJava
    Sep 4 '14 at 6:02
  • 1
    \$\begingroup\$ Some more code, maybe the Sprite class, if it is more than a wrapped array, but particularly the drawing code (where you are using the pixels from the 1D array) may be helpful. I assume that the intention is not to create a new sprite with the rotated image, but only to draw it with a certain angle of rotation. A general aside: One could (or should!) probably encapsulate these operations (rotation and flipping, and more) in a docs.oracle.com/javase/8/docs/api/java/awt/geom/… ... \$\endgroup\$
    – Marco13
    Sep 4 '14 at 9:01
0
\$\begingroup\$

I'll try to give a two-fold answer: On the one hand, giving a bit of source code as a MCVE. On the other hand, I'll quickly try to point out why implementing this on your own is a bad idea.

I'll start with the latter.

Why you should not implement it this way

You mentioned in the comments

I was always under the impression that this was a more efficient system (more work means it's better, right? ;))

NO!

(It's a pity that I can't emphasize this more...)

The best way to do image- and sprite rendering and manipulation is to do it in the way that it was intended to be done, by the developers of the respective API. And if these developers did it right, this implies the least work: The easiest way should be the right way.

In this particular case: If you want to draw images (sprites) efficiently, you really should use the built-in mechanisms. Store the sprite data as a BufferedImage, and draw the sprites using the corresponding methods of the Graphics2D class, utilizing an AffineTransform to transform the images. This allows the underlying library (Swing/AWT) to use hardware support for drawing, and you'll not even remotely achieve a similar performance when you are doing all this manually in software...

(Of course, when using the intended API functionality, there still are some "tricks" to squeeze out the last bit of performance, but this is a different story than the fundamental decision between hardware- and software rendering...)


This recommendation finds its limitations in the (rare) cases where one needs the image data in a format that is "not directly accessible" with the vanilla API. For example, when you have a BufferedImage and want to modify individual pixels, this can be done with the BufferedImage#setRGB(x,y,rgb) method. But if you have to do this often, then there may be better options.

I'd really recommend you to read the question Java single Array best choice for accessing pixels for manipulation?. I wrote an elaborate answer pointing out the caveats when trying to access the raw pixel data.


If you still wanted to implement it this way...

The question itself touches a very general (and broad) topic. Namely the topic of Texture Mapping. (As such, this usually refers to mapping a 3D polygon to 2D, but the techniques that are employed there are basically the same as for a 2D->2D mapping).

The fact that you are using 1D arrays for the pixels is only a minor (and actually rather unimportant) artifact. For the mapping techniques it does not matter whether a (pseudocode) method like int rgb = readPixel(x,y) or writePixel(x,y,rgb) internally accesses a 1D array or a 2D array (or a BufferedImage).


So the question can be boiled down to: How to paint an image that was transformed with some (affine) transformation?

You're not the first one who tries this ;-) And summarizing 50 years of research in one answer is not possible. You may start with reading this (PDF file) with a Survey of Texture Mapping.

It starts with a first, rough classification of the possible approaches:

  • For each pixel of the image, you can compute its position on the screen
  • For each pixel on the screen, you can compute which pixel of the image has to be painted there

These approaches are implemented here in a dummy example.

But...

...this is not how it is done "properly" (see the notes below). It's just to have some code showing the idea:

import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class PixelImageTest
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final PixelImagePanel pixelImagePanel = new PixelImagePanel();
        f.getContentPane().add(pixelImagePanel);

        BufferedImage image = loadImage("Julia01.jpg");
        final int w = image.getWidth();
        final int h = image.getHeight();
        final int pixels[] = PixelImages.getPixels(image);

        Timer t = new Timer(20, new ActionListener()
        {
            private double angleRad = 0;
            @Override
            public void actionPerformed(ActionEvent e)
            {
                AffineTransform at = new AffineTransform();
                at.concatenate(AffineTransform.getTranslateInstance(400,400));
                at.concatenate(AffineTransform.getRotateInstance(angleRad));
                angleRad += 0.05;
                pixelImagePanel.clear();
                //pixelImagePanel.drawImageTextureScanning(pixels, w, h, at);
                pixelImagePanel.drawImageScreenScanning(pixels, w, h, at);
            }
        });
        t.start();

        f.setSize(800,800);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static BufferedImage loadImage(String fileName)
    {
        try
        {
            return convertToARGB(ImageIO.read(new File(fileName)));
        }
        catch (IOException e)
        {
            e.printStackTrace();
            return null;
        }
    }

    private static BufferedImage convertToARGB(BufferedImage image)
    {
        BufferedImage result = new BufferedImage(
            image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics g = result.getGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
        return result;
    }
}

class PixelImagePanel extends JPanel
{
    private BufferedImage canvasImage;
    private int canvasPixels[];

    void clear()
    {
        validateCanvas();
        Arrays.fill(canvasPixels, 0);
    }

    // For each pixel of the image, compute its location
    // on the screen when transformed with the given AT
    void drawImageTextureScanning(
        int pixels[], int w, int h, AffineTransform at)
    {
        Point p = new Point(0,0);
        for (int x=0; x<w; x++)
        {
            for (int y=0; y<h; y++)
            {
                p.x = x;
                p.y = y;
                at.transform(p, p);
                int index = x+y*w;
                int rgb = pixels[index];
                setPixel(p.x, p.y, rgb);
            }
        }
        repaint();
    }

    // For each pixel on the screen, compute which pixel
    // of the image is placed there with the given AT
    void drawImageScreenScanning(
        int pixels[], int w, int h, AffineTransform at)
    {
        Point p = new Point(0,0);
        AffineTransform ati = at;
        try
        {
            ati = at.createInverse();
        }
        catch (NoninvertibleTransformException e)
        {
            e.printStackTrace();
        }

        int sw = getWidth();
        int sh = getHeight();
        for (int x=0; x<sw; x++)
        {
            for (int y=0; y<sh; y++)
            {
                p.x = x;
                p.y = y;
                ati.transform(p, p);
                if (p.x < 0 || p.x >= w || p.y < 0 || p.y >= h)
                {
                    continue;
                }
                int index = p.x+p.y*w;
                int rgb = pixels[index];
                setPixel(x, y, rgb);
            }
        }
        repaint();
    }


    private void setPixel(int x, int y, int rgb)
    {
        if (getWidth() <= 0 || getHeight() <= 0)
        {
            return;
        }
        validateCanvas();
        int index = x+y*getWidth();
        canvasPixels[index] = rgb;
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        validateCanvas();
        g.drawImage(canvasImage, 0, 0, this);
    }

    private void validateCanvas()
    {
        if (getWidth() <= 0 || getHeight() <= 0)
        {
            return;
        }
        if (canvasImage == null ||
            canvasImage.getWidth() != getWidth() ||
            canvasImage.getHeight() != getHeight())
        {
            canvasImage = new BufferedImage(
                getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
            canvasPixels = PixelImages.getPixels(canvasImage);
        }
    }
}


class PixelImages
{
    public static int[] getPixels(BufferedImage image)
    {
        WritableRaster writableRaster = image.getRaster();
        DataBuffer dataBuffer = writableRaster.getDataBuffer();
        if (!(dataBuffer instanceof DataBufferInt))
        {
            throw new IllegalArgumentException("Image must be TYPE_INT...");
        }
        DataBufferInt dataBufferInt = (DataBufferInt)dataBuffer;
        return dataBufferInt.getData();
    }
}

It has two methods for painting an image (consisting of a 1D array of pixels) into the "canvas" (which is also a 1D array of pixels):

  • drawImageTextureScanning: For each image pixel, this computes the the position on the screen where the pixel has to be placed
  • drawImageScreenScanning: For each screen pixel, this computes the image pixel that has to be placed there

Both approaches are horribly inefficient, because each pixel location is transformed individually. For a more efficient implementation, one would have to use more sophisticated algorithms. One common approach is to use an algorithm that is similar to the Bresenham line algorithm to efficiently compute the "next" pixel that has to be painted.


To emphasize this once more: If you want to draw and handle sprites efficiently, then use the built-in infrastructure of BufferedImage. Preferably managed images of type TYPE_INT_ARGB or TYPE_INT_RGB - see my answer to the linked question, particularly the section about "Implementation Details" (no fear, it's not as technical as it may sound...).

\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much! Your answer has gone above and beyond. Excellent. \$\endgroup\$
    – MikeJava
    Sep 7 '14 at 16:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.