4
\$\begingroup\$

Edit- ive noticed that the issue is caused when drawing to a bufferedimage

I've been struggling to create 2D lighting in my 2D sandbox game. I've been reading through multiple threads and have tried many things, heres a list:

  • I've tried the method of creating an image of a light (instead of creating a radialgradient each time I want to make a light circle), but this gets very laggy the more I have or the bigger they get. I've tried downscaling the quality but then it just looks terrible, and I still have issues with lag. Also, it is very difficult to change color and other such things this way (since the image is already prebuilt at the start). enter image description here

    You can see the FPS already garbage after only a few lights placed down.

    This code below is a much simplified version

    public void makeLightMap(Graphics2D g)
    {
        Graphics2D gl = light.createGraphics();
        gl.setColor(new Color(0, 0, 0, 255));
        gl.fillRect(0, 0, Handler.width,Handler.height);
        gl.setColor(new Color(0, 0, 0, 255));
    
           Composite oldComp = gl.getComposite();
           for(int i = 0; i < lights.size(); i++)
           {
                Point2D.Double pt = new Point2D.Double(lights.get(i).x,     lights.get(i).y);
                 int radius = 500;
                  gl.setComposite(AlphaComposite.DstIn);
                  gl.drawImage(lightImage, (int) pt.x, (int) pt.y, radius * 1, radius * 1, null);
           }
    
    gl.setComposite(oldComp);
    gl.dispose();
    
    }
    
  • I've also tried to draw directly onto my JFrame (instead of drawing it to a bufferedimage than drawing it onto the Jframe), which would remove this lag, but I could not get this to work.

  • I've also heard about doing it per pixel like in Notch's Left4KDead, but I heard this was very inefficient so I decided not to bother.

So, is there any way I could have it so I have lots of lights on the screen, some moving around, without lag and with the quality of gradient that I have now? Any help would be appreciated! PS: I would really like to avoid rough lighting, such as seen in this [post][2]

----edit 2----

I've narrowed the issue to drawing on bufferedimages ( which seems to create this lag).
In this picture, you can see I'm drawing 500 images of dirt(32x32 png image) directly onto my frame (the brown line extending from the top left to the bottom left), and getting normal FPS. Code:

//I call super.paintComponent(g);   at the beginning
for(int i = 0; i < 500; i++)
    {
        g.drawImage(Sources.DIRT, 0 + i, 0 + i, 100, 100, null);
    }

enter image descridon here


However, if I attempt to draw the same 500 images on the bufferedimages, I start to lag (30 - 40fps) C

 BufferedImage bufferedimage = new BufferedImage(Screen.width, Screen.height, BufferedImage.TYPE_INT_ARGB);
    public void render(Graphics2D g)
    {
        BufferedImageLag()
        g.drawImage(bufferedimage, 0, 0, null);
    public void BufferedImageLag()
    {
        Graphics2D g = bufferedimage.createGraphics(); 
        for(int i = 0; i < 500; i++)
        {
            g.drawImage(Sources.DIRT, 0 + i, 0 + i, 100, 100, null);
        }
        g.dispose();
    }

enter image descridon here
Now of course I wouldn't being doing something like this in my game, but I beleive you should be able to copy paste this into a simple program and get the same effect (not sure how you'd get fps, but you could look at task manager and see how much cpu java is eating up).

\$\endgroup\$
14
  • \$\begingroup\$ Why don't you use OpenGL btw? It's way faster and manageable after a while \$\endgroup\$
    – Bálint
    Commented Jul 31, 2016 at 21:46
  • \$\begingroup\$ @Bálint I've also tried to stay away from external libraries because I thought it was 'cheaty', but I'll probably end up changing sooner or later. \$\endgroup\$
    – J4S
    Commented Jul 31, 2016 at 21:48
  • 2
    \$\begingroup\$ OpenGL is an API, not a library \$\endgroup\$
    – Bálint
    Commented Jul 31, 2016 at 21:50
  • \$\begingroup\$ @Bálint I don't just has always been this way for me I cant use any external things or i feel its cheaty, Api or library :) But, I'll probably end up switching sooner or later \$\endgroup\$
    – J4S
    Commented Jul 31, 2016 at 21:52
  • 2
    \$\begingroup\$ Java 2D is more "cheaty" than OpenGL. OpenGL (in core profile) is really low level and doesn't give you any "ready-to-use" lighting. You have to simulate it yourself. All it does is just put some values somewhere (it can be the screen pixels, an offscreen image, a buffer...). Java 2D provide you high level functionnalities, such as gradients, alpha blending, image drawing... With OpenGL, you have to do almost everything it yourself, and since it uses the GPU, it's faster. But this is out of the scope of this question. \$\endgroup\$
    – nasso
    Commented Jul 31, 2016 at 22:16

2 Answers 2

4
\$\begingroup\$

This may not be a perfectly satisfactory answer, but: Modifying a BufferedImage in the main memory and uploading it to the video memory in each frame simply causes a considerable performance impact.

I created the following MCVE to reproduce the issue: It allows switching between the "direct" painting, an the painting that uses an intermediate BufferedImage

Note that some parts of this test are rather crude, and should/would not be done like this in "real" application code. This is really only to reproduce the issue:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Locale;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;


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

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

        f.getContentPane().setLayout(new BorderLayout());

        final BufferedImageLagsPanel p = new BufferedImageLagsPanel();
        f.getContentPane().add(p, BorderLayout.CENTER);

        final JToggleButton tb = new JToggleButton("Draw direct");
        tb.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                p.setDrawDirect(tb.isSelected());
                p.drawImagesToMainImage();
            }
        });
        f.getContentPane().add(tb, BorderLayout.SOUTH);

        Thread t = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                while (true)
                {
                    // Brutally flood the system with paint events.
                    // DON'T DO THIS IN PRODUCTION CODE!!!
                    p.repaint();
                }
            }
        });
        t.setDaemon(true);
        t.start();

        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
}

class SimplePerformanceMeasure
{
    private static long startNs;
    private static long endNs;
    private static Deque<Long> durationsNs = new LinkedList<Long>();
    private static int averaging = 20;
    private static int counter = 0;

    static void startFrame()
    {
        startNs = System.nanoTime();
    }

    static void endFrame()
    {
        endNs = System.nanoTime();
        long durationNs = endNs - startNs;
        durationsNs.addLast(durationNs);
        while (durationsNs.size() > averaging)
        {
            durationsNs.removeFirst();
        }
        counter++;
        if ((counter % averaging) == 0)
        {
            long sumNs = 0;
            for (long ns : durationsNs)
            {
                sumNs += ns;
            }
            double averageMs = (double)sumNs / durationsNs.size() / 1e6;
            System.out.printf(Locale.ENGLISH,
                "Average time was %5.3fms\n", averageMs);
        }
    }
}


class BufferedImageLagsPanel extends JPanel
{
    private final int w = 800;
    private final int h = 800;
    private boolean drawDirect = false;

    private final BufferedImage mainImage = 
        new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);

    private final BufferedImage tileImage = 
        new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);

    public BufferedImageLagsPanel()
    {
        Graphics2D g = tileImage.createGraphics();
        g.setColor(Color.BLACK);
        g.drawLine(0, 0, 32, 32);
        g.drawLine(0, 32, 32, 0);
        g.dispose();
    }

    public void drawImagesToMainImage()
    {
        Graphics2D ig = mainImage.createGraphics();
        drawImages(ig);
        ig.dispose();
    }

    public void setDrawDirect(boolean drawDirect)
    {
        this.drawDirect = drawDirect;
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        render(g);
    }

    public void render(Graphics2D g)
    {
        SimplePerformanceMeasure.startFrame();

        if (drawDirect)
        {
            drawImages(g);
        }
        else
        {
            drawImagesToMainImage();
            g.drawImage(mainImage, 0, 0, null);
        }

        SimplePerformanceMeasure.endFrame();
    }

    private void drawImages(Graphics2D g)
    {
        for (int i = 0; i < 500; i++)
        {
            g.drawImage(tileImage, 0 + i, 0 + i, null);
        }
    }

    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(w, h);
    }
}

It can be seen that the time for painting is much higher when going through the BufferedImage path.


Side note: Notice that I used

g.drawImage(tileImage, 0 + i, 0 + i, null);

which draws the tiles in their original size of 32x32 pixels. Your call,

g.drawImage(tileImage, 0 + i, 0 + i, 100, 100, null);

caused the image to be scaled each time it was painted. This scaling alone can decrease performance dramatically! If possible, you should create the images in the size in which you want to paint them. In doubt, you can always create a scaled version of the original image once, in the constructor, and then use the first version of the drawImage call, to directly draw the scaled image.


Admittedly, although I already worked a bit with Swing and BufferedImage, I was not aware of the fact that the performance impact of updating a BufferedImage before painting it could be so significant for this particular application pattern. Until now, I only considered using such an "offscreen" image for the case where painting the image contents was expensive, but the image itself changed rarely. And then, it will of course bring a speedup. I did not yet have the case where the image had to be updated in each frame.

However, the usual recommendation in this case is to use a VolatileImage. It allows hardware-accelerated rendering and can achieve a higher performance. This higher performance comes at the price of being a bit more fiddly to use.

In the following example, I converted the MCVE from above so that it uses a VolatileImage. For convenience, I wrapped this into a VolatileImageHandle class that basically encapsulates the two most important functionalities:

  • It receives a Consumer<Graphics2D> in the constructor. This is the function that will be used to fill the image with the desired content. In your case (and this example), it just draws the 500 tile images
  • It offers a draw method, to draw the volatile image into a Graphics2D

The bookkeping of checking the volatile image against the graphics configuration is hidden here, which simplifies stuff a bit.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Locale;
import java.util.function.Consumer;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

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

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

        f.getContentPane().setLayout(new BorderLayout());

        final VolatileImageNoLagsPanel p = new VolatileImageNoLagsPanel();
        f.getContentPane().add(p, BorderLayout.CENTER);

        Thread t = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                while (true)
                {
                    // Brutally flood the system with paint events.
                    // DON'T DO THIS IN PRODUCTION CODE!!!
                    p.repaint();
                }
            }
        });
        t.setDaemon(true);
        t.start();

        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
}

class SimplePerformanceMeasure
{
    private static long startNs;
    private static long endNs;
    private static Deque<Long> durationsNs = new LinkedList<Long>();
    private static int averaging = 20;
    private static int counter = 0;

    static void startFrame()
    {
        startNs = System.nanoTime();
    }

    static void endFrame()
    {
        endNs = System.nanoTime();
        long durationNs = endNs - startNs;
        durationsNs.addLast(durationNs);
        while (durationsNs.size() > averaging)
        {
            durationsNs.removeFirst();
        }
        counter++;
        if ((counter % averaging) == 0)
        {
            long sumNs = 0;
            for (long ns : durationsNs)
            {
                sumNs += ns;
            }
            double averageMs = (double)sumNs / durationsNs.size() / 1e6;
            System.out.printf(Locale.ENGLISH,
                "Average time was %5.3fms\n", averageMs);
        }
    }
}

class VolatileImageHandle 
{
    private VolatileImage image;
    private final Component component;
    private final int width;
    private final int height;
    private final boolean transparent;
    private Consumer<? super Graphics2D> painter;

    VolatileImageHandle(Component component, 
        int width, int height, boolean transparent,
        Consumer<? super Graphics2D> painter)
    {
        this.component = component;
        this.width = width;
        this.height = height;
        this.transparent = transparent;
        this.painter = painter;
    }

    private void createImage()
    {
        GraphicsConfiguration graphicsConfiguration = 
            component.getGraphicsConfiguration();
        int transparency = transparent ? 
            Transparency.TRANSLUCENT : Transparency.OPAQUE;
        image = graphicsConfiguration.createCompatibleVolatileImage(
            width, height, transparency);
    }

    private void validateImage()
    {
        if (image == null)
        {
            createImage();
        }
        else
        {
            GraphicsConfiguration graphicsConfiguration = 
                component.getGraphicsConfiguration();
            int validationResult = 
                image.validate(graphicsConfiguration);
            if (validationResult == VolatileImage.IMAGE_INCOMPATIBLE)
            {
                image.flush();
                createImage();
            }
        }
    }

    private void updateImage()
    {
        do
        {
            validateImage();
            Graphics2D g = image.createGraphics();
            painter.accept(g);
            g.dispose();
        } while (image.contentsLost());
    }

    public void draw(Graphics2D g, int x, int y)
    {
        do
        {
            validateImage();
            updateImage();
            g.drawImage(image, x, y, null);
        } while (image.contentsLost());
    }
}


class VolatileImageNoLagsPanel extends JPanel
{
    private final int w = 800;
    private final int h = 800;
    private boolean drawDirect = false;

    private final VolatileImageHandle mainImage = 
        new VolatileImageHandle(this, w, h, true, this::drawImages);

    private final BufferedImage tileImage = 
        new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);

    public VolatileImageNoLagsPanel()
    {
        Graphics2D g = tileImage.createGraphics();
        g.setColor(Color.BLACK);
        g.drawLine(0, 0, 32, 32);
        g.drawLine(0, 32, 32, 0);
        g.dispose();
    }

    public void setDrawDirect(boolean drawDirect)
    {
        this.drawDirect = drawDirect;
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        render(g);
    }

    public void render(Graphics2D g)
    {
        SimplePerformanceMeasure.startFrame();

        mainImage.draw(g, 0, 0);

        SimplePerformanceMeasure.endFrame();
    }

    private void drawImages(Graphics2D g)
    {
        for (int i = 0; i < 500; i++)
        {
            g.drawImage(tileImage, 0 + i, 0 + i, null);
        }
    }

    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(w, h);
    }
}

The increase of performance (on my machine) is remarkable. Again, this performance test is questionable in some ways, but the rough time measures show that in the original version, it took ~25ms to update and paint the image, and in the version that uses the volatile image, this decreased to less than 1 millisecond. YMMV.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you very much! This worked with about 5x the FPS, and I can improve that to probably about 50x if I were to use images instead of creating a gradient each time! Thanks! \$\endgroup\$
    – J4S
    Commented Aug 2, 2016 at 18:57
  • \$\begingroup\$ Update : Used images now I can have almost infinite lights, thanks again! \$\endgroup\$
    – J4S
    Commented Aug 2, 2016 at 19:08
  • \$\begingroup\$ Also, one more small question, does alphacomposite support blending in colours? So for example coloured lighting \$\endgroup\$
    – J4S
    Commented Aug 2, 2016 at 20:02
  • \$\begingroup\$ Colored lights are not trivial. I think I once tried this with a RadialGradientPaint, but it did not work: The mixing of the lights is subtle, and I think that the RadialGradientPaint can not do this (although I don't know for sure - maybe there is a way to accomplish it, and I just did not find it). For something like codereview.stackexchange.com/a/83455/41175 I ended up with "mixing" light images, to achieve the desired en.wikipedia.org/wiki/Additive_color effect: Magenta+Green=White, for example... \$\endgroup\$
    – Marco13
    Commented Aug 2, 2016 at 21:10
1
\$\begingroup\$

My solution is actual not to different from yours. First start by creating a black image the size of the screen that we will call the light map and on it you will draw all of the lights. After all the lights have been drawn on the light map you will draw the light map on top of your scene.

Each light will be just a bunch of overlapping circles that all have the same center, but each one's radius is smaller than the last one's. But instead of simply setting the color of the pixels that the light affects, you will subtract the luminosity of the pixels from their alpha value.

In that way the closer a pixel is to a light, the the more transparent it becomes. Thus since we are drawing the light map above the scene, the more transparent the pixel is, the more of the scene will be visible.

Light map:

private BufferedImage lightMap = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

Light:

private BufferedImage image;
private int x;
private int y;

public Light(int x, int y, int radius, float luminosity) {
    //Recommended luminosity between 1 and 2
    this.x = x;
    this.y = y;
    image = new BufferedImage(radius * 2, radius * 2, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = (Graphics2D) image.getGraphics();

    for(int i = 0; i < radius; i++) {
        double luma = 1.0D - ((i + 0.001) / radius);
        int alpha = Math.min((int)(255.0D * luma * luminosity), 255);
        g2.setColor(new Color(0, 0, 0, alpha));
        g2.setStroke(new BasicStroke(2));
        g2.drawOval(radius - i, radius - i, i * 2, i * 2);
    }
}

public void render(Graphics2D g2) {
    g2.drawImage(image, x - image.getWidth() / 2, y - image.getHeight() / 2, image.getWidth(), image.getHeight(), null);
}

Light Rendering:

private ArrayList<Light> lights = new ArrayList<Light>();

public void makeLightMap(BufferedImage lightMap) {
   Graphics2D gl = lightMap.createGraphics();
   gl.setColor(new Color(0, 0, 0, 255));
   gl.fillRect(0, 0, getWidth(), getHeight());
   Composite oldComp = gl.getComposite();
   gl.setComposite(AlphaComposite.DstOut);

   for(Light light : lights) light.render(gl);

   gl.setComposite(oldComp);
   gl.dispose();
}
\$\endgroup\$
3
  • \$\begingroup\$ Unfortunately I wasn't able to get this to work with optimal performance, when the circle was created, it lagged a bit and I still had the issue with having multiple lights and lag. \$\endgroup\$
    – J4S
    Commented Aug 1, 2016 at 12:40
  • \$\begingroup\$ I have improved the performance of the algorithm. Hopefully this should cause less lag when the lights are being created. \$\endgroup\$
    – J22o
    Commented Aug 1, 2016 at 14:44
  • \$\begingroup\$ Also fixed the issue with multiple lights. \$\endgroup\$
    – J22o
    Commented Aug 1, 2016 at 14:55

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .