Sam MacPherson

Flash, Haxe, Game Dev and more…

Monthly Archives: May 2011

2D Dynamic Lighting Demo

In my previous post I went over how to implement 2d dynamic lighting on the GPU. After some tweaking I finally came up with a suitable solution for general purpose dynamic lighting. Here is a video demonstration of this in action in a game I am currently developing – codename Zed.

Currently the lighting only works with static objects as light blockers, but it’s not very hard to extend this to moving objects as well.

To get the effect right I had to modify my previous code and split the rendering into 2 separate tasks. First I do an additive light pass which fills in the glowing light you can see in the video. Next I had to do a subtractive shadow pass which worked by just starting with a black texture and subtracting off the alpha component as necessary. After each of these runs I do a 5x Gaussian blur effect to make things look nice and smooth and voila you have general purpose lighting that runs reasonably fast.

Depending on the quality of the light and your graphics card there are some limitations. I still plan on doing some more optimizations and benchmarking, but it seems that you are stuck with ~40 lights max on screen at any given moment. Possible optimizations could be caching of light textures that are static which would allow for a lot more stationary lights like what you saw in the game.

Advertisements

2D Dynamic Lighting with Molehill/HxSL

So I’ve really been digging into molehill lately and have come up with a solution for dynamic 2d lighting on the GPU. Basically what I did was ported this (http://www.catalinzima.com/2010/07/my-technique-for-the-shader-based-dynamic-2d-shadows/) method into flash. The method described in that article is built on XNA 4.0 which I’m pretty sure is a C# library for writing gpu shaders.

There are still a few things to iron out and optimize, but the first draft is looking good. Here is a screen shot of my algorithm working with 9 lights with a bunch of 10×10 pillers:

As of now this is about the limit that my graphics card can handle, but I’m fairly certain that I can get at least a 200% speed increase with some adjustments I plan on making (Probably a lot more if I really go at it).

My plan of attack for this was to start simple and only allow rectangles for obstacles and circles for the lights. I started by creating the vertex and index buffers for the obstacles.

for (i in rects) {
	//Vertex buffer
	vpts.push(i.xmin);
	vpts.push(i.ymin);
	
	vpts.push(i.xmax);
	vpts.push(i.ymin);
	
	vpts.push(i.xmin);
	vpts.push(i.ymax);
	
	vpts.push(i.xmax);
	vpts.push(i.ymax);
	
	//Index buffer
	ipts.push(index);
	ipts.push(index + 1);
	ipts.push(index + 3);
	
	ipts.push(index);
	ipts.push(index + 3);
	ipts.push(index + 2);
	
	index += 4;
}

_vbuf = Canvas.getContext().createVertexBuffer(Std.int(vpts[i].length / 2), 2);
_vbuf.uploadFromVector(vpts[i], 0, Std.int(vpts[i].length / 2));
_ibuf = Canvas.getContext().createIndexBuffer(ipts[i].length);
_ibuf.uploadFromVector(ipts[i], 0, ipts[i].length);

I also setup 4 textures for intermediate buffers between shader calls. Two of the textures are used for rendering individual lights while the other two are used to store the overall shadow map as lights are added. Really this is all that is needed for initialization. Now we move onto the render cycle which occurs during every frame update.

For every visible light the follow sequence of shaders gets run.

//The Shader Program
@:shader({
	var input:{
		pos:Float2
	};
	function vertex (mpos:M44, mproj:M44) {
		out = pos.xyzw * mpos * mproj;
	}
	function fragment () {
		out = [1, 1, 1, 1];
	}
}) class ObjectShader extends format.hxsl.Shader {
}

//The shader call
m.identity();
m.appendTranslation(-i.bounds.xmin, -i.bounds.ymin, 0);
var texCam:Matrix3D = Molehill.get2DOrthographicMatrix(i.bounds.intervalX, i.bounds.intervalY);
c.setRenderToTexture(_tbuf1);
c.clear();
_objectShader.init(
	{ mpos:m, mproj:texCam },
	{ }
);
_objectShader.draw(_vbuf, _ibuf);

This shader’s job is fairly straight forward. All it does is center the camera around the light and make every pixel which is inside an obstacle white. Once this shader has done its job we now have an image which looks like the following stored in _tbuf1.

In the interest of saving time and seeing as this is a port of an existing method I will re-use the pictures provided in the original post. Okay, we can now store the distances to the pixels as outlined in the first step of the original post.

//The Shader Program
@:shader({
	var input:{
		pos:Float2,
		uv:Float2
	};
	var tuv:Float2;
	function vertex (mproj:M44) {
		out = pos.xyzw * mproj;
		tuv = uv;
	}
	function fragment (t:Texture) {
		out = if (t.get(tuv, nearest).x > 0) len(tuv - [0.5, 0.5]).xxxx else 1.xxxx;
	}
}) class DistanceShader extends format.hxsl.Shader {
}

//The shader call
var vbuf:VertexBuffer3D = Molehill.getTextureVertexBuffer(c, 0, 0, i.bounds.intervalX, i.bounds.intervalY);
var ibuf:IndexBuffer3D = Molehill.getTextureIndexBuffer(c);
c.setRenderToTexture(_tbuf2);
c.clear();
_distanceShader.init(
	{ mproj:texCam },
	{ t:_tbuf1 }
);
_distanceShader.draw(vbuf, ibuf);

Now that we have shaded the pixels based on how far they are from the center of the image we have completed step one and have the following image stored in _tbuf2.

Now here is where things get cool. We take the image stored in _tbuf2 and distort it so the rays of light from the light source are aligned along the horizontal axis as outlined in step 2 from the original post.

//The Shader Program
@:shader({
	var input:{
		pos:Float2,
		uv:Float2
	};
	var tuv:Float2;
	function vertex (mproj:M44) {
		out = pos.xyzw * mproj;
		tuv = uv;
	}
	function fragment (t:Texture) {
		var u0 = tuv.x * 2 - 1;
		var v0 = tuv.y * 2 - 1;
		v0 = v0 * abs(u0);
		v0 = (v0 + 1) / 2;
		out = [t.get([tuv.x, v0], nearest).x, t.get([v0, tuv.x], nearest).x, 0, 1];
	}
}) class DistortionShader extends format.hxsl.Shader {
}

//The shader call
c.setRenderToTexture(_tbuf1);
c.clear();
_distortionShader.init(
	{ mproj:texCam },
	{ t:_tbuf2 }
);
_distortionShader.draw(vbuf, ibuf);

After this step we are left with the following image stored in _tbuf1.

This may look a bit weird and if you are confused at this point I would recommend reading over the original post. I know I found this a bit confusing when I first looked at it.

Ok, now that we have a view from the light’s perspective we need to calculate the closest obstacle edge by successively halving the image.

//The Shader Program
@:shader({
	var input:{
		pos:Float2,
		uv:Float2
	};
	var tuv:Float2;
	var dx:Float;
	function vertex (mproj:M44, pixel:Float) {
		out = pos.xyzw * mproj;
		tuv = uv;
		dx = pixel;
	}
	function fragment (t:Texture) {
		out = min(t.get(tuv + [-dx, 0], nearest), t.get(tuv + [0, 0], nearest));
	}
}) class MinDistanceShader extends format.hxsl.Shader {
}

//The shader call
for (i in 0 ... _distBufs.length) {
	c.setRenderToTexture(_distBufs[i]);
	c.clear();
	_minDistanceShader.init(
		{ mproj:texCam, pixel:1/(tdim >> i) },
		{ t:if (i == 0) _tbuf1 else _distBufs[i - 1] }
	);
	_minDistanceShader.draw(vbuf, ibuf);
}

The variable “tdim” is the size of the texture buffers. For the sake of clarity we will assume that tdim is 512 pixels. We need to call this shader 8 times to get the image down to 2×512. At each stage we compare every pixel with its closest neighbor in the x direction and throw away the higher of the two. The end result is a 2×512 image where each pixel contains the minimum distance to an obstacle in that direction. Again, if you are confused at this point please refer to the original article. It does a much better job at explaining the reasoning.

After we have the minimum distances we can now draw the shadow map for this light.

//The Shader Program
@:shader({
	var input:{
		pos:Float2,
		uv:Float2,
		copy:Float,
		suv:Float2
	};
	function getShadowDistanceH (t:Texture, pos:Float2):Float {
		var u:Float = pos.x;
		var v:Float = pos.y;
		
		u = abs(u-0.5) * 2;
		v = v * 2 - 1;
		var v0:Float = v/u;
		v0 = (v0 + 1) / 2;
		
		return t.get([pos.x, v0], nearest).x;
	}
	function getShadowDistanceV (t:Texture, pos:Float2):Float {
		var u:Float = pos.y;
		var v:Float = pos.x;
		
		u = abs(u-0.5) * 2;
		v = v * 2 - 1;
		var v0:Float = v/u;
		v0 = (v0 + 1) / 2;
		
		return t.get([pos.y, v0], nearest).y;
	}
	var tuv:Float2;
	var tcopy:Float;
	var stuv:Float2;
	function vertex (mproj:M44) {
		out = pos.xyzw * mproj;
		tuv = uv;
		tcopy = copy;
		stuv = suv;
	}
	function fragment (t:Texture, sm:Texture, baseIntensity:Float, intensity:Float) {
		var d:Float = len(tuv - [0.5, 0.5]);
		var sd:Float = if (tcopy > 0) 0 else if (abs(duv.y) < abs(duv.x)) getShadowDistanceH(t, tuv) else getShadowDistanceV(t, tuv);
		var a:Float = (0.5 - d) * 2;
		var shadow:Float = min((1 - intensity) * a + (1 - baseIntensity) * (1 - a), sm.get(stuv).w);
		var inside:Float4 = [0, 0, 0, shadow];
		var outside:Float4 = min([0, 0, 0, (1 - baseIntensity)], sm.get(stuv));
		out = if (tcopy > 0) sm.get(tuv) else if (d < sd) inside else outside;
	}
}) class ShadowMapShader extends format.hxsl.Shader {
}

//The shader call
var camera:Matrix3D = Molehill.get2DOrthographicMatrix(SCREEN_WIDTH, SCREEN_HEIGHT);
var svbuf:VertexBuffer3D = _getShadowMapVertexBuffer(i.bounds);
var sibuf:IndexBuffer3D = _getShadowMapIndexBuffer();
c.setRenderToTexture(if (index % 2 == 0) _sbuf2 else _sbuf1);
c.clear(0, 0, 0, 1 - _baseIntensity);
_shadowMapShader.init(
	{ mproj:camera },
	{ t:_distBufs[_distBufs.length - 1], sm:if (index % 2 == 0) _sbuf1 else _sbuf2, baseIntensity:_baseIntensity, intensity:i.light.getLightIntensity() }
);
_shadowMapShader.draw(svbuf, sibuf);

index++

This is probably the most complicated piece because I couldn’t think of a better way of doing this. Currently (to my knowledge) molehill does not allow drawing to the same texture twice. This was kind of annoying so I put together a work-around which takes the previously rendered lights and does a straight one to one copy. After this is done, during the same pass, the new light is supplied. The difference between the two is handled by the copy value in the vertex buffer. If copy is set to one then the shader just does a straight one to one copy. If not then it will render the light by performing the normal routine described in the original post. The “suv” coordinates are used to compare the previous render with the current one for pixel blending.

As you can see I’ve also added in a simple gradient effect which makes things look a bit nicer. I plan on expanding on the post-processing in the near future with blurring and colored light. There is also a lot of room for optimization. For one, I can get rid of some of the extra shaders and combine them. Also, since there is only ever 2 color channels used at once there is the possibility of rendering two lights at the same time. This is the 200% efficiency increase I was talking about earlier. I also plan on allowing the user to scale down the quality of the image in order to improve render time.

So there you have it — dynamic lighting on the GPU. All of the code above was taken from my gaming library and will be available to the public once I feel it is stable enough.