Skip navigation

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.

In the first installment of my "Trickery" series, I demonstrated a technique for achieving a soft clipping effect. Now let's put it to good use. In this installment I'll show how to add a lighting effect to give your otherwise flat shapes a 3D-ish appearance.

If you like your literature illustrated, perhaps a picture will help. I'll show you how to go from the boring, flat shape on the left to the slightly less boring, glossy shape on the right:

Flat Shape Glowing Shape

With the right colors, you can use this technique to simulate a colored light shining across your shape, producing a subtle glow. How do we achieve this effect? Check out the code below; the comments above the drawBorderGlow() method explain the core approach in a bit more detail:

import java.awt.geom.*;
import java.awt.image.*;

private static final Color clrHi = new Color(255, 229, 63);
private static final Color clrLo = new Color(255, 105, 0);

private static final Color clrGlowInnerHi = new Color(253, 239, 175, 148);
private static final Color clrGlowInnerLo = new Color(255, 209, 0);
private static final Color clrGlowOuterHi = new Color(253, 239, 175, 124);
private static final Color clrGlowOuterLo = new Color(255, 179, 0);

private Shape createClipShape() {
    float border = 20.0f;

    float x1 = border;
    float y1 = border;
    float x2 = width - border;
    float y2 = height - border;

    float adj = 3.0f; // helps round out the sharp corners
    float arc = 8.0f;
    float dcx = 0.18f * width;
    float cx1 = x1-dcx;
    float cy1 = 0.40f * height;
    float cx2 = x1+dcx;
    float cy2 = 0.50f * height;

    GeneralPath gp = new GeneralPath();
    gp.moveTo(x1-adj, y1+adj);
    gp.quadTo(x1, y1, x1+adj, y1);
    gp.lineTo(x2-arc, y1);
    gp.quadTo(x2, y1, x2, y1+arc);
    gp.lineTo(x2, y2-arc);
    gp.quadTo(x2, y2, x2-arc, y2);
    gp.lineTo(x1+adj, y2);
    gp.quadTo(x1, y2, x1, y2-adj);
    gp.curveTo(cx2, cy2, cx1, cy1, x1-adj, y1+adj);
    gp.closePath();
    return gp;
}

private BufferedImage createClipImage(Shape s) {
    // 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.fill(s);
    g2.dispose();

    return img;
}

private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
    float[] clr1 = c1.getComponents(null);
    float[] clr2 = c2.getComponents(null);
    for (int i = 0; i < clr1.length; i++) {
        clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
    }
    return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
}

// Here's the trick... To render the glow, we start with a thick pen
// of the "inner" color and stroke the desired shape.  Then we repeat
// with increasingly thinner pens, moving closer to the "outer" color
// and increasing the opacity of the color so that it appears to
// fade towards the interior of the shape.  We rely on the "clip shape"
// having been rendered into our destination image already so that
// the SRC_ATOP rule will take care of clipping out the part of the
// stroke that lies outside our shape.
private void paintBorderGlow(Graphics2D g2, int glowWidth) {
    int gw = glowWidth*2;
    for (int i=gw; i >= 2; i-=2) {
        float pct = (float)(gw - i) / (gw - 1);

        Color mixHi = getMixedColor(clrGlowInnerHi, pct,
                                    clrGlowOuterHi, 1.0f - pct);
        Color mixLo = getMixedColor(clrGlowInnerLo, pct,
                                    clrGlowOuterLo, 1.0f - pct);
        g2.setPaint(new GradientPaint(0.0f, height*0.25f,  mixHi,
                                      0.0f, height, mixLo));
        //g2.setColor(Color.WHITE);

        // See my "Java 2D Trickery: Soft Clipping" entry for more
        // on why we use SRC_ATOP here
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
        g2.setStroke(new BasicStroke(i));
        g2.draw(clipShape);
    }
}

Shape clipShape = createClipShape();
//Shape clipShape = new Ellipse2D.Float(width/4, height/4, width/2, height/2);

// Clear the background to white
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);

// Set the clip shape
BufferedImage clipImage = createClipImage(clipShape);
Graphics2D g2 = clipImage.createGraphics();

// Fill the shape with a gradient
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setComposite(AlphaComposite.SrcAtop);
g2.setPaint(new GradientPaint(0, 0, clrHi, 0, height, clrLo));
g2.fill(clipShape);

// Apply the border glow effect
paintBorderGlow(g2, 8);

g2.dispose();

g.drawImage(clipImage, 0, 0, null);

Note that I've left a few "alternate" lines of code commented out in the example above. Feel free to try uncommenting those lines in IGE and see how they affect the rendering.

Bonus: Astute readers may notice that the same technique used in the paintBorderGlow() method above could be used to add a drop shadow around the shape. Care to guess how this would work? Okay, time's up. Instead of rendering the border on top of our shape (remember that the clip ensures that the stroke only touches the inside of the shape), we can render a varying gray border around our shape beforehand. This means the shadow stroke will appear outside of our shape; the inner part of the shadow stroke will be effectively rendered over by our shape.

Here's some more code you can insert into the above example to add a shadow border to that same shape:

private void paintBorderShadow(Graphics2D g2, int shadowWidth) {
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    int sw = shadowWidth*2;
    for (int i=sw; i >= 2; i-=2) {
        float pct = (float)(sw - i) / (sw - 1);
        g2.setColor(getMixedColor(Color.LIGHT_GRAY, pct,
                                  Color.WHITE, 1.0f-pct));
        g2.setStroke(new BasicStroke(i));
        g2.draw(clipShape);
    }
}

// Apply the border shadow before we paint the rest of the shape
paintBorderShadow(g, 6);

And here's the resulting image:

Shadow

Note that this is just a quick attempt at adding a drop shadow for demonstration purposes. If I wasn't so lazy, I'd probably use a lighter gray color and a non-linear ramp to achieve a more realistic effect. Also note that this is just one of many ways to add a drop shadow using Java 2D. Romain has discussed different drop shadow implementations in his blog hereand here. The SwingLabs folks have aDropShadowBorder in the SwingX project, andDropShadowPanel is currently in the Incubator.

In the next installment of my "Trickery" series, I'll move away from low-level Java 2D effects and show you how to add animations to your applications using the SwingX Painters API and Chet's Timing Framework [tm]. In the meantime, let's see if Josh and Richard pick up the slack and turn some of the above code into stock painters (nudge nudge).



In my ears: Wire, "Chairs Missing" [for about the 3 billionth time in my life]
In my eyes: "Idiot's Guide To Buying A Home" [I really hate growing up...]

I'm sure you're all sick of reading my blogs at this point, because each time I state definitively that the OpenGL-based Java 2D pipeline is "now better than ever," only to follow it up a couple months later with a blog that says, "no really, this time it's even better," and so on. It reminds me of a nearby store called "Cheaper Then Cheaper Cigarettes" [sic]; can you really improve on "cheaper" like that?

Anyway, at the risk of sounding like the boy who cried wolf, as of Mustang b92, the OGL pipeline is now "better than better", one might even say it's superfantastic. In the spirit of my recent "Five Easy Pieces" entry, here are five more reasons why the OGL pipeline is better than ever...


OGL: scrolling is extremely slow on Nvidia boards
(Bug 6298243)

This bug frustrated me for a number of months, but I was confident that the Nvidia driver engineers would be able to come up with a solution as they've done with so many other issues. Unfortunately, the problem is specific to the way data is laid out in texture memory on certain Nvidia hardware, so a simple driver fix wasn't possible. However, they were kind enough to offer a (somewhat odd) workaround of using the GL_ARB_texture_rectangle extension, since rectangular textures are laid out more efficiently on said hardware and therefore do not exhibit the performance issues reported in this bug.

It took quite a bit of work, but the good news is that we were able to get support for the GL_ARB_texture_rectangle extension implemented for b92, so scrolling/dragging performance is back to normal on Nvidia hardware, for both pbuffers and framebuffer objects (FBOs). As an added bonus, the use of that extension allows us to use less texture memory since we no longer have to pad up to power-of-two sized textures on older hardware. (This is what Michael Scott would call a "win win win" situation.)


OGL: enable sun.java2d.opengl.fbobject by default
(Bug 6439320)

Another big benefit in fixing the previous scrolling performance bug is that it removed the last big barrier that prevented us from enabling the FBO codepath by default. (I mentioned the FBO codepath in a blog entry from last autumn. Note that since then, Ken Russell and I managed to get JOGL's GLJPanel working with the FBO codepath, so that is no longer a barrier.) So as of b92, we've enabled the FBO codepath by default, which has many benefits when compared to the existing pbuffer-based codepath:

  • Reduces costly context-switching overhead. This has helped to improve SwingMark (an internal Swing benchmark) scores by 15-20% on both ATI and Nvidia hardware, when compared tofbobject=false.
  • Greatly improves performance when rendering VolatileImages (especially when applying an AffineTransform).
  • Decreases the amount of VRAM consumed by VolatileImages, due to the fact that we have more control over which buffers are/aren't created.
  • Improves maintainability, since the extension is cross-platform and not tied to any windowing system-isms.
  • Works around a number of pbuffer-specific bugs in ATI's drivers, most notably the one that causes a HotSpot crash when exiting an application.


 

If you've been been bitten by the OGL pipeline in the past (especially those folks with ATI hardware), I'd highly recommend installing b92 for this change alone.


OGL: use new AffineTransform methods in enableGradientPaint()
(Bug 6434670)

This one's not terribly exciting... Just a three-line change, but I wanted to mention it in case folks are not aware of some new convenience methods added to the AffineTransform class in Mustang. The first is AffineTransform.invert(), which is useful in those cases where you want to invert the transform matrix in place (hence no need to create a new AffineTransform object). The second is AffineTransform.rotate(vecx, vecy), which is helpful when you want to rotate about a vector (instead of using a relative angle value).

In this particular case, the old code was not a major bottleneck in the gradient/texture acceleration path, so there wasn't a huge performance benefit. However, if you have some code that exercises AffineTransform heavily, you might want to check to see if these new methods give you a boost.


OGL: accelerate Gradient/TexturePaint when antialiasing is enabled
(Bug 6436942)

This one is terribly exciting, to me at least. We've had some nifty acceleration for GradientPaint and TexturePaint since the OGL pipeline was introduced in JDK 5, but it always came with the caveat that only non-antialiased primitives were accelerated. But as of Mustang b92, we've figured out a way to use multitexturing to combine the gradient/texture acceleration code with our existing antialiased rendering code, which means GradientPaint and TexturePaint are now fully accelerated by the OGL pipeline, regardless of whether antialiasing is enabled. The bug report has more details, but I'll include the executive summary here, because the numbers speak for themselves:

  • Up to 25x improvement for antialiased TexturePaint operations
  • At least 2-7x improvement for antialiased GradientPaint operations
  • No significant impact on existing accelerated operations



OGL: overlapping LCD glyphs not rendered properly
(Bug 6439274)

This was a visible problem caused by an obscure bug, but fortunately the fix was easy. It was the last known problem with the LCD text acceleration code mentioned in my last blog on the subject, and the driver bugs mentioned there have since been fixed as well.

So if you're a fan of LCD text (meaning everyone but Scott, wink wink) and you have shader level hardware (Nvidia GeForce FX/6/7 series, ATI Radeon 9500 and up, etc), please try-Dsun.java2d.opengl.lcdshader=true and let us know if you see any problems. I'm fairly confident that things are working well, but it hasn't received quite enough testing yet, so we're not planning to enable OpenGL acceleration of LCD text by default in Mustang. Assuming all looks good in the next few months, we can consider turning it on by default in a Mustang update release.


Bonus: The Driver Report

As I've mentioned in past blogs, we've been working with driver teams from Nvidia, ATI, and Sun to ensure that any remaining driver issues are resolved by the time Mustang ships. We have updated our basic acceptance test (BAT) suite with over 50 tests that said driver teams can use to prevent the introduction of new regressions and to test that their existing drivers work well with our OpenGL-based pipeline.

For the past couple years, ATI's drivers have been in a sad state of affairs w.r.t. our OGL pipeline (lots of crashes and rendering artifacts). However, I'm now happy to report that ATI has fixed the remaining Linux-specific OpenGL driver bugs that affect our OGL pipeline, so our BAT suite now runs without failure on their forthcoming 8.27 series release. If you're a Linux user with ATI hardware, I'd suggest upgrading to 8.27 when it becomes available in the next couple weeks.

As for ATI's OpenGL drivers for Windows, some bugs remain and we are continuing to push those with ATI. But as I mentioned above, the change in b92 that makes the FBO codepath the default means that most of those bugs are no longer reproducible for most newer boards.

On the Nvidia and Sun fronts, things are looking really good. There were a couple driver bugs found recently on certain Nvidia laptop GPUs that have since been fixed. If your laptop has an Nvidia GPU and you've been seeing weird artifacts when enabling the OGL pipeline, please install Nvidia's 95.xx series driver when it becomes available in the near future.



In my ears: Viktor Vaughn, "Vaudeville Villain"
In my eyes: Edward P. Jones, "The Known World"

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:

hard.png

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:

soft.png

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.



In my ears: Asobi Seksu, "Citrus" [just got back from their show at the Bowery Ballroom no less]
In my eyes: Edward P. Jones, "The Known World"