0

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

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • You could do just that: add a shape with a color that has an alpha value of 0. But be sure that the BufferedImage is of type that understands alpha. – Hovercraft Full Of Eels Jul 16 '23 at 18:55
  • I would do that, but if I just draw a shape with a 0 alpha value, it won't show up. At least I'm pretty sure not. When I use a value between 0-1, it will just draw over it. Do you know if java.awt.Graphics works that way when the color has an alpha value of 0? – BlockManBlue Jul 16 '23 at 18:59
  • ? It would show up if the image is first filled with a dark background. Otherwise, what use is a 0 alpha? But also note that there are some issues with Swing displaying semi-transparent things. – Hovercraft Full Of Eels Jul 16 '23 at 19:09
  • Maybe you should draw an illustration of what you mean and add it to your question. I'm not sure I follow. What does erasing pixels have to do with light? Is the buffered image overlayed on top of some other image? Is the light coming from behind the image? How? – RealSkeptic Jul 16 '23 at 19:16
  • In general, this is something that you tend to want to "fake", for [example](https://stackoverflow.com/questions/15488853/java-mouse-flashlight-effect/15489299#15489299), [example](https://stackoverflow.com/questions/18388942/clear-portion-of-graphics-with-underlying-image/18392674#18392674); [example](https://stackoverflow.com/questions/50698351/how-to-create-a-transparent-shape-in-the-image/50699591#50699591); [example](https://stackoverflow.com/questions/73283500/making-a-part-of-an-image-transparent-in-java-swing/73284770#73284770) – MadProgrammer Jul 16 '23 at 21:27
  • Ok, I made a super basic drawing to try and show what I'm trying to say, does that help clear things up at all? Side note, the darkness image would be the same size as the window, though that's not super important. Also I tried just setting the color to a color with 0 alpha, but it didn't do anything. – BlockManBlue Jul 17 '23 at 00:07

1 Answers1

1

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

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366