1

Using Graphics2D, how can I take a black and white image, and use to define what should and what shouldn't rendered on another image?

E.g if I had an image of say, a field, and on that field is a cow, and on another image of the same dimensions I draw a white box on a black background, at the same coordinates of the cow, when rendered in Java the image would be all black, apart from the cow where I had the white box?

Ashot Karakhanyan
  • 2,804
  • 3
  • 23
  • 28
Shaun Wild
  • 1,237
  • 3
  • 17
  • 34
  • 2
    Should the actual color of the mask have any effect, or will there *always* be *either* completely BLACK *or* completely *WHITE* pixels? Do you have any influence on how the mask is created or represented (i.e. could the mask also be stored as alpha values?). Could it be benficial for you if the mask was reprsented as a `Shape` (this would be much easier), or does it have to be an image? Do you want to create a masked image (and then paint it), or do you want to paint the original image, applying the mask *while* it is painted? (The latter would be far more difficult) – Marco13 Feb 11 '14 at 15:41
  • It doesn't HAVE to be anything, if you have any better way of doing it, please recommend something. I just want it to be used to create a lighting map, so I could have a level of tiles, and then my lighting map and the tiles brightness depends on the lighting map. I was told Alpha Composites were an excellent way to do it. – Shaun Wild Feb 14 '14 at 11:34

1 Answers1

4

EDIT: Based on a long discussion in the chat, it became clear that there was a misunderstanding about the intention, and the original question suffered from the XY-Problem: The question of how to compose an image with a masking image was only about one solution attempt for the actual problem - namely painting some shadow/light effects on a tile map. The original versions of the post can be seen in the revision history.

The actual goal was obviously to add a "light effect" over the image. Here is an example of how this can be achieved:

  • The original image is painted in the background
  • A "shadow image" is painted over the image.

The "shadow image" is initially a nearly opaqe, nearly black image. The lights are painted into this image, with a RadialGradientPaint. The colors for this paint are chosen so that they make the shadow image less opaque and less black at the places where the lights should be. This causes these areas to appear lighted, while the other parts remain dark.

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.Random;

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

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


    public LightEffectTest2()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(new LightEffectPanel2());
        f.setSize(600,600);
        f.setVisible(true);
    }
}


class LightEffectPanel2 extends JPanel implements MouseMotionListener
{
    private Point point = new Point(0,0);
    private BufferedImage image;
    private BufferedImage shadow;

    public LightEffectPanel2()
    {
        image = createExampleImage(600,600);
        shadow = new BufferedImage(image.getWidth(), image.getHeight(),
            BufferedImage.TYPE_INT_ARGB);

        addMouseMotionListener(this);
    }

    private static BufferedImage createExampleImage(int w, int h)
    {
        BufferedImage image = new BufferedImage(w, h, 
            BufferedImage.TYPE_INT_ARGB);
        Graphics g = image.getGraphics();
        Random random = new Random(0);
        for (int i=0; i<200; i++)
        {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            Color c = new Color(
                random.nextInt(255),
                random.nextInt(255),
                random.nextInt(255));
            g.setColor(c);
            g.fillOval(x-20, y-20, 40, 40);
        }
        g.dispose();
        return image;
    }


    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.drawImage(image, 0,0,null);

        drawLights();

        g.drawImage(shadow, 0,0, null);
    }

    private void drawLights()
    {
        Graphics2D g = shadow.createGraphics();
        g.setComposite(AlphaComposite.Src);
        g.setColor(new Color(0,0,16,240));
        g.fillRect(0,0,getWidth(),getHeight());

        drawLight(g, new Point(100,100));
        drawLight(g, point);

        g.dispose();
    }

    private void drawLight(Graphics2D g, Point pt)
    {
        float radius = 100;
        g.setComposite(AlphaComposite.DstOut);
        Point2D center = new Point2D.Float(pt.x, pt.y);
        float[] dist = {0.0f, 1.0f};
        Color[] colors = {new Color(255,255,255,255), new Color(0,0,0,0) };
        RadialGradientPaint p =
            new RadialGradientPaint(
                center, radius, dist, colors, CycleMethod.NO_CYCLE);
        g.setPaint(p);
        g.fillOval(pt.x-(int)radius,pt.y-(int)radius,(int)radius*2,(int)radius*2);
    }



    @Override
    public void mouseDragged(MouseEvent e) {
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        point = e.getPoint();
        repaint();
    }
}

(late) EDIT For the request in the comments:

To add another shadow (regardless of the existing lights), one can create a drawShadow method that re-applies the shadows after the lights have been drawn. It basically uses another RadialGradientPaint that partially "restores" the original, opaqe, dark shadow image.

LightEffectTest3

(The shadow is given a somewhat sharper border here, to make the effect more visible)

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.Random;

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

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


    public LightEffectTest3()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(new LightEffectPanel3());
        f.setSize(600,600);
        f.setVisible(true);
    }
}


class LightEffectPanel3 extends JPanel implements MouseMotionListener
{
    private Point point = new Point(0,0);
    private BufferedImage image;
    private BufferedImage shadow;

    public LightEffectPanel3()
    {
        image = createExampleImage(600,600);
        shadow = new BufferedImage(image.getWidth(), image.getHeight(),
            BufferedImage.TYPE_INT_ARGB);

        addMouseMotionListener(this);
    }

    private static BufferedImage createExampleImage(int w, int h)
    {
        BufferedImage image = new BufferedImage(w, h, 
            BufferedImage.TYPE_INT_ARGB);
        Graphics g = image.getGraphics();
        Random random = new Random(0);
        for (int i=0; i<200; i++)
        {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            Color c = new Color(
                random.nextInt(255),
                random.nextInt(255),
                random.nextInt(255));
            g.setColor(c);
            g.fillOval(x-20, y-20, 40, 40);
        }
        g.dispose();
        return image;
    }


    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.drawImage(image, 0,0,null);

        drawLights();

        g.drawImage(shadow, 0,0, null);
    }

    private void drawLights()
    {
        Graphics2D g = shadow.createGraphics();
        g.setComposite(AlphaComposite.Src);
        g.setColor(new Color(0,0,16,240));
        g.fillRect(0,0,getWidth(),getHeight());

        drawLight(g, new Point(200,200));
        drawLight(g, point);
        drawShadow(g, new Point(250,250));

        g.dispose();
    }

    private void drawLight(Graphics2D g, Point pt)
    {
        float radius = 150;
        g.setComposite(AlphaComposite.DstOut);
        Point2D center = new Point2D.Float(pt.x, pt.y);
        float[] dist = {0.0f, 1.0f};
        Color[] colors = {new Color(255,255,255,255), new Color(0,0,0,0) };
        RadialGradientPaint p =
            new RadialGradientPaint(
                center, radius, dist, colors, CycleMethod.NO_CYCLE);
        g.setPaint(p);
        g.fillOval(pt.x-(int)radius,pt.y-(int)radius,
            (int)radius*2,(int)radius*2);
    }

    private void drawShadow(Graphics2D g, Point pt)
    {
        float radius = 75;
        g.setComposite(AlphaComposite.DstOver);
        Point2D center = new Point2D.Float(pt.x, pt.y);
        float[] dist = {0.0f, 0.7f, 1.0f};
        Color[] colors = { 
            new Color(0,0,0,200),
            new Color(0,0,0,150),
            new Color(255,255,255,0) };
        RadialGradientPaint p =
            new RadialGradientPaint(
                center, radius, dist, colors, CycleMethod.NO_CYCLE);
        g.setPaint(p);
        g.fillOval(pt.x-(int)radius,pt.y-(int)radius,
            (int)radius*2,(int)radius*2);
    }



    @Override
    public void mouseDragged(MouseEvent e) {
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        point = e.getPoint();
        repaint();
    }
}
Community
  • 1
  • 1
Marco13
  • 53,703
  • 9
  • 80
  • 159
  • So I draw the base image, set the composite to DstOut and then draw the mask? – Shaun Wild Feb 14 '14 at 13:25
  • In this case, yes. It depends on how you define the mask. But when the part that should be visible is defined by opacity 0, and the part that should be hidden is defined by opacity 255, then you can do it like this. – Marco13 Feb 14 '14 at 13:37
  • Most image editing software represents alpha maps as black and white images, that must of been where I was getting confused. – Shaun Wild Feb 14 '14 at 14:17
  • I'm drawing to my lightmap like so: `lightMap.getGraphics().setColor(new Color(0, 0, 0, 0)); lightMap.getGraphics().fillRect(0, 0, 1080, 720); lightMap.getGraphics().setColor(new Color(0, 0, 0, 0)); lightMap.getGraphics().fillRect(300, 300, 300, 300);` And the screen is still draw completely black? – Shaun Wild Feb 14 '14 at 14:25
  • @Shaun Wild : About first comment: How such a mask is *shown* to the user is another aspect, and so to say independent of the actual internal representation. Of course you can convert any "real" black-and-white image into the *internal* representation that is required for the alpha stenciling (and vice versa). I just showed ONE option. Whether there is a "better" one depends on the context and the remaining architecture of the application. – Marco13 Feb 14 '14 at 14:28
  • @Shaun Wild : About second comment: In both cases, you are drawing a completely transparent color, and whether this works in this form also depends on, for example, the type of the 'lightMap' image (it must support alpha values, usually it has to be BufferedImage.TYPE_INT_ARGB). Otherwise, you could ask a new question or append your (not-working) approach as an "EDIT" to your original question, if possible – Marco13 Feb 14 '14 at 14:29
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47512/discussion-between-shaun-wild-and-marco13) – Shaun Wild Feb 14 '14 at 14:31
  • Thank you so much for this!! How could I add a shadow also? I have my light sources and they are working great, but how can I make some parts even darker than the overlay?? – KisnardOnline Aug 14 '16 at 19:09
  • 1
    @KisnardOnline Regarding images, I once created this example http://stackoverflow.com/a/23971327/3182664 and I basically combined them to build http://codereview.stackexchange.com/a/83455/41175 , **BUT** the latter uses multiple lights to achieve (somewhat) soft shadows, and this, is too slow to be used in practice. (However, combining both approaches with a *single* light (and thus "hard shadows") is perfectly feasible. Maybe I'll just throw all this on GitHub one day, there seems to be a demand for that.... – Marco13 Aug 14 '16 at 19:53
  • Here is what I am talking about. I have all of the circular light sources with different intensities - they work great. The AlphaComposite.Src has 205 opacity but for the trees I would like them to be darker (so a cast a circular shadow). I can only seem to make them another light source. Any ideas? https://s3.postimg.org/wc2l0sgmr/DEV_Proskier2016_08_14_04_03_29_PM.png – KisnardOnline Aug 14 '16 at 20:09
  • @Marco13 sorry if I complicated or confused things. My question is, how can I also add points with circular shadows (in addition to the lights) on your LightEffectTest2 example? – KisnardOnline Aug 14 '16 at 20:52
  • @KisnardOnline I added another EDIT, and edited the post to better explain what's going on there. – Marco13 Aug 14 '16 at 21:46
  • @Marco13 Awesome thank you so much!! Such a huge help! – KisnardOnline Aug 14 '16 at 22:16