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

Related Discussions
...

See here:
https://help.github.com/articles/creati ... ll-request
https://help.github.com/articles/using-pull-requests
I suggest SmartGit, not the command line client:
http://www.syntevo.com/smartgithg/

I see your changes, though I don't know how to test them. drawOrder means something in the skeleton object model and should not be modified solely for rendering. You could keep a boolean flipX on the SkeletonComponent and if that is not equal to the skeleton.flipX then do whatever logic and set it equal.

@Nate - This is all I meant. (Web build in link)

http://www.cinoptstudios.com/lighttest/

I grabbed the latest version of the runtime and brought the ambient lighting way down with only one point light next to the example animation. It just doesn't get affected by any form of lighting in Unity with the current shader available in the runtime. Is there something new in the runtime that makes this happen easily or is the process still the same as what stray_train has been doing?

@hedayk, the built in shader is a vertex lit shader, similar to the "unlit transparent" that comes with unity, so lighting won't work in any case.

15 dana kasnije

Hey guys, this looks absolutely amazing. I'm assuming the shadow casting only works with unity pro?

Hopefully stray_train or someone else will help Nate to incorporate this as part of Spine Unity, it's pretty incredible!

Keep watching the post, awesome stuff. Any chance for a current progress demo?

@stray_train - Is there any progress on your shaders for spine? Since this is taking up a lot of your time, I just wanted to let you know that I am willing to donate some money to you for the time you spent on this when it is complete... If I am able to get the code for it of course 😉

6 dana kasnije

This is some excellent work! :clap: Even just some basic lighting on my Spine character will really take him to the next level.

I've tried following along with stray_train's progress over throughout this thread, but I'm not sure why I'm getting the result that am.

Here's my character with the default unlit skeleton shader along side the flat lit skeleton shader:
 Loading Image

animated:
 Loading Image
scene view, stopped vs play mode:
 Loading Image

Any idea what I'm doing wrong? What specific part of the shader or SkeletonComponent do I need to adjust to stop the pieces from blending together? Thanks.

Hey guys, it's awesome to see that people are still interested 🙂

@heydayk I haven't made any more progress because it's been hectic at varsity recently, might take a while before I get back to working on this.

@nomad I'm not really sure whats up, are you generating normals? I'll get the latest skeleton code and try this again when I have time.

I quickly pulled the latest code and dumped the lit shader into it. Result:
 Loading Image

What I observe:

When using multiple atlas pages, sorting doesn't work. @Nate mentioned that this should be fixed in the next unity update.

Some sort of additive blending occurs when skeletons get lit from close, I'm not sure why this is. If I create a quad, slap a texture on it and apply the lit shader everything works as intended. No z-fighting, no weird blending, nothing.

A note, for this screenshot I set zWrite = true in the shader. What this does is solve the sorting issues but introduces z-fighting in the skeletons (not for the quads, even when they occupy the same position).

What I suspect is the issue:

I think it's the way the skeletons are generated. They're not double sided so they go invisible if you flip them, turning backface culling off solves this but stops the lighting from working because normals are facing the wrong direction and the triangle order is wrong. I've tried to reverse both with no luck 🙁 Because the skeletons generate quads with the same z-value, z-fighting (order of images keeps changing rapidly) occurs. This ONLY happens with skeletons and I have no idea why flips table I'll study @Nate's code again when I can and see if I can perform some wizardry.

Maybe when the awesome new 2D stuff (sorting layers I'm looking at you) comes in we'll find a better solution :yes:

@stray_train:
Yep, I'm generating normals. In fact no texture appears at all without doing so.
I don't have much technical knowledge to lend, but I've continued experimenting with different conditions and I've found just one potential clue. I noticed that directional lights do not cause the blending effect, but point lights & spot lights do but only when their render mode is set to "Important" (or "Auto").

2 point lights, renderMode="Auto"
 Loading Image

2 point lights, renderMode="Not Important"
 Loading Image

Maybe you already knew about this and I missed part of the setup, but hopefully it informs more progress! By the way, I have Zwrite off in these examples, and my skeleton and quads appear double-sided.

@stray_train I agree with you about the new 2D stuff being updated in Unity. It looks like a lot of what Spine gives you will be integrated into Unity now. I'm glad I supported Spine, but I think I'm going to wait for Unity's update instead.

@nomad I'm also seeing the same issue you were with sprites blending with each other at the spots where they overlap. I'm not using the shader that stray_train wrote though, I'm using the built-in Unity Transparent Bumped Diffuse Shader.

Unfortunately, setting the Light to be "Not Important" means it will then use per vertex lighting for that light which isn't a good solution for my case. If I figure out what causing this I'll report back.

18 dana kasnije

I fixed all my issues by using a very similar shader to stray_train with a few modifications. In SkeletonComponent.cs when you are assigning the vertices to each mesh, I had to give z values other than just 0 so the sprites wouldn't overlap. Just replaced "0" with "i * -0.001". "i" is an index signifying the draw order of that attachment.

I removed the cull tag because my sprites never get reversed. Can add that back in if you need it.

Shader "Spine/LitNormalSkeleton" {

Properties {
   _Color ("Main Color", Color) = (1,1,1,1)
   _MainTex ("Texture Atlas", 2D) = "white" {}
   _BumpMap ("Normalmap", 2D) = "bump" {}
   _Brightness ("Brightness Boost", Range(0,3)) = 2
   _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
}

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

FallBack "Transparent/Cutout/Bumped Diffuse"
}

Thanks @Duré 😃

Only issue left is when flipping the mesh, light acts in the opposite direction.

Recap:

Shader without normal map

Shader "Spine/Lit Skeleton" {

   Properties {
      _Color ("Main Color", Color) = (1,1,1,1)
      _MainTex ("Texture Atlas", 2D) = "white" {}
      _Brightness ("Brightness Boost", Range(0,3)) = 2
      _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
   }

   SubShader {
      Tags {"IgnoreProjector"="True" "RenderType"="TransparentCutout"}
      Fog { Mode Off }
      Blend One OneMinusSrcAlpha
      //Cull Off
      
CGPROGRAM #pragma surface surf Lambert alphatest:_Cutoff sampler2D _MainTex; sampler2D _BumpMap; fixed4 _Color; half _Brightness; struct Input { float2 uv_MainTex; fixed4 color : COLOR; float2 uv_BumpMap; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color * _Color; o.Albedo = c.rgb * _Brightness; o.Alpha = c.a; } ENDCG } FallBack "Transparent/Cutout/Bumped Diffuse" }

Shader with normal map

Shader "Spine/LitNormalSkeleton" {

   Properties {
      _Color ("Main Color", Color) = (1,1,1,1)
      _MainTex ("Texture Atlas", 2D) = "white" {}
      _BumpMap ("Normalmap", 2D) = "bump" {}
      _Brightness ("Brightness Boost", Range(0,3)) = 2
      _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
   }

   SubShader {
      Tags {"IgnoreProjector"="True" "RenderType"="TransparentCutout"}
      Fog { Mode Off }
      Blend One OneMinusSrcAlpha
      //Cull Off
      
CGPROGRAM #pragma surface surf Lambert alphatest:_Cutoff sampler2D _MainTex; sampler2D _BumpMap; fixed4 _Color; half _Brightness; struct Input { float2 uv_MainTex; fixed4 color : COLOR; float2 uv_BumpMap; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color * _Color; o.Albedo = c.rgb * _Brightness; o.Alpha = c.a; o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); } ENDCG } FallBack "Transparent/Cutout/Bumped Diffuse" }

SkeletonComponent.cs

/******************************************************************************
 * Spine Runtime Software License - Version 1.0
 * 
 * Copyright (c) 2013, Esoteric Software
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms in whole or in part, with
 * or without modification, are permitted provided that the following conditions
 * are met:
 * 
 * 1. A Spine Single User License or Spine Professional License must be
 *    purchased from Esoteric Software and the license must remain valid:
 *    http://esotericsoftware.com/
 * 2. Redistributions of source code must retain this license, which is the
 *    above copyright notice, this declaration of conditions and the following
 *    disclaimer.
 * 3. Redistributions in binary form must reproduce this license, which is the
 *    above copyright notice, this declaration 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;
	public bool calculateNormals;
	public bool calculateTangents;
	private Mesh mesh;
	private float[] vertexPositions = new float[8];
	private int lastVertexCount;
	private Vector3[] vertices;
	private Color32[] colors;
	private Vector2[] uvs;
	private Material[] sharedMaterials = new Material[0];
	private List<Material> submeshMaterials = new List<Material>();
	private List<int[]> submeshIndexes = new List<int[]>();
	private List<int> submeshFirstVertex = new List<int>();
	private Vector4[] tangents = new Vector4[0];

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 = "Skeleton Mesh";
	mesh.hideFlags = HideFlags.HideAndDontSave;
	mesh.MarkDynamic();

	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 () {
	if (skeletonDataAsset == null) {
		Clear();
		return;
	}

	SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(false);

	if (skeletonData == null) {
		Clear();
		return;
	}
	
	// Initialize fields.
	if (skeleton == null || skeleton.Data != skeletonData)
		Initialize();

	UpdateSkeleton();

	// Count quads and submeshes.
	int quadCount = 0, submeshQuadCount = 0;
	Material lastMaterial = null;
	submeshMaterials.Clear();
	List<Slot> drawOrder = skeleton.DrawOrder;
	for (int i = 0, n = drawOrder.Count; i < n; i++) {
		RegionAttachment regionAttachment = drawOrder[i].Attachment as RegionAttachment;
		if (regionAttachment == null)
			continue;
		
		// Add submesh when material changes.
		Material material = (Material)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
		if (lastMaterial != material && lastMaterial != null) {
			addSubmesh(lastMaterial, quadCount, submeshQuadCount, false);
			submeshQuadCount = 0;
		} 
		lastMaterial = material;

		quadCount++;
		submeshQuadCount++;
	}
	addSubmesh(lastMaterial, quadCount, submeshQuadCount, true);
	
	// Set materials.
	if (submeshMaterials.Count == sharedMaterials.Length)
		submeshMaterials.CopyTo(sharedMaterials);
	else
		sharedMaterials = submeshMaterials.ToArray();
	renderer.sharedMaterials = sharedMaterials;

	// Ensure mesh data is the right size.
	Mesh mesh = this.mesh;
	Vector3[] vertices = this.vertices;
	int vertexCount = quadCount * 4;
	bool newTriangles = vertexCount > vertices.Length;
	if (newTriangles) {
		// Not enough vertices, increase size.
		this.vertices = vertices = new Vector3[vertexCount];
		this.colors = new Color32[vertexCount];
		this.uvs = new Vector2[vertexCount];
		mesh.Clear();
	} 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;
	Vector2[] uvs = this.uvs;
	Color32[] colors = this.colors;
	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], i * -0.001f);
		vertices[vertexIndex + 1] = new Vector3(vertexPositions[RegionAttachment.X4], vertexPositions[RegionAttachment.Y4], i * -0.001f);
		vertices[vertexIndex + 2] = new Vector3(vertexPositions[RegionAttachment.X2], vertexPositions[RegionAttachment.Y2], i * -0.001f);
		vertices[vertexIndex + 3] = new Vector3(vertexPositions[RegionAttachment.X3], vertexPositions[RegionAttachment.Y3], i * -0.001f);
		
		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;

	mesh.subMeshCount = submeshMaterials.Count;
	for (int i = 0; i < mesh.subMeshCount; ++i)
		mesh.SetTriangles(submeshIndexes[i], i);

	if (calculateNormals) {
		mesh.RecalculateNormals();
		if (calculateTangents) {
			Vector4[] tangents = this.tangents;
			int count = mesh.normals.Length;
			if (tangents.Length != count) {
				this.tangents = tangents = new Vector4[count];
				for (int i = 0; i < count; i++)
					tangents[i] = new Vector4(1, 0, 0, 1);
			}
			mesh.tangents = tangents;
		}
	}
}

/** Adds a material. Adds submesh indexes if existing indexes aren't sufficient. */
private void addSubmesh (Material material, int endQuadCount, int submeshQuadCount, bool lastSubmesh) {
	int submeshIndex = submeshMaterials.Count;
	submeshMaterials.Add(material);

	int indexCount = submeshQuadCount * 6;
	int vertexIndex = (endQuadCount - submeshQuadCount) * 4;

	int[] indexes;
	if (submeshIndexes.Count > submeshIndex) {
		indexes = submeshIndexes[submeshIndex];
		// Don't reallocate if existing indexes are right size. Skip setting vertices if already set correctly.
		if (!lastSubmesh) {
			if (indexes.Length == indexCount) {
				if (submeshFirstVertex[submeshIndex] == vertexIndex) return;
			} else
				submeshIndexes[submeshIndex] = indexes = new int[indexCount];
		} else {
			if (indexes.Length >= indexCount) { // Allow last submesh to have more indices than required.
				if (submeshFirstVertex[submeshIndex] == vertexIndex) return;
				indexCount = indexes.Length; // Update vertices to the end.
			} else
				submeshIndexes[submeshIndex] = indexes = new int[indexCount];
		}
		submeshFirstVertex[submeshIndex] = vertexIndex;
	} else {
		// Need new indexes.
		indexes = new int[indexCount];
		submeshIndexes.Add(indexes);
		submeshFirstVertex.Add(vertexIndex);
	}

	for (int i = 0; i < indexCount; i += 6, vertexIndex += 4) {
		indexes[i] = vertexIndex;
		indexes[i + 1] = vertexIndex + 2;
		indexes[i + 2] = vertexIndex + 1;
		indexes[i + 3] = vertexIndex + 2;
		indexes[i + 4] = vertexIndex + 3;
		indexes[i + 5] = vertexIndex + 1;
	}
}

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

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

#if UNITY_EDITOR
	void OnDrawGizmos() {
		if (vertices == null) return;
		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
}

 Loading Image

If you notice, the edges of your sprites now look jagged. That's a result of using the cutoff. Would need to switch back to a non-cutout shader to get rid of that. I believe other problems arise when I did that though (I'm no shader expert)

As for light reacting to your sprites in the wrong way, did you try modifying the ComputeTangents function? The part you might want to play with is the line:

tangents[a].w = (Vector3.Dot(Vector3.Cross(n, t), tan2[a]) < 0.0f) ? -1.0f : 1.0f;

This controls the handedness of the tangent. Since my game is top down and I'm never flipping the sprite I had to always have this set to 1 otherwise my normals were pointing in the wrong direction when rotating the sprite.

Edit: Looks like you were using Cutoff before too, so the jagged edges were probably still present for you previously.

@Duré where is the ComputeTangents function located? tried looking and no luck

It's from your post on the first page of this thread. The code is at:
http://answers.unity3d.com/questions/77 ... ctor4.html

I don't think the lighting will work properly the way you are calculating tangents in the SkeletonComponent.cs code in your last post. I believe that's the code in git spine runtime repo that just does basic lighting.

9 dana kasnije

Bah, disappointed to install Spine, set it all up and then stumble across the thread discussing the issue I'm having!

I was having the same problem in SmoothMoves, had hoped I'd be able to use Spine / TK2d instead.

Is that possible? Are there on-going efforts to get this Officially fixed? Decent sprite lighting is kind of vital...

4 dana kasnije

I've committed a "Skeleton Lit" shader for spine-unity and updated the goblins and dragon examples to show that lighting works.