package tests; import box2dLight.*; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.InputAdapter; import com.badlogic.gdx.graphics.*; import com.badlogic.gdx.graphics.g2d.Batch; 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.math.*; import com.badlogic.gdx.physics.box2d.*; import com.badlogic.gdx.physics.box2d.BodyDef.BodyType; import com.badlogic.gdx.physics.box2d.joints.MouseJoint; import com.badlogic.gdx.physics.box2d.joints.MouseJointDef; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.viewport.FitViewport; import java.util.ArrayList; public class Box2dLightCustomShaderTest extends InputAdapter implements ApplicationListener { static final int RAYS_PER_BALL = 64; static final int BALLSNUM = 8; static final float LIGHT_DISTANCE = 16f; static final float RADIUS = 1f; public static final float SCALE = 1.f/16.f; public static final float viewportWidth = 48; public static final float viewportHeight = 32; OrthographicCamera camera; FitViewport viewport; SpriteBatch batch; BitmapFont font; // TextureRegion textureRegion; /** our box2D world **/ World world; /** our boxes **/ ArrayList<Body> balls = new ArrayList<Body>(BALLSNUM); /** our ground box **/ Body groundBody; /** our mouse joint **/ MouseJoint mouseJoint = null; /** a hit body **/ Body hitBody = null; /** pixel perfect projection for font rendering */ Matrix4 normalProjection = new Matrix4(); boolean showText = true; /** BOX2D LIGHT STUFF */ RayHandler rayHandler; ArrayList<Light> lights = new ArrayList<Light>(BALLSNUM); float sunDirection = -90f; Texture bg, bgN; TextureRegion objectReg, objectRegN; FrameBuffer normalFbo; Array<DeferredObject> assetArray = new Array<DeferredObject>(); DeferredObject marble; ShaderProgram lightShader; ShaderProgram normalShader; @Override public void create() { bg = new Texture(Gdx.files.internal("data/bg-deferred.png")); bgN = new Texture(Gdx.files.internal("data/bg-deferred-n.png")); MathUtils.random.setSeed(Long.MIN_VALUE); camera = new OrthographicCamera(viewportWidth, viewportHeight); camera.update(); viewport = new FitViewport(viewportWidth, viewportHeight, camera); batch = new SpriteBatch(); font = new BitmapFont(); font.setColor(Color.RED); TextureRegion marbleD = new TextureRegion(new Texture( Gdx.files.internal("data/marble.png"))); TextureRegion marbleN = new TextureRegion(new Texture( Gdx.files.internal("data/marble-n.png"))); marble = new DeferredObject(marbleD, marbleN); marble.width = RADIUS * 2; marble.height = RADIUS * 2; createPhysicsWorld(); Gdx.input.setInputProcessor(this); normalProjection.setToOrtho2D(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); /** BOX2D LIGHT STUFF BEGIN */ RayHandler.setGammaCorrection(true); RayHandler.useDiffuseLight(true); normalShader = createNormalShader(); lightShader = createLightShader(); rayHandler = new RayHandler(world, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()) { @Override protected void updateLightShader () {} @Override protected void updateLightShaderPerLight (Light light) { // light position must be normalized float x = (light.getX())/viewportWidth; float y = (light.getY())/viewportHeight; lightShader.setUniformf("u_lightpos", x, y, 0.05f); lightShader.setUniformf("u_intensity", 5); } }; rayHandler.setLightShader(lightShader); rayHandler.setAmbientLight(0.1f, 0.1f, 0.1f, 0.5f); rayHandler.setBlurNum(0); initPointLights(); /** BOX2D LIGHT STUFF END */ objectReg = new TextureRegion(new Texture(Gdx.files.internal("data/object-deferred.png"))); objectRegN = new TextureRegion(new Texture(Gdx.files.internal("data/object-deferred-n.png"))); for (int x = 0; x < 4; x++) { for (int y = 0; y < 3; y++) { DeferredObject deferredObject = new DeferredObject(objectReg, objectRegN); deferredObject.x = 4 + x * (deferredObject.diffuse.getRegionWidth()*SCALE + 8); deferredObject.y = 4 + y * (deferredObject.diffuse.getRegionHeight()*SCALE + 7); deferredObject.color.set(MathUtils.random(0.5f, 1), MathUtils.random(0.5f, 1), MathUtils.random(0.5f, 1), 1); if (x > 0) deferredObject.rot = true; deferredObject.rotation = MathUtils.random(90); assetArray.add(deferredObject); } } once = false; normalFbo = new FrameBuffer(Pixmap.Format.RGB565, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false); } private ShaderProgram createLightShader () { // Shader adapted from https://github.com/mattdesl/lwjgl-basics/wiki/ShaderLesson6 final String vertexShader = "attribute vec4 vertex_positions;\n" // + "attribute vec4 quad_colors;\n" // + "attribute float s;\n" + "uniform mat4 u_projTrans;\n" // + "varying vec4 v_color;\n" // + "void main()\n" // + "{\n" // + " v_color = s * quad_colors;\n" // + " gl_Position = u_projTrans * vertex_positions;\n" // + "}\n"; final String fragmentShader = "#ifdef GL_ES\n" // + "precision lowp float;\n" // + "#define MED mediump\n" + "#else\n" + "#define MED \n" + "#endif\n" // + "varying vec4 v_color;\n" // + "uniform sampler2D u_normals;\n" // + "uniform vec3 u_lightpos;\n" // + "uniform vec2 u_resolution;\n" // + "uniform float u_intensity = 1.0;\n" // + "void main()\n"// + "{\n" + " vec2 screenPos = gl_FragCoord.xy / u_resolution.xy;\n" + " vec3 NormalMap = texture2D(u_normals, screenPos).rgb; " + " vec3 LightDir = vec3(u_lightpos.xy - screenPos, u_lightpos.z);\n" + " vec3 N = normalize(NormalMap * 2.0 - 1.0);\n" + " vec3 L = normalize(LightDir);\n" + " float maxProd = max(dot(N, L), 0.0);\n" + "" // + " gl_FragColor = v_color * maxProd * u_intensity;\n" // + "}"; ShaderProgram.pedantic = false; ShaderProgram lightShader = new ShaderProgram(vertexShader, fragmentShader); if (!lightShader.isCompiled()) { Gdx.app.log("ERROR", lightShader.getLog()); } lightShader.begin(); lightShader.setUniformi("u_normals", 1); lightShader.setUniformf("u_resolution", Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); lightShader.end(); return lightShader; } private ShaderProgram createNormalShader () { String vertexShader = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + "uniform mat4 u_projTrans;\n" // + "uniform float u_rot;\n" // + "varying vec4 v_color;\n" // + "varying vec2 v_texCoords;\n" // + "varying mat2 v_rot;\n" // + "\n" // + "void main()\n" // + "{\n" // + " vec2 rad = vec2(-sin(u_rot), cos(u_rot));\n" // + " v_rot = mat2(rad.y, -rad.x, rad.x, rad.y);\n" // + " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + " v_color.a = v_color.a * (255.0/254.0);\n" // + " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + "}\n"; String fragmentShader = "#ifdef GL_ES\n" // + "#define LOWP lowp\n" // + "precision mediump float;\n" // + "#else\n" // + "#define LOWP \n" // + "#endif\n" // + "varying LOWP vec4 v_color;\n" // + "varying vec2 v_texCoords;\n" // + "varying mat2 v_rot;\n" // + "uniform sampler2D u_texture;\n" // + "void main()\n"// + "{\n" // + " vec4 normal = texture2D(u_texture, v_texCoords).rgba;\n" // // got to translate normal vector to -1, 1 range + " vec2 rotated = v_rot * (normal.xy * 2.0 - 1.0);\n" // // and back to 0, 1 + " rotated = (rotated.xy / 2.0 + 0.5 );\n" // + " gl_FragColor = vec4(rotated.xy, normal.z, normal.a);\n" // + "}"; ShaderProgram shader = new ShaderProgram(vertexShader, fragmentShader); if (!shader.isCompiled()) throw new IllegalArgumentException("Error compiling shader: " + shader.getLog()); return shader; } boolean drawNormals = false; Color bgColor = new Color(); @Override public void render() { /** Rotate directional light like sun :) */ if (lightsType == 3) { sunDirection += Gdx.graphics.getDeltaTime() * 4f; lights.get(0).setDirection(sunDirection); } camera.update(); boolean stepped = fixedStep(Gdx.graphics.getDeltaTime()); Gdx.gl.glClearColor(1f, 1f, 1f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.setProjectionMatrix(camera.combined); for (DeferredObject deferredObject :assetArray) { deferredObject.update(); } normalFbo.begin(); batch.disableBlending(); batch.begin(); batch.setShader(normalShader); normalShader.setUniformf("u_rot", 0f); float bgWidth = bgN.getWidth() * SCALE; float bgHeight = bgN.getHeight() * SCALE; for (int x = 0; x < 6; x++) { for (int y = 0; y < 6; y++) { batch.draw(bgN, x * bgWidth, y * bgHeight, bgWidth, bgHeight); } } batch.enableBlending(); for (DeferredObject deferredObject :assetArray) { normalShader.setUniformf("u_rot", MathUtils.degreesToRadians * deferredObject.rotation); deferredObject.drawNormal(batch); // flush batch or uniform wont change // TODO this is baaaad, maybe modify SpriteBatch to add rotation in the attributes? Flushing after each defeats the point of batch batch.flush(); } for (int i = 0; i < BALLSNUM; i++) { Body ball = balls.get(i); Vector2 position = ball.getPosition(); float angle = MathUtils.radiansToDegrees * ball.getAngle(); marble.x = position.x - RADIUS; marble.y = position.y - RADIUS; marble.rotation = angle; normalShader.setUniformf("u_rot", MathUtils.degreesToRadians * marble.rotation); marble.drawNormal(batch); // TODO same as above batch.flush(); } batch.end(); normalFbo.end(); Texture normals = normalFbo.getColorBufferTexture(); batch.disableBlending(); batch.begin(); batch.setShader(null); if (drawNormals) { // draw flipped so it looks ok batch.draw(normals, 0, 0, // x, y viewportWidth / 2, viewportHeight / 2, // origx, origy viewportWidth, viewportHeight, // width, height 1, 1, // scale x, y 0,// rotation 0, 0, normals.getWidth(), normals.getHeight(), // tex dimensions false, true); // flip x, y } else { for (int x = 0; x < 6; x++) { for (int y = 0; y < 6; y++) { batch.setColor(bgColor.set(x/5.0f, y/6.0f, 0.5f, 1)); batch.draw(bg, x * bgWidth, y * bgHeight, bgWidth, bgHeight); } } batch.setColor(Color.WHITE); batch.enableBlending(); for (DeferredObject deferredObject :assetArray) { deferredObject.draw(batch); } for (int i = 0; i < BALLSNUM; i++) { Body ball = balls.get(i); Vector2 position = ball.getPosition(); float angle = MathUtils.radiansToDegrees * ball.getAngle(); marble.x = position.x - RADIUS; marble.y = position.y - RADIUS; marble.rotation = angle; marble.draw(batch); } } batch.end(); /** BOX2D LIGHT STUFF BEGIN */ if (!drawNormals) { rayHandler.setCombinedMatrix(camera); if (stepped) rayHandler.update(); normals.bind(1); rayHandler.render(); } /** BOX2D LIGHT STUFF END */ long time = System.nanoTime(); boolean atShadow = rayHandler.pointAtShadow(testPoint.x, testPoint.y); aika += System.nanoTime() - time; /** FONT */ if (showText) { batch.setProjectionMatrix(normalProjection); batch.begin(); font.draw(batch, "F1 - PointLight", 0, Gdx.graphics.getHeight()); font.draw(batch, "F2 - ConeLight", 0, Gdx.graphics.getHeight() - 15); font.draw(batch, "F3 - ChainLight", 0, Gdx.graphics.getHeight() - 30); font.draw(batch, "F4 - DirectionalLight", 0, Gdx.graphics.getHeight() - 45); font.draw(batch, "F5 - random lights colors", 0, Gdx.graphics.getHeight() - 75); font.draw(batch, "F6 - random lights distance", 0, Gdx.graphics.getHeight() - 90); font.draw(batch, "F7 - toggle drawing of normals", 0, Gdx.graphics.getHeight() - 105); font.draw(batch, "F9 - default blending (1.3)", 0, Gdx.graphics.getHeight() - 120); font.draw(batch, "F10 - over-burn blending (default in 1.2)", 0, Gdx.graphics.getHeight() - 135); font.draw(batch, "F11 - some other blending", 0, Gdx.graphics.getHeight() - 150); font.draw(batch, "F12 - toggle help text", 0, Gdx.graphics.getHeight() - 180); font.draw(batch, Integer.toString(Gdx.graphics.getFramesPerSecond()) + "mouse at shadows: " + atShadow + " time used for shadow calculation:" + aika / ++times + "ns" , 0, 20); batch.end(); } } void clearLights() { if (lights.size() > 0) { for (Light light : lights) { light.remove(); } lights.clear(); } groundBody.setActive(true); } void initPointLights() { clearLights(); for (int i = 0; i < BALLSNUM; i++) { PointLight light = new PointLight( rayHandler, RAYS_PER_BALL, null, LIGHT_DISTANCE, 0f, 0f); light.attachToBody(balls.get(i), RADIUS / 2f, RADIUS / 2f); light.setColor( MathUtils.random(), MathUtils.random(), MathUtils.random(), 1f); lights.add(light); } } void initConeLights() { clearLights(); for (int i = 0; i < BALLSNUM; i++) { ConeLight light = new ConeLight( rayHandler, RAYS_PER_BALL, null, LIGHT_DISTANCE, 0, 0, 0f, MathUtils.random(15f, 40f)); light.attachToBody( balls.get(i), RADIUS / 2f, RADIUS / 2f, MathUtils.random(0f, 360f)); light.setColor( MathUtils.random(), MathUtils.random(), MathUtils.random(), 1f); lights.add(light); } } void initChainLights() { clearLights(); for (int i = 0; i < BALLSNUM; i++) { ChainLight light = new ChainLight( rayHandler, RAYS_PER_BALL, null, LIGHT_DISTANCE, 1, new float[]{-5, 0, 0, 3, 5, 0}); light.attachToBody( balls.get(i), MathUtils.random(0f, 360f)); light.setColor( MathUtils.random(), MathUtils.random(), MathUtils.random(), 1f); lights.add(light); } } void initDirectionalLight() { clearLights(); groundBody.setActive(false); sunDirection = MathUtils.random(0f, 360f); DirectionalLight light = new DirectionalLight( rayHandler, 4 * RAYS_PER_BALL, null, sunDirection); lights.add(light); } private final static int MAX_FPS = 30; private final static int MIN_FPS = 15; public final static float TIME_STEP = 1f / MAX_FPS; private final static float MAX_STEPS = 1f + MAX_FPS / MIN_FPS; private final static float MAX_TIME_PER_FRAME = TIME_STEP * MAX_STEPS; private final static int VELOCITY_ITERS = 6; private final static int POSITION_ITERS = 2; float physicsTimeLeft; long aika; int times; private boolean fixedStep(float delta) { physicsTimeLeft += delta; if (physicsTimeLeft > MAX_TIME_PER_FRAME) physicsTimeLeft = MAX_TIME_PER_FRAME; boolean stepped = false; while (physicsTimeLeft >= TIME_STEP) { world.step(TIME_STEP, VELOCITY_ITERS, POSITION_ITERS); physicsTimeLeft -= TIME_STEP; stepped = true; } return stepped; } private void createPhysicsWorld() { world = new World(new Vector2(0, 0), true); float halfWidth = viewportWidth / 2f; ChainShape chainShape = new ChainShape(); chainShape.createLoop(new Vector2[] { new Vector2(0, 0f), new Vector2(viewportWidth, 0f), new Vector2(viewportWidth, viewportHeight), new Vector2(0, viewportHeight) }); BodyDef chainBodyDef = new BodyDef(); chainBodyDef.type = BodyType.StaticBody; groundBody = world.createBody(chainBodyDef); groundBody.createFixture(chainShape, 0); chainShape.dispose(); createBoxes(); } private void createBoxes() { CircleShape ballShape = new CircleShape(); ballShape.setRadius(RADIUS); FixtureDef def = new FixtureDef(); def.restitution = 0.9f; def.friction = 0.01f; def.shape = ballShape; def.density = 1f; BodyDef boxBodyDef = new BodyDef(); boxBodyDef.type = BodyType.DynamicBody; for (int i = 0; i < BALLSNUM; i++) { // Create the BodyDef, set a random position above the // ground and create a new body boxBodyDef.position.x = 1 + (float) (Math.random() * (viewportWidth - 2)); boxBodyDef.position.y = 1 + (float) (Math.random() * (viewportHeight - 2)); Body boxBody = world.createBody(boxBodyDef); boxBody.createFixture(def); boxBody.setFixedRotation(true); balls.add(boxBody); } ballShape.dispose(); } /** * we instantiate this vector and the callback here so we don't irritate the * GC **/ Vector3 testPoint = new Vector3(); QueryCallback callback = new QueryCallback() { @Override public boolean reportFixture(Fixture fixture) { if (fixture.getBody() == groundBody) return true; if (fixture.testPoint(testPoint.x, testPoint.y)) { hitBody = fixture.getBody(); return false; } else return true; } }; @Override public boolean touchDown(int x, int y, int pointer, int newParam) { // translate the mouse coordinates to world coordinates testPoint.set(x, y, 0); camera.unproject(testPoint); // ask the world which bodies are within the given // bounding box around the mouse pointer hitBody = null; world.QueryAABB(callback, testPoint.x - 0.1f, testPoint.y - 0.1f, testPoint.x + 0.1f, testPoint.y + 0.1f); // if we hit something we create a new mouse joint // and attach it to the hit body. if (hitBody != null) { MouseJointDef def = new MouseJointDef(); def.bodyA = groundBody; def.bodyB = hitBody; def.collideConnected = true; def.target.set(testPoint.x, testPoint.y); def.maxForce = 1000.0f * hitBody.getMass(); mouseJoint = (MouseJoint) world.createJoint(def); hitBody.setAwake(true); } return false; } /** another temporary vector **/ Vector2 target = new Vector2(); @Override public boolean touchDragged(int x, int y, int pointer) { camera.unproject(testPoint.set(x, y, 0)); target.set(testPoint.x, testPoint.y); // if a mouse joint exists we simply update // the target of the joint based on the new // mouse coordinates if (mouseJoint != null) { mouseJoint.setTarget(target); } return false; } @Override public boolean touchUp(int x, int y, int pointer, int button) { // if a mouse joint exists we simply destroy it if (mouseJoint != null) { world.destroyJoint(mouseJoint); mouseJoint = null; } return false; } @Override public void dispose() { rayHandler.dispose(); world.dispose(); objectReg.getTexture().dispose(); objectRegN.getTexture().dispose(); normalFbo.dispose(); } /** * Type of lights to use: * 0 - PointLight * 1 - ConeLight * 2 - ChainLight * 3 - DirectionalLight */ int lightsType = 0; @Override public boolean keyDown(int keycode) { switch (keycode) { case Input.Keys.F1: if (lightsType != 0) { initPointLights(); lightsType = 0; } return true; case Input.Keys.F2: if (lightsType != 1) { initConeLights(); lightsType = 1; } return true; case Input.Keys.F3: if (lightsType != 2) { initChainLights(); lightsType = 2; } return true; case Input.Keys.F4: if (lightsType != 3) { initDirectionalLight(); lightsType = 3; } return true; case Input.Keys.F5: for (Light light : lights) light.setColor( MathUtils.random(), MathUtils.random(), MathUtils.random(), 1f); return true; case Input.Keys.F6: for (Light light : lights) light.setDistance(MathUtils.random(LIGHT_DISTANCE * 0.5f, LIGHT_DISTANCE * 2f)); return true; case Input.Keys.F7: drawNormals = !drawNormals; return true; case Input.Keys.F9: rayHandler.diffuseBlendFunc.reset(); return true; case Input.Keys.F10: rayHandler.diffuseBlendFunc.set( GL20.GL_DST_COLOR, GL20.GL_SRC_COLOR); return true; case Input.Keys.F11: rayHandler.diffuseBlendFunc.set( GL20.GL_SRC_COLOR, GL20.GL_DST_COLOR); return true; case Input.Keys.F12: showText = !showText; return true; default: return false; } } @Override public boolean mouseMoved(int x, int y) { testPoint.set(x, y, 0); camera.unproject(testPoint); return false; } @Override public boolean scrolled(int amount) { camera.rotate((float) amount * 3f, 0, 0, 1); return false; } @Override public void pause() { } boolean once = true; @Override public void resize(int width, int height) { viewport.update(width, height, true); } @Override public void resume() { } private static class DeferredObject { TextureRegion diffuse; TextureRegion normal; Color color = new Color(Color.WHITE); float x, y; float width, height; float rotation; boolean rot; public DeferredObject (TextureRegion diffuse, TextureRegion normal) { this.diffuse = diffuse; this.normal = normal; width = diffuse.getRegionWidth() * SCALE; height = diffuse.getRegionHeight() * SCALE; } public void update() { if (rot) { rotation += 1f; if (rotation > 360) rotation = 0; } } public void drawNormal(Batch batch) { batch.draw(normal, x, y, width/2, height/2, width, height, 1, 1, rotation); } public void draw (Batch batch) { batch.setColor(color); batch.draw(diffuse, x, y, width/2, height/2, width, height, 1, 1, rotation); batch.setColor(Color.WHITE); } } }