Forráskód Böngészése

[xna] Added normalmap support via `SpineEffectNormalmap` and added support for loading multiple texture layers following a suffix-pattern. Closes #1645.

Harald Csaszar 5 éve
szülő
commit
e1274f096d

+ 1 - 0
CHANGELOG.md

@@ -255,6 +255,7 @@
 * Updated to latest MonoGame version 3.7.1
 * Rewrote example project to be cleaner and better demonstrate basic Spine features.
 * Added mix-and-match example to demonstrate the new Skin API.
+* Added normalmap support via `SpineEffectNormalmap` and support for loading multiple texture layers following a suffix-pattern. Please see the example code on how to use them.
 
 ## Java
 * **Breaking changes**

+ 115 - 0
spine-xna/example-content/SpineEffectNormalmap.fx

@@ -0,0 +1,115 @@
+float4x4 World;
+float4x4 View;
+float4x4 Projection;
+
+// Light0 parameters.
+// Default values set below, change them via spineEffect.Parameters["Light0_Direction"] and similar.
+float3 Light0_Direction = float3(-0.5265408f, -0.5735765f, -0.6275069f);
+float3 Light0_Diffuse = float3(1, 1, 1);
+float3 Light0_Specular = float3(1, 1, 1);
+float Light0_SpecularExponent = 2.0; // also called "shininess", "specular hardness"
+
+sampler TextureSampler : register(s0);
+sampler NormalmapSampler : register(s1);
+
+// TODO: add effect parameters here.
+
+float NormalmapIntensity = 1;
+
+float3 GetNormal(sampler normalmapSampler, float2 uv, float3 worldPos, float3 vertexNormal)
+{
+	// Reconstruct tangent space TBN matrix
+	float3 pos_dx = ddx(worldPos);
+	float3 pos_dy = ddy(worldPos);
+	float3 tex_dx = float3(ddx(uv), 0.0);
+	float3 tex_dy = float3(ddy(uv), 0.0);
+	float divisor = (tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y);
+	float3 t = (tex_dy.y * pos_dx - tex_dx.y * pos_dy) / divisor;
+
+	float divisorBinormal = (tex_dy.y * tex_dx.x - tex_dx.y * tex_dy.x);
+	float3 b = (tex_dx.x * pos_dy - tex_dy.x * pos_dx) / divisorBinormal;
+
+	t = normalize(t - vertexNormal * dot(vertexNormal, t));
+	b = normalize(b - vertexNormal * dot(vertexNormal, b));
+	float3x3 tbn = float3x3(t, b, vertexNormal);
+
+	float3 n = 2.0 * tex2D(normalmapSampler, uv).rgb - 1.0;
+#ifdef INVERT_NORMALMAP_Y
+	n.y = -n.y;
+#endif
+	n = normalize(mul(n * float3(NormalmapIntensity, NormalmapIntensity, 1.0), tbn));
+	return n;
+}
+
+void GetLightContributionBlinnPhong(inout float3 diffuseResult, inout float3 specularResult,
+	float3 lightDirection, float3 lightDiffuse, float3 lightSpecular, float specularExponent, float3 normal, float3 viewDirection)
+{
+	diffuseResult += lightDiffuse * max(0.0, dot(normal, -lightDirection));
+	half3 halfVector = normalize(-lightDirection + viewDirection);
+	float nDotH = max(0, dot(normal, halfVector));
+	specularResult += lightSpecular * pow(nDotH, specularExponent);
+}
+
+struct VertexShaderInput
+{
+    float4 Position : POSITION0;
+	float4 Color : COLOR0;
+	float4 TextureCoordinate : TEXCOORD0;
+	float4 Color2 : COLOR1;
+};
+
+struct VertexShaderOutput
+{
+    float4 Position : POSITION0;
+	float4 Color : COLOR0;
+	float4 TextureCoordinate : TEXCOORD0;
+	float4 Color2 : COLOR1;
+	float3 WorldNormal : TEXCOORD1;
+	float4 WorldPosition : TEXCOORD2; // for tangent reconstruction
+};
+
+VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
+{
+    VertexShaderOutput output;
+
+    float4 worldPosition = mul(input.Position, World);
+    float4 viewPosition = mul(worldPosition, View);
+    output.Position = mul(viewPosition, Projection);
+	output.TextureCoordinate = input.TextureCoordinate;
+	output.Color = input.Color;
+	output.Color2 = input.Color2;
+
+	output.WorldNormal = mul(transpose(View), float4(0, 0, 1, 0)).xyz;
+	output.WorldPosition = worldPosition;
+    return output;
+}
+
+float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
+{
+	float4 texColor = tex2D(TextureSampler, input.TextureCoordinate);
+	float3 normal = GetNormal(NormalmapSampler, input.TextureCoordinate, input.WorldPosition, input.WorldNormal);
+	float3 viewDirection = -input.WorldNormal;
+
+	float alpha = texColor.a * input.Color.a;
+	float4 output;
+	output.a = alpha;
+	output.rgb = ((texColor.a - 1.0) * input.Color2.a + 1.0 - texColor.rgb) * input.Color2.rgb + texColor.rgb * input.Color.rgb;
+
+	float3 diffuseLight = float3(0, 0, 0);
+	float3 specularLight = float3(0, 0, 0);
+	GetLightContributionBlinnPhong(diffuseLight, specularLight,
+		Light0_Direction, Light0_Diffuse, Light0_Specular, Light0_SpecularExponent, normal, viewDirection);
+	output.rgb = diffuseLight * output.rgb + specularLight;
+	return output;
+}
+
+technique Technique1
+{
+    pass Pass1
+    {
+        // TODO: set renderstates here.
+
+        VertexShader = compile vs_3_0 VertexShaderFunction();
+        PixelShader = compile ps_3_0 PixelShaderFunction();
+    }
+}

+ 14 - 0
spine-xna/example-content/spine-xna-example-content.contentproj

@@ -26,6 +26,20 @@
       <Processor>EffectProcessor</Processor>
     </Compile>
   </ItemGroup>
+  <ItemGroup>
+    <Compile Include="SpineEffectNormalmap.fx">
+      <Name>SpineEffectNormalmap</Name>
+      <Importer>EffectImporter</Importer>
+      <Processor>EffectProcessor</Processor>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="SpineEffectOutline.fx">
+      <Name>SpineEffectOutline</Name>
+      <Importer>EffectImporter</Importer>
+      <Processor>EffectProcessor</Processor>
+    </Compile>
+  </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA Game Studio\$(XnaFrameworkVersion)\Microsoft.Xna.GameStudio.ContentPipeline.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.

+ 3 - 6
spine-xna/example/spine-xna-example.csproj

@@ -122,18 +122,15 @@
     <None Include="data\coin-pro.skel">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
-    <None Include="data\goblins-mesh.atlas">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Include="data\goblins.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
     <None Include="data\raptor.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
     <None Include="data\spineboy.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
+    <None Include="data\raptor_normals.png">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
     <Content Include="data\tank.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 26 - 6
spine-xna/example/src/ExampleGame.cs

@@ -70,8 +70,20 @@ namespace Spine {
 		}
 
 		protected override void LoadContent () {
-			// Two color tint effect, comment line 80 to disable
-			var spineEffect = Content.Load<Effect>("spine-xna-example-content\\SpineEffect");
+
+			bool useNormalmapShader = false;
+			Effect spineEffect;
+			if (!useNormalmapShader) {
+				// Two color tint effect. Note that you can also use the default BasicEffect instead.
+				spineEffect = Content.Load<Effect>("spine-xna-example-content\\SpineEffect");
+			}
+			else {
+				spineEffect = Content.Load<Effect>("spine-xna-example-content\\SpineEffectNormalmap");
+				spineEffect.Parameters["Light0_Direction"].SetValue(new Vector3(-0.5265408f, 0.5735765f, -0.6275069f));
+				spineEffect.Parameters["Light0_Diffuse"].SetValue(new Vector3(1, 0.9607844f, 0.8078432f));
+				spineEffect.Parameters["Light0_Specular"].SetValue(new Vector3(1, 0.9607844f, 0.8078432f));
+				spineEffect.Parameters["Light0_SpecularExponent"].SetValue(2.0f);
+			}
 			spineEffect.Parameters["World"].SetValue(Matrix.Identity);
 			spineEffect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up));
 
@@ -85,15 +97,23 @@ namespace Spine {
 
 			// String name = "spineboy-ess";
 			// String name = "goblins-pro";
-			// String name = "raptor-pro";
+			String name = "raptor-pro";
 			// String name = "tank-pro";
-			String name = "coin-pro";
+			//String name = "coin-pro";
+			if (useNormalmapShader)
+				name = "raptor-pro"; // we only have normalmaps for raptor
 			String atlasName = name.Replace("-pro", "").Replace("-ess", "");
 			if (name == "goblins-pro") atlasName = "goblins-mesh";
 			bool binaryData = false;
 
-			Atlas atlas = new Atlas(assetsFolder + atlasName + ".atlas", new XnaTextureLoader(GraphicsDevice));
-
+			Atlas atlas;
+			if (!useNormalmapShader) {
+				atlas = new Atlas(assetsFolder + atlasName + ".atlas", new XnaTextureLoader(GraphicsDevice));
+			}
+			else {
+				atlas = new Atlas(assetsFolder + atlasName + ".atlas", new XnaTextureLoader(GraphicsDevice,
+								loadMultipleTextureLayers: true, textureSuffixes: new string[] { "", "_normals" }));
+			}
 			float scale = 1;
 			if (name == "spineboy-ess") scale = 0.6f;
 			if (name == "raptor-pro") scale = 0.5f;

+ 6 - 1
spine-xna/src/MeshBatcher.cs

@@ -147,6 +147,10 @@ namespace Spine {
 					triangleCount = 0;
 					lastTexture = item.texture;
 					device.Textures[0] = lastTexture;
+					if (item.textureLayers != null) {
+						for (int layer = 1; layer < item.textureLayers.Length; ++layer)
+							device.Textures[layer] = item.textureLayers[layer];
+					}
 				}
 
 				int[] itemTriangles = item.triangles;
@@ -182,7 +186,8 @@ namespace Spine {
 	}
 
 	public class MeshItem {
-		public Texture2D texture;
+		public Texture2D texture = null;
+		public Texture2D[] textureLayers = null;
 		public int vertexCount, triangleCount;
 		public VertexPositionColorTextureColor[] vertices = { };
 		public int[] triangles = { };

+ 1 - 1
spine-xna/src/ShapeRenderer.cs

@@ -36,7 +36,7 @@ using System.Text;
 
 namespace Spine {
 	/// <summary>
-	/// Batch drawing of lines and shapes that can be derrived from lines.
+	/// Batch drawing of lines and shapes that can be derived from lines.
 	///
 	/// Call drawing methods in between Begin()/End()
 	/// </summary>

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

@@ -109,7 +109,7 @@ namespace Spine {
 				float attachmentZOffset = zSpacing * i;
 
 				float attachmentColorR, attachmentColorG, attachmentColorB, attachmentColorA;
-				Texture2D texture = null;
+				object textureObject = null;
 				int verticesCount = 0;
 				float[] vertices = this.vertices;
 				int indicesCount = 0;
@@ -120,7 +120,7 @@ namespace Spine {
 					RegionAttachment regionAttachment = (RegionAttachment)attachment;
 					attachmentColorR = regionAttachment.R; attachmentColorG = regionAttachment.G; attachmentColorB = regionAttachment.B; attachmentColorA = regionAttachment.A;
 					AtlasRegion region = (AtlasRegion)regionAttachment.RendererObject;
-					texture = (Texture2D)region.page.rendererObject;
+					textureObject = region.page.rendererObject;
 					verticesCount = 4;
 					regionAttachment.ComputeWorldVertices(slot.Bone, vertices, 0, 2);
 					indicesCount = 6;
@@ -131,7 +131,7 @@ namespace Spine {
 					MeshAttachment mesh = (MeshAttachment)attachment;
 					attachmentColorR = mesh.R; attachmentColorG = mesh.G; attachmentColorB = mesh.B; attachmentColorA = mesh.A;
 					AtlasRegion region = (AtlasRegion)mesh.RendererObject;
-					texture = (Texture2D)region.page.rendererObject;
+					textureObject = region.page.rendererObject;
 					int vertexCount = mesh.WorldVerticesLength;
 					if (vertices.Length < vertexCount) vertices = new float[vertexCount];
 					verticesCount = vertexCount >> 1;
@@ -196,7 +196,12 @@ namespace Spine {
 
 				// submit to batch
 				MeshItem item = batcher.NextItem(verticesCount, indicesCount);
-				item.texture = texture;
+				if (textureObject is Texture2D)
+					item.texture = (Texture2D) textureObject;
+				else {
+					item.textureLayers = (Texture2D[]) textureObject;
+					item.texture = item.textureLayers[0];
+				}
 				for (int ii = 0, nn = indicesCount; ii < nn; ii++) {
 					item.triangles[ii] = indices[ii];
 				}

+ 42 - 2
spine-xna/src/XnaTextureLoader.cs

@@ -35,20 +35,60 @@ using Microsoft.Xna.Framework.Graphics;
 namespace Spine {
 	public class XnaTextureLoader : TextureLoader {
 		GraphicsDevice device;
+		string[] textureLayerSuffixes = null;
 
-		public XnaTextureLoader (GraphicsDevice device) {
+		/// <summary>
+		/// Constructor.
+		/// </summary>
+		/// <param name="device">The graphics device to be used.</param>
+		/// <param name="loadMultipleTextureLayers">If <c>true</c> multiple textures layers
+		/// (e.g. a diffuse/albedo texture and a normal map) are loaded instead of a single texture.
+		/// Names are constructed based on suffixes added according to the <c>textureSuffixes</c> parameter.</param>
+		/// <param name="textureSuffixes">If <c>loadMultipleTextureLayers</c> is <c>true</c>, the strings of this array
+		/// define the path name suffix of each layer to be loaded. Array size must be equal to the number of layers to be loaded.
+		/// The first array entry is the suffix to be <c>replaced</c> (e.g. "_albedo", or "" for a first layer without a suffix),
+		/// subsequent array entries contain the suffix to replace the first entry with (e.g. "_normals").
+		///
+		/// An example would be:
+		/// <code>new string[] { "", "_normals" }</code> for loading a base diffuse texture named "skeletonname.png" and
+		/// a normalmap named "skeletonname_normals.png".</param>
+		public XnaTextureLoader (GraphicsDevice device, bool loadMultipleTextureLayers = false, string[] textureSuffixes = null) {
 			this.device = device;
+			if (loadMultipleTextureLayers)
+				this.textureLayerSuffixes = textureSuffixes;
 		}
 
 		public void Load (AtlasPage page, String path) {
 			Texture2D texture = Util.LoadTexture(device, path);
-			page.rendererObject = texture;
 			page.width = texture.Width;
 			page.height = texture.Height;
+
+			if (textureLayerSuffixes == null) {
+				page.rendererObject = texture;
+			}
+			else {
+				Texture2D[] textureLayersArray = new Texture2D[textureLayerSuffixes.Length];
+				textureLayersArray[0] = texture;
+				for (int layer = 1; layer < textureLayersArray.Length; ++layer) {
+					string layerPath = GetLayerName(path, textureLayerSuffixes[0], textureLayerSuffixes[layer]);
+					textureLayersArray[layer] = Util.LoadTexture(device, layerPath);
+				}
+				page.rendererObject = textureLayersArray;
+			}
 		}
 
 		public void Unload (Object texture) {
 			((Texture2D)texture).Dispose();
 		}
+
+		private string GetLayerName (string firstLayerPath, string firstLayerSuffix, string replacementSuffix) {
+
+			int suffixLocation = firstLayerPath.LastIndexOf(firstLayerSuffix + ".");
+			if (suffixLocation == -1) {
+				throw new Exception(string.Concat("Error composing texture layer name: first texture layer name '", firstLayerPath,
+								"' does not contain suffix to be replaced: '", firstLayerSuffix, "'"));
+			}
+			return firstLayerPath.Remove(suffixLocation, firstLayerSuffix.Length).Insert(suffixLocation, replacementSuffix);
+		}
 	}
 }