diff --git a/.gitignore b/.gitignore index 12eb36f..fb3b6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .project .settings/ .idea/ +.gradle/ +build/ libs/ target/ *.class diff --git a/build.gradle b/build.gradle index 025d5df..0f14a4f 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ sourceSets { } } -ext.gdxVersion = "1.9.10" +ext.gdxVersion = "1.9.11" version = "1.6-SNAPSHOT" repositories { diff --git a/src/box2dLight/ChainLight.java b/src/box2dLight/ChainLight.java index 0d09bc7..67086fd 100644 --- a/src/box2dLight/ChainLight.java +++ b/src/box2dLight/ChainLight.java @@ -1,5 +1,6 @@ package box2dLight; +import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Mesh; @@ -115,14 +116,19 @@ public ChainLight(RayHandler rayHandler, int rays, Color color, startY = new float[rays]; this.chain = (chain != null) ? new FloatArray(chain) : new FloatArray(); - + + Mesh.VertexDataType vertexDataType = Mesh.VertexDataType.VertexArray; + if (Gdx.gl30 != null) { + vertexDataType = VertexDataType.VertexBufferObjectWithVAO; + } + lightMesh = new Mesh( - VertexDataType.VertexArray, false, vertexNum, 0, + vertexDataType, false, vertexNum, 0, new VertexAttribute(Usage.Position, 2, "vertex_positions"), new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), new VertexAttribute(Usage.Generic, 1, "s")); softShadowMesh = new Mesh( - VertexDataType.VertexArray, false, vertexNum * 2, + vertexDataType, false, vertexNum * 2, 0, new VertexAttribute(Usage.Position, 2, "vertex_positions"), new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), new VertexAttribute(Usage.Generic, 1, "s")); diff --git a/src/box2dLight/ConeLight.java b/src/box2dLight/ConeLight.java index 7adec16..b84aa09 100644 --- a/src/box2dLight/ConeLight.java +++ b/src/box2dLight/ConeLight.java @@ -45,6 +45,11 @@ public ConeLight(RayHandler rayHandler, int rays, Color color, @Override public void update () { + if (rayHandler.pseudo3d) { + prepareFixtureData(); + updateDynamicShadowMeshes(); + } + updateBody(); if (dirty) setEndPoints(); diff --git a/src/box2dLight/DirectionalLight.java b/src/box2dLight/DirectionalLight.java index 99dd603..c7e64a0 100644 --- a/src/box2dLight/DirectionalLight.java +++ b/src/box2dLight/DirectionalLight.java @@ -1,15 +1,23 @@ - package box2dLight; +import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.Mesh.VertexDataType; import com.badlogic.gdx.graphics.VertexAttribute; import com.badlogic.gdx.graphics.VertexAttributes.Usage; +import com.badlogic.gdx.math.Intersector; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Body; +import com.badlogic.gdx.physics.box2d.ChainShape; +import com.badlogic.gdx.physics.box2d.CircleShape; +import com.badlogic.gdx.physics.box2d.EdgeShape; +import com.badlogic.gdx.physics.box2d.Fixture; +import com.badlogic.gdx.physics.box2d.PolygonShape; +import com.badlogic.gdx.physics.box2d.Shape; +import com.badlogic.gdx.physics.box2d.Shape.Type; /** * Light which source is at infinite distance @@ -19,6 +27,8 @@ * @author kalle_h */ public class DirectionalLight extends Light { + protected boolean flipDirection = false; + Color tmpColor = new Color(); protected final Vector2 start[]; protected final Vector2 end[]; @@ -28,6 +38,13 @@ public class DirectionalLight extends Light { /** The body that could be set as ignored by this light type **/ protected Body body; + /** + * Dynamic shadows variables * + */ + protected final Vector2 lstart = new Vector2(); + protected float xDisp; + protected float yDisp; + /** * Creates directional light which source is at infinite distance, * direction and intensity is same everywhere @@ -56,14 +73,19 @@ public DirectionalLight(RayHandler rayHandler, int rays, Color color, start[i] = new Vector2(); end[i] = new Vector2(); } + + Mesh.VertexDataType vertexDataType = Mesh.VertexDataType.VertexArray; + if (Gdx.gl30 != null) { + vertexDataType = VertexDataType.VertexBufferObjectWithVAO; + } lightMesh = new Mesh( - VertexDataType.VertexArray, staticLight, vertexNum, 0, + vertexDataType, staticLight, vertexNum, 0, new VertexAttribute(Usage.Position, 2, "vertex_positions"), new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), new VertexAttribute(Usage.Generic, 1, "s")); softShadowMesh = new Mesh( - VertexDataType.VertexArray, staticLight, vertexNum, 0, + vertexDataType, staticLight, vertexNum, 0, new VertexAttribute(Usage.Position, 2, "vertex_positions"), new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), new VertexAttribute(Usage.Generic, 1, "s")); @@ -73,6 +95,7 @@ public DirectionalLight(RayHandler rayHandler, int rays, Color color, @Override public void setDirection (float direction) { + if (flipDirection) direction += 180; this.direction = direction; sin = MathUtils.sinDeg(direction); cos = MathUtils.cosDeg(direction); @@ -80,8 +103,21 @@ public void setDirection (float direction) { } @Override - void update () { - if (staticLight && !dirty) return; + void update() { + if (rayHandler.pseudo3d) { + float width = (rayHandler.x2 - rayHandler.x1); + float height = (rayHandler.y2 - rayHandler.y1); + float sizeOfScreen = width > height ? width : height; + xDisp = -sizeOfScreen * cos; + yDisp = -sizeOfScreen * sin; + + prepareFixtureData(); + updateDynamicShadowMeshes(); + } + + if (staticLight && !dirty) { + return; + } dirty = false; final float width = (rayHandler.x2 - rayHandler.x1); @@ -117,7 +153,7 @@ void update () { mx[i] = end[i].x = steppedX + xAxelOffSet; my[i] = end[i].y = steppedY + yAxelOffSet; - if (rayHandler.world != null && !xray) { + if (rayHandler.world != null && !xray && !rayHandler.pseudo3d) { rayHandler.world.rayCast(ray, start[i], end[i]); } } @@ -139,7 +175,9 @@ void update () { } lightMesh.setVertices(segments, 0, size); - if (!soft || xray) return; + if (!soft || xray || rayHandler.pseudo3d) { + return; + } size = 0; for (int i = 0; i < arraySize; i++) { @@ -159,15 +197,227 @@ void update () { @Override void render () { rayHandler.lightRenderedLastFrame++; + rayHandler.simpleBlendFunc.apply(); + lightMesh.render( rayHandler.lightShader, GL20.GL_TRIANGLE_STRIP, 0, vertexNum); - - if (soft && !xray) { + + if (soft && !xray && !rayHandler.pseudo3d) { softShadowMesh.render( - rayHandler.lightShader, GL20.GL_TRIANGLE_STRIP, 0, vertexNum); + rayHandler.lightShader, GL20.GL_TRIANGLE_STRIP, 0, vertexNum); } } - + + protected void prepareFixtureData() { + rayHandler.world.QueryAABB( + dynamicShadowCallback, + rayHandler.x1, rayHandler.y1, + rayHandler.x2, rayHandler.y2); + } + + protected void updateDynamicShadowMeshes() { + int meshInd = 0; + float colBits = rayHandler.ambientLight.toFloatBits(); + //We never clear the affectedFixtures array except the lightsource moves. + //This prevents shadows from disappearing when fixture is out of sight but shadow should be still there + for (Fixture fixture : affectedFixtures) { + LightData data = (LightData) fixture.getUserData(); + if (data == null) { + continue; + } + + Shape fixtureShape = fixture.getShape(); + Type type = fixtureShape.getType(); + Body body = fixture.getBody(); + center.set(body.getWorldCenter()); + lstart.set(center).add(xDisp, yDisp); + + int shadowSize = 0; + float l = data.height / (float) Math.tan(pseudo3dHeight * MathUtils.degRad); + float f = 1f; + + tmpColor.set(Color.BLACK); + float startColBits = rayHandler.shadowColorInterpolation + ? tmpColor.lerp(rayHandler.ambientLight, 1 - f).toFloatBits() + : oneColorBits; + tmpColor.set(Color.WHITE); + float endColBits = rayHandler.shadowColorInterpolation + ? tmpColor.lerp(rayHandler.ambientLight, 1 - f).toFloatBits() + : colBits; + + if (type == Type.Polygon || type == Type.Chain) { + boolean isPolygon = (type == Type.Polygon); + ChainShape cShape = isPolygon + ? null : (ChainShape) fixtureShape; + PolygonShape pShape = isPolygon + ? (PolygonShape) fixtureShape : null; + int vertexCount = isPolygon + ? pShape.getVertexCount() : cShape.getVertexCount(); + int minN = -1; + int maxN = -1; + int minDstN = -1; + float minDst = Float.POSITIVE_INFINITY; + boolean hasGasp = false; + tmpVerts.clear(); + for (int n = 0; n < vertexCount; n++) { + if (isPolygon) { + pShape.getVertex(n, tmpVec); + } else { + cShape.getVertex(n, tmpVec); + } + tmpVec.set(body.getWorldPoint(tmpVec)); + tmpVerts.add(tmpVec.cpy()); + + tmpEnd.set(tmpVec).sub(lstart).limit2(0.0001f).add(tmpVec); + if (fixture.testPoint(tmpEnd)) { + if (minN == -1) { + minN = n; + } + maxN = n; + hasGasp = true; + continue; + } + float currDist = tmpVec.dst2(lstart); + if (currDist < minDst) { + minDst = currDist; + minDstN = n; + } + } + + ind.clear(); + if (!hasGasp) { + tmpVec.set(tmpVerts.get(minDstN)); + for (int n = minDstN; n < vertexCount; n++) { + ind.add(n); + } + for (int n = 0; n < minDstN; n++) { + ind.add(n); + } + if (Intersector.pointLineSide(lstart, center, tmpVec) > 0) { + int z = ind.get(0); + ind.removeIndex(0); + ind.reverse(); + ind.insert(0, z); + } + } else if (minN == 0 && maxN == vertexCount - 1) { + for (int n = maxN - 1; n > minN; n--) { + ind.add(n); + } + } else { + for (int n = minN - 1; n > -1; n--) { + ind.add(n); + } + for (int n = vertexCount - 1; n > maxN; n--) { + ind.add(n); + } + } + + for (int n : ind.toArray()) { + tmpVec.set(tmpVerts.get(n)); + tmpEnd.set(tmpVec).sub(lstart).setLength(l).add(tmpVec); + + segments[shadowSize++] = tmpVec.x; + segments[shadowSize++] = tmpVec.y; + segments[shadowSize++] = startColBits; + segments[shadowSize++] = f; + + segments[shadowSize++] = tmpEnd.x; + segments[shadowSize++] = tmpEnd.y; + segments[shadowSize++] = endColBits; + segments[shadowSize++] = f; + } + if (data.shadow) { + for (int n = 0; n < vertexCount; n++) { + tmpVec.set(tmpVerts.get(n)); + segments[shadowSize++] = tmpVec.x; + segments[shadowSize++] = tmpVec.y; + segments[shadowSize++] = startColBits; + segments[shadowSize++] = f; + if (n == vertexCount - 1) { + tmpVec.set(tmpVerts.get(0)); + segments[shadowSize++] = tmpVec.x; + segments[shadowSize++] = tmpVec.y; + segments[shadowSize++] = startColBits; + segments[shadowSize++] = f; + } + } + } + } else if (type == Type.Circle) { + CircleShape shape = (CircleShape) fixtureShape; + + float r = shape.getRadius(); + float dst = tmpVec.set(center).dst(lstart); + float a = (float) Math.acos(r / dst); + + tmpVec.set(lstart).sub(center).clamp(r, r).rotateRad(a); + tmpStart.set(center).add(tmpVec); + + float angle = (MathUtils.PI2 - 2f * a) + / RayHandler.CIRCLE_APPROX_POINTS; + for (int k = 0; k < RayHandler.CIRCLE_APPROX_POINTS; k++) { + tmpStart.set(center).add(tmpVec); + segments[shadowSize++] = tmpStart.x; + segments[shadowSize++] = tmpStart.y; + segments[shadowSize++] = startColBits; + segments[shadowSize++] = f; + + tmpEnd.set(tmpStart).sub(lstart).setLength(l).add(tmpStart); + segments[shadowSize++] = tmpEnd.x; + segments[shadowSize++] = tmpEnd.y; + segments[shadowSize++] = endColBits; + segments[shadowSize++] = f; + + tmpVec.rotateRad(angle); + } + } else if (type == Type.Edge) { + EdgeShape shape = (EdgeShape) fixtureShape; + + shape.getVertex1(tmpVec); + tmpVec.set(body.getWorldPoint(tmpVec)); + + segments[shadowSize++] = tmpVec.x; + segments[shadowSize++] = tmpVec.y; + segments[shadowSize++] = startColBits; + segments[shadowSize++] = f; + + tmpEnd.set(tmpVec).sub(lstart).setLength(l).add(tmpVec); + segments[shadowSize++] = tmpEnd.x; + segments[shadowSize++] = tmpEnd.y; + segments[shadowSize++] = endColBits; + segments[shadowSize++] = f; + + shape.getVertex2(tmpVec); + tmpVec.set(body.getWorldPoint(tmpVec)); + segments[shadowSize++] = tmpVec.x; + segments[shadowSize++] = tmpVec.y; + segments[shadowSize++] = startColBits; + segments[shadowSize++] = f; + + tmpEnd.set(tmpVec).sub(lstart).setLength(l).add(tmpVec); + segments[shadowSize++] = tmpEnd.x; + segments[shadowSize++] = tmpEnd.y; + segments[shadowSize++] = endColBits; + segments[shadowSize++] = f; + } + + Mesh shadowMesh = null; + if (meshInd >= dynamicShadowMeshes.size) { + shadowMesh = new Mesh( + VertexDataType.VertexArray, false, 128, 0, + new VertexAttribute(Usage.Position, 2, "vertex_positions"), + new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), + new VertexAttribute(Usage.Generic, 1, "s")); + dynamicShadowMeshes.add(shadowMesh); + } else { + shadowMesh = dynamicShadowMeshes.get(meshInd); + } + shadowMesh.setVertices(segments, 0, shadowSize); + meshInd++; + + } + dynamicShadowMeshes.truncate(meshInd); + } + @Override public boolean contains (float x, float y) { boolean oddNodes = false; @@ -191,21 +441,46 @@ public boolean contains (float x, float y) { return oddNodes; } - /** Not applicable for this light type **/ + /** + * Sets the horizontal angle for directional light in degrees + * + *

+ * This could be used to simulate sun cycles * + */ + @Override + public void setHeight(float degrees) { + flipDirection = false; + if (degrees < 0f) pseudo3dHeight = 0f; + else { + degrees = degrees % 360; + if (degrees > 180f) { + pseudo3dHeight = -1f; + } + else if (degrees > 90f) { + pseudo3dHeight = 180f - degrees; + flipDirection = true; + } + else pseudo3dHeight = degrees; + } + } + + /** + * Not applicable for this light type * + */ @Deprecated @Override - public void attachToBody (Body body) { + public void attachToBody(Body body) { } /** Not applicable for this light type **/ @Deprecated @Override - public void setPosition (float x, float y) { + public void setPosition(float x, float y) { } /** Returns the ignored by this light body or {@code null} if not set **/ @Override - public Body getBody () { + public Body getBody() { return body; } @@ -214,7 +489,7 @@ public Body getBody () { **/ @Deprecated @Override - public float getX () { + public float getX() { return 0; } @@ -223,14 +498,14 @@ public float getX () { **/ @Deprecated @Override - public float getY () { + public float getY() { return 0; } /** Not applicable for this light type **/ @Deprecated @Override - public void setPosition (Vector2 position) { + public void setPosition(Vector2 position) { } /** Not applicable for this light type **/ diff --git a/src/box2dLight/Light.java b/src/box2dLight/Light.java index f3de27a..232fc1c 100644 --- a/src/box2dLight/Light.java +++ b/src/box2dLight/Light.java @@ -1,13 +1,17 @@ package box2dLight; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Body; import com.badlogic.gdx.physics.box2d.Filter; import com.badlogic.gdx.physics.box2d.Fixture; +import com.badlogic.gdx.physics.box2d.QueryCallback; import com.badlogic.gdx.physics.box2d.RayCastCallback; +import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.IntArray; /** * Light is data container for all the light parameters. When created lights @@ -22,6 +26,7 @@ public abstract class Light implements Disposable { static final Color DefaultColor = new Color(0.75f, 0.75f, 0.5f, 0.75f); static final float zeroColorBits = Color.toFloatBits(0f, 0f, 0f, 0f); + static final float oneColorBits = Color.toFloatBits(1f, 1f, 1f, 1f); static final int MIN_RAYS = 3; protected final Color color = new Color(); @@ -54,7 +59,26 @@ public abstract class Light implements Disposable { protected float[] f; protected int m_index = 0; - /** + /** + * Dynamic shadows variables * + */ + protected static final LightData tmpData = new LightData(0f); + + protected float pseudo3dHeight = 0f; + + protected final Array dynamicShadowMeshes = new Array(); + //Should never be cleared except when the light changes position (not direction). Prevents shadows from disappearing when fixture is out of sight. + protected final Array affectedFixtures = new Array(); + protected final Array tmpVerts = new Array(); + + protected final IntArray ind = new IntArray(); + + protected final Vector2 tmpStart = new Vector2(); + protected final Vector2 tmpEnd = new Vector2(); + protected final Vector2 tmpVec = new Vector2(); + protected final Vector2 center = new Vector2(); + + /** * Creates new active light and automatically adds it to the specified * {@link RayHandler} instance. * @@ -71,7 +95,7 @@ public abstract class Light implements Disposable { * direction in degrees (if applicable) */ public Light(RayHandler rayHandler, int rays, Color color, - float distance, float directionDegree) { + float distance, float directionDegree) { rayHandler.lightList.add(this); this.rayHandler = rayHandler; setRayNum(rays); @@ -90,7 +114,16 @@ public Light(RayHandler rayHandler, int rays, Color color, * Render this light */ abstract void render(); - + + /** + * Render this light shadow + */ + protected void dynamicShadowRender() { + for (Mesh m : dynamicShadowMeshes) { + m.render(rayHandler.lightShader, GL20.GL_TRIANGLE_STRIP); + } + } + /** * Sets light distance * @@ -230,8 +263,13 @@ public void remove(boolean doDispose) { * Disposes all light resources */ public void dispose() { + affectedFixtures.clear(); lightMesh.dispose(); softShadowMesh.dispose(); + for (Mesh mesh : dynamicShadowMeshes) { + mesh.dispose(); + } + dynamicShadowMeshes.clear(); } /** @@ -353,7 +391,6 @@ public float getDistance() { return distance / RayHandler.gammaCorrectionParameter; } - /** * @return direction in degrees (0 if not applicable) */ @@ -388,7 +425,11 @@ public void setIgnoreAttachedBody(boolean flag) { public boolean getIgnoreAttachedBody() { return ignoreBody; } - + + public void setHeight(float height) { + this.pseudo3dHeight = height; + } + /** * Internal method for mesh update depending on ray number */ @@ -434,7 +475,6 @@ final public float reportRayFixture(Fixture fixture, Vector2 point, // if (fixture.isSensor()) // return -1; - mx[m_index] = point.x; my[m_index] = point.y; f[m_index] = fraction; @@ -508,4 +548,38 @@ static public void setGlobalContactFilter(short categoryBits, short groupIndex, globalFilterA.maskBits = maskBits; } + protected boolean onDynamicCallback(Fixture fixture) { + + if ((globalFilterA != null) && !globalContactFilter(fixture)) { + return false; + } + + if ((filterA != null) && !contactFilter(fixture)) { + return false; + } + + if (ignoreBody && fixture.getBody() == getBody()) { + return false; + } + //We only add the affectedFixtures once + return !affectedFixtures.contains(fixture, true); + } + + final QueryCallback dynamicShadowCallback = new QueryCallback() { + + @Override + public boolean reportFixture(Fixture fixture) { + if (!onDynamicCallback(fixture)) { + return true; + } + affectedFixtures.add(fixture); + if (fixture.getUserData() instanceof LightData) { + LightData data = (LightData) fixture.getUserData(); + data.shadowsDropped++; + } + return true; + } + + }; + } diff --git a/src/box2dLight/LightData.java b/src/box2dLight/LightData.java new file mode 100644 index 0000000..43d52d6 --- /dev/null +++ b/src/box2dLight/LightData.java @@ -0,0 +1,56 @@ +package box2dLight; + +import com.badlogic.gdx.physics.box2d.Fixture; + +public class LightData { + + public Object userData = null; + + public float height; + + public boolean shadow; + + int shadowsDropped = 0; + + public LightData (float h) { + height = h; + } + + public LightData (float h, boolean shadow) { + height = h; + this.shadow = shadow; + } + + public LightData (Object data, float h, boolean shadow) { + height = h; + userData = data; + this.shadow = shadow; + } + + public float getLimit (float distance, float lightHeight, float lightRange) { + float l = 0f; + if (lightHeight > height) { + l = distance * height / (lightHeight - height); + float diff = lightRange - distance; + if (l > diff) { + l = diff; + } + } else if (lightHeight == 0f) { + l = lightRange; + } else { + l = lightRange - distance; + } + + return l > 0 ? l : 0f; + } + + public static Object getUserData (Fixture fixture) { + Object data = fixture.getUserData(); + if (data instanceof LightData) { + return ((LightData) data).userData; + } else { + return data; + } + } + +} diff --git a/src/box2dLight/LightMap.java b/src/box2dLight/LightMap.java index 01206e6..5d4c3bf 100644 --- a/src/box2dLight/LightMap.java +++ b/src/box2dLight/LightMap.java @@ -1,9 +1,6 @@ package box2dLight; -import shaders.DiffuseShader; -import shaders.Gaussian; -import shaders.ShadowShader; -import shaders.WithoutShadowShader; +import shaders.*; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; @@ -27,117 +24,154 @@ class LightMap { private ShaderProgram blurShader; private ShaderProgram diffuseShader; + FrameBuffer shadowBuffer; + boolean lightMapDrawingDisabled; - public void render() { + private final int fboWidth, fboHeight; - boolean needed = rayHandler.lightRenderedLastFrame > 0; + public LightMap(RayHandler rayHandler, int fboWidth, int fboHeight) { + this.rayHandler = rayHandler; + + if (fboWidth <= 0) + fboWidth = 1; + if (fboHeight <= 0) + fboHeight = 1; + + this.fboWidth = fboWidth; + this.fboHeight = fboHeight; + + frameBuffer = new FrameBuffer(Format.RGBA8888, fboWidth, + fboHeight, false); + pingPongBuffer = new FrameBuffer(Format.RGBA8888, fboWidth, + fboHeight, false); + shadowBuffer = new FrameBuffer(Format.RGBA8888, fboWidth, + fboHeight, false); + + lightMapMesh = createLightMapMesh(); + + createShaders(); + } + public void render() { + boolean needed = rayHandler.lightRenderedLastFrame > 0; if (lightMapDrawingDisabled) return; - frameBuffer.getColorBufferTexture().bind(0); + + if (rayHandler.pseudo3d) { + frameBuffer.getColorBufferTexture().bind(1); + shadowBuffer.getColorBufferTexture().bind(0); + } else { + frameBuffer.getColorBufferTexture().bind(0); + } // at last lights are rendered over scene if (rayHandler.shadows) { final Color c = rayHandler.ambientLight; ShaderProgram shader = shadowShader; - if (RayHandler.isDiffuse) { + if (rayHandler.pseudo3d) { + shader.bind(); + if (RayHandler.isDiffuse) { + rayHandler.diffuseBlendFunc.apply(); + shader.setUniformf("ambient", c.r, c.g, c.b, c.a); + } else { + rayHandler.shadowBlendFunc.apply(); + shader.setUniformf("ambient", c.r * c.a, c.g * c.a, + c.b * c.a, 1f - c.a); + } + shader.setUniformi("isDiffuse", RayHandler.isDiffuse ? 1 : 0); + shader.setUniformi("u_texture", 1); + shader.setUniformi("u_shadows", 0); + } else if (RayHandler.isDiffuse) { shader = diffuseShader; - shader.begin(); + shader.bind(); rayHandler.diffuseBlendFunc.apply(); shader.setUniformf("ambient", c.r, c.g, c.b, c.a); } else { - shader.begin(); + shader.bind(); rayHandler.shadowBlendFunc.apply(); shader.setUniformf("ambient", c.r * c.a, c.g * c.a, c.b * c.a, 1f - c.a); } - // shader.setUniformi("u_texture", 0); + lightMapMesh.render(shader, GL20.GL_TRIANGLE_FAN); - shader.end(); } else if (needed) { rayHandler.simpleBlendFunc.apply(); - withoutShadowShader.begin(); - // withoutShadowShader.setUniformi("u_texture", 0); + withoutShadowShader.bind(); + lightMapMesh.render(withoutShadowShader, GL20.GL_TRIANGLE_FAN); - withoutShadowShader.end(); } Gdx.gl20.glDisable(GL20.GL_BLEND); } - public void gaussianBlur() { - + public void gaussianBlur(FrameBuffer buffer, int blurNum) { Gdx.gl20.glDisable(GL20.GL_BLEND); - for (int i = 0; i < rayHandler.blurNum; i++) { - frameBuffer.getColorBufferTexture().bind(0); + for (int i = 0; i < blurNum; i++) { + buffer.getColorBufferTexture().bind(0); // horizontal pingPongBuffer.begin(); { - blurShader.begin(); - // blurShader.setUniformi("u_texture", 0); + blurShader.bind(); blurShader.setUniformf("dir", 1f, 0f); lightMapMesh.render(blurShader, GL20.GL_TRIANGLE_FAN, 0, 4); - blurShader.end(); + } pingPongBuffer.end(); pingPongBuffer.getColorBufferTexture().bind(0); // vertical - frameBuffer.begin(); + buffer.begin(); { - blurShader.begin(); - // blurShader.setUniformi("u_texture", 0); + blurShader.bind(); blurShader.setUniformf("dir", 0f, 1f); lightMapMesh.render(blurShader, GL20.GL_TRIANGLE_FAN, 0, 4); - blurShader.end(); - } if (rayHandler.customViewport) { - frameBuffer.end( + buffer.end( rayHandler.viewportX, rayHandler.viewportY, rayHandler.viewportWidth, rayHandler.viewportHeight); } else { - frameBuffer.end(); + buffer.end(); } } Gdx.gl20.glEnable(GL20.GL_BLEND); } - public LightMap(RayHandler rayHandler, int fboWidth, int fboHeight) { - this.rayHandler = rayHandler; + void dispose() { + disposeShaders(); - if (fboWidth <= 0) - fboWidth = 1; - if (fboHeight <= 0) - fboHeight = 1; - frameBuffer = new FrameBuffer(Format.RGBA8888, fboWidth, - fboHeight, false); - pingPongBuffer = new FrameBuffer(Format.RGBA8888, fboWidth, - fboHeight, false); + lightMapMesh.dispose(); - lightMapMesh = createLightMapMesh(); + frameBuffer.dispose(); + shadowBuffer.dispose(); + pingPongBuffer.dispose(); + } + + void createShaders() { + disposeShaders(); - shadowShader = ShadowShader.createShadowShader(); + shadowShader = rayHandler.pseudo3d ? DynamicShadowShader.createShadowShader() : ShadowShader.createShadowShader(); diffuseShader = DiffuseShader.createShadowShader(); withoutShadowShader = WithoutShadowShader.createShadowShader(); blurShader = Gaussian.createBlurShader(fboWidth, fboHeight); - } - void dispose() { - shadowShader.dispose(); - blurShader.dispose(); - lightMapMesh.dispose(); - frameBuffer.dispose(); - pingPongBuffer.dispose(); - + private void disposeShaders() { + if (shadowShader != null) + shadowShader.dispose(); + if (diffuseShader != null) + diffuseShader.dispose(); + if (withoutShadowShader != null) + withoutShadowShader.dispose(); + if (blurShader != null) + blurShader.dispose(); } private Mesh createLightMapMesh() { @@ -194,5 +228,4 @@ private Mesh createLightMapMesh() { static public final int Y4 = 13; static public final int U4 = 14; static public final int V4 = 15; - } diff --git a/src/box2dLight/PointLight.java b/src/box2dLight/PointLight.java index e8c5da0..abc403f 100644 --- a/src/box2dLight/PointLight.java +++ b/src/box2dLight/PointLight.java @@ -1,7 +1,12 @@ package box2dLight; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Mesh; +import com.badlogic.gdx.graphics.VertexAttribute; +import com.badlogic.gdx.graphics.VertexAttributes; +import com.badlogic.gdx.math.Intersector; import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.physics.box2d.*; /** * Light shaped as a circle with given radius @@ -50,6 +55,11 @@ public PointLight(RayHandler rayHandler, int rays, Color color, @Override public void update () { + if (rayHandler.pseudo3d) { + prepareFixtureData(); + updateDynamicShadowMeshes(); + } + updateBody(); if (dirty) setEndPoints(); @@ -59,7 +69,7 @@ public void update () { dirty = false; updateMesh(); } - + /** * Sets light distance * diff --git a/src/box2dLight/PositionalLight.java b/src/box2dLight/PositionalLight.java index 1a95c99..8bb4da3 100644 --- a/src/box2dLight/PositionalLight.java +++ b/src/box2dLight/PositionalLight.java @@ -1,15 +1,14 @@ package box2dLight; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.Mesh; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.*; import com.badlogic.gdx.graphics.Mesh.VertexDataType; -import com.badlogic.gdx.graphics.VertexAttribute; import com.badlogic.gdx.graphics.VertexAttributes.Usage; +import com.badlogic.gdx.math.Intersector; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.physics.box2d.Body; +import com.badlogic.gdx.physics.box2d.*; /** * Abstract base class for all positional lights @@ -20,6 +19,8 @@ */ public abstract class PositionalLight extends Light { + Color tmpColor = new Color(); + protected final Vector2 tmpEnd = new Vector2(); protected final Vector2 start = new Vector2(); @@ -59,10 +60,14 @@ public PositionalLight(RayHandler rayHandler, int rays, Color color, float dista start.x = x; start.y = y; - lightMesh = new Mesh(VertexDataType.VertexArray, false, vertexNum, 0, new VertexAttribute(Usage.Position, 2, + Mesh.VertexDataType vertexDataType = Mesh.VertexDataType.VertexArray; + if (Gdx.gl30 != null) { + vertexDataType = VertexDataType.VertexBufferObjectWithVAO; + } + lightMesh = new Mesh(vertexDataType, false, vertexNum, 0, new VertexAttribute(Usage.Position, 2, "vertex_positions"), new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), new VertexAttribute(Usage.Generic, 1, "s")); - softShadowMesh = new Mesh(VertexDataType.VertexArray, false, vertexNum * 2, 0, new VertexAttribute(Usage.Position, 2, + softShadowMesh = new Mesh(vertexDataType, false, vertexNum * 2, 0, new VertexAttribute(Usage.Position, 2, "vertex_positions"), new VertexAttribute(Usage.ColorPacked, 4, "quad_colors"), new VertexAttribute(Usage.Generic, 1, "s")); setMesh(); @@ -84,10 +89,9 @@ void render() { if (rayHandler.culling && culled) return; rayHandler.lightRenderedLastFrame++; - lightMesh.render( - rayHandler.lightShader, GL20.GL_TRIANGLE_FAN, 0, vertexNum); - - if (soft && !xray) { + lightMesh.render(rayHandler.lightShader, GL20.GL_TRIANGLE_FAN, 0, vertexNum); + + if (soft && !xray && !rayHandler.pseudo3d) { softShadowMesh.render( rayHandler.lightShader, GL20.GL_TRIANGLE_STRIP, @@ -177,6 +181,10 @@ public void setPosition(Vector2 position) { if (staticLight) dirty = true; } + public boolean contains(Vector2 pos) { + return contains(pos.x, pos.y); + } + @Override public boolean contains(float x, float y) { // fast fail @@ -238,13 +246,20 @@ protected void updateMesh() { mx[i] = tmpEnd.x; tmpEnd.y = endY[i] + start.y; my[i] = tmpEnd.y; - if (rayHandler.world != null && !xray) { + if (rayHandler.world != null && !xray && !rayHandler.pseudo3d) { rayHandler.world.rayCast(ray, start, tmpEnd); } } setMesh(); } + protected void prepareFixtureData() { + rayHandler.world.QueryAABB( + dynamicShadowCallback, + start.x - distance, start.y - distance, + start.x + distance, start.y + distance); + } + protected void setMesh() { // ray starting point int size = 0; @@ -262,7 +277,7 @@ protected void setMesh() { } lightMesh.setVertices(segments, 0, size); - if (!soft || xray) return; + if (!soft || xray || rayHandler.pseudo3d) return; size = 0; // rays ending points. @@ -280,6 +295,246 @@ protected void setMesh() { softShadowMesh.setVertices(segments, 0, size); } + protected void updateDynamicShadowMeshes() { + int meshInd = 0; + float colBits = rayHandler.ambientLight.toFloatBits(); + for (Fixture fixture : affectedFixtures) { + LightData data = (LightData)fixture.getUserData(); + if (data == null || fixture.isSensor()) continue; + + int size = 0; + float l; + + Shape fixtureShape = fixture.getShape(); + Shape.Type type = fixtureShape.getType(); + Body body = fixture.getBody(); + center.set(body.getWorldCenter()); + + if (type == Shape.Type.Polygon || type == Shape.Type.Chain) { + boolean isPolygon = (type == Shape.Type.Polygon); + ChainShape cShape = isPolygon ? + null : (ChainShape)fixtureShape; + PolygonShape pShape = isPolygon ? + (PolygonShape)fixtureShape : null; + int vertexCount = isPolygon ? + pShape.getVertexCount() : cShape.getVertexCount(); + int minN = -1; + int maxN = -1; + int minDstN = -1; + float minDst = Float.POSITIVE_INFINITY; + boolean hasGasp = false; + tmpVerts.clear(); + for (int n = 0; n < vertexCount; n++) { + if (isPolygon) { + pShape.getVertex(n, tmpVec); + } else { + cShape.getVertex(n, tmpVec); + } + tmpVec.set(body.getWorldPoint(tmpVec)); + tmpVerts.add(tmpVec.cpy()); + tmpEnd.set(tmpVec).sub(start).limit2(0.0001f).add(tmpVec); + if (fixture.testPoint(tmpEnd)) { + if (minN == -1) minN = n; + maxN = n; + hasGasp = true; + continue; + } + + float currDist = tmpVec.dst2(start); + if (currDist < minDst) { + minDst = currDist; + minDstN = n; + } + } + + ind.clear(); + if (!hasGasp) { + tmpVec.set(tmpVerts.get(minDstN)); + for (int n = minDstN; n < vertexCount; n++) { + ind.add(n); + } + for (int n = 0; n < minDstN; n++) { + ind.add(n); + } + if (Intersector.pointLineSide(start, center, tmpVec) > 0) { + ind.reverse(); + ind.insert(0, ind.pop()); + } + } else if (minN == 0 && maxN == vertexCount - 1) { + for (int n = maxN - 1; n > minN; n--) { + ind.add(n); + } + } else { + for (int n = minN - 1; n > -1; n--) { + ind.add(n); + } + for (int n = vertexCount - 1; n > maxN ; n--) { + ind.add(n); + } + } + + boolean contained = false; + for (int n : ind.toArray()) { + tmpVec.set(tmpVerts.get(n)); + if (contains(tmpVec.x, tmpVec.y)){ + contained = true; + break; + } + } + + if (!contained) + continue; + + for (int n : ind.toArray()) { + tmpVec.set(tmpVerts.get(n)); + + float dst = tmpVec.dst(start); + l = data.getLimit(dst, pseudo3dHeight, distance); + tmpEnd.set(tmpVec).sub(start).setLength(l).add(tmpVec); + + float f1 = 1f - dst / distance; + float f2 = 1f - (dst + l) / distance; + + tmpColor.set(Color.BLACK); + float startColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f1).toFloatBits() : + oneColorBits; + tmpColor.set(Color.WHITE); + float endColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f2).toFloatBits() : + colBits; + + segments[size++] = tmpVec.x; + segments[size++] = tmpVec.y; + segments[size++] = startColBits; + segments[size++] = f1; + + segments[size++] = tmpEnd.x; + segments[size++] = tmpEnd.y; + segments[size++] = endColBits; + segments[size++] = f2; + } + } else if (type == Shape.Type.Circle) { + CircleShape shape = (CircleShape)fixtureShape; + float r = shape.getRadius(); + if (!contains(tmpVec.set(center).add(r, r)) && !contains(tmpVec.set(center).add(-r, -r)) + && !contains(tmpVec.set(center).add(r, -r)) && !contains(tmpVec.set(center).add(-r, r))) { + continue; + } + + float dst = tmpVec.set(center).dst(start); + float a = (float) Math.acos(r/dst); + l = data.getLimit(dst, pseudo3dHeight, distance); + float f1 = 1f - dst / distance; + float f2 = 1f - (dst + l) / distance; + tmpColor.set(Color.BLACK); + float startColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f1).toFloatBits() : + oneColorBits; + tmpColor.set(Color.WHITE); + float endColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f2).toFloatBits() : + colBits; + + tmpVec.set(start).sub(center).clamp(r, r).rotateRad(a); + tmpStart.set(center).add(tmpVec); + + float angle = (MathUtils.PI2 - 2f * a) / + RayHandler.CIRCLE_APPROX_POINTS; + for (int k = 0; k < RayHandler.CIRCLE_APPROX_POINTS; k++) { + tmpStart.set(center).add(tmpVec); + segments[size++] = tmpStart.x; + segments[size++] = tmpStart.y; + segments[size++] = startColBits; + segments[size++] = f1; + + tmpEnd.set(tmpStart).sub(start).setLength(l).add(tmpStart); + segments[size++] = tmpEnd.x; + segments[size++] = tmpEnd.y; + segments[size++] = endColBits; + segments[size++] = f2; + + tmpVec.rotateRad(angle); + } + } else if (type == Shape.Type.Edge) { + EdgeShape shape = (EdgeShape)fixtureShape; + + shape.getVertex1(tmpVec); + tmpVec.set(body.getWorldPoint(tmpVec)); + if (!contains(tmpVec)) { + continue; + } + float dst = tmpVec.dst(start); + l = data.getLimit(dst, pseudo3dHeight, distance); + float f1 = 1f - dst / distance; + float f2 = 1f - (dst + l) / distance; + tmpColor.set(Color.BLACK); + float startColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f1).toFloatBits() : + oneColorBits; + tmpColor.set(Color.WHITE); + float endColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f2).toFloatBits() : + colBits; + + segments[size++] = tmpVec.x; + segments[size++] = tmpVec.y; + segments[size++] = startColBits; + segments[size++] = f1; + + tmpEnd.set(tmpVec).sub(start).setLength(l).add(tmpVec); + segments[size++] = tmpEnd.x; + segments[size++] = tmpEnd.y; + segments[size++] = endColBits; + segments[size++] = f2; + + shape.getVertex2(tmpVec); + tmpVec.set(body.getWorldPoint(tmpVec)); + if (!contains(tmpVec)) { + continue; + } + dst = tmpVec.dst(start); + l = data.getLimit(dst, pseudo3dHeight, distance); + f1 = 1f - dst / distance; + f2 = 1f - (dst + l) / distance; + tmpColor.set(Color.BLACK); + startColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f1).toFloatBits() : + oneColorBits; + tmpColor.set(Color.WHITE); + endColBits = rayHandler.shadowColorInterpolation ? + tmpColor.lerp(rayHandler.ambientLight, f2).toFloatBits() : + colBits; + + segments[size++] = tmpVec.x; + segments[size++] = tmpVec.y; + segments[size++] = startColBits; + segments[size++] = f1; + + tmpEnd.set(tmpVec).sub(start).setLength(l).add(tmpVec); + segments[size++] = tmpEnd.x; + segments[size++] = tmpEnd.y; + segments[size++] = endColBits; + segments[size++] = f2; + } + + Mesh mesh = null; + if (meshInd >= dynamicShadowMeshes.size) { + mesh = new Mesh( + Mesh.VertexDataType.VertexArray, false, RayHandler.MAX_SHADOW_VERTICES, 0, + new VertexAttribute(VertexAttributes.Usage.Position, 2, "vertex_positions"), + new VertexAttribute(VertexAttributes.Usage.ColorPacked, 4, "quad_colors"), + new VertexAttribute(VertexAttributes.Usage.Generic, 1, "s")); + dynamicShadowMeshes.add(mesh); + } else { + mesh = dynamicShadowMeshes.get(meshInd); + } + mesh.setVertices(segments, 0, size); + meshInd++; + } + dynamicShadowMeshes.truncate(meshInd); + } + public float getBodyOffsetX() { return bodyOffsetX; } diff --git a/src/box2dLight/RayHandler.java b/src/box2dLight/RayHandler.java index f7aeea1..3de93d8 100644 --- a/src/box2dLight/RayHandler.java +++ b/src/box2dLight/RayHandler.java @@ -30,11 +30,18 @@ public class RayHandler implements Disposable { static boolean gammaCorrection = false; static float gammaCorrectionParameter = 1f; - /** if this is public why we have a setter? - * TODO: remove public modifier and add getter - * */ - public static boolean isDiffuse = false; + /** + * TODO: This could be made adaptive to ratio of camera sizes * zoom vs the + * CircleShape radius - thus will provide smooth radial shadows while + * resizing and zooming in and out + */ + static int CIRCLE_APPROX_POINTS = 32; + + static float dynamicShadowColorReduction = 1; + + static int MAX_SHADOW_VERTICES = 64; + static boolean isDiffuse = false; /** * Blend function for lights rendering with both shadows and diffusion *

Default: (GL20.GL_DST_COLOR, GL20.GL_ZERO) @@ -80,7 +87,11 @@ public class RayHandler implements Disposable { boolean culling = true; boolean shadows = true; boolean blur = true; - + + /** Experimental mode */ + boolean pseudo3d = false; + boolean shadowColorInterpolation = false; + int blurNum = 1; boolean customViewport = false; @@ -113,11 +124,16 @@ public class RayHandler implements Disposable { *

  • ambientLight = 0f * * - * @see #RayHandler(World, int, int) + * @see #RayHandler(World, int, int, RayHandlerOptions) */ public RayHandler(World world) { this(world, Gdx.graphics.getWidth() / 4, Gdx.graphics - .getHeight() / 4); + .getHeight() / 4, null); + } + + public RayHandler(World world, RayHandlerOptions options) { + this(world, Gdx.graphics.getWidth() / 4, Gdx.graphics + .getHeight() / 4, options); } /** @@ -127,8 +143,19 @@ public RayHandler(World world) { * @see #RayHandler(World) */ public RayHandler(World world, int fboWidth, int fboHeight) { + this(world, fboWidth, fboHeight, null); + } + + public RayHandler(World world, int fboWidth, int fboHeight, RayHandlerOptions options) { this.world = world; + if (options != null) { + isDiffuse = options.isDiffuse; + gammaCorrection = options.gammaCorrection; + pseudo3d = options.pseudo3d; + shadowColorInterpolation = options.shadowColorInterpolation; + } + resizeFBO(fboWidth, fboHeight); lightShader = LightShader.createLightShader(); } @@ -300,7 +327,6 @@ public void prepareRender() { Gdx.gl.glDepthMask(false); Gdx.gl.glEnable(GL20.GL_BLEND); - simpleBlendFunc.apply(); boolean useLightMap = (shadows || blur); if (useLightMap) { @@ -309,17 +335,20 @@ public void prepareRender() { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); } + simpleBlendFunc.apply(); + ShaderProgram shader = customLightShader != null ? customLightShader : lightShader; - shader.begin(); + shader.bind(); { + lightShader.setUniformMatrix("u_projTrans", combined); shader.setUniformMatrix("u_projTrans", combined); if (customLightShader != null) updateLightShader(); + for (Light light : lightList) { if (customLightShader != null) updateLightShaderPerLight(light); light.render(); } } - shader.end(); if (useLightMap) { if (customViewport) { @@ -331,12 +360,34 @@ public void prepareRender() { } else { lightMap.frameBuffer.end(); } + } + + if (useLightMap && pseudo3d) { + lightMap.shadowBuffer.begin(); + Gdx.gl.glClearColor(0f, 0f, 0f, 0f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + + for (Light light : lightList) { + light.dynamicShadowRender(); + } - boolean needed = lightRenderedLastFrame > 0; - // this way lot less binding - if (needed && blur) - lightMap.gaussianBlur(); + if (customViewport) { + lightMap.shadowBuffer.end( + viewportX, + viewportY, + viewportWidth, + viewportHeight); + } else { + lightMap.shadowBuffer.end(); + } } + + boolean needed = lightRenderedLastFrame > 0; + // this way lot less binding + if (needed && blur) + lightMap.gaussianBlur(lightMap.frameBuffer, blurNum); + if (needed && blur && pseudo3d) + lightMap.gaussianBlur(lightMap.shadowBuffer, blurNum); } /** @@ -571,9 +622,10 @@ public static boolean getGammaCorrection() { *

    NOTE: To match the visuals with gamma uncorrected lights the light * distance parameters is modified implicitly. */ - public static void setGammaCorrection(boolean gammaCorrectionWanted) { + public void applyGammaCorrection(boolean gammaCorrectionWanted) { gammaCorrection = gammaCorrectionWanted; gammaCorrectionParameter = gammaCorrection ? GAMMA_COR : 1f; + lightMap.createShaders(); } /** @@ -583,10 +635,35 @@ public static void setGammaCorrection(boolean gammaCorrectionWanted) { * more realistic model than normally used as it preserve colors but might * look bit darker and also it might improve performance slightly. */ - public static void useDiffuseLight(boolean useDiffuse) { + public void setDiffuseLight(boolean useDiffuse) { isDiffuse = useDiffuse; + lightMap.createShaders(); } - + + public static boolean isDiffuseLight() { + return isDiffuse; + } + + public static float getDynamicShadowColorReduction () { + return dynamicShadowColorReduction; + } + + /** + * Static setters are deprecated, use {@link RayHandlerOptions} + */ + @Deprecated + public static void useDiffuseLight(boolean useDiffuse) { + + } + + /** + * Static setters are deprecated, use {@link RayHandlerOptions} + */ + @Deprecated + public static void setGammaCorrection(boolean gammaCorrectionWanted) { + + } + /** * Sets rendering to custom viewport with specified position and size *

    Note: you will be responsible for update of viewport via this method @@ -609,6 +686,28 @@ public void useDefaultViewport() { customViewport = false; } + /** + * /!\ Experimental mode with dynamic shadowing in pseudo-3d world + * + * @param flag enable pseudo 3d effect + */ + public void setPseudo3dLight(boolean flag) { + setPseudo3dLight(flag, false); + } + + /** + * /!\ Experimental mode with dynamic shadowing in pseudo-3d world + * + * @param flag enable pseudo 3d effect + * @param interpolateShadows interpolate shadow color + */ + public void setPseudo3dLight(boolean flag, boolean interpolateShadows) { + pseudo3d = flag; + shadowColorInterpolation = interpolateShadows; + + lightMap.createShaders(); + } + /** * Enables/disables lightMap automatic rendering. * @@ -641,5 +740,4 @@ public Texture getLightMapTexture() { public FrameBuffer getLightMapBuffer() { return lightMap.frameBuffer; } - } diff --git a/src/box2dLight/RayHandlerOptions.java b/src/box2dLight/RayHandlerOptions.java new file mode 100644 index 0000000..1abe7dd --- /dev/null +++ b/src/box2dLight/RayHandlerOptions.java @@ -0,0 +1,26 @@ +package box2dLight; + +public class RayHandlerOptions { + boolean gammaCorrection = false; + boolean isDiffuse = false; + + boolean pseudo3d = false; + boolean shadowColorInterpolation = false; + + public void setDiffuse (boolean diffuse) { + isDiffuse = diffuse; + } + + public void setGammaCorrection (boolean gammaCorrection) { + this.gammaCorrection = gammaCorrection; + } + + public void setPseudo3d (boolean pseudo3d) { + setPseudo3d(pseudo3d, false); + } + + public void setPseudo3d (boolean pseudo3d, boolean shadowColorInterpolation) { + this.pseudo3d = pseudo3d; + this.shadowColorInterpolation = shadowColorInterpolation; + } +} diff --git a/src/shaders/DiffuseShader.java b/src/shaders/DiffuseShader.java index ed0895e..92ec9e3 100644 --- a/src/shaders/DiffuseShader.java +++ b/src/shaders/DiffuseShader.java @@ -15,7 +15,7 @@ static final public ShaderProgram createShadowShader() { + " gl_Position = a_position;\n" // + "}\n"; - // this is allways perfect precision + // this is always perfect precision final String fragmentShader = "#ifdef GL_ES\n" // + "precision lowp float;\n" // + "#define MED mediump\n" diff --git a/src/shaders/DynamicShadowShader.java b/src/shaders/DynamicShadowShader.java new file mode 100644 index 0000000..ec6029a --- /dev/null +++ b/src/shaders/DynamicShadowShader.java @@ -0,0 +1,62 @@ +package shaders; + +import box2dLight.RayHandler; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; + +public class DynamicShadowShader { + static final public ShaderProgram createShadowShader() { + final String vertexShader = "attribute vec4 a_position;\n" // + + "attribute vec2 a_texCoord;\n" // + + "varying vec2 v_texCoords;\n" // + + "\n" // + + "void main()\n" // + + "{\n" // + + " v_texCoords = a_texCoord;\n" // + + " gl_Position = a_position;\n" // + + "}\n"; + + // this is always perfect precision + final String fragmentShader = "#ifdef GL_ES\n" // + + "precision lowp float;\n" // + + "#define MED mediump\n" + + "#else\n" + + "#define MED \n" + + "#endif\n" // + + "varying MED vec2 v_texCoords;\n" // + + "uniform sampler2D u_texture;\n" // + + "uniform sampler2D u_shadows;\n" // + + "uniform vec4 ambient;\n" + + "uniform int isDiffuse;\n" // + + "void main()\n"// + + "{\n" // + + "if(isDiffuse == 0)\n"// + + "{\n"// + + "vec4 c = texture2D(u_texture, v_texCoords);\n"// + + "vec4 sh = texture2D(u_shadows, v_texCoords);\n"// + + "gl_FragColor.rgb = (ambient.rgb + c.rgb * c.a) - sh.rgb;\n"// + + "gl_FragColor.a = ambient.a - c.a;\n"// + + "}\n"// + + "else\n"// + + "{\n"// + + " vec4 c = texture2D(u_texture, v_texCoords);\n"// + + " vec4 sh = texture2D(u_shadows, v_texCoords);\n"// + + " gl_FragColor.rgb = (ambient.rgb + (" + RayHandler.getDynamicShadowColorReduction() + " * c.rgb)) - sh.rgb;\n" + + " gl_FragColor.a = 1.0;\n"// + + "}\n"// + + "}\n"; + ShaderProgram.pedantic = false; + ShaderProgram shadowShader = new ShaderProgram(vertexShader, + fragmentShader); + if (!shadowShader.isCompiled()) { + shadowShader = new ShaderProgram("#version 330 core\n" +vertexShader, + "#version 330 core\n" +fragmentShader); + if(!shadowShader.isCompiled()){ + Gdx.app.log("ERROR", shadowShader.getLog()); + } + } + + return shadowShader; + } + +} diff --git a/src/shaders/Gaussian.java b/src/shaders/Gaussian.java index 119752d..c822509 100644 --- a/src/shaders/Gaussian.java +++ b/src/shaders/Gaussian.java @@ -10,7 +10,7 @@ public class Gaussian { public static ShaderProgram createBlurShader(int width, int heigth) { final String FBO_W = Integer.toString(width); final String FBO_H = Integer.toString(heigth); - final String rgb = RayHandler.isDiffuse ? ".rgb" : ""; + final String rgb = RayHandler.isDiffuseLight() ? ".rgb" : ""; final String vertexShader = "attribute vec4 a_position;\n" // + "uniform vec2 dir;\n" // + "attribute vec2 a_texCoord;\n" // diff --git a/test/tests/Box2dLightCustomShaderTest.java b/test/tests/Box2dLightCustomShaderTest.java index be32ae6..acbbc02 100644 --- a/test/tests/Box2dLightCustomShaderTest.java +++ b/test/tests/Box2dLightCustomShaderTest.java @@ -110,13 +110,13 @@ public void create() { 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()) { + RayHandlerOptions options = new RayHandlerOptions(); + options.setDiffuse(true); + options.setGammaCorrection(true); + rayHandler = new RayHandler(world, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), options) { @Override protected void updateLightShader () {} @Override protected void updateLightShaderPerLight (Light light) { diff --git a/test/tests/Box2dLightTest.java b/test/tests/Box2dLightTest.java index 2c82ad0..e67a538 100644 --- a/test/tests/Box2dLightTest.java +++ b/test/tests/Box2dLightTest.java @@ -2,12 +2,7 @@ import java.util.ArrayList; -import box2dLight.ChainLight; -import box2dLight.ConeLight; -import box2dLight.DirectionalLight; -import box2dLight.Light; -import box2dLight.PointLight; -import box2dLight.RayHandler; +import box2dLight.*; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; @@ -104,10 +99,10 @@ public void create() { 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); /** BOX2D LIGHT STUFF BEGIN */ - RayHandler.setGammaCorrection(true); - RayHandler.useDiffuseLight(true); - - rayHandler = new RayHandler(world); + RayHandlerOptions options = new RayHandlerOptions(); + options.setDiffuse(true); + options.setGammaCorrection(true); + rayHandler = new RayHandler(world, options); rayHandler.setAmbientLight(0f, 0f, 0f, 0.5f); rayHandler.setBlurNum(3); diff --git a/test/tests/Box2dLightTest2.java b/test/tests/Box2dLightTest2.java new file mode 100644 index 0000000..d39b091 --- /dev/null +++ b/test/tests/Box2dLightTest2.java @@ -0,0 +1,542 @@ +package tests; + +import java.util.ArrayList; + +import box2dLight.*; +import com.badlogic.gdx.Application; + +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.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.OrthographicCamera; +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.g2d.TextureRegion; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.physics.box2d.Body; +import com.badlogic.gdx.physics.box2d.BodyDef; +import com.badlogic.gdx.physics.box2d.BodyDef.BodyType; +import com.badlogic.gdx.physics.box2d.ChainShape; +import com.badlogic.gdx.physics.box2d.Fixture; +import com.badlogic.gdx.physics.box2d.FixtureDef; +import com.badlogic.gdx.physics.box2d.PolygonShape; +import com.badlogic.gdx.physics.box2d.QueryCallback; +import com.badlogic.gdx.physics.box2d.World; +import com.badlogic.gdx.physics.box2d.joints.MouseJoint; +import com.badlogic.gdx.physics.box2d.joints.MouseJointDef; + +public class Box2dLightTest2 extends InputAdapter implements ApplicationListener { + + static final int RAYS_PER_BALL = 128; + static final int BALLSNUM = 5; + static final float LIGHT_DISTANCE = 16f; + static final float RADIUS = 1f; + + static final float viewportWidth = 48; + static final float viewportHeight = 32; + + OrthographicCamera camera; + + SpriteBatch batch; + BitmapFont font; + TextureRegion textureRegion; + Texture bg; + + /** our box2D world **/ + World world; + + /** our boxes **/ + ArrayList balls = new ArrayList(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 lights = new ArrayList(BALLSNUM); + + float sunDirection = -90f; + + @Override + public void create() { + Gdx.app.setLogLevel(Application.LOG_DEBUG); + MathUtils.random.setSeed(Long.MIN_VALUE); + + camera = new OrthographicCamera(viewportWidth, viewportHeight); + camera.position.set(0, viewportHeight / 2f, 0); + camera.update(); + + batch = new SpriteBatch(); + font = new BitmapFont(); + font.setColor(Color.RED); + + textureRegion = new TextureRegion(new Texture( + Gdx.files.internal("test/data/marble.png"))); + bg = new Texture(Gdx.files.internal("test/data/bg.png")); + + createPhysicsWorld(); + Gdx.input.setInputProcessor(this); + + normalProjection.setToOrtho2D( + 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + + RayHandlerOptions options = new RayHandlerOptions(); + options.setDiffuse(true); + options.setGammaCorrection(true); + options.setPseudo3d(true); + /** BOX2D LIGHT STUFF BEGIN */ + rayHandler = new RayHandler(world, options); + //rayHandler.setAmbientLight(0f, 0f, 0f, 0.5f); + //rayHandler.setBlurNum(3); + rayHandler.setShadows(true); + initPointLights(); + /** BOX2D LIGHT STUFF END */ + + } + + @Override + public void render() { + + /** Rotate directional light like sun :) */ + if (lightsType == 3) { + sunDirection += Gdx.graphics.getDeltaTime() * 8f; + float degrees = (sunDirection % 360); + lights.get(0).setDirection(degrees); + lights.get(0).setHeight(degrees); + Gdx.app.debug("Degrees:", degrees+""); + } + + camera.update(); + + boolean stepped = fixedStep(Gdx.graphics.getDeltaTime()); + Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BITS | GL20.GL_BLEND_SRC_ALPHA); + + batch.setProjectionMatrix(camera.combined); + batch.disableBlending(); + batch.begin(); + { + batch.draw(bg, -viewportWidth / 2f, 0, viewportWidth, viewportHeight); + batch.enableBlending(); + for (int i = 0; i < BALLSNUM; i++) { + Body ball = balls.get(i); + Vector2 position = ball.getPosition(); + float angle = MathUtils.radiansToDegrees * ball.getAngle(); + batch.draw( + textureRegion, + position.x - RADIUS, position.y - RADIUS, + RADIUS, RADIUS, + RADIUS * 2, RADIUS * 2, + 1f, 1f, + angle); + } + } + batch.end(); + + /** BOX2D LIGHT STUFF BEGIN */ + rayHandler.setCombinedMatrix(camera); + + if (stepped) rayHandler.update(); + 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, + "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); + light.setHeight(i+2); + 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, new Color(1, 1, 1, 0.5f), sunDirection); + light.setHeight(0); + 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(-halfWidth, 0f), + new Vector2(halfWidth, 0f), + new Vector2(halfWidth, viewportHeight), + new Vector2(-halfWidth, viewportHeight) }); + BodyDef chainBodyDef = new BodyDef(); + chainBodyDef.type = BodyType.StaticBody; + groundBody = world.createBody(chainBodyDef); + groundBody.createFixture(chainShape, 0); + chainShape.dispose(); + createBoxes(); + } + + private void createBoxes() { + PolygonShape polygonShape = new PolygonShape(); + polygonShape.setAsBox(RADIUS, RADIUS); + FixtureDef def = new FixtureDef(); + def.restitution = 0.9f; + def.friction = 0.01f; + def.shape = polygonShape; + 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 = -20 + (float) (Math.random() * 40); + boxBodyDef.position.y = 10 + (float) (Math.random() * 15); + Body boxBody = world.createBody(boxBodyDef); + boxBody.createFixture(def).setUserData(new LightData(1,true)); + balls.add(boxBody); + } + polygonShape.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(); + } + + /** + * 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.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() { + } + + @Override + public void resize(int arg0, int arg1) { + } + + @Override + public void resume() { + } + +} diff --git a/test/tests/DesktopLauncher.java b/test/tests/DesktopLauncher.java index 3f22486..a2cd60b 100644 --- a/test/tests/DesktopLauncher.java +++ b/test/tests/DesktopLauncher.java @@ -15,7 +15,7 @@ public static void main(String[] argv) { config.vSyncEnabled = true; config.fullscreen = false; - new LwjglApplication(new Box2dLightTest(), config); + new LwjglApplication(new Box2dLightTest2(), config); } }