Note: The code snippets in this blog entry are intended to be used with Scott Violet's nifty Interactive Graphics Editor. Just cut and paste the code into that application, et voila: instant gratification. This allows you to tinker with the code and immediately see how your changes affect the rendering.
If you're familiar with Java 2D, you probably already know that you can clip out a portion of your rendering with any arbitrary shape. (If not, go check out the Java Tutorial.) For example, you can give the illusion of someone shining a flashlight on your application by setting a circular clip and then rendering your scene normally.
When you clip using a complex shape, you will typically see jaggies around the edges of the region being clipped, which can uglify your app. To illustrate the effect that I'll call "hard clipping", try the following example:
// Clear the background to black g.setColor(Color.BLACK); g.fillRect(0, 0, width, height); // Set the clip shape g.clip(new java.awt.geom.Ellipse2D.Float(width/4, height/4, width/2, height/2)); // Fill the area with a gradient; this will be clipped by our ellipse, but // note the ugly jaggies g.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW)); g.fillRect(0, 0, width, height);
Here's the resulting image:
Wouldn't it be nice if you could antialias those hard edges to remove the jaggies caused by clipping? Well, unfortunately Java 2D (or at least Sun's current implementation) does not support "soft clipping."
Sidebar: I add the caveat about Sun's implementation because I was surprised to find that when I tried the above code on my Mac, there were no jaggies! What's going on here? Well, it turns out that Apple's Java 2D implementation uses Quartz under the hood, which appears to do soft clipping by default. In Mustang, Apple is planning to use Sun's software renderer instead of their Quartz renderer by default, so the tips in this blog entry should be relevant for Macs as well.
You'd think there would be a RenderingHint to control this behavior, but sorry, no such luck. A few developers have asked for soft clipping in the past, but it doesn't seem to be common enough to warrant adding support for it in our implementation. (I was going to say that it's too much work, and it probably would be, but then my esteemed readers would probably say "well if Apple could implement it in Quartz, why can't you?" Damn those show-offs at Apple. But I digress...)
Fortunately, we've found a fairly simple way to achieve a soft clipping effect using an intermediate image (see Chet's articleon that subject) and a little known
AlphaCompositerule known as
SrcAtop. (Note that
SrcIn would work equally well in this example, but
SrcAtop has the added benefit that it blends the source with the destination, as opposed to simply replacing it, which will come in handy in the next installment.) Try out the following code snippet:
import java.awt.image.*; // Clear the background to black g.setColor(Color.BLACK); g.fillRect(0, 0, width, height); // Create a translucent intermediate image in which we can perform // the soft clipping GraphicsConfiguration gc = g.getDeviceConfiguration(); BufferedImage img = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT); Graphics2D g2 = img.createGraphics(); // Clear the image so all pixels have zero alpha g2.setComposite(AlphaComposite.Clear); g2.fillRect(0, 0, width, height); // Render our clip shape into the image. Note that we enable // antialiasing to achieve the soft clipping effect. Try // commenting out the line that enables antialiasing, and // you will see that you end up with the usual hard clipping. g2.setComposite(AlphaComposite.Src); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(Color.WHITE); g2.fillOval(width/4, height/4, width/2, height/2); // Here's the trick... We use SrcAtop, which effectively uses the // alpha value as a coverage value for each pixel stored in the // destination. For the areas outside our clip shape, the destination // alpha will be zero, so nothing is rendered in those areas. For // the areas inside our clip shape, the destination alpha will be fully // opaque, so the full color is rendered. At the edges, the original // antialiasing is carried over to give us the desired soft clipping // effect. g2.setComposite(AlphaComposite.SrcAtop); g2.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW)); g2.fillRect(0, 0, width, height); g2.dispose(); // Copy our intermediate image to the screen g.drawImage(img, 0, 0, null);
Compare the resulting image to the jaggy one above:
Looks better, no? I'll admit that this example is a bit contrived, and it might be hard to see the real world applicability. In the next installment of my "Trickery" series, I'll show you how to apply this technique when creating a lighting effect for arbitrary shapes.