Browse Source

Refactoring.

API clean up, better JSON parsing, better atlas, easier to extend.
NathanSweet 12 years ago
parent
commit
3b1cf6579c

+ 1 - 1
spine-csharp/spine-csharp.csproj

@@ -60,12 +60,12 @@
   <ItemGroup>
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="src\Animation.cs" />
+    <Compile Include="src\Atlas.cs" />
     <Compile Include="src\Attachments\AttachmentLoader.cs" />
     <Compile Include="src\Attachments\AtlasAttachmentLoader.cs" />
     <Compile Include="src\Attachments\Attachment.cs" />
     <Compile Include="src\Attachments\AttachmentType.cs" />
     <Compile Include="src\Attachments\RegionAttachment.cs" />
-    <Compile Include="src\BaseAtlas.cs" />
     <Compile Include="src\Bone.cs" />
     <Compile Include="src\BoneData.cs" />
     <Compile Include="src\Json.cs" />

+ 109 - 85
spine-csharp/src/BaseAtlas.cs → spine-csharp/src/Atlas.cs

@@ -22,101 +22,133 @@
  * (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.IO;
 
 namespace Spine {
-	abstract public class BaseAtlas {
-		List<AtlasPage> pages = new List<AtlasPage>();
-		List<AtlasRegion> regions = new List<AtlasRegion>();
+	public class Atlas {
+		public Format Format;
+		public TextureFilter MinFilter;
+		public TextureFilter MagFilter;
+		public TextureWrap UWrap;
+		public TextureWrap VWrap;
+		public int TextureWidth;
+		public int TextureHeight;
+		public List<AtlasRegion> Regions;
+		public Object Texture;
+
+		public Atlas (String path, Object texture, int textureWidth, int textureHeight) {
+			using (Stream input = new FileStream(path, FileMode.Open, FileAccess.Read)) {
+				try {
+					initialize(input, texture, textureWidth, textureHeight);
+				} catch (Exception ex) {
+					throw new Exception("Error reading atlas file: " + path, ex);
+				}
+			}
+		}
+
+		public Atlas (Stream input, Object texture, int textureWidth, int textureHeight) {
+			initialize(input, texture, textureWidth, textureHeight);
+		}
 
-		abstract protected AtlasPage NewAtlasPage (String path);
+		private void initialize (Stream input, Object texture, int textureWidth, int textureHeight) {
+			TextureWidth = textureWidth;
+			TextureHeight = textureHeight;
+			Texture = texture;
 
-		public void load (StreamReader reader, String imagesDir) {
+			Regions = new List<AtlasRegion>();
+			float invTexWidth = 1f / textureWidth;
+			float invTexHeight = 1f / textureHeight;
 			String[] tuple = new String[4];
-			AtlasPage page = null;
+
+			StreamReader reader = new StreamReader(input);
+			// Skip to first page entry.
 			while (true) {
 				String line = reader.ReadLine();
-				if (line == null) break;
 				if (line.Trim().Length == 0)
-					page = null;
-				else if (page == null) {
-					page = NewAtlasPage(Path.Combine(imagesDir, line));
-
-					page.Format = (Format)Enum.Parse(typeof(Format), readValue(reader), false);
-
-					readTuple(reader, tuple);
-					page.MinFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0]);
-					page.MagFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[1]);
-
-					String direction = readValue(reader);
-					page.UWrap = TextureWrap.ClampToEdge;
-					page.VWrap = TextureWrap.ClampToEdge;
-					if (direction == "x")
-						page.UWrap = TextureWrap.Repeat;
-					else if (direction == "y")
-						page.VWrap = TextureWrap.Repeat;
-					else if (direction == "xy")
-						page.UWrap = page.VWrap = TextureWrap.Repeat;
-
-					pages.Add(page);
-
-				} else {
-					AtlasRegion region = new AtlasRegion();
-					region.Name = line;
-					region.Page = page;
-
-					region.Rotate = Boolean.Parse(readValue(reader));
-
-					readTuple(reader, tuple);
-					int x = int.Parse(tuple[0]);
-					int y = int.Parse(tuple[1]);
-
-					readTuple(reader, tuple);
-					int width = int.Parse(tuple[0]);
-					int height = int.Parse(tuple[1]);
-
-					float invTexWidth = 1f / page.GetTextureWidth();
-					float invTexHeight = 1f / page.GetTextureHeight();
-					region.U = x * invTexWidth;
-					region.V = y * invTexHeight;
-					region.U2 = (x + width) * invTexWidth;
-					region.V2 = (y + height) * invTexHeight;
-					region.Width = Math.Abs(width);
-					region.Height = Math.Abs(height);
-
-					if (readTuple(reader, tuple) == 4) { // split is optional
-						region.Splits = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]),
+					break;
+			}
+			reader.ReadLine(); // Skip first page name.
+
+			Format = (Format)Enum.Parse(typeof(Format), readValue(reader), false);
+
+			readTuple(reader, tuple);
+			MinFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0]);
+			MagFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[1]);
+
+			String direction = readValue(reader);
+			UWrap = TextureWrap.ClampToEdge;
+			VWrap = TextureWrap.ClampToEdge;
+			if (direction == "x")
+				UWrap = TextureWrap.Repeat;
+			else if (direction == "y")
+				VWrap = TextureWrap.Repeat;
+			else if (direction == "xy")
+				UWrap = VWrap = TextureWrap.Repeat;
+
+			while (true) {
+				String line = reader.ReadLine();
+				if (line == null || line.Trim().Length == 0) break;
+
+				AtlasRegion region = new AtlasRegion();
+				region.Atlas = this;
+				region.Name = line;
+
+				region.Rotate = Boolean.Parse(readValue(reader));
+
+				readTuple(reader, tuple);
+				int x = int.Parse(tuple[0]);
+				int y = int.Parse(tuple[1]);
+
+				readTuple(reader, tuple);
+				int width = int.Parse(tuple[0]);
+				int height = int.Parse(tuple[1]);
+
+				region.U = x * invTexWidth;
+				region.V = y * invTexHeight;
+				region.U2 = (x + width) * invTexWidth;
+				region.V2 = (y + height) * invTexHeight;
+				region.Width = Math.Abs(width);
+				region.Height = Math.Abs(height);
+
+				if (readTuple(reader, tuple) == 4) { // split is optional
+					region.Splits = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]),
 								int.Parse(tuple[2]), int.Parse(tuple[3])};
 
-						if (readTuple(reader, tuple) == 4) { // pad is optional, but only present with splits
-							region.Pads = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]),
+					if (readTuple(reader, tuple) == 4) { // pad is optional, but only present with splits
+						region.Pads = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]),
 									int.Parse(tuple[2]), int.Parse(tuple[3])};
 
-							readTuple(reader, tuple);
-						}
+						readTuple(reader, tuple);
 					}
+				}
 
-					region.OriginalWidth = int.Parse(tuple[0]);
-					region.OriginalHeight = int.Parse(tuple[1]);
+				region.OriginalWidth = int.Parse(tuple[0]);
+				region.OriginalHeight = int.Parse(tuple[1]);
 
-					readTuple(reader, tuple);
-					region.OffsetX = int.Parse(tuple[0]);
-					region.OffsetY = int.Parse(tuple[1]);
+				readTuple(reader, tuple);
+				region.OffsetX = int.Parse(tuple[0]);
+				region.OffsetY = int.Parse(tuple[1]);
 
-					region.Index = int.Parse(readValue(reader));
+				region.Index = int.Parse(readValue(reader));
 
-					regions.Add(region);
-				}
+				Regions.Add(region);
+			}
+
+			while (true) {
+				String line = reader.ReadLine();
+				if (line == null)
+					break;
+				if (line.Trim().Length != 0) throw new Exception("An atlas with multiple images is not supported.");
 			}
 		}
 
 		static String readValue (StreamReader reader) {
 			String line = reader.ReadLine();
 			int colon = line.IndexOf(':');
-			if (colon == -1) throw new Exception("Invalid line: " + line);
+			if (colon == -1)
+				throw new Exception("Invalid line: " + line);
 			return line.Substring(colon + 1).Trim();
 		}
 
@@ -124,12 +156,14 @@ namespace Spine {
 		static int readTuple (StreamReader reader, String[] tuple) {
 			String line = reader.ReadLine();
 			int colon = line.IndexOf(':');
-			if (colon == -1) throw new Exception("Invalid line: " + line);
+			if (colon == -1)
+				throw new Exception("Invalid line: " + line);
 			int i = 0, lastMatch = colon + 1;
 			for (i = 0; i < 3; i++) {
 				int comma = line.IndexOf(',', lastMatch);
 				if (comma == -1) {
-					if (i == 0) throw new Exception("Invalid line: " + line);
+					if (i == 0)
+						throw new Exception("Invalid line: " + line);
 					break;
 				}
 				tuple[i] = line.Substring(lastMatch, comma - lastMatch).Trim();
@@ -143,8 +177,9 @@ namespace Spine {
 		 * should be cached rather than calling this method multiple times.
 		 * @return The region, or null. */
 		public AtlasRegion FindRegion (String name) {
-			for (int i = 0, n = regions.Count; i < n; i++)
-				if (regions[i].Name == name) return regions[i];
+			for (int i = 0, n = Regions.Count; i < n; i++)
+				if (Regions[i].Name == name)
+					return Regions[i];
 			return null;
 		}
 	}
@@ -175,19 +210,8 @@ namespace Spine {
 		Repeat
 	}
 
-	abstract public class AtlasPage {
-		public Format Format;
-		public TextureFilter MinFilter;
-		public TextureFilter MagFilter;
-		public TextureWrap UWrap;
-		public TextureWrap VWrap;
-
-		abstract public int GetTextureWidth ();
-		abstract public int GetTextureHeight ();
-	}
-
 	public class AtlasRegion {
-		public AtlasPage Page;
+		public Atlas Atlas;
 		public float U, V;
 		public float U2, V2;
 		public int Width, Height;

+ 2 - 2
spine-csharp/src/Attachments/AtlasAttachmentLoader.cs

@@ -27,9 +27,9 @@ using System;
 
 namespace Spine {
 	public class AtlasAttachmentLoader : AttachmentLoader {
-		private BaseAtlas atlas;
+		private Atlas atlas;
 
-		public AtlasAttachmentLoader (BaseAtlas atlas) {
+		public AtlasAttachmentLoader (Atlas atlas) {
 			if (atlas == null) throw new ArgumentNullException("atlas cannot be null.");
 			this.atlas = atlas;
 		}

+ 12 - 10
spine-csharp/src/Attachments/RegionAttachment.cs

@@ -96,16 +96,18 @@ namespace Spine {
 			float localX = -localX2;
 			float localY = -localY2;
 			AtlasRegion region = Region;
-			if (region.Rotate) {
-				localX += region.OffsetX / region.OriginalWidth * height;
-				localY += region.OffsetY / region.OriginalHeight * width;
-				localX2 -= (region.OriginalWidth - region.OffsetX - region.Height) / region.OriginalWidth * width;
-				localY2 -= (region.OriginalHeight - region.OffsetY - region.Width) / region.OriginalHeight * height;
-			} else {
-				localX += region.OffsetX / region.OriginalWidth * width;
-				localY += region.OffsetY / region.OriginalHeight * height;
-				localX2 -= (region.OriginalWidth - region.OffsetX - region.Width) / region.OriginalWidth * width;
-				localY2 -= (region.OriginalHeight - region.OffsetY - region.Height) / region.OriginalHeight * height;
+			if (region != null) {
+				if (region.Rotate) {
+					localX += region.OffsetX / region.OriginalWidth * height;
+					localY += region.OffsetY / region.OriginalHeight * width;
+					localX2 -= (region.OriginalWidth - region.OffsetX - region.Height) / region.OriginalWidth * width;
+					localY2 -= (region.OriginalHeight - region.OffsetY - region.Width) / region.OriginalHeight * height;
+				} else {
+					localX += region.OffsetX / region.OriginalWidth * width;
+					localY += region.OffsetY / region.OriginalHeight * height;
+					localX2 -= (region.OriginalWidth - region.OffsetX - region.Width) / region.OriginalWidth * width;
+					localY2 -= (region.OriginalHeight - region.OffsetY - region.Height) / region.OriginalHeight * height;
+				}
 			}
 			float scaleX = ScaleX;
 			float scaleY = ScaleY;

+ 8 - 18
spine-csharp/src/Json.cs

@@ -81,12 +81,10 @@ namespace Spine
         /// </summary>
         /// <param name="json">A JSON string.</param>
 		 /// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a float, an integer,a string, null, true, or false</returns>
-        public static object Deserialize(string json) {
-            // save the string for debug information
+		 public static object Deserialize (TextReader json) {
             if (json == null) {
                 return null;
-            }
- 
+            } 
             return Parser.Parse(json);
         }
  
@@ -109,14 +107,14 @@ namespace Spine
                 NULL
             };
  
-            StringReader json;
+            TextReader json;
  
-            Parser(string jsonString) {
-                json = new StringReader(jsonString);
+            Parser(TextReader reader) {
+					json = reader;
             }
- 
-            public static object Parse(string jsonString) {
-                using (var instance = new Parser(jsonString)) {
+
+				public static object Parse (TextReader reader) {
+                using (var instance = new Parser(reader)) {
                     return instance.ParseValue();
                 }
             }
@@ -288,14 +286,6 @@ namespace Spine
  
             object ParseNumber() {
                 string number = NextWord;
-
-					 //NO!! always serialize to a float
-                //if (number.IndexOf('.') == -1) {
-                //    long parsedInt;
-                //    Int64.TryParse(number, out parsedInt);
-                //    return parsedInt;
-                //}
-
 					 float parsedFloat;
 					 float.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedFloat); 
 					 return parsedFloat;

+ 12 - 5
spine-csharp/src/SkeletonJson.cs

@@ -41,7 +41,7 @@ namespace Spine {
 		private AttachmentLoader attachmentLoader;
 		public float Scale { get; set; }
 
-		public SkeletonJson (BaseAtlas atlas) {
+		public SkeletonJson (Atlas atlas) {
 			this.attachmentLoader = new AtlasAttachmentLoader(atlas);
 			Scale = 1;
 		}
@@ -51,14 +51,21 @@ namespace Spine {
 			Scale = 1;
 		}
 
-		public SkeletonData readSkeletonData (String name, String json) {
-			if (json == null)
+		public SkeletonData ReadSkeletonData (String path) {
+			using (Stream input = new FileStream(path, FileMode.Open, FileAccess.Read)) {
+				SkeletonData skeletonData = ReadSkeletonData(input);
+				skeletonData.Name = Path.GetFileNameWithoutExtension(path);
+				return skeletonData;
+			}
+		}
+
+		public SkeletonData ReadSkeletonData (Stream input) {
+			if (input == null)
 				throw new ArgumentNullException("json cannot be null.");
 
 			SkeletonData skeletonData = new SkeletonData();
-			skeletonData.Name = name;
 
-			var root = Json.Deserialize(json) as Dictionary<String, Object>;
+			var root = Json.Deserialize(new StreamReader(input)) as Dictionary<String, Object>;
 
 			// Bones.
 			foreach (Dictionary<String, Object> boneMap in (List<Object>)root["bones"]) {

+ 5 - 3
spine-xna/example/src/ExampleGame.cs

@@ -58,11 +58,13 @@ namespace Spine {
 
 		protected override void LoadContent () {
 			skeletonRenderer = new SkeletonRenderer(GraphicsDevice);
-			Atlas atlas = new Atlas(GraphicsDevice, "data/goblins.atlas");
+
+			Texture2D texture = Util.LoadTexture(GraphicsDevice, "data/goblins.png");
+			Atlas atlas = new Atlas("data/goblins.atlas", texture, texture.Width, texture.Height);
 			SkeletonJson json = new SkeletonJson(atlas);
-			skeleton = new Skeleton(json.readSkeletonData("goblins", File.ReadAllText("data/goblins.json")));
+			skeleton = new Skeleton(json.ReadSkeletonData("data/goblins.json"));
 			skeleton.SetSkin("goblingirl");
-			skeleton.SetSlotsToBindPose();
+			skeleton.SetSlotsToBindPose(); // Without this the skin attachments won't be attached. See SetSkin.
 			animation = skeleton.Data.FindAnimation("walk");
 
 			skeleton.RootBone.X = 320;

+ 1 - 1
spine-xna/spine-xna.csproj

@@ -93,7 +93,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="src\Atlas.cs" />
+    <Compile Include="src\Util.cs" />
     <Compile Include="src\SkeletonRenderer.cs" />
     <Compile Include="src\SpriteBatcher.cs" />
   </ItemGroup>

+ 0 - 109
spine-xna/src/Atlas.cs

@@ -1,109 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2013, Esoteric Software
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * 
- * 1. Redistributions of source code must retain the above copyright notice, this
- *    list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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.IO;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Audio;
-using Microsoft.Xna.Framework.Content;
-using Microsoft.Xna.Framework.GamerServices;
-using Microsoft.Xna.Framework.Graphics;
-using Microsoft.Xna.Framework.Input;
-using Microsoft.Xna.Framework.Media;
-
-namespace Spine {
-	public class Atlas : BaseAtlas {
-		private GraphicsDevice device;
-
-		public Atlas (GraphicsDevice device, String atlasFile) {
-			this.device = device;
-			using (StreamReader reader = new StreamReader(atlasFile)) {
-				load(reader, Path.GetDirectoryName(atlasFile));
-			}
-		}
-
-		override protected AtlasPage NewAtlasPage (String path) {
-			XnaAtlasPage page = new XnaAtlasPage();
-			page.Texture = loadTexture(path);
-			return page;
-		}
-
-		private Texture2D loadTexture (string path) {
-			Texture2D file;
-			using (Stream fileStream = new FileStream(path, FileMode.Open)) {
-				file = Texture2D.FromStream(device, fileStream);
-			}
-
-			// Setup a render target to hold our final texture which will have premulitplied alpha values
-			RenderTarget2D result = new RenderTarget2D(device, file.Width, file.Height);
-			device.SetRenderTarget(result);
-			device.Clear(Color.Black);
-
-			// Multiply each color by the source alpha, and write in just the color values into the final texture
-			BlendState blendColor = new BlendState();
-			blendColor.ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue;
-			blendColor.AlphaDestinationBlend = Blend.Zero;
-			blendColor.ColorDestinationBlend = Blend.Zero;
-			blendColor.AlphaSourceBlend = Blend.SourceAlpha;
-			blendColor.ColorSourceBlend = Blend.SourceAlpha;
-
-			SpriteBatch spriteBatch = new SpriteBatch(device);
-			spriteBatch.Begin(SpriteSortMode.Immediate, blendColor);
-			spriteBatch.Draw(file, file.Bounds, Color.White);
-			spriteBatch.End();
-
-			// Now copy over the alpha values from the PNG source texture to the final one, without multiplying them
-			BlendState blendAlpha = new BlendState();
-			blendAlpha.ColorWriteChannels = ColorWriteChannels.Alpha;
-			blendAlpha.AlphaDestinationBlend = Blend.Zero;
-			blendAlpha.ColorDestinationBlend = Blend.Zero;
-			blendAlpha.AlphaSourceBlend = Blend.One;
-			blendAlpha.ColorSourceBlend = Blend.One;
-
-			spriteBatch.Begin(SpriteSortMode.Immediate, blendAlpha);
-			spriteBatch.Draw(file, file.Bounds, Color.White);
-			spriteBatch.End();
-
-			// Release the GPU back to drawing to the screen
-			device.SetRenderTarget(null);
-
-			return result as Texture2D;
-		}
-	}
-
-	public class XnaAtlasPage : AtlasPage {
-		public Texture2D Texture { get; set; }
-
-		override public int GetTextureWidth () {
-			return Texture.Width;
-		}
-
-		override public int GetTextureHeight () {
-			return Texture.Height;
-		}
-	}
-}

+ 5 - 4
spine-xna/src/SkeletonRenderer.cs

@@ -34,6 +34,7 @@ namespace Spine {
 		SpriteBatcher batcher;
 		BasicEffect effect;
 		RasterizerState rasterizerState;
+		public BlendState BlendState { get; set; }
 
 		public SkeletonRenderer (GraphicsDevice device) {
 			this.device = device;
@@ -49,12 +50,14 @@ namespace Spine {
 			rasterizerState = new RasterizerState();
 			rasterizerState.CullMode = CullMode.None;
 
+			BlendState = BlendState.AlphaBlend;
+
 			Bone.yDown = true;
 		}
 
 		public void Begin () {
 			device.RasterizerState = rasterizerState;
-			device.BlendState = BlendState.AlphaBlend;
+			device.BlendState = BlendState;
 
 			effect.Projection = Matrix.CreateOrthographicOffCenter(0, device.Viewport.Width, device.Viewport.Height, 0, 1, 0);
 		}
@@ -71,13 +74,11 @@ namespace Spine {
 			for (int i = 0, n = drawOrder.Count; i < n; i++) {
 				Slot slot = drawOrder[i];
 				Attachment attachment = slot.Attachment;
-				if (attachment == null)
-					continue;
 				if (attachment is RegionAttachment) {
 					RegionAttachment regionAttachment = (RegionAttachment)attachment;
 
 					SpriteBatchItem item = batcher.CreateBatchItem();
-					item.Texture = ((XnaAtlasPage)regionAttachment.Region.Page).Texture;
+					item.Texture = (Texture2D)regionAttachment.Region.Atlas.Texture;
 
 					byte r = (byte)(slot.R * 255);
 					byte g = (byte)(slot.G * 255);

+ 57 - 0
spine-xna/src/Util.cs

@@ -0,0 +1,57 @@
+using System;
+using System.IO;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Spine {
+	static public class Util {
+		static public Texture2D LoadTexture (GraphicsDevice device, String path) {
+			using (Stream input = new FileStream(path, FileMode.Open, FileAccess.Read)) {
+				try {
+					return Util.LoadTexture(device, input);
+				} catch (Exception ex) {
+					throw new Exception("Error reading texture file: " + path, ex);
+				}
+			}
+		}
+
+		static public Texture2D LoadTexture (GraphicsDevice device, Stream input) {
+			Texture2D file = Texture2D.FromStream(device, input);
+
+			// Setup a render target to hold our final texture which will have premulitplied alpha values
+			RenderTarget2D result = new RenderTarget2D(device, file.Width, file.Height);
+			device.SetRenderTarget(result);
+			device.Clear(Color.Black);
+
+			// Multiply each color by the source alpha, and write in just the color values into the final texture
+			BlendState blendColor = new BlendState();
+			blendColor.ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue;
+			blendColor.AlphaDestinationBlend = Blend.Zero;
+			blendColor.ColorDestinationBlend = Blend.Zero;
+			blendColor.AlphaSourceBlend = Blend.SourceAlpha;
+			blendColor.ColorSourceBlend = Blend.SourceAlpha;
+
+			SpriteBatch spriteBatch = new SpriteBatch(device);
+			spriteBatch.Begin(SpriteSortMode.Immediate, blendColor);
+			spriteBatch.Draw(file, file.Bounds, Color.White);
+			spriteBatch.End();
+
+			// Now copy over the alpha values from the PNG source texture to the final one, without multiplying them
+			BlendState blendAlpha = new BlendState();
+			blendAlpha.ColorWriteChannels = ColorWriteChannels.Alpha;
+			blendAlpha.AlphaDestinationBlend = Blend.Zero;
+			blendAlpha.ColorDestinationBlend = Blend.Zero;
+			blendAlpha.AlphaSourceBlend = Blend.One;
+			blendAlpha.ColorSourceBlend = Blend.One;
+
+			spriteBatch.Begin(SpriteSortMode.Immediate, blendAlpha);
+			spriteBatch.Draw(file, file.Bounds, Color.White);
+			spriteBatch.End();
+
+			// Release the GPU back to drawing to the screen
+			device.SetRenderTarget(null);
+
+			return result as Texture2D;
+		}
+	}
+}