[xna][monogame] Added support for vertex effects, see #898

This commit is contained in:
badlogic 2017-06-26 17:28:34 +02:00
parent c06bd8f181
commit b5f79f80c3
8 changed files with 195 additions and 45 deletions

View File

@ -137,6 +137,7 @@
* Removed `RegionBatcher` and `SkeletonRegionRenderer`, renamed `SkeletonMeshRenderer` to `SkeletonRenderer` * 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<Effect>("SpineEffect");`, and set it on the `SkeletonRenderer`. See the example project for code. * 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<Effect>("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 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 ## Java
* **Breaking changes** * **Breaking changes**

View File

@ -46,6 +46,8 @@ namespace Spine {
const float DegToIndex = SIN_COUNT / DegFull; const float DegToIndex = SIN_COUNT / DegFull;
static float[] sin = new float[SIN_COUNT]; static float[] sin = new float[SIN_COUNT];
static Random random = new Random();
static MathUtils () { static MathUtils () {
for (int i = 0; i < SIN_COUNT; i++) for (int i = 0; i < SIN_COUNT; i++)
sin[i] = (float)Math.Sin((i + 0.5f) / SIN_COUNT * RadFull); sin[i] = (float)Math.Sin((i + 0.5f) / SIN_COUNT * RadFull);
@ -96,5 +98,49 @@ namespace Spine {
if (value > max) return max; if (value > max) return max;
return value; 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;
}
} }
} }

View File

@ -119,7 +119,7 @@
<None Include="data\coin.png"> <None Include="data\coin.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\coin.skel"> <None Include="data\coin-pro.skel">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\goblins.png"> <None Include="data\goblins.png">
@ -134,26 +134,17 @@
<Content Include="data\tank.png"> <Content Include="data\tank.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="data\TwoColorTest.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Game.ico" /> <Content Include="Game.ico" />
<Content Include="GameThumbnail.png" /> <Content Include="GameThumbnail.png" />
<None Include="data\tank.atlas"> <None Include="data\tank.atlas">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\tank.json"> <None Include="data\tank-pro.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\tank.skel"> <None Include="data\tank-pro.skel">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\TwoColorTest.atlas">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="data\TwoColorTest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\spine-csharp\spine-csharp.csproj"> <ProjectReference Include="..\..\spine-csharp\spine-csharp.csproj">
@ -201,28 +192,28 @@
<None Include="data\coin.atlas"> <None Include="data\coin.atlas">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\coin.json"> <None Include="data\coin-pro.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\goblins-mesh.atlas"> <None Include="data\goblins-mesh.atlas">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\goblins-mesh.json"> <None Include="data\goblins-pro.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\goblins-mesh.skel"> <None Include="data\goblins-pro.skel">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\raptor.atlas"> <None Include="data\raptor.atlas">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\raptor.json"> <None Include="data\raptor-pro.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\spineboy.atlas"> <None Include="data\spineboy.atlas">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="data\spineboy.json"> <None Include="data\spineboy-ess.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>

View File

@ -47,6 +47,9 @@ namespace Spine {
Slot headSlot; Slot headSlot;
AnimationState state; AnimationState state;
SkeletonBounds bounds = new SkeletonBounds(); SkeletonBounds bounds = new SkeletonBounds();
JitterEffect jitter = new JitterEffect(10, 10);
SwirlEffect swirl = new SwirlEffect(600);
float swirlTime;
#if WINDOWS_STOREAPP #if WINDOWS_STOREAPP
private string assetsFolder = @"Assets\"; private string assetsFolder = @"Assets\";
@ -77,24 +80,23 @@ namespace Spine {
skeletonRenderer = new SkeletonRenderer(GraphicsDevice); skeletonRenderer = new SkeletonRenderer(GraphicsDevice);
skeletonRenderer.PremultipliedAlpha = false; skeletonRenderer.PremultipliedAlpha = false;
skeletonRenderer.Effect = spineEffect; skeletonRenderer.Effect = spineEffect;
skeletonRenderer.VertexEffect = swirl;
// String name = "spineboy"; // String name = "spineboy-ess";
// String name = "goblins-mesh"; // String name = "goblins-pro";
// String name = "raptor"; String name = "raptor-pro";
// String name = "tank"; // String name = "tank-pro";
// String name = "coin"; // String name = "coin-pro";
String name = "TwoColorTest";
bool binaryData = false; 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; float scale = 1;
if (name == "spineboy") scale = 0.6f; if (name == "spineboy-ess") scale = 0.6f;
if (name == "raptor") scale = 0.5f; if (name == "raptor-pro") scale = 0.5f;
if (name == "tank") scale = 0.3f; if (name == "tank-pro") scale = 0.3f;
if (name == "coin") scale = 1; if (name == "coin-pro") scale = 1;
if (name == "TwoColorTest") scale = 0.5f;
SkeletonData skeletonData; SkeletonData skeletonData;
if (binaryData) { if (binaryData) {
@ -107,13 +109,13 @@ namespace Spine {
skeletonData = json.ReadSkeletonData(assetsFolder + name + ".json"); skeletonData = json.ReadSkeletonData(assetsFolder + name + ".json");
} }
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData);
if (name == "goblins-mesh") skeleton.SetSkin("goblin"); if (name == "goblins-pro") skeleton.SetSkin("goblin");
// Define mixing between animations. // Define mixing between animations.
AnimationStateData stateData = new AnimationStateData(skeleton.Data); AnimationStateData stateData = new AnimationStateData(skeleton.Data);
state = new AnimationState(stateData); state = new AnimationState(stateData);
if (name == "spineboy") { if (name == "spineboy-ess") {
stateData.SetMix("run", "jump", 0.2f); stateData.SetMix("run", "jump", 0.2f);
stateData.SetMix("jump", "run", 0.4f); stateData.SetMix("jump", "run", 0.4f);
@ -122,31 +124,27 @@ namespace Spine {
state.End += End; state.End += End;
state.Complete += Complete; state.Complete += Complete;
state.Event += Event; state.Event += Event;
state.SetAnimation(0, "test", false); TrackEntry entry = state.SetAnimation(0, "jump", false);
TrackEntry entry = state.AddAnimation(0, "jump", false, 0);
entry.End += End; // Event handling for queued animations. entry.End += End; // Event handling for queued animations.
state.AddAnimation(0, "run", true, 0); state.AddAnimation(0, "run", true, 0);
} }
else if (name == "raptor") { else if (name == "raptor-pro") {
state.SetAnimation(0, "walk", true); 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); state.SetAnimation(0, "rotate", true);
} }
else if (name == "tank") { else if (name == "tank-pro") {
state.SetAnimation(0, "drive", true); state.SetAnimation(0, "drive", true);
} }
else if (name == "TwoColorTest") {
state.SetAnimation(0, "animation", true);
}
else { else {
state.SetAnimation(0, "walk", true); state.SetAnimation(0, "walk", true);
} }
skeleton.X = 400 + (name == "tank" ? 300: 0); skeleton.X = 400 + (name == "tank" ? 300: 0);
skeleton.Y = 580 + (name == "TwoColorTest" ? -400 : 0); skeleton.Y = 600;
skeleton.UpdateWorldTransform(); skeleton.UpdateWorldTransform();
headSlot = skeleton.FindSlot("head"); headSlot = skeleton.FindSlot("head");
@ -168,14 +166,21 @@ namespace Spine {
protected override void Draw (GameTime gameTime) { protected override void Draw (GameTime gameTime) {
GraphicsDevice.Clear(Color.Black); 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); state.Apply(skeleton);
skeleton.UpdateWorldTransform(); skeleton.UpdateWorldTransform();
if (skeletonRenderer.Effect is BasicEffect) { if (skeletonRenderer.Effect is BasicEffect) {
((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0); ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0);
} else { } else {
skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0)); skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0));
} }
skeletonRenderer.Begin(); skeletonRenderer.Begin();
skeletonRenderer.Draw(skeleton); skeletonRenderer.Draw(skeleton);
skeletonRenderer.End(); skeletonRenderer.End();

View File

@ -110,6 +110,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="src\MeshBatcher.cs" /> <Compile Include="src\MeshBatcher.cs" />
<Compile Include="src\VertexEffect.cs" />
<Compile Include="src\XnaTextureLoader.cs" /> <Compile Include="src\XnaTextureLoader.cs" />
<Compile Include="src\Util.cs" /> <Compile Include="src\Util.cs" />
</ItemGroup> </ItemGroup>

View File

@ -25,11 +25,13 @@ Global
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|Any CPU.ActiveCfg = Debug|x86 {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.ActiveCfg = Debug|x86
{7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|Mixed Platforms.Build.0 = 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.ActiveCfg = Debug|x86
{7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|x86.Build.0 = 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.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.ActiveCfg = Release|x86
{7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|Mixed Platforms.Build.0 = Release|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|Mixed Platforms.Build.0 = Release|x86
{7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|x86.ActiveCfg = Release|x86 {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|x86.ActiveCfg = Release|x86

View File

@ -52,6 +52,7 @@ namespace Spine {
Effect effect; Effect effect;
public Effect Effect { get { return effect; } set { effect = value; } } public Effect Effect { get { return effect; } set { effect = value; } }
public IVertexEffect VertexEffect { get; set; }
private bool premultipliedAlpha; private bool premultipliedAlpha;
public bool PremultipliedAlpha { get { return premultipliedAlpha; } set { premultipliedAlpha = value; } } 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; float skeletonR = skeleton.R, skeletonG = skeleton.G, skeletonB = skeleton.B, skeletonA = skeleton.A;
Color color = new Color(); Color color = new Color();
if (VertexEffect != null) VertexEffect.Begin(skeleton);
for (int i = 0, n = drawOrder.Count; i < n; i++) { for (int i = 0, n = drawOrder.Count; i < n; i++) {
Slot slot = drawOrderItems[i]; Slot slot = drawOrderItems[i];
Attachment attachment = slot.Attachment; Attachment attachment = slot.Attachment;
@ -194,11 +197,13 @@ namespace Spine {
itemVertices[ii].Position.Z = 0; itemVertices[ii].Position.Z = 0;
itemVertices[ii].TextureCoordinate.X = uvs[v]; itemVertices[ii].TextureCoordinate.X = uvs[v];
itemVertices[ii].TextureCoordinate.Y = uvs[v + 1]; itemVertices[ii].TextureCoordinate.Y = uvs[v + 1];
if (VertexEffect != null) VertexEffect.Transform(ref itemVertices[ii]);
} }
clipper.ClipEnd(slot); clipper.ClipEnd(slot);
} }
clipper.ClipEnd(); clipper.ClipEnd();
if (VertexEffect != null) VertexEffect.End();
} }
} }
} }

View File

@ -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;
}
}
}
}