> I'd like to submit a small Swing program that shows the effects of the
>various AlphaComposite variants on the classic red-green-blue circles.
> I don't have a lot of experience with OpenGL, but I am aware that the
>equivalent of what this program does can also be programmed with it. If
>anyone on this list with previous OpenGL experience would be so kind as
>to write a version of this program in OpenGL, I'd be very grateful.
> The point here is that I have seen different behavior between Java 2D
>and OpenGL when using equivalent alpha compositing rules. This has been
>detected with two image viewing programs under development here at work,
>one is Java 2D based and the other is OpenGL based. As those programs
>are a bit complex, I'd like to determine if the source of this
>difference is due to disparities between Java 2D and OpenGL or problems
>in the programs themselves.
> Maybe this small program could be used as an example of alpha
>compositing in a tutorial somewhere, if anyone thinks it is worthy. I
>give you all full permission to use this code as you wish, no
>guarantees. 8-)
Hi Alexandre,
Thanks for submitting this example program. We've modified it and written
an equivalent OpenGL program to illustrate the relationship between the
built-in compositing rules in Java 2D and those in OpenGL. The programs
are attached below.
The built-in Java 2D rules are a subset of those described in the paper
"Compositing Digital Images" by Thomas Porter and Tom Duff which appeared
in the SIGGRAPH 1984 Proceedings (Computer Graphics, vol. 18, no. 3, July
1984). Another paper which covers a lot of the same ground is "Compositing,
Part 1: Theory" by Jim Blinn from IEEE Computer Graphics and Applications,
September 1994 (reprinted in Blinn's book Jim Blinn's Corner: Dirty Pixels).
Of course, Java 2D has the Composite interface which lets you define your
own compositing rules, but the built-in rules in the AlphaComposite class
are all defined in the above papers.
OpenGL's blending function takes source and destination blending factors
from a defined set of possible factors. As the programs below demonstrate,
these factors can be set up to yield results which are equivalent to each
of Java 2D's built-in rules. But they can also be set up in ways that
don't correspond to any of the Java 2D rules. OpenGL doesn't allow you
to define your own arbitrary blending function like Java 2D does, but the
numerous choices for blending factors can achieve many different effects.
One thing about your original program that didn't allow it to show the
effects of various compositing modes was that it always composited to the
screen, which in all Java 2D implementations that I'm aware of (and in
many of the pixel formats supported by OpenGL implementations) has no
storage bits allocated for alpha. This means that the destination alpha
is treated as being implicitly 1.0 in all computations requiring it. It
also means that the destination alpha values computed are simply lost
after each calculation, so that multiple compositing operations on the
same pixel which do not all result in a calculated alpha value of 1.0
will be performed incorrectly. (For example, running some OpenGL programs
on two different pixel formats, one with no bits allocated to alpha and
one with 8 bits allocated to alpha, can yield different results.) What
the modified programs do is compute the results of the various
compositing rules using pixel buffers with 8 bits each of R, G, B, and
A. We start with a buffer which is mostly completely clear, (RGBA) =
(0, 0, 0, 0), except for a background grid of opaque gray lines. Then
we composite several circles over the grid using a given compositing
rule. The resulting image will have pixels with varying degrees of
transparency. We then composite these images (one for each rule) to the
screen, starting with an opaque white window and using the Source Over
Destination rule in Java 2D and it's equivalent in OpenGL. This gives
a much better feel for what each rule has done.
Another thing that has to be taken into account is that the compositing
rules in Java 2D are defined in terms of pixel values with premultipled
alpha. This means that the values being operated on are r*alpha, g*alpha,
b*alpha, and alpha, rather than r, g, b, and alpha. The papers by Porter
and Duff and by Blinn show that the compositing equations are symmetric
in all components for premultiplied values. Java 2D lets you store pixels
in either premultiplied or non-premultiplied form, but converts to
premultiplied form during compositing operations (or does some more optimal
but equivalent calculation). The OpenGL Programming Guide and Reference
Manual don't mention premultiplied alpha as far as I can tell, but most
of the possible blending factors are symmetric. This means that, in the
general case (see below for one useful exception), you should really be
handing premultiplied colors to OpenGL and treating the results of blending
as being premultiplied values, if you want to get results equivalent to the
original Porter-Duff operations. That is what the attached OpenGL program
does. For example, since OpenGL doesn't automatically convert the drawing
color to premultiplied form (like Java 2D does), the program does this before
handing colors to OpenGL. Red with an alpha of 0.35 is defined as
(0.35, 0.0, 0.0, 0.35), rather than (1.0, 0.0, 0.0, 0.35).
Given that we're compositing to a destination with allocated alpha bits
and using premultiplied values, here is the correspondence between the
8 built-in Java 2D compositing rules and the OpenGL source and destination
factors.
Java 2D OpenGL Source OpenGL Destination
Compositing Rule Blending Factor Blending Factor
================ =============== ==================
AlphaComposite.SrcOver GL_ONE GL_ONE_MINUS_SRC_ALPHA
AlphaComposite.SrcOut GL_ONE_MINUS_DST_ALPHA GL_ZERO
AlphaComposite.SrcIn GL_DST_ALPHA GL_ZERO
AlphaComposite.Src GL_ONE GL_ZERO
AlphaComposite.DstOver GL_ONE_MINUS_DST_ALPHA GL_ONE
AlphaComposite.DstOut GL_ZERO GL_ONE_MINUS_SRC_ALPHA
AlphaComposite.DstIn GL_ZERO GL_SRC_ALPHA
AlphaComposite.Clear GL_ZERO GL_ZERO
If you look at the documentation of the AlphaComposite class, you'll see
that the OpenGL factors here correspond exactly to the equations there for
each rule.
The attached OpenGL program is for Win32 platforms. You'd have to rewrite
the Windows-specific parts to run it on Unix. It also requires an OpenGL
implementation that supports pixel formats with some positive number of
bits of alpha. I've tested it on an NVIDIA Riva TNT card, and it produces
results visually identical to the attached Java program. There may be
some single-bit differences in pixel values due to precision issues, but
I haven't checked. I wanted to draw the eight images to offscreen buffers
(as the Java program does), but had to draw them into the frame buffer
instead and then read them out using glReadPixels, since the OpenGL
implementation I was using didn't support drawing to a BITMAP for any
pixel format that had an alpha channel.
Finally, I mentioned above that there was one useful exception to the
general case of needing to use premultiplied values and destinations with
an alpha channel in order to get equivalent results between Java 2D
compositing and OpenGL blending. This is the case of SrcOver rendering
to the screen, which is of course the most common case. First, you have to
understand what Java 2D and OpenGL do when rendering to such a destination.
Java 2D treat screens as opaque destinations with no alpha channel and
uses an implicit alpha of 1.0 in compositing calculations. The result of
the calculation for a destination pixel is (at least conceptually) in
premultiplied form and may have an alpha different from 1.0. If so, Java 2D
will divide out the non-1.0 alpha from the color components before writing
them into the frame buffer (if alpha = 0.0, the color components are written
as zeroes). OpenGL uses an implicit alpha of 1.0 if the screen has no
alpha channel (just like Java 2D), but does not divide out the alpha
resulting from the blending calculation if it's not 1.0. It simply discards
the alpha value and displays resulting color values as if the pixel
were opaque. If the screen does have an alpha channel, it uses the alpha
as an input value in the blending calculations, stores the resulting
destination alpha in the alpha channel, but again does not divide out the
alpha value from what is displayed.
What this means is that for screens without an alpha channel, only three
of the Java 2D compositing rules can be implemented equivalently via
OpenGL for all possible source alpha values. These are SrcOver, DstOver,
and Clear. The useful case is SrcOver. If you are sending premultiplied
color values to OpenGL, use the source and destination factors above. If
you are sending non-premultiplied color values to OpenGL, use a source
factor of GL_SRC_ALPHA and a destination factor of GL_ONE_MINUS_SRC_ALPHA
to get results equivalent to SrcOver. If the screen does have an alpha
channel, you need to sure to initialize the alpha values to all ones and
then you will get equivalent results to Java 2D for SrcOver just as for
the non-alpha channel case.
I hope all of this makes sense. Let me know if you have any questions.
Jerry
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;
/* This class is a Swing program that shows the effect of alpha compositing
on three circles (red, green and blue) with various compositing rules.
*/
public class Alpha extends JComponent
{
/* the names of each of the alpha compositing modes */
static final String[] names = {
"Src Over Dst",
"Src Held Out By Dst",
"Src In Dst",
"Src",
"Dst Over Src",
"Dst Held Out By Src",
"Dst In Src",
"Clear"
};
/* the alpha compositing modes */
static final AlphaComposite[] composite = {
AlphaComposite.SrcOver,
AlphaComposite.SrcOut,
AlphaComposite.SrcIn,
AlphaComposite.Src,
AlphaComposite.DstOver,
AlphaComposite.DstOut,
AlphaComposite.DstIn,
AlphaComposite.Clear
};
static final BufferedImage images[] = new BufferedImage[9];
static final double radius = 50.0;
/* The repaint method. */
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
/* clear background */
g2d.setPaint(Color.white);
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setComposite(AlphaComposite.SrcOver);
/* draw images */
for (int i = 0; i < 3; i++) {
int y = i * 200;
for(int j = 0; j < 3; j++) {
if ((i == 2) && (j == 2)) {
continue;
}
int x = j * 200;
g2d.drawImage(images[i * 3 + j], x, y, null);
}
}
/* draw yellow grid */
g2d.setPaint(Color.yellow);
g2d.fillRect(0, 0, 615, 15);
g2d.fillRect(0, 200, 615, 15);
g2d.fillRect(0, 400, 615, 15);
g2d.fillRect(0, 600, 615, 15);
g2d.fillRect(0, 0, 15, 615);
g2d.fillRect(200, 0, 15, 615);
g2d.fillRect(400, 0, 15, 615);
g2d.fillRect(600, 0, 15, 615);
/* draw mode names */
g2d.setPaint(Color.black);
for (int i = 0; i < 3; i++) {
int y = i * 200;
for(int j = 0; j < 3; j++) {
if ((i == 2) && (j == 2)) {
continue;
}
int x = j * 200;
g2d.drawString(names[i*3 + j],
(float) (x + 30),
(float) y + 12.5f);
}
}
}
static void drawCircles(Graphics2D g, double x, double y) {
int alpha = (int) (0.35f * 255.0f);
Color color;
/* red with alpha = 0.35 */
color = new Color(255, 0, 0, alpha);
g.setPaint(color);
g.fill(new Ellipse2D.Double(
x - 1.5 * radius,
y - 1.5 * radius,
radius*2.0,
radius*2.0));
/* green with alpha = 0.35 */
color = new Color(0, 255, 0, alpha);
g.setPaint(color);
g.fill(new Ellipse2D.Double(
x - radius,
y - 0.5 * radius,
radius*2.0,
radius*2.0));
/* blue with alpha = 0.35 */
color = new Color(0, 0, 255, alpha);
g.setPaint(color);
g.fill(new Ellipse2D.Double(
x - 0.5 * radius,
y - 1.5 * radius,
radius*2.0,
radius*2.0));
/* white with alpha = 0.35 */
color = new Color(255, 255, 255, alpha);
g.setPaint(color);
g.fill(new Ellipse2D.Double(
x - radius/8.0,
y - radius/8.0,
radius/4.0,
radius/4.0));
}
public static void main(String[] args)
{
JFrame frame = new JFrame("Alpha Composite Test");
// initialize images
for (int i = 0; i < 8; i++) {
// image is created with RGBA = (0,0,0,0)
images[i] = new BufferedImage(200, 200,
BufferedImage.TYPE_INT_ARGB_PRE);
Graphics2D g = images[i].createGraphics();
// now draw a gray grid into the image
Color color = new Color(128, 128, 128, 255);
g.setPaint(color);
g.setComposite(AlphaComposite.SrcOver);
for (int j = 0; j < 10; j++) {
g.fillRect(j*20, 0, 4, 200);
g.fillRect(0, j*20, 200, 4);
}
g.setComposite(composite[i]);
drawCircles(g, 100.0, 100.0);
}
frame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e)
{
System.out.println("Exiting...");
System.exit(0);
}
}
);
frame.getContentPane().add(new Alpha(), BorderLayout.CENTER);
frame.setSize(627, 646);
frame.setVisible(true);
}
}
#include <windows.h>
#include <GL/gl.h> // OpenGL
#include <GL/glu.h> // OpenGL utility methods
#include <stdio.h>
/* the names of each of the alpha compositing modes */
static char *names[8] = {
"Src Over Dst",
"Src Held Out By Dst",
"Src In Dst",
"Src",
"Dst Over Src",
"Dst Held Out By Src",
"Dst In Src",
"Clear"
};
static GLenum srccomp[] = {
GL_ONE,
GL_ONE_MINUS_DST_ALPHA,
GL_DST_ALPHA,
GL_ONE,
GL_ONE_MINUS_DST_ALPHA,
GL_ZERO,
GL_ZERO,
GL_ZERO
};
static GLenum dstcomp[] = {
GL_ONE_MINUS_SRC_ALPHA,
GL_ZERO,
GL_ZERO,
GL_ZERO,
GL_ONE,
GL_ONE_MINUS_SRC_ALPHA,
GL_SRC_ALPHA,
GL_ZERO
};
GLfloat radius = 50.0;
HDC hdc;
GLboolean bDoubleBuffer;
void fatalError(char *message) {
MessageBox(NULL, message, "Alpha Composite Test", MB_OK);
exit(1);
}
GLvoid errorCallback(GLenum errorCode) {
const GLubyte *estring;
char buffer[256];
estring = gluErrorString(errorCode);
sprintf(buffer, "Quadric Error: %s", estring);
fatalError(buffer);
}
GLuint makeCircle() {
GLuint list = glGenLists(1);
GLUquadricObj *qobj = gluNewQuadric();
gluQuadricCallback(qobj, GLU_ERROR, (void (CALLBACK*)()) errorCallback);
gluQuadricDrawStyle(qobj, GLU_FILL);
glNewList(list, GL_COMPILE);
gluDisk(qobj, 0.0, radius, 100, 1);
glEndList();
return list;
}
void drawCircles(GLfloat x, GLfloat y) {
static GLuint circle = 0;
double halfrad = 0.5 * radius;
if (circle == 0) {
circle = makeCircle();
}
/* red with alpha = 0.35, in premultiplied form */
glColor4f(0.35, 0.0, 0.0, 0.35);
glPushMatrix();
glTranslatef(x - halfrad, y - halfrad, 0.0);
glCallList(circle);
glPopMatrix();
/* green with alpha = 0.35, in premultiplied form */
glColor4f(0.0, 0.35, 0.0, 0.35);
glPushMatrix();
glTranslatef(x, y + halfrad, 0.0);
glCallList(circle);
glPopMatrix();
/* blue with alpha = 0.35, in premultiplied form */
glColor4f(0.0, 0.0, 0.35, 0.35);
glPushMatrix();
glTranslatef(x + halfrad, y - halfrad, 0.0);
glCallList(circle);
glPopMatrix();
/* white with alpha = 0.35, in premultiplied form */
glColor4f(0.35, 0.35, 0.35, 0.35);
glPushMatrix();
glTranslatef(x, y, 0.0);
glScalef(0.125, 0.125, 0.0);
glCallList(circle);
glPopMatrix();
}
GLubyte **makeImages() {
GLubyte **images;
int i, j;
images = (GLubyte **) malloc(8 * sizeof(GLubyte *));
glEnable(GL_BLEND);
for (i = 0; i < 8; i++) {
images[i] = (GLubyte *) malloc(200 * 200 * 4);
// use frame buffer to build each image, then save using glReadPixels
glBlendFunc(GL_ONE, GL_ZERO);
glColor4f(0.0, 0.0, 0.0, 0.0);
glRecti(0, 0, 200, 200); // clear to RGBA = (0, 0, 0, 0)
glColor4f(0.5, 0.5, 0.5, 1.0); // now draw an opaque gray grid
for (j = 0; j < 10; j++) {
glRecti(j*20, 0, j*20 + 4, 200);
glRecti(0, j*20, 200, j*20 + 4);
}
glBlendFunc(srccomp[i], dstcomp[i]);
drawCircles(100.0, 100.0);
glFlush();
// read back image from frame buffer
glReadPixels(0, 415, 200, 200, GL_RGBA,
GL_UNSIGNED_BYTE, (GLvoid *) images[i]);
}
return images;
}
void drawText(int x, int y, char *string) {
static GLuint list = 0;
if (list == 0) {
list = glGenLists(256);
SelectObject(hdc, GetStockObject(SYSTEM_FONT));
wglUseFontBitmaps(hdc, 0, 255, list);
}
glRasterPos2i(x, y);
glListBase(list);
glCallLists(strlen(string), GL_UNSIGNED_BYTE, string);
}
void redraw() {
int x, y;
int i, j;
static GLubyte **images = 0;
if (images == 0) {
images = makeImages();
}
glClearColor(1.0, 1.0, 1.0, 1.0); // start with opaque white background
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // use source over destination
// rule
// Note that each frame buffer destination pixel starts out with
// alpha == 1.0 and thus the source over destination rule will keep the
// destination alpha at 1.0, no matter what the source alpha is.
// Therefore, we can treat the destination as a premultiplied alpha
// buffer, and since all our source images are in premultiplied form,
// the above blend function is correct.
/* draw images */
for (i = 0; i < 3; i++) {
y = i * 200 + 200;
for (j = 0; j < 3; j++) {
x = j * 200;
if ((i == 2) && (j == 2)) {
continue;
}
glRasterPos2i(x, y);
glDrawPixels(200, 200, GL_RGBA, GL_UNSIGNED_BYTE,
images[i * 3 + j]);
}
}
/* draw yellow grid */
glColor4f(1.0, 1.0, 0.0, 1.0);
glRecti(0, 0, 615, 15);
glRecti(0, 200, 615, 215);
glRecti(0, 400, 615, 415);
glRecti(0, 600, 615, 615);
glRecti(0, 0, 15, 615);
glRecti(200, 0, 215, 615);
glRecti(400, 0, 415, 615);
glRecti(600, 0, 615, 615);
/* draw mode names */
glColor4f(0.0, 0.0, 0.0, 1.0);
for (i = 0; i < 3; i++) {
y = i * 200;
for (j = 0; j < 3; j++) {
x = j * 200;
if ((i == 2) && (j == 2)) {
continue;
}
drawText(x + 30, y + 11, names[i * 3 + j]);
}
}
glFlush();
if (bDoubleBuffer) {
SwapBuffers(hdc);
}
}
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL SetWindowPixelFormat(HDC);
HGLRC hGLContext;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR szCmdLine, int iCmdShow) {
static char szAppName[] = "Alpha Composite Test";
HWND hwnd;
MSG msg;
WNDCLASSEX wndclass;
int width = 615;
int height = 615;
wndclass.cbSize = sizeof(wndclass);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
RegisterClassEx(&wndclass);
hwnd = CreateWindow(szAppName,
"Alpha Composite Test",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
width + 8,
height + 27,
NULL,
NULL,
hInstance,
NULL);
hdc = GetDC(hwnd);
if (SetWindowPixelFormat(hdc) != TRUE) {
fatalError("Can't get RGBA pixel format");
}
hGLContext = wglCreateContext(hdc);
if (hGLContext == NULL) {
fatalError("Can't get a GL context");
}
if (wglMakeCurrent(hdc, hGLContext) == FALSE) {
fatalError("Can't make GL context current");
}
/* configure the OpenGL context for rendering */
glClearColor(1.0, 1.0, 1.0, 1.0);
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D((GLdouble)0, (GLdouble)width, (GLdouble)height, (GLdouble)0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) {
HDC hdc;
PAINTSTRUCT ps;
switch(iMsg) {
case WM_PAINT:
BeginPaint(hwnd, &ps);
redraw();
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, iMsg, wParam, lParam);
}
}
BOOL SetWindowPixelFormat(HDC hDC) {
PIXELFORMATDESCRIPTOR pixelDesc;
int pixelIndex;
pixelDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pixelDesc.nVersion = 1;
pixelDesc.dwFlags = PFD_DRAW_TO_WINDOW |
PFD_DRAW_TO_BITMAP |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER_DONTCARE |
PFD_STEREO_DONTCARE;
pixelDesc.iPixelType = PFD_TYPE_RGBA;
pixelDesc.cColorBits = 32;
pixelDesc.cRedBits = 8;
pixelDesc.cRedShift = 0;
pixelDesc.cGreenBits = 8;
pixelDesc.cGreenShift = 0;
pixelDesc.cBlueBits = 8;
pixelDesc.cBlueShift = 0;
pixelDesc.cAlphaBits = 8;
pixelDesc.cAlphaShift = 0;
pixelDesc.cAccumBits = 0;
pixelDesc.cAccumRedBits = 0;
pixelDesc.cAccumGreenBits = 0;
pixelDesc.cAccumBlueBits = 0;
pixelDesc.cAccumAlphaBits = 0;
pixelDesc.cDepthBits = 0;
pixelDesc.cStencilBits = 0;
pixelDesc.cAuxBuffers = 0;
pixelDesc.iLayerType = PFD_MAIN_PLANE;
pixelDesc.bReserved = 0;
pixelDesc.dwLayerMask = 0;
pixelDesc.dwVisibleMask = 0;
pixelDesc.dwDamageMask = 0;
pixelIndex = ChoosePixelFormat(hDC, &pixelDesc);
if (pixelIndex == 0) {
// choose default index
pixelIndex = 1;
}
if (DescribePixelFormat(hDC, pixelIndex,
sizeof(PIXELFORMATDESCRIPTOR),
&pixelDesc) == 0) {
return FALSE;
}
if (((pixelDesc.dwFlags & PFD_SUPPORT_OPENGL) != PFD_SUPPORT_OPENGL) ||
((pixelDesc.dwFlags & PFD_DRAW_TO_WINDOW) != PFD_DRAW_TO_WINDOW) ||
(pixelDesc.iPixelType != PFD_TYPE_RGBA) ||
(pixelDesc.cAlphaBits <= 0)) {
return FALSE;
}
if (pixelDesc.dwFlags & PFD_DOUBLEBUFFER) {
bDoubleBuffer = GL_TRUE;
} else {
bDoubleBuffer = GL_FALSE;
}
if (SetPixelFormat(hDC, pixelIndex, &pixelDesc) == FALSE) {
return FALSE;
}
return TRUE;
}