diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/VertexEffectTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/VertexEffectTest.java new file mode 100644 index 000000000..7b8984dc0 --- /dev/null +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/VertexEffectTest.java @@ -0,0 +1,126 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF + * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +package com.esotericsoftware.spine; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.backends.lwjgl.LwjglApplication; +import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.math.Interpolation; +import com.esotericsoftware.spine.vertexeffects.JitterEffect; +import com.esotericsoftware.spine.vertexeffects.SwirlEffect; + +public class VertexEffectTest extends ApplicationAdapter { + OrthographicCamera camera; + PolygonSpriteBatch batch; + SkeletonRenderer renderer; + + SwirlEffect swirl; + float swirlTime; + + TextureAtlas atlas; + Skeleton skeleton; + AnimationState state; + + public void create () { + camera = new OrthographicCamera(); + batch = new PolygonSpriteBatch(); // Required to render meshes. SpriteBatch can't render meshes. + renderer = new SkeletonRenderer(); + renderer.setPremultipliedAlpha(true); + + atlas = new TextureAtlas(Gdx.files.internal("raptor/raptor-pma.atlas")); + SkeletonJson json = new SkeletonJson(atlas); // This loads skeleton JSON data, which is stateless. + json.setScale(0.5f); // Load the skeleton at 50% the size it was in Spine. + SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("raptor/raptor.json")); + + skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc). + skeleton.setPosition(350, 45); + + AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations. + + state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc). + state.setTimeScale(0.6f); // Slow all animations down to 60% speed. + + // Queue animations on tracks 0 and 1. + state.setAnimation(0, "walk", true); + state.setAnimation(1, "empty", false); + state.addAnimation(1, "gungrab", false, 2); // Keys in higher tracks override the pose from lower tracks. + + swirl = new SwirlEffect(400); + swirl.setCenter(0, 200); + renderer.setVertexEffect(swirl); + renderer.setVertexEffect(new JitterEffect(10, 10)); + } + + public void render () { + // Update the skeleton and animation time. + float delta = Gdx.graphics.getDeltaTime(); + skeleton.update(delta); + state.update(delta); + + swirlTime += delta; + float percent = swirlTime % 2; + if (percent > 1) percent = 1 - (percent - 1); + swirl.setAngle(Interpolation.pow2.apply(-60, 60, percent)); + + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + + state.apply(skeleton); // Poses skeleton using current animations. This sets the bones' local SRT. + skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT. + + // Configure the camera, SpriteBatch, and SkeletonRendererDebug. + camera.update(); + batch.getProjectionMatrix().set(camera.combined); + + batch.begin(); + renderer.draw(batch, skeleton); // Draw the skeleton images. + batch.end(); + } + + public void resize (int width, int height) { + camera.setToOrtho(false); // Update camera with new size. + } + + public void dispose () { + atlas.dispose(); + } + + public static void main (String[] args) throws Exception { + LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); + config.width = 800; + config.height = 600; + new LwjglApplication(new VertexEffectTest(), config); + } +} diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java index 0a4621fef..681ec1315 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java @@ -35,6 +35,7 @@ import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch; import com.badlogic.gdx.graphics.glutils.ImmediateModeRenderer; +import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.FloatArray; @@ -55,8 +56,13 @@ public class SkeletonRenderer implements Disposable { private final FloatArray vertices = new FloatArray(32); private final SkeletonClipping clipper = new SkeletonClipping(); private ImmediateModeRenderer renderer; + private VertexEffect vertexEffect; + private final Vector2 temp = new Vector2(); public void draw (Batch batch, Skeleton skeleton) { + VertexEffect vertexEffect = this.vertexEffect; + if (vertexEffect != null) vertexEffect.begin(skeleton); + boolean premultipliedAlpha = this.premultipliedAlpha; float[] vertices = this.vertices.items; Color skeletonColor = skeleton.color; @@ -81,6 +87,8 @@ public class SkeletonRenderer implements Disposable { vertices[v + 2] = uvs[u + 1]; } + if (vertexEffect != null) applyVertexEffect(vertices, 20, 5); + BlendMode blendMode = slot.data.getBlendMode(); batch.setBlendFunction(blendMode.getSource(premultipliedAlpha), blendMode.getDest()); batch.draw(region.getRegion().getTexture(), vertices, 0, 20); @@ -122,10 +130,15 @@ public class SkeletonRenderer implements Disposable { clipper.clipEnd(slot); } clipper.clipEnd(); + if (vertexEffect != null) vertexEffect.end(); } @SuppressWarnings("null") public void draw (PolygonSpriteBatch batch, Skeleton skeleton) { + Vector2 temp = this.temp; + VertexEffect vertexEffect = this.vertexEffect; + if (vertexEffect != null) vertexEffect.begin(skeleton); + boolean premultipliedAlpha = this.premultipliedAlpha; BlendMode blendMode = null; int verticesLength = 0; @@ -209,13 +222,27 @@ public class SkeletonRenderer implements Disposable { clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false); FloatArray clippedVertices = clipper.getClippedVertices(); ShortArray clippedTriangles = clipper.getClippedTriangles(); + if (vertexEffect != null) applyVertexEffect(clippedVertices.items, clippedVertices.size, 5); batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0, clippedTriangles.size); } else { - for (int v = 2, u = 0; v < verticesLength; v += 5, u += 2) { - vertices[v] = c; - vertices[v + 1] = uvs[u]; - vertices[v + 2] = uvs[u + 1]; + if (vertexEffect != null) { + for (int v = 0, u = 0; v < verticesLength; v += 5, u += 2) { + temp.x = vertices[v]; + temp.y = vertices[v + 1]; + vertexEffect.transform(temp); + vertices[v] = temp.x; + vertices[v + 1] = temp.y; + vertices[v + 2] = c; + vertices[v + 3] = uvs[u]; + vertices[v + 4] = uvs[u + 1]; + } + } else { + for (int v = 2, u = 0; v < verticesLength; v += 5, u += 2) { + vertices[v] = c; + vertices[v + 1] = uvs[u]; + vertices[v + 2] = uvs[u + 1]; + } } batch.draw(texture, vertices, 0, verticesLength, triangles, 0, triangles.length); } @@ -224,10 +251,15 @@ public class SkeletonRenderer implements Disposable { clipper.clipEnd(slot); } clipper.clipEnd(); + if (vertexEffect != null) vertexEffect.end(); } @SuppressWarnings("null") public void draw (TwoColorPolygonBatch batch, Skeleton skeleton) { + Vector2 temp = this.temp; + VertexEffect vertexEffect = this.vertexEffect; + if (vertexEffect != null) vertexEffect.begin(skeleton); + boolean premultipliedAlpha = this.premultipliedAlpha; BlendMode blendMode = null; int verticesLength = 0; @@ -317,14 +349,29 @@ public class SkeletonRenderer implements Disposable { clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, light, dark, true); FloatArray clippedVertices = clipper.getClippedVertices(); ShortArray clippedTriangles = clipper.getClippedTriangles(); + if (vertexEffect != null) applyVertexEffect(clippedVertices.items, clippedVertices.size, 6); batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0, clippedTriangles.size); } else { - for (int v = 2, u = 0; v < verticesLength; v += 6, u += 2) { - vertices[v] = light; - vertices[v + 1] = dark; - vertices[v + 2] = uvs[u]; - vertices[v + 3] = uvs[u + 1]; + if (vertexEffect != null) { + for (int v = 0, u = 0; v < verticesLength; v += 6, u += 2) { + temp.x = vertices[v]; + temp.y = vertices[v + 1]; + vertexEffect.transform(temp); + vertices[v] = temp.x; + vertices[v + 1] = temp.y; + vertices[v + 2] = light; + vertices[v + 3] = dark; + vertices[v + 4] = uvs[u]; + vertices[v + 5] = uvs[u + 1]; + } + } else { + for (int v = 2, u = 0; v < verticesLength; v += 6, u += 2) { + vertices[v] = light; + vertices[v + 1] = dark; + vertices[v + 2] = uvs[u]; + vertices[v + 3] = uvs[u + 1]; + } } batch.draw(texture, vertices, 0, verticesLength, triangles, 0, triangles.length); } @@ -333,13 +380,46 @@ public class SkeletonRenderer implements Disposable { clipper.clipEnd(slot); } clipper.clipEnd(); + if (vertexEffect != null) vertexEffect.end(); + } + + private void applyVertexEffect (float[] vertices, int verticesLength, int stride) { + VertexEffect vertexEffect = this.vertexEffect; + Vector2 temp = this.temp; + for (int v = 0; v < verticesLength; v += stride) { + temp.x = vertices[v]; + temp.y = vertices[v + 1]; + vertexEffect.transform(temp); + vertices[v] = temp.x; + vertices[v + 1] = temp.y; + } + } + + public boolean getPremultipliedAlpha () { + return premultipliedAlpha; } public void setPremultipliedAlpha (boolean premultipliedAlpha) { this.premultipliedAlpha = premultipliedAlpha; } + public VertexEffect getVertexEffect () { + return vertexEffect; + } + + public void setVertexEffect (VertexEffect vertexEffect) { + this.vertexEffect = vertexEffect; + } + public void dispose () { renderer.dispose(); } + + static public interface VertexEffect { + public void begin (Skeleton skeleton); + + public void transform (Vector2 vertex); + + public void end (); + } } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/vertexeffects/JitterEffect.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/vertexeffects/JitterEffect.java new file mode 100644 index 000000000..52a40b593 --- /dev/null +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/vertexeffects/JitterEffect.java @@ -0,0 +1,40 @@ + +package com.esotericsoftware.spine.vertexeffects; + +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector2; +import com.esotericsoftware.spine.Skeleton; +import com.esotericsoftware.spine.SkeletonRenderer.VertexEffect; + +public class JitterEffect implements VertexEffect { + private float x, y; + + public JitterEffect (float x, float y) { + this.x = x; + this.y = y; + } + + public void setJitter (float x, float y) { + this.x = x; + this.y = y; + } + + public void setJitterX (float x) { + this.x = x; + } + + public void setJitterY (float y) { + this.y = y; + } + + public void begin (Skeleton skeleton) { + } + + public void transform (Vector2 vertex) { + vertex.x += MathUtils.randomTriangular(-x, y); + vertex.y += MathUtils.randomTriangular(-x, y); + } + + public void end () { + } +} diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/vertexeffects/SwirlEffect.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/vertexeffects/SwirlEffect.java new file mode 100644 index 000000000..191676061 --- /dev/null +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/vertexeffects/SwirlEffect.java @@ -0,0 +1,60 @@ + +package com.esotericsoftware.spine.vertexeffects; + +import com.badlogic.gdx.math.Interpolation; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector2; +import com.esotericsoftware.spine.Skeleton; +import com.esotericsoftware.spine.SkeletonRenderer.VertexEffect; +import com.esotericsoftware.spine.utils.SpineUtils; + +public class SwirlEffect implements VertexEffect { + private float worldX, worldY, radius, angle; + private Interpolation interpolation = Interpolation.pow2Out; + private float centerX, centerY; + + public SwirlEffect (float radius) { + this.radius = radius; + } + + public void setRadius (float radius) { + this.radius = radius; + } + + public void setCenter (float centerX, float centerY) { + this.centerX = centerX; + this.centerY = centerY; + } + + public void setCenterX (float centerX) { + this.centerX = centerX; + } + + public void setCenterY (float centerY) { + this.centerY = centerY; + } + + public void setAngle (float degrees) { + this.angle = degrees * MathUtils.degRad; + } + + public void begin (Skeleton skeleton) { + worldX = skeleton.getX() + centerX; + worldY = skeleton.getY() + centerY; + } + + public void transform (Vector2 vertex) { + float x = vertex.x - worldX; + float y = vertex.y - worldY; + float dist = (float)Math.sqrt(x * x + y * y); + if (dist < radius) { + float theta = interpolation.apply(0, angle, (radius - dist) / radius); + float cos = SpineUtils.cos(theta), sin = SpineUtils.sin(theta); + vertex.x = cos * x - sin * y + worldX; + vertex.y = sin * x + cos * y + worldY; + } + } + + public void end () { + } +}