package atmos; import java.text.DecimalFormat; import java.text.MessageFormat; import java.util.Random; 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.backends.lwjgl.LwjglApplicationConfiguration; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; /** * Simple illumination model with shaders in LibGDX. * @author davedes */ public class Illumination2D implements ApplicationListener { Texture texture, texture_n; boolean flipY; Texture normalBase; OrthographicCamera cam; SpriteBatch fxBatch, batch; Matrix4 transform = new Matrix4(); Random rnd = new Random(); // position of our light final Vector3 DEFAULT_LIGHT_POS = new Vector3(0f, 0f, 0.07f); // the color of our light final Vector3 DEFAULT_LIGHT_COLOR = new Vector3(1f, 0.7f, 0.6f); // the ambient color (color to use when unlit) final Vector3 DEFAULT_AMBIENT_COLOR = new Vector3(0.3f, 0.3f, 1f); // the attenuation factor: x=constant, y=linear, z=quadratic final Vector3 DEFAULT_ATTENUATION = new Vector3(0.4f, 3f, 20f); // the ambient intensity (brightness to use when unlit) final float DEFAULT_AMBIENT_INTENSITY = 0.2f; final float DEFAULT_STRENGTH = 1f; final Color NORMAL_VCOLOR = new Color(1f,1f,1f,DEFAULT_STRENGTH); // the position of our light in 3D space Vector3 lightPos = new Vector3(DEFAULT_LIGHT_POS); // the resolution of our game/graphics Vector2 resolution = new Vector2(); // the current attenuation Vector3 attenuation = new Vector3(DEFAULT_ATTENUATION); // the current ambient intensity float ambientIntensity = DEFAULT_AMBIENT_INTENSITY; float strength = DEFAULT_STRENGTH; // whether to use attenuation/shadows boolean useShadow = true; // whether to use lambert shading (with our normal map) boolean useNormals = true; DecimalFormat DEC_FMT = new DecimalFormat("0.00000"); ShaderProgram program; BitmapFont font; private int texWidth, texHeight; final String TEXT = "Use number keys to adjust parameters:\n" + "1: Randomize Ambient Color\n" + "2: Randomize Ambient Intensity {0}\n" + "3: Randomize Light Color\n" + "4/5: Increase/decrease constant attenuation: {1}\n" + "6/7: Increase/decrease linear attenuation: {2}\n" + "8/9: Increase/decrease quadratic attenuation: {3}\n" + "0: Reset parameters\n" + "RIGHT/LEFT: Increase/decrease normal map intensity: {4}\n" + "UP/DOWN: Increase/decrease lightDir.z: {5}\n\n" + "S toggles attenuation, N toggles normal shading\n" + "T to toggle textures"; private Texture rock, rock_n, teapot, teapot_n; public void create() { // load our textures rock = new Texture(Gdx.files.internal("data/teapot.png")); rock_n = new Texture(Gdx.files.internal("data/teapot_n.png")); teapot = new Texture(Gdx.files.internal("data/rock.png")); teapot_n = new Texture(Gdx.files.internal("data/rock_n.png")); texture = teapot; texture_n = teapot_n; flipY = texture==rock; //we only use this to show what the strength-adjusted normal map looks like on screen Pixmap pix = new Pixmap(1, 1, Format.RGB565); pix.setColor(0.5f, 0.5f, 1.0f, 1.0f); pix.fill(); normalBase = new Texture(pix); texWidth = texture.getWidth(); texHeight = texture.getHeight(); // a simple 2D orthographic camera cam = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); cam.setToOrtho(false); // create our shader program... program = createShader(); // now we create our sprite batch for our shader fxBatch = new SpriteBatch(100, program); // setShader is needed; perhaps this is a LibGDX bug? fxBatch.setShader(program); fxBatch.setProjectionMatrix(cam.combined); fxBatch.setTransformMatrix(transform); // usually we would just use a single batch for our application, // but for demonstration let's also show the un-affected image batch = new SpriteBatch(100); batch.setProjectionMatrix(cam.combined); batch.setTransformMatrix(transform); // quick little input for debugging -- press S to toggle shadows, N to // toggle normals Gdx.input.setInputProcessor(new InputAdapter() { public boolean keyDown(int key) { if (key == Keys.S) { useShadow = !useShadow; return true; } else if (key == Keys.N) { useNormals = !useNormals; return true; } else if (key == Keys.NUM_1) { program.begin(); program.setUniformf("ambientColor", rndColor()); program.end(); return true; } else if (key == Keys.NUM_2) { ambientIntensity = rnd.nextFloat(); return true; } else if (key == Keys.NUM_3) { program.begin(); program.setUniformf("lightColor", rndColor()); program.end(); return true; } else if (key == Keys.NUM_0) { attenuation.set(DEFAULT_ATTENUATION); ambientIntensity = DEFAULT_AMBIENT_INTENSITY; lightPos.set(DEFAULT_LIGHT_POS); strength = DEFAULT_STRENGTH; program.begin(); program.setUniformf("lightColor", DEFAULT_LIGHT_COLOR); program.setUniformf("ambientColor", DEFAULT_AMBIENT_COLOR); program.setUniformf("ambientIntensity", ambientIntensity); program.setUniformf("attenuation", attenuation); program.setUniformf("lightPos", lightPos); program.setUniformf("strength", strength); program.end(); } else if (key == Keys.T) { texture = texture==teapot ? rock : teapot; texture_n = texture_n==teapot_n ? rock_n : teapot_n; flipY = texture==rock; texWidth = texture.getWidth(); texHeight = texture.getHeight(); program.begin(); program.setUniformi("yInvert", flipY ? 1 : 0); program.end(); } return false; } }); font = new BitmapFont(); } private Vector3 rndColor() { return new Vector3(rnd.nextFloat(), rnd.nextFloat(), rnd.nextFloat()); } private ShaderProgram createShader() { // see the code here: http://pastebin.com/7fkh1ax8 // simple illumination model using ambient, diffuse (lambert) and attenuation // see here: http://nccastaff.bournemouth.ac.uk/jmacey/CGF/slides/IlluminationModels4up.pdf String vert = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + "uniform mat4 u_proj;\n" // + "uniform mat4 u_trans;\n" // + "uniform mat4 u_projTrans;\n" // + "varying vec4 v_color;\n" // + "varying vec2 v_texCoords;\n" // + "\n" // + "void main()\n" // + "{\n" // + " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + "}\n"; String frag = "#ifdef GL_ES\n" + "precision mediump float;\n" + "#endif\n" + "varying vec4 v_color;\n" + "varying vec2 v_texCoords;\n" + "uniform sampler2D u_texture;\n" + "uniform sampler2D u_normals;\n" + "uniform vec3 light;\n" + "uniform vec3 ambientColor;\n" + "uniform float ambientIntensity; \n" + "uniform vec2 resolution;\n" + "uniform vec3 lightColor;\n" + "uniform bool useNormals;\n" + "uniform bool useShadow;\n" + "uniform vec3 attenuation;\n" + "uniform float strength;\n" + "uniform bool yInvert;\n"+ "\n" + "void main() {\n" + " //sample color & normals from our textures\n" + " vec4 color = texture2D(u_texture, v_texCoords.st);\n" + " vec3 nColor = texture2D(u_normals, v_texCoords.st).rgb;\n\n" + " //some bump map programs will need the Y value flipped..\n" + " nColor.g = yInvert ? 1.0 - nColor.g : nColor.g;\n\n" + " //this is for debugging purposes, allowing us to lower the intensity of our bump map\n" + " vec3 nBase = vec3(0.5, 0.5, 1.0);\n" + " nColor = mix(nBase, nColor, strength);\n\n" + " //normals need to be converted to [-1.0, 1.0] range and normalized\n" + " vec3 normal = normalize(nColor * 2.0 - 1.0);\n\n" + " //here we do a simple distance calculation\n" + " vec3 deltaPos = vec3( (light.xy - gl_FragCoord.xy) / resolution.xy, light.z );\n\n" + " vec3 lightDir = normalize(deltaPos);\n" + " float lambert = useNormals ? clamp(dot(normal, lightDir), 0.0, 1.0) : 1.0;\n" + " \n" + " //now let's get a nice little falloff\n" + " float d = sqrt(dot(deltaPos, deltaPos));"+ " \n" + " float att = useShadow ? 1.0 / ( attenuation.x + (attenuation.y*d) + (attenuation.z*d*d) ) : 1.0;\n" + " \n" + " vec3 result = (ambientColor * ambientIntensity) + (lightColor.rgb * lambert) * att;\n" + " result *= color.rgb;\n" + " \n" + " gl_FragColor = v_color * vec4(result, color.a);\n" + "}"; System.out.println("VERTEX PROGRAM:\n------------\n\n"+vert); System.out.println("FRAGMENT PROGRAM:\n------------\n\n"+frag); ShaderProgram program = new ShaderProgram(vert, frag); // u_proj and u_trans will not be active but SpriteBatch will still try to set them... program.pedantic = false; if (program.isCompiled() == false) throw new IllegalArgumentException("couldn't compile shader: " + program.getLog()); // set resolution vector resolution.set(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); // we are only using this many uniforms for testing purposes...!! program.begin(); program.setUniformi("u_texture", 0); program.setUniformi("u_normals", 1); program.setUniformf("light", lightPos); program.setUniformf("strength", strength); program.setUniformf("ambientIntensity", ambientIntensity); program.setUniformf("ambientColor", DEFAULT_AMBIENT_COLOR); program.setUniformf("resolution", resolution); program.setUniformf("lightColor", DEFAULT_LIGHT_COLOR); program.setUniformf("attenuation", attenuation); program.setUniformi("useShadow", useShadow ? 1 : 0); program.setUniformi("useNormals", useNormals ? 1 : 0); program.setUniformi("yInvert", flipY ? 1 : 0); program.end(); return program; } public void dispose() { fxBatch.dispose(); batch.dispose(); texture.dispose(); texture_n.dispose(); } public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // draw our sprites without any effects batch.begin(); final int IMG_Y = texHeight/2; //let's first simulate our resulting normal map by blending a blue square atop it //we also could have achieved this with glTexEnv in the fixed function pipeline NORMAL_VCOLOR.a = 1.0f - strength; batch.draw(texture_n, texWidth + 10, IMG_Y); batch.setColor(NORMAL_VCOLOR); batch.draw(normalBase, texWidth + 10, IMG_Y, texWidth, texHeight); batch.setColor(Color.WHITE); batch.draw(texture, 0, IMG_Y); //now let's simulate how our normal map will be sampled using strength //we can do this simply by blending a blue fill overtop String str = MessageFormat.format(TEXT, ambientIntensity, attenuation.x, attenuation.y, DEC_FMT.format(attenuation.z), strength, lightPos.z); font.drawMultiLine(batch, str, 10, Gdx.graphics.getHeight()-10); font.draw(batch, "Diffuse Color", 10, IMG_Y+texHeight + 30); font.draw(batch, "Normal Map", texWidth+20, IMG_Y+texHeight + 30); font.draw(batch, "Final Color", texWidth*2+30, IMG_Y+texHeight + 30); batch.end(); // start our FX batch, which will bind our shader program fxBatch.begin(); // get y-down light position based on mouse/touch lightPos.x = Gdx.input.getX(); lightPos.y = Gdx.graphics.getHeight() - Gdx.input.getY(); // handle attenuation input if (Gdx.input.isKeyPressed(Keys.NUM_4)) { attenuation.x += 0.025f; } else if (Gdx.input.isKeyPressed(Keys.NUM_5)) { attenuation.x -= 0.025f; if (attenuation.x < 0) attenuation.x = 0; } else if (Gdx.input.isKeyPressed(Keys.NUM_6)) { attenuation.y += 0.25f; } else if (Gdx.input.isKeyPressed(Keys.NUM_7)) { attenuation.y -= 0.25f; if (attenuation.y < 0) attenuation.y = 0; } else if (Gdx.input.isKeyPressed(Keys.NUM_8)) { attenuation.z += 0.25f; } else if (Gdx.input.isKeyPressed(Keys.NUM_9)) { attenuation.z -= 0.25f; if (attenuation.z < 0) attenuation.z = 0; } else if (Gdx.input.isKeyPressed(Keys.RIGHT)) { strength += 0.025f; if (strength > 1f) strength = 1f; } else if (Gdx.input.isKeyPressed(Keys.LEFT)) { strength -= 0.025f; if (strength < 0) strength = 0; } else if (Gdx.input.isKeyPressed(Keys.UP)) { lightPos.z += 0.0025f; } else if (Gdx.input.isKeyPressed(Keys.DOWN)) { lightPos.z -= 0.0025f; } // update our uniforms program.setUniformf("ambientIntensity", ambientIntensity); program.setUniformf("attenuation", attenuation); program.setUniformf("light", lightPos); program.setUniformi("useNormals", useNormals ? 1 : 0); program.setUniformi("useShadow", useShadow ? 1 : 0); program.setUniformf("strength", strength); // bind the normal first at texture1 texture_n.bind(1); // bind the actual texture at texture0 texture.bind(0); // we bind texture0 second since draw(texture) will end up binding it at // texture0... fxBatch.draw(texture, texWidth*2 + 20, IMG_Y); fxBatch.end(); } public void resize(int width, int height) { cam.setToOrtho(false, width, height); resolution.set(width, height); program.setUniformf("resolution", resolution); } public void pause() { } public void resume() { } public static void main(String[] args) { LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration(); cfg.title = "Lighting Test"; cfg.useGL20 = true; cfg.width = 1024; cfg.height = 768; cfg.resizable = false; new LwjglApplication(new Illumination2D(), cfg); } }