Skip to content

Instantly share code, notes, and snippets.

@KTRosenberg
Forked from mattdesl/GpuShadows.java
Created June 16, 2020 03:25
Show Gist options
  • Select an option

  • Save KTRosenberg/353d8ba93262da4266a0e048275fde46 to your computer and use it in GitHub Desktop.

Select an option

Save KTRosenberg/353d8ba93262da4266a0e048275fde46 to your computer and use it in GitHub Desktop.

Revisions

  1. @mattdesl mattdesl revised this gist Apr 1, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion GpuShadows.java
    Original file line number Diff line number Diff line change
    @@ -20,7 +20,7 @@

    /**
    * Per-pixel shadows on GPU: https://github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows
    * @author mattdesl*/
    * @author mattdesl */
    public class GpuShadows implements ApplicationListener {

    public static void main(String[] args) {
  2. @mattdesl mattdesl revised this gist Apr 1, 2013. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions GpuShadows.java
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,3 @@
    package mingdx;

    import com.badlogic.gdx.ApplicationListener;
    import com.badlogic.gdx.Gdx;
    import com.badlogic.gdx.Input.Keys;
    @@ -20,6 +18,9 @@
    import com.badlogic.gdx.utils.Array;
    import com.badlogic.gdx.utils.GdxRuntimeException;

    /**
    * Per-pixel shadows on GPU: https://github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows
    * @author mattdesl*/
    public class GpuShadows implements ApplicationListener {

    public static void main(String[] args) {
  3. @mattdesl mattdesl created this gist Apr 1, 2013.
    321 changes: 321 additions & 0 deletions GpuShadows.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,321 @@
    package mingdx;

    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.graphics.Color;
    import com.badlogic.gdx.graphics.GL10;
    import com.badlogic.gdx.graphics.OrthographicCamera;
    import com.badlogic.gdx.graphics.Pixmap.Format;
    import com.badlogic.gdx.graphics.Texture;
    import com.badlogic.gdx.graphics.Texture.TextureFilter;
    import com.badlogic.gdx.graphics.Texture.TextureWrap;
    import com.badlogic.gdx.graphics.g2d.BitmapFont;
    import com.badlogic.gdx.graphics.g2d.SpriteBatch;
    import com.badlogic.gdx.graphics.g2d.TextureRegion;
    import com.badlogic.gdx.graphics.glutils.FrameBuffer;
    import com.badlogic.gdx.graphics.glutils.ShaderProgram;
    import com.badlogic.gdx.utils.Array;
    import com.badlogic.gdx.utils.GdxRuntimeException;

    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)
    Gdx.app.log("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;
    }
    }

    @Override
    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.graphics.getWidth(), Gdx.graphics.getHeight());
    cam.setToOrtho(false);

    Gdx.input.setInputProcessor(new InputAdapter() {

    public boolean touchDown(int x, int y, int pointer, int button) {
    float mx = x;
    float my = Gdx.graphics.getHeight() - y;
    lights.add(new Light(mx, my, randomColor()));
    return true;
    }

    public boolean keyDown(int key) {
    if (key==Keys.SPACE){
    clearLights();
    return true;
    } else if (key==Keys.A){
    additive = !additive;
    return true;
    } else if (key==Keys.S){
    softShadows = !softShadows;
    return true;
    }
    return false;
    }
    });

    clearLights();
    }

    @Override
    public void resize(int width, int height) {
    cam.setToOrtho(false, width, height);
    batch.setProjectionMatrix(cam.combined);
    }

    @Override
    public void render() {
    //clear frame
    Gdx.gl.glClearColor(0.25f,0.25f,0.25f,1f);
    Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    float mx = Gdx.input.getX();
    float my = Gdx.graphics.getHeight() - 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;
    }
    renderLight(o);
    }

    if (additive)
    batch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);

    //STEP 4. render sprites in full colour
    batch.begin();
    batch.setShader(null); //default shader

    batch.draw(casterSprites, 0, 0);

    //DEBUG RENDERING -- show occluder map and 1D shadow map
    batch.setColor(Color.BLACK);
    batch.draw(occluders, Gdx.graphics.getWidth()-lightSize, 0);
    batch.setColor(Color.WHITE);
    batch.draw(shadowMap1D, Gdx.graphics.getWidth()-lightSize, lightSize+5);

    //DEBUG RENDERING -- show light
    batch.draw(light, mx-light.getWidth()/2f, my-light.getHeight()/2f); //mouse
    batch.draw(light, Gdx.graphics.getWidth()-lightSize/2f-light.getWidth()/2f, lightSize/2f-light.getHeight()/2f);

    //draw FPS
    font.drawMultiLine(batch, "FPS: "+Gdx.graphics.getFramesPerSecond()
    +"\n\nLights: "+lights.size
    +"\nSPACE to clear lights"
    +"\nA to toggle additive blending"
    +"\nS to toggle soft shadows", 10, Gdx.graphics.getHeight()-10);

    batch.end();
    }

    void clearLights() {
    lights.clear();
    lights.add(new Light(Gdx.input.getX(), Gdx.graphics.getHeight()-Gdx.input.getY(), 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
    occludersFBO.begin();

    //clear the FBO
    Gdx.gl.glClearColor(0f,0f,0f,0f);
    Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    //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
    cam.update();

    //set up our batch for the occluder pass
    batch.setProjectionMatrix(cam.combined);
    batch.setShader(null); //use default shader
    batch.begin();
    // ... draw any sprites that will cast shadows here ... //
    batch.draw(casterSprites, 0, 0);

    //end the batch before unbinding the FBO
    batch.end();

    //unbind the FBO
    occludersFBO.end();

    //STEP 2. build a 1D shadow map from occlude FBO

    //bind shadow map
    shadowMapFBO.begin();

    //clear it
    Gdx.gl.glClearColor(0f,0f,0f,0f);
    Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    //set our shadow map shader
    batch.setShader(shadowMapShader);
    batch.begin();
    shadowMapShader.setUniformf("resolution", lightSize, lightSize);
    shadowMapShader.setUniformf("upScale", upScale);

    //reset our projection matrix to the FBO size
    cam.setToOrtho(false, shadowMapFBO.getWidth(), shadowMapFBO.getHeight());
    batch.setProjectionMatrix(cam.combined);

    //draw the occluders texture to our 1D shadow map FBO
    batch.draw(occluders.getTexture(), 0, 0, lightSize, shadowMapFBO.getHeight());

    //flush batch
    batch.end();

    //unbind shadow map FBO
    shadowMapFBO.end();

    //STEP 3. render the blurred shadows

    //reset projection matrix to screen
    cam.setToOrtho(false);
    batch.setProjectionMatrix(cam.combined);

    //set the shader which actually draws the light/shadow
    batch.setShader(shadowRenderShader);
    batch.begin();

    shadowRenderShader.setUniformf("resolution", lightSize, lightSize);
    shadowRenderShader.setUniformf("softShadows", softShadows ? 1f : 0f);
    //set color to light
    batch.setColor(o.color);

    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
    batch.end();

    //reset color
    batch.setColor(Color.WHITE);
    }

    @Override
    public void pause() {

    }

    @Override
    public void resume() {
    // TODO Auto-generated method stub

    }

    @Override
    public void dispose() {
    // TODO Auto-generated method stub

    }

    }
    13 changes: 13 additions & 0 deletions pass.vert
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    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;
    }
    48 changes: 48 additions & 0 deletions shadowMap.frag
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    #ifdef GL_ES
    #define LOWP lowp
    precision mediump float;
    #else
    #define LOWP
    #endif

    #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);
    }
    61 changes: 61 additions & 0 deletions shadowRender.frag
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    #ifdef GL_ES
    #define LOWP lowp
    precision mediump float;
    #else
    #define LOWP
    #endif

    #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 = vTexCoord0.st * 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));
    }