Search code examples
javagraphicsawtbufferedimage

How to delete pixels on a BufferedImage (or other components using Graphics)?


So I want to make a super basic lighting system for my games. And how I want to do that is draw the overall darkness on a BufferedImage, and then use ovals to remove pixels in the BufferedImage to fake a light. So how can I do something similar to Graphics.drawOval() but instead of adding color, setting the pixel to a color with an alpha value of 0?

Drawing I made really quick to try to show what I mean:

enter image description here


Solution

  • There are a number of ways you might be able to achieve this. Since you seem to have a number of possible light sources, making use of Area might be the simpler solution.

    This would allow you to simply "subtract" the light source area from a pre-existing area, which would "cut out" the area and allow the background to show through.

    For example...

    enter image description here

    import java.awt.AlphaComposite;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Point;
    import java.awt.RenderingHints;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import java.awt.geom.Area;
    import java.awt.geom.Ellipse2D;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.imageio.ImageIO;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        JFrame frame = new JFrame();
                        frame.add(new TestPane());
                        frame.pack();
                        frame.setLocationRelativeTo(null);
                        frame.setVisible(true);
                    } catch (IOException ex) {
                        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private BufferedImage master;
            private BufferedImage lightOverlay;
    
            public TestPane() throws IOException {
                master = ImageIO.read(getClass().getResource("/images/Poster.png"));
                // Baseline darkness
                lightOverlay = new BufferedImage(master.getWidth(), master.getHeight(), BufferedImage.TYPE_INT_ARGB);
                Graphics2D g2d = lightOverlay.createGraphics();
                g2d.setColor(Color.BLACK);
                g2d.setComposite(AlphaComposite.SrcOver.derive(0.9f));
                g2d.fillRect(0, 0, lightOverlay.getWidth(), lightOverlay.getHeight());
                g2d.dispose();
    
                addMouseMotionListener(new MouseAdapter() {
                    private Color transparentColor = new Color(0, 0, 0, 0);
                    @Override
                    public void mouseMoved(MouseEvent e) {
                        Point p = e.getPoint();
    
                        Graphics2D g2d = lightOverlay.createGraphics();
                        // This is a little trick which will clear the graphics
                        // using a transparent color, otherwise any additional
                        // painting operations will accumalte over the top of 
                        // what was previously painted ... this way we can reduce
                        // the GC overhead by not creating so many short lived
                        // objects
                        g2d.setBackground(transparentColor);
                        g2d.clearRect(0, 0, lightOverlay.getWidth(), lightOverlay.getWidth());
    
                        // You "could" cache this area and simply subtract the areas
                        // you want to expose as needed.  Removing light sources
                        // would simply be a process of "adding" the area back in
                        Area spotLight = new Area(new Rectangle2D.Double(0, 0, lightOverlay.getWidth(), lightOverlay.getHeight()));
                        // You could cache Area's you want exposed, cutting them out
                        // only once.  When you want to remove the light source, you'd
                        // just "add" that Area back in to the spotLight, further
                        // reducing the GC overhead associated with short lived
                        // objects
                        spotLight.subtract(new Area(new Ellipse2D.Double(p.x - 80, p.y - 80, 160, 160)));
    
                        // Make it look pretty
                        applyRenderHints(g2d);
    
                        g2d.setColor(Color.BLACK);
                        g2d.setComposite(AlphaComposite.SrcOver.derive(0.9f));
                        g2d.fill(spotLight);
                        // After this would could use a series of RadialGradientPaints
                        // to make the effect look more "pretty"
                        g2d.dispose();
    
                        repaint();
                    }
                });
            }
    
            protected void applyRenderHints(Graphics2D g2d) {
                g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
                g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
            }
    
            @Override
            public Dimension getPreferredSize() {
                return master == null ? new Dimension(200, 200) : new Dimension(master.getWidth(), master.getHeight());
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D) g.create();
                int x = (getWidth() - master.getWidth()) / 2;
                int y = (getHeight() - master.getHeight()) / 2;
                g2d.drawImage(master, x, y, this);
                g2d.drawImage(lightOverlay, x, y, this);
                g2d.dispose();
            }
    
        }
    }
    

    Please, beware, this is just a proof of concept, how you implement in your code is entirely up to you. I've tried to apply some basic optimisation, but how you manage your light sources may effect how you update the "darkness" overlay.

    You also consider having a look at:

    to get some more ideas