• Editor
  • (Solved-ish) A normal map skeleton shader for unity

Related Discussions
...

stray_train, you are awesome. Thank you so much for keeping up with these shader issues for Spine in Unity.

sounds great 🙂
a normal map shader would be much appreciated.

Nice! I'm definitely interested to see more. It'd fit really well in the game I'm working on.

Both spine-unity and spine-tk2d now have calculateNormals and calculateTangents on SkeletonComponent (also in the inspector).

Shadows and normal maps ALMOST working but z-ordering is broken 🙁 Will post whatever I have by the end of the day.

@Nate Newest addition to skeleton component breaks the spine unity example. Error:

InvalidCastException: Cannot cast from source type to destination type.
SkeletonComponent.Update () (at Assets/Spine/SkeletonComponent.cs:107)

The offending line being:

Material material = (Material)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;

I tried changing it to:

Material material = (Material)regionAttachment.RendererObject;

Displays spineboy but ordering is incorrect 🙁

Thank goodness for source control 🙂 (which, if anyone is interested I wrote a tutorial about)

Probably you don't have the latest spine-csharp.

Note z-order with submeshes is broken in Unity 4.2. They will probably hotfix it, but until then, enjoy. :p

Pulled the latest code and still have the same error. Then tried downloading a zip straight from the git page and running that but no luck. Am I doing something really silly?

No you are right, I'm doing something silly. I committed to spine-csharp, should work now. Sorry! :$

Alright, this is what I have so far:

Shadow receiving, shadow casting skeletons!

Image removed due to the lack of support for HTTPS. | Show Anyway

Normal mapped skeletons that work with more than 1 light!

Image removed due to the lack of support for HTTPS. | Show Anyway

The bad stuff:

Setting FlipX to true, messes up shadow recieving:

Image removed due to the lack of support for HTTPS. | Show Anyway

Dat sorting...

Image removed due to the lack of support for HTTPS. | Show Anyway

flips table

Image removed due to the lack of support for HTTPS. | Show Anyway

Le Shader Code:

The default shader (the one that comes with Spine):

Shader "Spine/Unlit Skeleton" {

Properties {
    _MainTex ("Texture Atlas", 2D) = "black" {}
}

SubShader {
    Tags { "Queue" = "Transparent"}
	Cull Off
	Lighting Off
	ZWrite Off
	Blend One OneMinusSrcAlpha
	
    Pass {
    	ColorMaterial AmbientAndDiffuse
    	
        SetTexture [_MainTex] {
    		combine texture * primary
    	}
    }
    
}
}

Lit shader (first screenshot)

Shader "Spine/Lit Skeleton" {
Properties {
	_Color ("Tint", Color) = (1,1,1,1)
	_MainTex ("Texture Atlas", 2D) = "white" {}
	_Cutoff ("Alpha cutoff", Range(0,1)) = 1
	_Brightness ("Brightness Boost", Range(0,3)) = 2
}

SubShader {
	Tags {"Queue"="AlphaTest"}
	Cull Off
	ZWrite Off
	Blend One OneMinusSrcAlpha
	
CGPROGRAM
	#pragma surface surf Lambert alphatest:_Cutoff

	sampler2D _MainTex;
	fixed4 _Color;
	half _Brightness;

	struct Input {
		float2 uv_MainTex;
	};

	void surf (Input IN, inout SurfaceOutput o) {
		fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
		o.Albedo = c.rgb * _Brightness;
		o.Alpha = c.a;
	}
ENDCG	
}

Fallback "Transparent/Cutout/VertexLit"
}

Lit, shadowing, normal mapped shader (second screenshot):

Shader "Spine/Lit Normal Mapped Skeleton" {
Properties {
	_Color ("Tint", Color) = (1,1,1,1)
	_MainTex ("Texture Atlas", 2D) = "white" {}
	_BumpMap ("Normalmap", 2D) = "bump" {}
	_Cutoff ("Alpha cutoff", Range(0,1)) = 1
	_Brightness ("Brightness Boost", Range(0,3)) = 2
}

SubShader {
	Tags {"Queue"="AlphaTest"}
	Cull Off
	ZWrite Off
	Blend One OneMinusSrcAlpha
	
CGPROGRAM
	#pragma surface surf Lambert alphatest:_Cutoff
	
	sampler2D _MainTex;
	sampler2D _BumpMap;
	fixed4 _Color;
	half _Brightness;
	
	struct Input {
		float2 uv_MainTex;
		float2 uv_BumpMap;
	};
	
	void surf (Input IN, inout SurfaceOutput o) {
		fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
		o.Albedo = c.rgb * _Brightness;
		o.Alpha = c.a;
		o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
	}
ENDCG
}

FallBack "Transparent/Cutout/Diffuse"
}

I'll make a guide once I've actually figured out whats going on. On a side note, the transparent cutout shaders work but suffer from z-fighting. To the Unity forums I go :nerd:

Awesome stuff! If you'd like, I can include your shaders in the runtime. Just let me know when you feel they are ready. 🙂

@Nate that would be awesome 🙂

I think that the shadows showing through the player when flipX is set to true isn't a shader issue but instead is because the mesh generated isn't double sided. @Anyone how would I go about making it double sided?

I found this script in the forums:

var mesh = GetComponent(MeshFilter).mesh;
	var vertices = mesh.vertices;
	var uv = mesh.uv;
	var normals = mesh.normals;
	var szV = vertices.length;
	var newVerts = new Vector3[szV*2];
	var newUv = new Vector2[szV*2];
	var newNorms = new Vector3[szV*2];
	for (var j=0; j< szV; j++){
		// duplicate vertices and uvs:
		newVerts[j] = newVerts[j+szV] = vertices[j];
		newUv[j] = newUv[j+szV] = uv[j];
		// copy the original normals...
		newNorms[j] = normals[j];
		// and revert the new ones
		newNorms[j+szV] = -normals[j];
	}
	var triangles = mesh.triangles;
	var szT = triangles.length;
	var newTris = new int[szT*2]; // double the triangles
	for (var i=0; i< szT; i+=3){
		// copy the original triangle
		newTris[i] = triangles[i];
		newTris[i+1] = triangles[i+1];
		newTris[i+2] = triangles[i+2];
		// save the new reversed triangle
		j = i+szT; 
		newTris[j] = triangles[i]+szV;
		newTris[j+2] = triangles[i+1]+szV;
		newTris[j+1] = triangles[i+2]+szV;
	}
	mesh.vertices = newVerts;
	mesh.uv = newUv;
	mesh.normals = newNorms;
    mesh.triangles = newTris; // assign triangles last!

I translated it to c#, placed it where I thought it would be most likely to work aaaaaand failure. The vertices increase really really fast and my editor locks up. Feel much retarded.

My SkeletonComponent script as is (the function of interest being WindTriangles()):

/*******************************************************************************
 * 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 UnityEngine;
using Spine;

/** Renders a skeleton. Extend to apply animations, get bones and manipulate them, etc. */
[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class SkeletonComponent : MonoBehaviour {
	public SkeletonDataAsset skeletonDataAsset;
	public Skeleton skeleton;
	public String initialSkinName;
	public float timeScale = 1;
	private Mesh mesh;
	private int lastVertexCount;
	private Vector3[] vertices;
	private Color32[] colors;
	private Vector2[] uvs;
	private int[] triangles;
	private float[] vertexPositions = new float[8];

public virtual void Clear () {
	GetComponent<MeshFilter>().mesh = null;
	DestroyImmediate(mesh);
	mesh = null;
	renderer.sharedMaterial = null;
	skeleton = null;
}

public virtual void Initialize () {
	mesh = new Mesh();
	GetComponent<MeshFilter>().mesh = mesh;
	mesh.name = this.gameObject.name;
	mesh.hideFlags = HideFlags.HideAndDontSave;
	mesh.MarkDynamic();

	renderer.sharedMaterial = skeletonDataAsset.atlasAsset.material;

	vertices = new Vector3[0];

	skeleton = new Skeleton(skeletonDataAsset.GetSkeletonData(false));

	if (initialSkinName != null && initialSkinName.Length > 0) {
		skeleton.SetSkin(initialSkinName);
		skeleton.SetSlotsToSetupPose();
	}
}

public virtual void UpdateSkeleton () {
	skeleton.Update(Time.deltaTime * timeScale);
	skeleton.UpdateWorldTransform();
}

public virtual void Update () {
	// Clear fields if missing information to render.
	if (skeletonDataAsset == null || skeletonDataAsset.GetSkeletonData(false) == null) {
		Clear();
		return;
	}
	
	// Initialize fields.
	if (skeleton == null || skeleton.Data != skeletonDataAsset.GetSkeletonData(false))
		Initialize();

	UpdateSkeleton();
	
	// Count quads.
	int quadCount = 0;
	List<Slot> drawOrder = skeleton.DrawOrder;
	for (int i = 0, n = drawOrder.Count; i < n; i++) {
		Slot slot = drawOrder[i];
		Attachment attachment = slot.Attachment;
		if (attachment is RegionAttachment)
			quadCount++;
	}

	// Ensure mesh data is the right size.
	Vector3[] vertices = this.vertices;
	int vertexCount = quadCount * 8;
	bool newTriangles = vertexCount > vertices.Length;
	
	if (newTriangles) {
		// Not enough vertices, increase size.
		this.vertices = vertices = new Vector3[vertexCount];
		colors = new Color32[vertexCount];
		uvs = new Vector2[vertexCount];
		triangles = new int[quadCount * 6];
		mesh.Clear();
		
		for (int i = 0, n = quadCount; i < n; i++) {
			int index = i * 6;
			int vertex = i * 4;
			triangles[index] = vertex;
			triangles[index + 1] = vertex + 2;
			triangles[index + 2] = vertex + 1;
			triangles[index + 3] = vertex + 2;
			triangles[index + 4] = vertex + 3;
			triangles[index + 5] = vertex + 1;
		}
	} else {
		// Too many vertices, zero the extra.
		Vector3 zero = new Vector3(0, 0, 0);
		for (int i = vertexCount, n = lastVertexCount; i < n; i++)
			vertices[i] = zero;
	}
	lastVertexCount = vertexCount;

	// Setup mesh.
	float[] vertexPositions = this.vertexPositions;
	int vertexIndex = 0;
	Color32 color = new Color32();
	for (int i = 0, n = drawOrder.Count; i < n; i++) {
		Slot slot = drawOrder[i];
		RegionAttachment regionAttachment = slot.Attachment as RegionAttachment;
		if (regionAttachment == null) continue;

		regionAttachment.ComputeVertices(skeleton.X, skeleton.Y, slot.Bone, vertexPositions);
		
		vertices[vertexIndex] = new Vector3(vertexPositions[RegionAttachment.X1], vertexPositions[RegionAttachment.Y1], 0);
		vertices[vertexIndex + 1] = new Vector3(vertexPositions[RegionAttachment.X4], vertexPositions[RegionAttachment.Y4], 0);
		vertices[vertexIndex + 2] = new Vector3(vertexPositions[RegionAttachment.X2], vertexPositions[RegionAttachment.Y2], 0);
		vertices[vertexIndex + 3] = new Vector3(vertexPositions[RegionAttachment.X3], vertexPositions[RegionAttachment.Y3], 0);
		
		color.a = (byte)(skeleton.A * slot.A * 255);
		color.r = (byte)(skeleton.R * slot.R * color.a);
		color.g = (byte)(skeleton.G * slot.G * color.a);
		color.b = (byte)(skeleton.B * slot.B * color.a);
		colors[vertexIndex] = color;
		colors[vertexIndex + 1] = color;
		colors[vertexIndex + 2] = color;
		colors[vertexIndex + 3] = color;

		float[] regionUVs = regionAttachment.UVs;
		uvs[vertexIndex] = new Vector2(regionUVs[RegionAttachment.X1], 1 - regionUVs[RegionAttachment.Y1]);
		uvs[vertexIndex + 1] = new Vector2(regionUVs[RegionAttachment.X4], 1 - regionUVs[RegionAttachment.Y4]);
		uvs[vertexIndex + 2] = new Vector2(regionUVs[RegionAttachment.X2], 1 - regionUVs[RegionAttachment.Y2]);
		uvs[vertexIndex + 3] = new Vector2(regionUVs[RegionAttachment.X3], 1 - regionUVs[RegionAttachment.Y3]);

		vertexIndex += 4;
	}
	
	mesh.vertices = vertices;
	mesh.colors32 = colors;
	mesh.uv = uvs;
	
	if (newTriangles) mesh.triangles = triangles;
	
    if (skeleton.FlipX)
    { 
		WindTriangles(mesh);
    }
	
	mesh.RecalculateNormals();
	ComputeTangents(mesh);
}

private void WindTriangles(Mesh SkeletonMesh) {
	
	Vector3[] _vertices = SkeletonMesh.vertices;
	Vector2[] _uv = SkeletonMesh.uv;
	Vector3[] _normals = SkeletonMesh.normals;
	
	int szV = _vertices.Length;
	
	Vector3[] newVerts = new Vector3[szV*2];
	Vector2[] newUv = new Vector2[szV*2];
	Vector3[] newNorms = new Vector3[szV*2];
	
	for (var j=0; j< szV; j++){
		// duplicate vertices and uvs:
		newVerts[j] = newVerts[j+szV] = _vertices[j];
		newUv[j] = newUv[j+szV] = _uv[j];
		// copy the original normals...
		newNorms[j] = _normals[j];
		// and revert the new ones
		newNorms[j+szV] = -_normals[j];
	}
	
	int[] _triangles = SkeletonMesh.triangles;
	int szT = _triangles.Length;
	int[] newTris = new int[szT*2]; // double the triangles
	for (var i=0; i< szT; i+=3){
		// copy the original triangle
		newTris[i] = _triangles[i];
		newTris[i+1] = _triangles[i+1];
		newTris[i+2] = _triangles[i+2];
		// save the new reversed triangle
		int j = i+szT; 
		newTris[j] = _triangles[i]+szV;
		newTris[j+2] = _triangles[i+1]+szV;
		newTris[j+1] = _triangles[i+2]+szV;
	}
	
	SkeletonMesh.vertices = newVerts;
	SkeletonMesh.uv = newUv;
	SkeletonMesh.normals = newNorms;
    SkeletonMesh.triangles = newTris; // assign triangles last!
}

private void ComputeTangents(Mesh mesh)
{
    int triangleCount = mesh.triangles.Length;
    int vertexCount = mesh.vertices.Length;
 
    Vector3[] tan1 = new Vector3[vertexCount];
    Vector3[] tan2 = new Vector3[vertexCount];
 
    Vector4[] tangents = new Vector4[vertexCount];
 
    for(long a = 0; a < triangleCount; a+=3)
    {
        long i1 = mesh.triangles[a+0];
        long i2 = mesh.triangles[a+1];
        long i3 = mesh.triangles[a+2];
 
        Vector3 v1 = mesh.vertices[i1];
        Vector3 v2 = mesh.vertices[i2];
        Vector3 v3 = mesh.vertices[i3];
 
        Vector2 w1 = mesh.uv[i1];
        Vector2 w2 = mesh.uv[i2];
        Vector2 w3 = mesh.uv[i3];
 
        float x1 = v2.x - v1.x;
        float x2 = v3.x - v1.x;
        float y1 = v2.y - v1.y;
        float y2 = v3.y - v1.y;
        float z1 = v2.z - v1.z;
        float z2 = v3.z - v1.z;
 
        float s1 = w2.x - w1.x;
        float s2 = w3.x - w1.x;
        float t1 = w2.y - w1.y;
        float t2 = w3.y - w1.y;
 
        float r = 1.0f / (s1 * t2 - s2 * t1);
 
        Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
        Vector3 tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);
 
        tan1[i1] += sdir;
        tan1[i2] += sdir;
        tan1[i3] += sdir;
 
        tan2[i1] += tdir;
        tan2[i2] += tdir;
        tan2[i3] += tdir;
    }
 
 
    for (long a = 0; a < vertexCount; ++a)
    {
        Vector3 n = mesh.normals[a];
        Vector3 t = tan1[a];
 
        Vector3 tmp = (t - n * Vector3.Dot(n, t)).normalized;
        tangents[a] = new Vector4(tmp.x, tmp.y, tmp.z);
 
        tangents[a].w = (Vector3.Dot(Vector3.Cross(n, t), tan2[a]) < 0.0f) ? -1.0f : 1.0f;
    }
 
    mesh.tangents = tangents;
}

public virtual void OnEnable () {
	Update();
}

public virtual void OnDisable () {
	if (Application.isEditor)
		Clear();
}

public virtual void Reset () {
	Update();
}

#region Unity Editor
#if UNITY_EDITOR
	void OnDrawGizmos() {
		Vector3 gizmosCenter = new Vector3();
		Vector3 gizmosSize = new Vector3();
		Vector3 min = new Vector3(float.MaxValue, float.MaxValue, 0f);
		Vector3 max = new Vector3(float.MinValue, float.MinValue, 0f);
		foreach (Vector3 vert in vertices) {
			min = Vector3.Min (min, vert);
			max = Vector3.Max (max, vert);
		}
		float width = max.x - min.x;
		float height = max.y - min.y;
		gizmosCenter = new Vector3(min.x + (width / 2f), min.y + (height / 2f), 0f);
		gizmosSize	= new Vector3(width, height, 1f);
		Gizmos.color = Color.clear;
		Gizmos.matrix = transform.localToWorldMatrix;
		Gizmos.DrawCube(gizmosCenter, gizmosSize);
	}
#endif
#endregion
}

Not sure about why flipX is borked. 🙁 Your SkeletonComponent is pretty out of date, spine-unity has seen a lot of love lately. Unfortunately it only got more complicated with submeshes to support multiple page atlases.

Nate wrote

Not sure about why flipX is borked. 🙁 Your SkeletonComponent is pretty out of date, spine-unity has seen a lot of love lately. Unfortunately it only got more complicated with submeshes to support multiple page atlases.

The latest version still doesn't support Unity's built-in lighting though. I know shaders are complicated and I wish I was familiar with them, but I'm quite bummed that Spine doesn't support it yet. Thank you stray_train for putting so much effort into doing it.

What needs to be done to support Unity's built-in lighting? The latest has normals and tangents, though the tangents aren't computed the same as stray_train's code.

@Nate, when flipX is set to true the normals point in the wrong direction so the lighting stops working. What I did at first is just reverse the direction of the normals. Fixed the lighting but shadows were getting drawn on top of the mesh (see first bad stuff screenshot). What I've tried doing now is reversing the order of the triangles as well and it sort of works but theres what appears to be z-fighting and incorrect sorting. I suspect the solution is to reverse all of the mesh data including the sorting order?

As for the shader sorting issues, it comes from this:
Unity doesn't support casting shadows from semi transparent objects because it's a "non-trivial problem". What you do instead is create a shader that discards pixels that are less than a certain cutoff value (so either a pixel is transparent or it isn't) and shadows are casted from what remains. This has sorting issues in the shader I made (it looks like it's getting sorted in reverse order). Using the built in unity cutout shaders actually works BUT there's z-fighting. The solution to this I'm investigating.

Question about these shaders:
How compatible might Unity ShaderLab shaders with CGPROGRAMs in them be with mobile?

I have no doubts about the current fixed function Skeleton shader 'cause I assume Unity is able to automatically take care of compatibility.
But I'm actually not knowledgeable regarding the compatibility of Unity and Cg vs. GLSL when it comes to mobile.

  • تم التحرير

@Pharan I actually don't know :/ havent tried them out on mobile. If they don't work however, I can use the mobile cutout shader as a base and remake them without too much issue I think. Will investigate.

Also @Nate my suspicions seem to be correct, by reversing the draworder

if (skeleton.FlipX) drawOrder.Reverse();

Everything draws correctly! Only problem left is that the mesh oscillates violently between lit and unlit, my assumption being that the normals keep getting reset and then reversed. I'll try track it down. EDIT: was because I kept reversing the order of the triangles every frame
Summary of what I do:

  • I skeleton.FlipX is set to true, reverse the draworder, normals and triangles and set that to the mesh

Alright, I've solved the first problem but it's super hacky.

When skeleton.FlipX is set to true you have to reverse the arrays of drawOrder, mesh.triangles and mesh.normals. I believe every normal has to be (0, 0, -1) and this is relative to the mesh.triangles.

@Nate could you please add this is in?

Can you make a pull request so I can see exactly the changes you want? I don't have the setup to test the changes.

@Nate, I'm not sure how to that?

Here's the project zipped up? <


using the latest version from the repo

I've cleaned it up and changed the spineboy scene to illustrate where I am (press space to set FlipX to true)