Multiple AlphaComposite Sources?

I’m having a little trouble with the AlphaComposite class in Java’s AWT package. I’m trying to use it as a “lighting” system, where I use radial gradients as sources of light, but I can’t figure out how to make multiple light sources, or, in other words, composite several light sources with the same overlaid translucent black rectangle. If you don’t quite understand what I mean, see the attached picture… it’s of the effect as it is right now in my game.

http://www.darinbeaudreau.com/images/generic-zombie-shooter_57.png

Is there a way to use this class to composite the same rectangle with multiple “light sources”, or do I have to use another method? If so, what method could I use for lighting of a similar effect? I’m using Java2D for my graphics.

Let one composite object know every light source, then determine the brightness at each point based on some function like the sum of the brightness of each individual source. This is the simplest way and each pixel only needs to be multiplied once. (Instead of once per light source.)

Beautiful! That looks really great. I’ve only seen work of this quality done with OpenGL up to now.

I’m curious how you are doing this. I’ve only used AlphaComposites on a graphics object, effecting whatever I draw on that graphics object. How do you get the radial gradient?

Here’s the snippet of code that draws it.


{ // Draw circle of light around player.
    float radius = 200.0f;
    float [] dist = {0.0f, 1.0f};
    Color [] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f), Color.BLACK};
    RadialGradientPaint p = new RadialGradientPaint(new Point2D.Double(player.getCenterX(), player.getCenterY()), 
                                                                    radius, dist, colors);
    g2d.setPaint(p);
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.95f));
    g2d.fillRect(0, 0, Globals.W_WIDTH, Globals.W_HEIGHT);
} // End drawing circle of light.

Not quite sure how to do that. Do you have an example? So far as I know, only one composite can be set on a shape.

Very cool! Now, it seems to me you could draw the very same graphic without using the setComposite(), if the opaque black in the radial paint were set to (0, 0, 0, 0.95), yes?

Similarly, you could also build a BufferedImage with the same colors, by computing the alpha component as am inverse function of distance to the center of the image. Then, the image could be moved around like a spotlight, and black rectangles drawn as fill in the area outside the image.

But I can’t think of how to combine two of these images and have the lit areas “add together”. It seems like by this method the opaque aspects would add, instead. I made some smoke in a similar fashion, by creating a bunch of graphics that grew more transparent towards the edges, and overlaid them, but of course it was the opaque pixels that accumulated visually.

I think what Several Kilo-Bytes is suggesting, is also to make a BufferedImage. As you go through the image setting each pixels’ color, compute the distance to the center of each light, and then add those alphas. Except, since the “added light” is actually a reduction in the alpha value, the adding would have to be in reverse somehow.

And computing all those distances seems like it would be really slow. Maybe this: construct a one-time reference array with the alphas all computed for a single spotlight. Then, to make an image to draw over the scene with two spotlights, offset your indexes into the reference array to simulate the spotlight locations and use the values you get from the two lookups.

I would also consider reversing the alpha values in the reference array. That way, they could be added together, then subtracted from 1 when you assign the alpha for the pixel in the new graphic.

Make sense?

This is going to chew up some cpus. OpenGL could do this a LOT faster.

Construct your shadow mask in a buffer (using DST_IN), and then draw it over the top of everything else.

Quick hacked together example (using my 4k template as a basis).


import java.applet.Applet;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.image.BufferedImage;
import java.util.Random;

public class G extends Applet implements Runnable {

	static final boolean DEBUG = true;
	static final int WIDTH = 800, HEIGHT = 600;

	static final long NANO_SECOND = 1000000000;
	static final long NANO_FRAMERATE_SAMPLE_LENGTH = NANO_SECOND/4;
	static final long NANO_FRAME_LENGTH = (NANO_SECOND / 60);

	public void start() {
		new Thread(this).start();
	}
	
	public void run() {
		
		Random r = new Random();

		BufferedImage buffer = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
		Graphics2D bg = (Graphics2D)buffer.getGraphics();

		BufferedImage shadowBuffer = new BufferedImage(WIDTH,HEIGHT,BufferedImage.TYPE_INT_ARGB);
		Graphics2D sg = (Graphics2D)shadowBuffer.getGraphics();
		
		long previousSampleStartTime;
		int frameCount;
		float fps;

		if (DEBUG) {
			previousSampleStartTime = System.nanoTime();
			frameCount = 0;
			fps = 0f;
		}

		long nextFrameStartTime, frameStartTime;
		nextFrameStartTime = System.nanoTime();

		//offsets in the ctrls[] that the mouse left/right/middle button states are stored.
		final int LMB = 0/Event.META_MASK;
		final int RMB = Event.META_MASK/Event.META_MASK;
		final int MMB = Event.ALT_MASK/Event.META_MASK;
		final int KEY_START = MMB+1;
		
		final int CONTROLS_LENGTH = 65536; // this needs to be large enough for no (remotely common) keys to throw out of bounds.
		
		// traditional key state array
		// For gui-type interfaces the processing should be done on a per event basis, so no state changes are missed.
		int [] ctrls = new int[CONTROLS_LENGTH];
		int mouseX=-1, mouseY=-1;

		final int LIGHT_COUNT = 10;
		
		int [] lightsX = new int[LIGHT_COUNT];
		int [] lightsY = new int[LIGHT_COUNT];
		int [] lightsDx = new int[LIGHT_COUNT];
		int [] lightsDy = new int[LIGHT_COUNT];
		
		for(int i = 1;i < LIGHT_COUNT;i++) {
			lightsX[i] = r.nextInt(WIDTH);
			lightsY[i] = r.nextInt(HEIGHT);
			lightsDx[i] = (r.nextInt(2)+1)*(r.nextInt(2)*2-1);
			lightsDy[i] = (r.nextInt(2)+1)*(r.nextInt(2)*2-1);
		}
				
		// enter game loop.
		do {

			while (nextFrameStartTime > (frameStartTime = System.nanoTime())) {
//					try{Thread.sleep(1);}catch(Exception e){} //ruins framerate accuracy, not necessary in Windows.
			}
			
			// Render
			bg.setColor(Color.black);
			bg.fillRect(0, 0, WIDTH, HEIGHT);			

			// note. Do processing of the queued key events as late as possible in the game loop so as to maximize responsiveness
			synchronized(this) {
				Event e;
				while((e = inputHead)!=null) {
					final int eventType = e.id;
					
					final int eventClass = eventType/100;
					
					final int KEY_EVENT = (Event.KEY_PRESS-1)/100;
					final int MOUSE_EVENT = (Event.MOUSE_DOWN-1)/100;
					
					if(eventClass==MOUSE_EVENT) {
						bg.setColor(Color.white);
						bg.drawLine(mouseX, mouseY, e.x, e.y);
						mouseX = e.x;
						mouseY = e.y;
						
						if(eventType<Event.MOUSE_MOVE) { // event must be MOUSE_DOWN or MOUSE_UP
							ctrls[(e.modifiers & (Event.META_MASK|Event.ALT_MASK))/Event.META_MASK] = Event.MOUSE_UP-eventType;
						}
					}
					
					if(eventClass==KEY_EVENT) {
						ctrls[e.key] = eventType &1;
					}
					inputHead = e.evt;
				}
			}
			// light 0 tracks the mouse position.
			lightsX[0] = mouseX;
			lightsY[0] = mouseY;
			// other lights just fly around a bit.
			for(int i = 1;i < LIGHT_COUNT;i++ ) {
				lightsX[i]+=WIDTH+lightsDx[i];
				lightsY[i]+=HEIGHT+lightsDy[i];
				
				lightsX[i]%=WIDTH;
				lightsY[i]%=HEIGHT;
			
			}
			
			// end of logic
			
			
			/// draw background patternation
			for(int i = 0;i < WIDTH;i+=40) {
				bg.setColor(Color.BLUE);
				bg.drawLine(i,0,i,HEIGHT);
			}
			for(int i = 0;i < HEIGHT;i+=40) {
				bg.setColor(Color.RED);
				bg.drawLine(0,i,WIDTH,i);
			}

			// clear the shadow buffer
			sg.setComposite(AlphaComposite.Src);
			sg.setColor(Color.BLACK);
			sg.fillRect(0, 0, WIDTH,HEIGHT);
			// and ready it for this frame's shadows
			sg.setComposite(AlphaComposite.DstIn);
			
			
			for(int i = 0;i < LIGHT_COUNT;i++) {
			    int radius = 100;
			    float [] dist = {0.0f, 1.0f};
			    Color [] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f), Color.BLACK};
			    RadialGradientPaint p = new RadialGradientPaint(new Point(lightsX[i], lightsY[i]), 
			                                                                    radius, dist, colors);
			    sg.setPaint(p);
			    // there might be an 'off by one' bug here. Haven't spent the time thinking whether fillRect bounds match circle+/-radius.
			    sg.fillRect(lightsX[i]-radius, lightsY[i]-radius, radius*2, radius*2);
			    
			}
			
			if (DEBUG) {
				//framerate counter & key/mouse state reporting
				frameCount++;
				long now = System.nanoTime();
				if (now - previousSampleStartTime >= NANO_FRAMERATE_SAMPLE_LENGTH) {
					fps = frameCount * (float)NANO_FRAMERATE_SAMPLE_LENGTH/(now - previousSampleStartTime) * (float)NANO_SECOND/NANO_FRAMERATE_SAMPLE_LENGTH;
					previousSampleStartTime = now;
					frameCount = 0;
				}

				bg.setColor(Color.white);
				bg.drawString(String.format("FPS: %.2f", fps), 20, 30);
				bg.drawString(String.format("Mouse[left,middle,right]=[%d,%d,%d]", ctrls[LMB], ctrls[MMB], ctrls[RMB]), 20, 45);

				int keyDownCount = 0;
				for (int i = KEY_START; i < CONTROLS_LENGTH; i++) {
					if (ctrls[i] != 0) {
						keyDownCount++;
						bg.drawString(new String(new char[] { (char) i }), 20, 60 + 15 * keyDownCount);
					}
				}				
				
			}
			
			bg.drawImage(shadowBuffer,0,0,null);

			// Draw the entire results on the screen.
			final Graphics g = getGraphics();

			if (g != null)
				g.drawImage(buffer, 0, 0, null);

			nextFrameStartTime = frameStartTime + NANO_FRAME_LENGTH;
		}
		while(isActive());
		
		// shutdown here
	}

	Event inputHead;

	public synchronized boolean handleEvent(Event e) {
		// we abuse input Events by constructing a linkedlist from them.
		if(inputHead==null) {
			inputHead = e;
		}
		else {
			// note we have to iterate to the end of the list every time we add an event
			// not a performance concern as the list will never grow very large (it's reset upto 60 times/second)
			Event ll = inputHead;
			while(ll.evt!=null) ll = ll.evt;
			ll.evt = e;
		}
		return false;
	}
}

Please note the above example is not written for optimal performance.

Ideally the backBuffer would be a Transparency.OPAQUE VolatileImage (or a BufferStrategy).

As to whether the shadowBuffer should be an Transparency.TRANSLUCENT VolatileImage, or an ARGB BufferedImage… that’s a little more complicated.

If draw operations with a RadialGradientPaint are accelerated by the DirectX (or OpenGL) pipeline, then the shadowBuffer should be a TRANSLUCENT VolatileImage.
On the other hand, if RadialGradientPaint is not accelerated, then the pixel readback from a VolatileImage required for the DST_IN composition will absolutely destroy performance, so a BufferedImage should be used.

If you’re serious about using Java2D, and want your code to run optimally (though not necessarily well) on lots of configurations then you have to do live profiling at runtime to determine which combinations of operations & surfaces perform acceptably on the user’s hardware/driver configuration.

I am so confused… there’s no simpler way to do this? That’s an awful lot of code for what seems like it should be a simple effect. Are there other methods of achieving this that don’t involve composites? I was hoping for a method similar to that used by Notch in the game Left 4k Dead.

https://mojang.com/notch/j4k/l4kd/

That’s an entire application, so you can see it running in situ.

If you want only the relevant code, here it is:
Create the shadow mask; do this once, and keep it around:


      BufferedImage shadowBuffer = new BufferedImage(WIDTH,HEIGHT,BufferedImage.TYPE_INT_ARGB);
      Graphics2D sg = (Graphics2D)shadowBuffer.getGraphics();

Then, each frame…
Clear the shadow mask:


// clear the shadow buffer
         sg.setComposite(AlphaComposite.Src);
         sg.setColor(Color.BLACK);
         sg.fillRect(0, 0, WIDTH,HEIGHT);
         // and ready it for this frame's shadows
         sg.setComposite(AlphaComposite.DstIn);

Then draw each light source onto the shadow mask:


         for(int i = 0;i < LIGHT_COUNT;i++) {
             int radius = 100;
             float [] dist = {0.0f, 1.0f};
             Color [] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f), Color.BLACK};
             RadialGradientPaint p = new RadialGradientPaint(new Point(lightsX[i], lightsY[i]), 
                                                                             radius, dist, colors);
             sg.setPaint(p);
             // there might be an 'off by one' bug here. Haven't spent the time thinking whether fillRect bounds match circle+/-radius.
             sg.fillRect(lightsX[i]-radius, lightsY[i]-radius, radius*2, radius*2);
             
         }

Finally, once you’ve rendered everything else in your world, draw the shadow mask over the top:


bg.drawImage(shadowBuffer,0,0,null);

It’s basically the code you posted, wrapped in a for loop, with a few extra lines of initialisation. Simples.

Yes, you can do the lighting yourself at a per-pixel level.
That’s probably how notch did it in L4KD; you can see for yourself, he did make the code available.

Doing it this way is far more flexible (you can do your own shadow casting calculations and such), but obviously it’s a lot more complicated too.

Alright… you had me a little worried with all the other code in your example.
In the link I posted to Left 4k Dead, how did Notch do his lighting? He used Java2D for the game, but seeing as how the code he released for the game is obfuscated, I couldn’t figure out how he did it. I think he used a raycasting system, but I haven’t been able to make a proper implementation of one of those yet, nor would I know what to do with the rays to achieve the “lighting”.

Yeah, I have the code downloaded, but it’s obfuscated and not commented, so I don’t know what to do with it. I think it’s a raycasting system, but like I said above, I’m not sure how to do that.

If you see obfuscated code, you must have the wrong file, the code isn’t obfuscated but the variable names are not the best as he had to fit it into 4 kb to fit the competition.

https://mojang.com/notch/j4k/l4kd/G.java

if it was obfuscated, you would literally have no idea whats going on, some things can give you hints, and you could probably be able to rename the variables to better ones, or comment sections of code to what is happening.

put the code into your IDE and just start changing stuff if needed to figure out what it does.

Got it! It wasn’t so hard after all. Now I’m going to create a LightSource class to store the light’s information in and add a new item to the game… Flares!

http://www.darinbeaudreau.com/images/generic-zombie-shooter_58.png

You don’t call that obfuscated? And yes, that’s the file I have. None of his variable names are descriptive, there’s no commenting, and everything is just one big mess of code.

Beautiful isn’t it :wink:

Just wait until you get asked to port code like that… but with comments and naming in another language… that uses a different character set :slight_smile:

It gets to the point where you work from the binary, and use reflection and instrumentation to make changes. Horrifying.

It should be relatively simple to add coloured lighting, and by altering the geometry of what you draw to the shadow mask you can do simple directional light volumes too. (CAG with the Area class would be a quick and dirty way of doing it)

I could just make a polygon in the shape of a front-facing cone and use a regular gradient paint on it, right?
Also, for colored lighting, could I just change the color values passed to the gradient?

Quick tips on this stuff. First it is faster to draw images then gradients in java2D and you can use images as lights and color them on the fly. Second, the way you have it right now, simply drawing multiple radial gradiant paints on top of your scene should be fine but don’t use a bufferedImage back buffer because it is very very slow. Second, stay away from java’s Area class. Super slow.

What I recommend. Keep using the gradients as you probably do not need hundreds of lights. Set the rgba of the color of a light to what ever you want. Then instead of clearing to black, clear with a color of (0,0,0,0), empty. Instead of using a BufferedImage to draw with, create a compatible volatileiamge and draw all the lights to that with the SCR_OVER rule. Once done, composite that over the scene like you are doing. Drawing with/clipping with polygons is rather fast. Go with that.


	public void render(Graphics g)
	{
		if(!LIGHT_ON) // mapg2d is the Graphics a a VolatileImage
			return;
		clear();
		mapg2d.setColor(ambientColor);
		mapg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, ambientLight));
		mapg2d.fillRect(offX,offY,mapWidth,mapHeight);
 		for(int i = 0; i <= lights.size()-1;i++)
		{
			lights.get(i).render(mapg2d);
		}

		Graphics2D g2d = (Graphics2D) g.create();
		g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, colorBlend)); // the amount of color we want from the light map to be composited into the scene 
		g2d.drawImage(lightmap,0,0,scrWidth,scrHeight,null);
		g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_ATOP, 1));
		g2d.drawImage(lightmap,0,0,scrWidth,scrHeight,null);
		g2d.dispose();
	}
	
	private void clear()
	{
		mapg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); // this line may not be necessary. 
		mapg2d.setColor(BLANK); // a static blank Color of (0,0,0,0);
		mapg2d.fillRect(0, 0, mapWidth, mapHeight);
	}