diff --git a/CHANGELOG.md b/CHANGELOG.md
index aaaacc5b8..730fb2a1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -383,6 +383,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**
diff --git a/spine-xna/example-content/SpineEffectNormalmap.fx b/spine-xna/example-content/SpineEffectNormalmap.fx
new file mode 100644
index 000000000..ff86b8bcb
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/spine-xna/example-content/spine-xna-example-content.contentproj b/spine-xna/example-content/spine-xna-example-content.contentproj
index bc2de899e..a9f87d98f 100644
--- a/spine-xna/example-content/spine-xna-example-content.contentproj
+++ b/spine-xna/example-content/spine-xna-example-content.contentproj
@@ -26,6 +26,20 @@
EffectProcessor
+
+
+ SpineEffectNormalmap
+ EffectImporter
+ EffectProcessor
+
+
+
+
+ SpineEffectOutline
+ EffectImporter
+ EffectProcessor
+
+