Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
* Per-pixel shadows on GPU:
* @author mattdesl */
public class GpuShadows implements ApplicationListener {
public static void main(String[] args) {
new LwjglApplication(new GpuShadows(), "Test", 800, 600, true);
* Compiles a new instance of the default shader for this batch and returns it. If compilation
* was unsuccessful, GdxRuntimeException will be thrown.
* @return the default shader
public static ShaderProgram createShader(String vert, String frag) {
ShaderProgram prog = new ShaderProgram(vert, frag);
if (!prog.isCompiled())
throw new GdxRuntimeException("could not compile shader: " + prog.getLog());
if (prog.getLog().length() != 0)"GpuShadows", prog.getLog());
return prog;
private int lightSize = 256;
private float upScale = 1f; //for example; try lightSize=128, upScale=1.5f
SpriteBatch batch;
OrthographicCamera cam;
BitmapFont font;
TextureRegion shadowMap1D; //1 dimensional shadow map
TextureRegion occluders; //occluder map
FrameBuffer shadowMapFBO;
FrameBuffer occludersFBO;
Texture casterSprites;
Texture light;
ShaderProgram shadowMapShader, shadowRenderShader;
Array<Light> lights = new Array<Light>();
boolean additive = true;
boolean softShadows = true;
class Light {
float x, y;
Color color;
public Light(float x, float y, Color color) {
this.x = x;
this.y = y;
this.color = color;
public void create() {
batch = new SpriteBatch();
ShaderProgram.pedantic = false;
//read vertex pass-through shader
final String VERT_SRC = Gdx.files.internal("data/pass.vert").readString();
// renders occluders to 1D shadow map
shadowMapShader = createShader(VERT_SRC, Gdx.files.internal("data/shadowMap.frag").readString());
// samples 1D shadow map to create the blurred soft shadow
shadowRenderShader = createShader(VERT_SRC, Gdx.files.internal("data/shadowRender.frag").readString());
//the occluders
casterSprites = new Texture("data/cat4.png");
//the light sprite
light = new Texture("data/light.png");
//build frame buffers
occludersFBO = new FrameBuffer(Format.RGBA8888, lightSize, lightSize, false);
occluders = new TextureRegion(occludersFBO.getColorBufferTexture());
occluders.flip(false, true);
//our 1D shadow map, lightSize x 1 pixels, no depth
shadowMapFBO = new FrameBuffer(Format.RGBA8888, lightSize, 1, false);
Texture shadowMapTex = shadowMapFBO.getColorBufferTexture();
//use linear filtering and repeat wrap mode when sampling
shadowMapTex.setFilter(TextureFilter.Linear, TextureFilter.Linear);
shadowMapTex.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
//for debugging only; in order to render the 1D shadow map FBO to screen
shadowMap1D = new TextureRegion(shadowMapTex);
shadowMap1D.flip(false, true);
font = new BitmapFont();
cam = new OrthographicCamera(,;
Gdx.input.setInputProcessor(new InputAdapter() {
public boolean touchDown(int x, int y, int pointer, int button) {
float mx = x;
float my = - y;
lights.add(new Light(mx, my, randomColor()));
return true;
public boolean keyDown(int key) {
if (key==Keys.SPACE){
return true;
} else if (key==Keys.A){
additive = !additive;
return true;
} else if (key==Keys.S){
softShadows = !softShadows;
return true;
return false;
public void resize(int width, int height) {
cam.setToOrtho(false, width, height);
public void render() {
//clear frame,0.25f,0.25f,1f);;
float mx = Gdx.input.getX();
float my = - Gdx.input.getY();
if (additive)
batch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE);
for (int i=0; i<lights.size; i++) {
Light o = lights.get(i);
if (i==lights.size-1) {
o.x = mx;
o.y = my;
if (additive)
batch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
//STEP 4. render sprites in full colour
batch.setShader(null); //default shader
batch.draw(casterSprites, 0, 0);
//DEBUG RENDERING -- show occluder map and 1D shadow map
batch.draw(occluders,, 0);
batch.draw(shadowMap1D,, lightSize+5);
//DEBUG RENDERING -- show light
batch.draw(light, mx-light.getWidth()/2f, my-light.getHeight()/2f); //mouse
batch.draw(light,, lightSize/2f-light.getHeight()/2f);
//draw FPS
font.drawMultiLine(batch, "FPS: "
+"\n\nLights: "+lights.size
+"\nSPACE to clear lights"
+"\nA to toggle additive blending"
+"\nS to toggle soft shadows", 10,;
void clearLights() {
lights.add(new Light(Gdx.input.getX(),, Color.WHITE));
static Color randomColor() {
float intensity = (float)Math.random() * 0.5f + 0.5f;
return new Color((float)Math.random(), (float)Math.random(), (float)Math.random(), intensity);
void renderLight(Light o) {
float mx = o.x;
float my = o.y;
//STEP 1. render light region to occluder FBO
//bind the occluder FBO
//clear the FBO,0f,0f,0f);;
//set the orthographic camera to the size of our FBO
cam.setToOrtho(false, occludersFBO.getWidth(), occludersFBO.getHeight());
//translate camera so that light is in the center
cam.translate(mx - lightSize/2f, my - lightSize/2f);
//update camera matrices
//set up our batch for the occluder pass
batch.setShader(null); //use default shader
// ... draw any sprites that will cast shadows here ... //
batch.draw(casterSprites, 0, 0);
//end the batch before unbinding the FBO
//unbind the FBO
//STEP 2. build a 1D shadow map from occlude FBO
//bind shadow map
//clear it,0f,0f,0f);;
//set our shadow map shader
shadowMapShader.setUniformf("resolution", lightSize, lightSize);
shadowMapShader.setUniformf("upScale", upScale);
//reset our projection matrix to the FBO size
cam.setToOrtho(false, shadowMapFBO.getWidth(), shadowMapFBO.getHeight());
//draw the occluders texture to our 1D shadow map FBO
batch.draw(occluders.getTexture(), 0, 0, lightSize, shadowMapFBO.getHeight());
//flush batch
//unbind shadow map FBO
//STEP 3. render the blurred shadows
//reset projection matrix to screen
//set the shader which actually draws the light/shadow
shadowRenderShader.setUniformf("resolution", lightSize, lightSize);
shadowRenderShader.setUniformf("softShadows", softShadows ? 1f : 0f);
//set color to light
float finalSize = lightSize * upScale;
//draw centered on light position
batch.draw(shadowMap1D.getTexture(), mx-finalSize/2f, my-finalSize/2f, finalSize, finalSize);
//flush the batch before swapping shaders
//reset color
public void pause() {
public void resume() {
// TODO Auto-generated method stub
public void dispose() {
// TODO Auto-generated method stub
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
uniform mat4 u_projTrans;
varying vec2 vTexCoord0;
varying vec4 vColor;
void main() {
vColor = a_color;
vTexCoord0 = a_texCoord0;
gl_Position = u_projTrans * a_position;
#ifdef GL_ES
#define LOWP lowp
precision mediump float;
#define LOWP
#define PI 3.14
varying vec2 vTexCoord0;
varying LOWP vec4 vColor;
uniform sampler2D u_texture;
uniform vec2 resolution;
//for debugging; use a constant value in final release
uniform float upScale;
//alpha threshold for our occlusion map
const float THRESHOLD = 0.75;
void main(void) {
float distance = 1.0;
for (float y=0.0; y<resolution.y; y+=1.0) {
//rectangular to polar filter
vec2 norm = vec2(vTexCoord0.s, y/resolution.y) * 2.0 - 1.0;
float theta = PI*1.5 + norm.x * PI;
float r = (1.0 + norm.y) * 0.5;
//coord which we will sample from occlude map
vec2 coord = vec2(-r * sin(theta), -r * cos(theta))/2.0 + 0.5;
//sample the occlusion map
vec4 data = texture2D(u_texture, coord);
//the current distance is how far from the top we've come
float dst = y/resolution.y / upScale;
//if we've hit an opaque fragment (occluder), then get new distance
//if the new distance is below the current, then we'll use that for our ray
float caster = data.a;
if (caster > THRESHOLD) {
distance = min(distance, dst);
gl_FragColor = vec4(vec3(distance), 1.0);
#ifdef GL_ES
#define LOWP lowp
precision mediump float;
#define LOWP
#define PI 3.14
varying vec2 vTexCoord0;
varying LOWP vec4 vColor;
uniform sampler2D u_texture;
uniform vec2 resolution;
uniform float softShadows;
//sample from the distance map
float sample(vec2 coord, float r) {
return step(r, texture2D(u_texture, coord).r);
void main(void) {
//rectangular to polar
vec2 norm = * 2.0 - 1.0;
float theta = atan(norm.y, norm.x);
float r = length(norm);
float coord = (theta + PI) / (2.0*PI);
//the tex coord to sample our 1D lookup texture
//always 0.0 on y axis
vec2 tc = vec2(coord, 0.0);
//the center tex coord, which gives us hard shadows
float center = sample(vec2(tc.x, tc.y), r);
//we multiply the blur amount by our distance from center
//this leads to more blurriness as the shadow "fades away"
float blur = (1./resolution.x) * smoothstep(0., 1., r);
//now we use a simple gaussian blur
float sum = 0.0;
sum += sample(vec2(tc.x - 4.0*blur, tc.y), r) * 0.05;
sum += sample(vec2(tc.x - 3.0*blur, tc.y), r) * 0.09;
sum += sample(vec2(tc.x - 2.0*blur, tc.y), r) * 0.12;
sum += sample(vec2(tc.x - 1.0*blur, tc.y), r) * 0.15;
sum += center * 0.16;
sum += sample(vec2(tc.x + 1.0*blur, tc.y), r) * 0.15;
sum += sample(vec2(tc.x + 2.0*blur, tc.y), r) * 0.12;
sum += sample(vec2(tc.x + 3.0*blur, tc.y), r) * 0.09;
sum += sample(vec2(tc.x + 4.0*blur, tc.y), r) * 0.05;
//1.0 -> in light, 0.0 -> in shadow
float lit = mix(center, sum, softShadows);
//multiply the summed amount by our distance, which gives us a radial falloff
//then multiply by vertex (light) color
gl_FragColor = vColor * vec4(vec3(1.0), lit * smoothstep(1.0, 0.0, r));

This comment has been minimized.

Copy link

@VeryBigT VeryBigT commented Mar 15, 2015

Hi really nice idea/tutorial!
I got a little optimisation suggestion for "shadowMap.frag":
You create "vec2 norm" in the for-loop but you only use the x and y values separate and x is the same everytime. Therefor you can declare this x and the resulting "theta", "sin(theta)" and "cos(theta))/2.0 + 0.5" once before you start the loop and safe a lot of calculating.

Sorry for possibly bad english.


This comment has been minimized.

Copy link

@mhelf mhelf commented Apr 29, 2016


I really like your shader tutorials. Thanks for the great work!

I recently implemented a flashlight and normal mapping. As well as simple pixel perfect hard shadows. Now i'm running into major performance issues on Android.

Basically i draw the shadow casters (walls) on a FrameBuffer. Then i draw the rest of the scene onto the screen. In the fragment shader i then check if a wall is between the current pixel and the light source by checking the alpha value of the wallbuffer at the current fragCoord.

If that is the case i just use the Ambient part of the light for that pixel to get some shadow like behaviour.

I divide the vector from the current pixel to the lightsource by a constant value to ensure that the loop has a constant number of iterations. When using the resolution as fidelity it will run with less than 1 frame per second :(
When i use more than 10 Iterations the fps drop dramatically. For a smooth visual quality i need at least 100 Iterations.
On desktop my lighting systems runs perfectly fine. But even my Galaxy S7 performs really poor.

Here you can find an image of how the effect looks like:



This comment has been minimized.

Copy link

@saucecode saucecode commented Jun 26, 2018

I've been trying to get this ported over to GLFW (C++ and OpenGL 3.3), but I'm having an awful time translating the libgdx code into the right OpenGL calls. Is there any way you could help me?

Here's what I've written as a rough translation
The translated code starts at line 64.


This comment has been minimized.

Copy link

@Roberts-Poznaks Roberts-Poznaks commented Feb 21, 2019

Has anyone made it work with tile based occluders? The light seems to bleed out when tiles are placed diagonally. Screenshot:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.