From b5f79f80c3f9a733b2bc9e1a7c982d3188db84e2 Mon Sep 17 00:00:00 2001 From: badlogic Date: Mon, 26 Jun 2017 17:28:34 +0200 Subject: [PATCH] [xna][monogame] Added support for vertex effects, see #898 --- CHANGELOG.md | 1 + spine-csharp/src/MathUtils.cs | 46 ++++++++++ spine-xna/example/spine-xna-example.csproj | 25 ++---- spine-xna/example/src/ExampleGame.cs | 61 +++++++------ spine-xna/spine-xna.csproj | 1 + spine-xna/spine-xna.sln | 2 + spine-xna/src/SkeletonRenderer.cs | 5 ++ spine-xna/src/VertexEffect.cs | 99 ++++++++++++++++++++++ 8 files changed, 195 insertions(+), 45 deletions(-) create mode 100644 spine-xna/src/VertexEffect.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d88f246d1..134b39444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,7 @@ * Removed `RegionBatcher` and `SkeletonRegionRenderer`, renamed `SkeletonMeshRenderer` to `SkeletonRenderer` * Added support for two color tint. For it to work, you need to add the `SpineEffect.fx` file to your content project, then load it via `var effect = Content.Load("SpineEffect");`, and set it on the `SkeletonRenderer`. See the example project for code. * Added support for any `Effect` to be used by `SkeletonRenderer` + * Added support for `IVertexEffect` to modify vertices of skeletons on the CPU. `IVertexEffect` instances can be set on the `SkeletonRenderer`. See example project. ## Java * **Breaking changes** diff --git a/spine-csharp/src/MathUtils.cs b/spine-csharp/src/MathUtils.cs index 62bb46dcd..1cf0af8a0 100644 --- a/spine-csharp/src/MathUtils.cs +++ b/spine-csharp/src/MathUtils.cs @@ -46,6 +46,8 @@ namespace Spine { const float DegToIndex = SIN_COUNT / DegFull; static float[] sin = new float[SIN_COUNT]; + static Random random = new Random(); + static MathUtils () { for (int i = 0; i < SIN_COUNT; i++) sin[i] = (float)Math.Sin((i + 0.5f) / SIN_COUNT * RadFull); @@ -96,5 +98,49 @@ namespace Spine { if (value > max) return max; return value; } + + static public float RandomTriangle(float min, float max) { + return RandomTriangle(min, max, (min + max) * 0.5f); + } + + static public float RandomTriangle(float min, float max, float mode) { + float u = (float)random.NextDouble(); + float d = max - min; + if (u <= (mode - min) / d) return min + (float)Math.Sqrt(u * d * (mode - min)); + return max - (float)Math.Sqrt((1 - u) * d * (max - mode)); + } + } + + public abstract class IInterpolation { + public static IInterpolation Pow2 = new Pow(2); + public static IInterpolation Pow2Out = new PowOut(2); + + protected abstract float Apply(float a); + + public float Apply(float start, float end, float a) { + return start + (end - start) * Apply(a); + } + } + + public class Pow: IInterpolation { + public float Power { get; set; } + + public Pow(float power) { + Power = power; + } + + protected override float Apply(float a) { + if (a <= 0.5f) return (float)Math.Pow(a * 2, Power) / 2; + return (float)Math.Pow((a - 1) * 2, Power) / (Power % 2 == 0 ? -2 : 2) + 1; + } + } + + public class PowOut : Pow { + public PowOut(float power) : base(power) { + } + + protected override float Apply(float a) { + return (float)Math.Pow(a - 1, Power) * (Power % 2 == 0 ? -1 : 1) + 1; + } } } diff --git a/spine-xna/example/spine-xna-example.csproj b/spine-xna/example/spine-xna-example.csproj index 6d3a481a6..87edb92d3 100644 --- a/spine-xna/example/spine-xna-example.csproj +++ b/spine-xna/example/spine-xna-example.csproj @@ -119,7 +119,7 @@ PreserveNewest - + PreserveNewest @@ -134,26 +134,17 @@ PreserveNewest - - Always - PreserveNewest - + PreserveNewest - + PreserveNewest - - Always - - - Always - @@ -201,28 +192,28 @@ PreserveNewest - + PreserveNewest PreserveNewest - + PreserveNewest - + PreserveNewest PreserveNewest - + PreserveNewest PreserveNewest - + PreserveNewest diff --git a/spine-xna/example/src/ExampleGame.cs b/spine-xna/example/src/ExampleGame.cs index ab5280585..b3189f5da 100644 --- a/spine-xna/example/src/ExampleGame.cs +++ b/spine-xna/example/src/ExampleGame.cs @@ -47,6 +47,9 @@ namespace Spine { Slot headSlot; AnimationState state; SkeletonBounds bounds = new SkeletonBounds(); + JitterEffect jitter = new JitterEffect(10, 10); + SwirlEffect swirl = new SwirlEffect(600); + float swirlTime; #if WINDOWS_STOREAPP private string assetsFolder = @"Assets\"; @@ -77,24 +80,23 @@ namespace Spine { skeletonRenderer = new SkeletonRenderer(GraphicsDevice); skeletonRenderer.PremultipliedAlpha = false; - skeletonRenderer.Effect = spineEffect; + skeletonRenderer.Effect = spineEffect; + skeletonRenderer.VertexEffect = swirl; - // String name = "spineboy"; - // String name = "goblins-mesh"; - // String name = "raptor"; - // String name = "tank"; - // String name = "coin"; - String name = "TwoColorTest"; + // String name = "spineboy-ess"; + // String name = "goblins-pro"; + String name = "raptor-pro"; + // String name = "tank-pro"; + // String name = "coin-pro"; bool binaryData = false; - Atlas atlas = new Atlas(assetsFolder + name + ".atlas", new XnaTextureLoader(GraphicsDevice)); + Atlas atlas = new Atlas(assetsFolder + name.Replace("-ess", "").Replace("-pro", "") + ".atlas", new XnaTextureLoader(GraphicsDevice)); float scale = 1; - if (name == "spineboy") scale = 0.6f; - if (name == "raptor") scale = 0.5f; - if (name == "tank") scale = 0.3f; - if (name == "coin") scale = 1; - if (name == "TwoColorTest") scale = 0.5f; + if (name == "spineboy-ess") scale = 0.6f; + if (name == "raptor-pro") scale = 0.5f; + if (name == "tank-pro") scale = 0.3f; + if (name == "coin-pro") scale = 1; SkeletonData skeletonData; if (binaryData) { @@ -107,13 +109,13 @@ namespace Spine { skeletonData = json.ReadSkeletonData(assetsFolder + name + ".json"); } skeleton = new Skeleton(skeletonData); - if (name == "goblins-mesh") skeleton.SetSkin("goblin"); + if (name == "goblins-pro") skeleton.SetSkin("goblin"); // Define mixing between animations. AnimationStateData stateData = new AnimationStateData(skeleton.Data); state = new AnimationState(stateData); - if (name == "spineboy") { + if (name == "spineboy-ess") { stateData.SetMix("run", "jump", 0.2f); stateData.SetMix("jump", "run", 0.4f); @@ -122,31 +124,27 @@ namespace Spine { state.End += End; state.Complete += Complete; state.Event += Event; - - state.SetAnimation(0, "test", false); - TrackEntry entry = state.AddAnimation(0, "jump", false, 0); + + TrackEntry entry = state.SetAnimation(0, "jump", false); entry.End += End; // Event handling for queued animations. state.AddAnimation(0, "run", true, 0); } - else if (name == "raptor") { + else if (name == "raptor-pro") { state.SetAnimation(0, "walk", true); - state.AddAnimation(1, "gungrab", false, 2); + state.AddAnimation(1, "gun-grab", false, 2); } - else if (name == "coin") { + else if (name == "coin-pro") { state.SetAnimation(0, "rotate", true); } - else if (name == "tank") { + else if (name == "tank-pro") { state.SetAnimation(0, "drive", true); } - else if (name == "TwoColorTest") { - state.SetAnimation(0, "animation", true); - } else { state.SetAnimation(0, "walk", true); } skeleton.X = 400 + (name == "tank" ? 300: 0); - skeleton.Y = 580 + (name == "TwoColorTest" ? -400 : 0); + skeleton.Y = 600; skeleton.UpdateWorldTransform(); headSlot = skeleton.FindSlot("head"); @@ -168,14 +166,21 @@ namespace Spine { protected override void Draw (GameTime gameTime) { GraphicsDevice.Clear(Color.Black); - state.Update(gameTime.ElapsedGameTime.Milliseconds / 1000f); + float delta = gameTime.ElapsedGameTime.Milliseconds / 1000f; + swirlTime += delta; + float percent = swirlTime % 2; + if (percent > 1) percent = 1 - (percent - 1); + swirl.Angle = (IInterpolation.Pow2.Apply(-60, 60, percent)); + + state.Update(delta); state.Apply(skeleton); skeleton.UpdateWorldTransform(); if (skeletonRenderer.Effect is BasicEffect) { ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0); } else { skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0)); - } + } + skeletonRenderer.Begin(); skeletonRenderer.Draw(skeleton); skeletonRenderer.End(); diff --git a/spine-xna/spine-xna.csproj b/spine-xna/spine-xna.csproj index 57d97ef19..88bf7f58e 100644 --- a/spine-xna/spine-xna.csproj +++ b/spine-xna/spine-xna.csproj @@ -110,6 +110,7 @@ + diff --git a/spine-xna/spine-xna.sln b/spine-xna/spine-xna.sln index 25ef001d1..e3ec9de83 100644 --- a/spine-xna/spine-xna.sln +++ b/spine-xna/spine-xna.sln @@ -25,11 +25,13 @@ Global EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|Any CPU.ActiveCfg = Debug|x86 + {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|Any CPU.Build.0 = Debug|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|Mixed Platforms.Build.0 = Debug|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|x86.ActiveCfg = Debug|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|x86.Build.0 = Debug|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|Any CPU.ActiveCfg = Release|x86 + {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|Any CPU.Build.0 = Release|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|Mixed Platforms.ActiveCfg = Release|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|Mixed Platforms.Build.0 = Release|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|x86.ActiveCfg = Release|x86 diff --git a/spine-xna/src/SkeletonRenderer.cs b/spine-xna/src/SkeletonRenderer.cs index 8f105dec1..aba917c08 100644 --- a/spine-xna/src/SkeletonRenderer.cs +++ b/spine-xna/src/SkeletonRenderer.cs @@ -52,6 +52,7 @@ namespace Spine { Effect effect; public Effect Effect { get { return effect; } set { effect = value; } } + public IVertexEffect VertexEffect { get; set; } private bool premultipliedAlpha; public bool PremultipliedAlpha { get { return premultipliedAlpha; } set { premultipliedAlpha = value; } } @@ -94,6 +95,8 @@ namespace Spine { float skeletonR = skeleton.R, skeletonG = skeleton.G, skeletonB = skeleton.B, skeletonA = skeleton.A; Color color = new Color(); + if (VertexEffect != null) VertexEffect.Begin(skeleton); + for (int i = 0, n = drawOrder.Count; i < n; i++) { Slot slot = drawOrderItems[i]; Attachment attachment = slot.Attachment; @@ -194,11 +197,13 @@ namespace Spine { itemVertices[ii].Position.Z = 0; itemVertices[ii].TextureCoordinate.X = uvs[v]; itemVertices[ii].TextureCoordinate.Y = uvs[v + 1]; + if (VertexEffect != null) VertexEffect.Transform(ref itemVertices[ii]); } clipper.ClipEnd(slot); } clipper.ClipEnd(); + if (VertexEffect != null) VertexEffect.End(); } } } diff --git a/spine-xna/src/VertexEffect.cs b/spine-xna/src/VertexEffect.cs new file mode 100644 index 000000000..a0223d9ab --- /dev/null +++ b/spine-xna/src/VertexEffect.cs @@ -0,0 +1,99 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.3 + * + * Copyright (c) 2013-2015, 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 (the "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 otherwise create derivative works, improvements of the + * Software or develop new applications using the Software 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; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Spine { + public interface IVertexEffect { + void Begin(Skeleton skeleton); + void Transform(ref VertexPositionColorTextureColor vertex); + void End(); + } + + public class JitterEffect : IVertexEffect { + public float JitterX { get; set; } + public float JitterY { get; set; } + + public JitterEffect(float jitterX, float jitterY) { + JitterX = jitterX; + JitterY = jitterY; + } + + public void Begin(Skeleton skeleton) { + } + + public void End() { + } + + public void Transform(ref VertexPositionColorTextureColor vertex) { + vertex.Position.X += MathUtils.RandomTriangle(-JitterX, JitterY); + vertex.Position.Y += MathUtils.RandomTriangle(-JitterX, JitterY); + } + } + + public class SwirlEffect : IVertexEffect { + private float worldX, worldY, angle; + + public float Radius { get; set; } + public float Angle { get { return angle; } set { angle = value * MathUtils.DegRad; } } + public float CenterX { get; set; } + public float CenterY { get; set; } + public IInterpolation Interpolation { get; set; } + + public SwirlEffect(float radius) { + Radius = radius; + Interpolation = IInterpolation.Pow2; + } + + public void Begin(Skeleton skeleton) { + worldX = skeleton.X + CenterX; + worldY = skeleton.Y + CenterY; + } + + public void End() { + } + + public void Transform(ref VertexPositionColorTextureColor vertex) { + float x = vertex.Position.X - worldX; + float y = vertex.Position.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 = MathUtils.Cos(theta), sin = MathUtils.Sin(theta); + vertex.Position.X = cos * x - sin * y + worldX; + vertex.Position.Y = sin * x + cos * y + worldY; + } + } + } +}