Эх сурвалжийг харах

Meshes, FFD and skinning for spine-lua.

NathanSweet 11 жил өмнө
parent
commit
81ae526a7b

+ 2 - 2
spine-corona/examples/spineboy.lua

@@ -12,8 +12,8 @@ function skeleton:createImage (attachment)
 	-- Customize where images are loaded.
 	return display.newImage("examples/spineboy/images/" .. attachment.name .. ".png")
 end
-skeleton.group.x = 150
-skeleton.group.y = 325
+skeleton.group.x = display.contentWidth * 0.5
+skeleton.group.y = display.contentHeight * 0.9
 skeleton.flipX = false
 skeleton.flipY = false
 skeleton.debug = true -- Omit or set to false to not draw debug lines on top of the images.

+ 3 - 1
spine-corona/spine-corona/spine.lua

@@ -29,7 +29,7 @@
 -------------------------------------------------------------------------------
 
 spine = {}
- 
+
 spine.utils = require "spine-lua.utils"
 spine.SkeletonJson = require "spine-lua.SkeletonJson"
 spine.SkeletonData = require "spine-lua.SkeletonData"
@@ -37,6 +37,8 @@ spine.BoneData = require "spine-lua.BoneData"
 spine.SlotData = require "spine-lua.SlotData"
 spine.Skin = require "spine-lua.Skin"
 spine.RegionAttachment = require "spine-lua.RegionAttachment"
+spine.MeshAttachment = require "spine-lua.MeshAttachment"
+spine.SkinnedMeshAttachment = require "spine-lua.SkinnedMeshAttachment"
 spine.Skeleton = require "spine-lua.Skeleton"
 spine.Bone = require "spine-lua.Bone"
 spine.Slot = require "spine-lua.Slot"

+ 22 - 24
spine-lua/Animation.lua

@@ -356,32 +356,30 @@ function Animation.ColorTimeline.new ()
 		local frames = self.frames
 		if time < frames[0] then return end -- Time is before first frame.
 
-		local slot = skeleton.slots[self.slotIndex]
-
+		local r, g, b, a
 		if time >= frames[#frames - 4] then -- Time is after last frame.
-			local r = frames[#frames - 3]
-			local g = frames[#frames - 2]
-			local b = frames[#frames - 1]
-			local a = frames[#frames]
-			slot:setColor(r, g, b, a)
-			return
+			r = frames[#frames - 3]
+			g = frames[#frames - 2]
+			b = frames[#frames - 1]
+			a = frames[#frames]
+		else
+			-- Interpolate between the last frame and the current frame.
+			local frameIndex = binarySearch(frames, time, 5)
+			local lastFrameR = frames[frameIndex - 4]
+			local lastFrameG = frames[frameIndex - 3]
+			local lastFrameB = frames[frameIndex - 2]
+			local lastFrameA = frames[frameIndex - 1]
+			local frameTime = frames[frameIndex]
+			local percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime)
+			if percent < 0 then percent = 0 elseif percent > 255 then percent = 255 end
+			percent = self:getCurvePercent(frameIndex / 5 - 1, percent)
+
+			r = lastFrameR + (frames[frameIndex + FRAME_R] - lastFrameR) * percent
+			g = lastFrameG + (frames[frameIndex + FRAME_G] - lastFrameG) * percent
+			b = lastFrameB + (frames[frameIndex + FRAME_B] - lastFrameB) * percent
+			a = lastFrameA + (frames[frameIndex + FRAME_A] - lastFrameA) * percent
 		end
-
-		-- Interpolate between the last frame and the current frame.
-		local frameIndex = binarySearch(frames, time, 5)
-		local lastFrameR = frames[frameIndex - 4]
-		local lastFrameG = frames[frameIndex - 3]
-		local lastFrameB = frames[frameIndex - 2]
-		local lastFrameA = frames[frameIndex - 1]
-		local frameTime = frames[frameIndex]
-		local percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime)
-		if percent < 0 then percent = 0 elseif percent > 255 then percent = 255 end
-		percent = self:getCurvePercent(frameIndex / 5 - 1, percent)
-
-		local r = lastFrameR + (frames[frameIndex + FRAME_R] - lastFrameR) * percent
-		local g = lastFrameG + (frames[frameIndex + FRAME_G] - lastFrameG) * percent
-		local b = lastFrameB + (frames[frameIndex + FRAME_B] - lastFrameB) * percent
-		local a = lastFrameA + (frames[frameIndex + FRAME_A] - lastFrameA) * percent
+		local slot = skeleton.slots[self.slotIndex]
 		if alpha < 1 then
 			slot:setColor(slot.r + (r - slot.r) * alpha, slot.g + (g - slot.g) * alpha, slot.b + (b - slot.b) * alpha, slot.a + (a - slot.a) * alpha)
 		else

+ 4 - 4
spine-lua/AnimationState.lua

@@ -75,11 +75,11 @@ function AnimationState.new (data)
 		for i = 0, self.trackCount do
 			local current = self.tracks[i]
 			if current then
-				local trackDelta = delta * current.timeScale
-				current.time = current.time + trackDelta
+				current.time = current.time + delta * current.timeScale
 				if current.previous then
-					current.previous.time = current.previous.time + trackDelta
-					current.mixTime = current.mixTime + trackDelta
+					local previousDelta = delta * current.previous.timeScale
+					current.previous.time = current.previous.time + previousDelta
+					current.mixTime = current.mixTime + previousDelta
 				end
 
 				local next = current.next

+ 1 - 1
spine-lua/AnimationStateData.lua

@@ -34,8 +34,8 @@ function AnimationStateData.new (skeletonData)
 	if not skeletonData then error("skeletonData cannot be nil", 2) end
 
 	local self = {
-		animationToMixTime = {},
 		skeletonData = skeletonData,
+		animationToMixTime = {},
 		defaultMix = 0
 	}
 

+ 15 - 11
spine-lua/AttachmentLoader.lua

@@ -32,20 +32,24 @@ local AttachmentType = require "spine-lua.AttachmentType"
 local RegionAttachment = require "spine-lua.RegionAttachment"
 local BoundingBoxAttachment = require "spine-lua.BoundingBoxAttachment"
 
-local AttachmentLoader = {
-	failed = {}
-}
+local AttachmentLoader = {}
 function AttachmentLoader.new ()
 	local self = {}
 
-	function self:newAttachment (type, name)
-		if type == AttachmentType.region then
-			return RegionAttachment.new(name)
-		end
-		if type == AttachmentType.boundingbox then
-			return BoundingBoxAttachment.new(name)
-		end
-		error("Unknown attachment type: " .. type .. " (" .. name .. ")")
+	function self:newRegionAttachment (skin, name, path)
+		return RegionAttachment.new(name)
+	end
+
+	function self:newMeshAttachment (skin, name, path)
+		return MeshAttachment.new(name)
+	end
+
+	function self:newSkinningMeshAttachment (skin, name, path)
+		return SkinningMeshAttachment.new(name)
+	end
+
+	function self:newBoundingBoxAttachment (skin, name)
+		return BoundingBoxAttachment.new(name)
 	end
 
 	return self

+ 3 - 1
spine-lua/AttachmentType.lua

@@ -30,6 +30,8 @@
 
 local AttachmentType = {
 	region = 0,
-	boundingbox = 1
+	boundingbox = 1,
+	mesh = 2,
+	skinnedmesh = 3
 }
 return AttachmentType

+ 93 - 0
spine-lua/MeshAttachment.lua

@@ -0,0 +1,93 @@
+-------------------------------------------------------------------------------
+-- Spine Runtimes Software License
+-- Version 2.1
+-- 
+-- Copyright (c) 2013, Esoteric Software
+-- All rights reserved.
+-- 
+-- You are granted a perpetual, non-exclusive, non-sublicensable and
+-- non-transferable license to install, execute and perform the Spine Runtimes
+-- Software (the "Software") solely for internal use. Without the written
+-- permission of Esoteric Software (typically granted by licensing Spine), you
+-- may not (a) modify, translate, adapt or otherwise create derivative works,
+-- improvements of the Software or develop new applications using the Software
+-- or (b) remove, delete, alter or obscure any trademarks or any copyright,
+-- trademark, patent or other intellectual property or proprietary rights
+-- notices on or in the Software, including any copy thereof. Redistributions
+-- in binary or source form must include this license and terms.
+-- 
+-- THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 ESOTERIC SOFTARE 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.
+-------------------------------------------------------------------------------
+
+local AttachmentType = require "spine-lua.AttachmentType"
+
+local MeshAttachment = {}
+function MeshAttachment.new (name)
+	if not name then error("name cannot be nil", 2) end
+	
+	local self = {
+		name = name,
+		type = AttachmentType.mesh,
+		vertices = nil,
+		uvs = nil,
+		regionUVs = nil,
+		triangles = nil,
+		hullLength = 0,
+		r = 1, g = 1, b = 1, a = 1,
+		path = nil,
+		rendererObject = nil,
+		regionU = 0, regionV = 0, regionU2 = 0, regionV2 = 0, regionRotate = false,
+		regionOffsetX = 0, regionOffsetY = 0,
+		regionWidth = 0, regionHeight = 0,
+		regionOriginalWidth = 0, regionOriginalHeight = 0,
+		edges = nil,
+		width = 0, height = 0
+	}
+
+	function self:updateUVs ()
+		local width, height = self.regionU2 - self.regionU, self.regionV2 - self.regionV
+		local n = #self.regionUVs
+		if not self.uvs or #self.uvs ~= n then
+			self.uvs = {}
+		end
+		if self.regionRotate then
+			for i = 1, n, 2 do
+				self.uvs[i] = self.regionU + self.regionUVs[i + 1] * width
+				self.uvs[i + 1] = self.regionV + height - self.regionUVs[i] * height
+			end
+		else
+			for i = 1, n, 2 do
+				self.uvs[i] = self.regionU + self.regionUVs[i] * width
+				self.uvs[i + 1] = self.regionV + self.regionUVs[i + 1] * height
+			end
+		end
+	end
+
+	function self:computeWorldVertices (x, y, slot, worldVertices)
+		local bone = slot.bone
+		x = x + bone.worldX
+		y = y + bone.worldY
+		local m00, m01, m10, m11 = bone.m00, bone.m01, bone.m10, bone.m11
+		local vertices = self.vertices
+		local verticesCount = vertices.length
+		if #slot.attachmentVertices == verticesCount then vertices = slot.attachmentVertices end
+		for i = 1, verticesCount, 2 do
+			local vx = vertices[i]
+			local vy = vertices[i + 1]
+			worldVertices[i] = vx * m00 + vy * m01 + x
+			worldVertices[i + 1] = vx * m10 + vy * m11 + y
+		end
+	end
+
+	return self
+end
+return MeshAttachment

+ 58 - 2
spine-lua/RegionAttachment.lua

@@ -36,9 +36,65 @@ function RegionAttachment.new (name)
 	
 	local self = {
 		name = name,
-		type = AttachmentType.region
+		type = AttachmentType.region,
+		x = 0, y = 0,
+		rotation = 0,
+		scaleX = 1, scaleY = 1,
+		width = 0, height = 0,
+		offset = {},
+		uvs = {},
+		r = 1, g = 1, b = 1, a = 1,
+		path = null,
+		rendererObject = null,
+		regionOffsetX = 0, regionOffsetY = 0,
+		regionWidth = 0, regionHeight = 0,
+		regionOriginalWidth = 0, regionOriginalHeight = 0
 	}
-	
+
+	function self:updateOffset ()
+		local regionScaleX = self.width / self.regionOriginalWidth * self.scaleX
+		local regionScaleY = self.height / self.regionOriginalHeight * self.scaleY
+		local localX = -self.width / 2 * self.scaleX + self.regionOffsetX * regionScaleX
+		local localY = -self.height / 2 * self.scaleY + self.regionOffsetY * regionScaleY
+		local localX2 = localX + self.regionWidth * regionScaleX
+		local localY2 = localY + self.regionHeight * regionScaleY
+		local radians = self.rotation * math.pi / 180
+		local cos = math.cos(radians)
+		local sin = math.sin(radians)
+		local localXCos = localX * cos + self.x
+		local localXSin = localX * sin
+		local localYCos = localY * cos + self.y
+		local localYSin = localY * sin
+		local localX2Cos = localX2 * cos + self.x
+		local localX2Sin = localX2 * sin
+		local localY2Cos = localY2 * cos + self.y
+		local localY2Sin = localY2 * sin
+		local offset = self.offset
+		offset[0] = localXCos - localYSin -- X1
+		offset[1] = localYCos + localXSin -- Y1
+		offset[2] = localXCos - localY2Sin -- X2
+		offset[3] = localY2Cos + localXSin -- Y2
+		offset[4] = localX2Cos - localY2Sin -- X3
+		offset[5] = localY2Cos + localX2Sin -- Y3
+		offset[6] = localX2Cos - localYSin -- X4
+		offset[7] = localYCos + localX2Sin -- Y4
+	end
+
+	function self:computeWorldVertices (x, y, bone, worldVertices)
+		x = x + bone.worldX
+		y = y + bone.worldY
+		local m00, m01, m10, m11 = bone.m00, bone.m01, bone.m10, bone.m11
+		local offset = self.offset
+		vertices[0] = offset[0] * m00 + offset[1] * m01 + x
+		vertices[1] = offset[0] * m10 + offset[1] * m11 + y
+		vertices[2] = offset[2] * m00 + offset[3] * m01 + x
+		vertices[3] = offset[2] * m10 + offset[3] * m11 + y
+		vertices[4] = offset[4] * m00 + offset[5] * m01 + x
+		vertices[5] = offset[4] * m10 + offset[5] * m11 + y
+		vertices[6] = offset[6] * m00 + offset[7] * m01 + x
+		vertices[7] = offset[6] * m10 + offset[7] * m11 + y
+	end
+
 	return self
 end
 return RegionAttachment

+ 24 - 4
spine-lua/Skeleton.lua

@@ -43,7 +43,10 @@ function Skeleton.new (skeletonData)
 		slotsByName = {},
 		drawOrder = {},
 		r = 1, g = 1, b = 1, a = 1,
-		x = 0, y = 0
+		x = 0, y = 0,
+		skin = nil,
+		flipX = false, flipY = false,
+		time = 0
 	}
 
 	function self:updateWorldTransform ()
@@ -65,6 +68,7 @@ function Skeleton.new (skeletonData)
 
 	function self:setSlotsToSetupPose ()
 		for i,slot in ipairs(self.slots) do
+			self.drawOrder[i] = slot
 			slot:setToSetupPose()
 		end
 	end
@@ -90,7 +94,7 @@ function Skeleton.new (skeletonData)
 		local newSkin
 		if skinName then
 			newSkin = self.data:findSkin(skinName)
-			if not newSkin then error("Skin not found: " .. skinName, 2) end
+			if not newSkin then error("Skin not found = " .. skinName, 2) end
 			if self.skin then
 				-- Attach all attachments from the new skin if the corresponding attachment from the old skin is currently attached.
 				for k,v in pairs(self.skin.attachments) do
@@ -103,6 +107,15 @@ function Skeleton.new (skeletonData)
 						if newAttachment then slot:setAttachment(newAttachment) end
 					end
 				end
+			else
+				-- No previous skin, attach setup pose attachments.
+				for i,slot in ipairs(self.slots) do
+					local name = slot.data.attachmentName
+					if name then
+						local attachment = newSkin:getAttachment(i, name)
+						if attachment then slot:setAttachment(attachment) end
+					end
+				end
 			end
 		end
 		self.skin = newSkin
@@ -112,7 +125,7 @@ function Skeleton.new (skeletonData)
 		if not slotName then error("slotName cannot be nil.", 2) end
 		if not attachmentName then error("attachmentName cannot be nil.", 2) end
 		local slotIndex = skeletonData.slotNameIndices[slotName]
-		if slotIndex == -1 then error("Slot not found: " .. slotName, 2) end
+		if slotIndex == -1 then error("Slot not found = " .. slotName, 2) end
 		if self.skin then
 			local attachment = self.skin:getAttachment(slotIndex, attachmentName)
 			if attachment then return attachment end
@@ -135,13 +148,20 @@ function Skeleton.new (skeletonData)
 				return
 			end
 		end
-		error("Slot not found: " .. slotName, 2)
+		error("Slot not found = " .. slotName, 2)
 	end
 
 	function self:update (delta)
 		self.time = self.time + delta
 	end
 
+	function self:setColor (r, g, b, a)
+		self.r = r
+		self.g = g
+		self.b = b
+		self.a = a
+	end
+
 	for i,boneData in ipairs(skeletonData.bones) do
 		local parent
 		if boneData.parent then parent = self.bones[spine.utils.indexOf(skeletonData.bones, boneData.parent)] end

+ 2 - 1
spine-lua/SkeletonData.lua

@@ -36,7 +36,8 @@ function SkeletonData.new ()
 		slotNameIndices = {},
 		skins = {},
 		events = {},
-		animations = {}
+		animations = {},
+		defaultSkin = nil
 	}
 
 	function self:findBone (boneName)

+ 248 - 78
spine-lua/SkeletonJson.lua

@@ -75,8 +75,16 @@ function SkeletonJson.new (attachmentLoader)
 			boneData.x = (boneMap["x"] or 0) * self.scale
 			boneData.y = (boneMap["y"] or 0) * self.scale
 			boneData.rotation = (boneMap["rotation"] or 0)
-			boneData.scaleX = (boneMap["scaleX"] or 1)
-			boneData.scaleY = (boneMap["scaleY"] or 1)
+			if boneMap["scaleX"] ~= nil then
+				boneData.scaleX = boneMap["scaleX"]
+			else
+				boneData.scaleX = 1
+			end
+			if boneMap["scaleY"] ~= nil then
+				boneData.scaleY = boneMap["scaleY"]
+			else
+				boneData.scaleY = 1
+			end
 			if boneMap["inheritScale"] == false then
 				boneData.inheritScale = false
 			else
@@ -124,7 +132,7 @@ function SkeletonJson.new (attachmentLoader)
 				for slotName,slotMap in pairs(skinMap) do
 					local slotIndex = skeletonData.slotNameIndices[slotName]
 					for attachmentName,attachmentMap in pairs(slotMap) do
-						local attachment = readAttachment(attachmentName, attachmentMap, self.scale)
+						local attachment = readAttachment(attachmentName, attachmentMap)
 						if attachment then
 							skin:addAttachment(slotIndex, attachmentName, attachment)
 						end
@@ -159,35 +167,171 @@ function SkeletonJson.new (attachmentLoader)
 		return skeletonData
 	end
 
-	readAttachment = function (name, map, scale)
+	readAttachment = function (name, map)
 		name = map["name"] or name
-		local attachment
+
 		local type = AttachmentType[map["type"] or "region"]
-		attachment = attachmentLoader:newAttachment(type, name)
-		if not attachment then return nil end
+		local path = map["path"] or name
 
+		local scale = self.scale
 		if type == AttachmentType.region then
-			attachment.x = (map["x"] or 0) * scale
-			attachment.y = (map["y"] or 0) * scale
-			attachment.scaleX = (map["scaleX"] or 1)
-			attachment.scaleY = (map["scaleY"] or 1)
-			attachment.rotation = (map["rotation"] or 0)
-			attachment.width = map["width"] * scale
-			attachment.height = map["height"] * scale
+			local region = attachmentLoader:newRegionAttachment(type, name, path)
+			if not region then return nil end
+			region.x = (map["x"] or 0) * scale
+			region.y = (map["y"] or 0) * scale
+			if map["scaleX"] ~= nil then
+				region.scaleX = map["scaleX"]
+			else
+				region.scaleX = 1
+			end
+			if map["scaleY"] ~= nil then
+				region.scaleY = map["scaleY"]
+			else
+				region.scaleY = 1
+			end
+			region.rotation = (map["rotation"] or 0)
+			region.width = map["width"] * scale
+			region.height = map["height"] * scale
+			
+			local color = map["color"]
+			if color then
+				region.r = tonumber(color:sub(1, 2), 16) / 255
+				region.g = tonumber(color:sub(3, 4), 16) / 255
+				region.b = tonumber(color:sub(5, 6), 16) / 255
+				region.a = tonumber(color:sub(7, 8), 16) / 255
+			end
+
+			region:updateOffset()
+			return region
+
+		elseif type == AttachmentType.mesh then
+			local mesh = attachmentLoader:newMeshAttachment(skin, name, path)
+			if not mesh then return null end
+			mesh.path = path 
+			mesh.vertices = getFloatArray(map, "vertices", scale)
+			mesh.triangles = getIntArray(map, "triangles")
+			mesh.regionUVs = getFloatArray(map, "uvs", 1)
+			mesh:updateUVs()
+
+			local color = map["color"]
+			if color then
+				mesh.r = tonumber(color:sub(1, 2), 16) / 255
+				mesh.g = tonumber(color:sub(3, 4), 16) / 255
+				mesh.b = tonumber(color:sub(5, 6), 16) / 255
+				mesh.a = tonumber(color:sub(7, 8), 16) / 255
+			end
+
+			mesh.hullLength = (map["hull"] or 0) * 2
+			if map["edges"] then mesh.edges = getIntArray(map, "edges") end
+			mesh.width = (map["width"] or 0) * scale
+			mesh.height = (map["height"] or 0) * scale
+			return mesh
+
+		elseif type == AttachmentType.skinnedmesh then
+			local mesh = self.attachmentLoader.newSkinnedMeshAttachment(skin, name, path)
+			if not mesh then return null end
+			mesh.path = path
+
+			local uvs = getFloatArray(map, "uvs", 1)
+			vertices = getFloatArray(map, "vertices", 1)
+			local weights = {}
+			local bones = {}
+			for i = 1, vertices do
+				local boneCount = vertices[i]
+				i = i + 1
+				table.insert(bones, boneCount)
+				for ii = 1, i + boneCount * 4 do
+					table.insert(bones, vertices[i])
+					table.insert(weights, vertices[i + 1] * scale)
+					table.insert(weights, vertices[i + 2] * scale)
+					table.insert(weights, vertices[i + 3])
+					i = i + 4
+				end
+			end
+			mesh.bones = bones
+			mesh.weights = weights
+			mesh.triangles = getIntArray(map, "triangles")
+			mesh.regionUVs = uvs
+			mesh:updateUVs()
+
+			local color = map["color"]
+			if color then
+				mesh.r = tonumber(color:sub(1, 2), 16) / 255
+				mesh.g = tonumber(color:sub(3, 4), 16) / 255
+				mesh.b = tonumber(color:sub(5, 6), 16) / 255
+				mesh.a = tonumber(color:sub(7, 8), 16) / 255
+			end
+
+			mesh.hullLength = (map["hull"] or 0) * 2
+			if map["edges"] then mesh.edges = getIntArray(map, "edges") end
+			mesh.width = (map["width"] or 0) * scale
+			mesh.height = (map["height"] or 0) * scale
+			return mesh
+
 		elseif type == AttachmentType.boundingbox then
+			local box = attachmentLoader:newBoundingBoxAttachment(type, name)
+			if not box then return nil end
 			local vertices = map["vertices"]
 			for i,point in ipairs(vertices) do
-				table.insert(attachment.vertices, vertices[i] * scale)
+				table.insert(box.vertices, vertices[i] * scale)
 			end
+			return box
 		end
 
-		return attachment
+		error("Unknown attachment type: " .. type .. " (" .. name .. ")")
 	end
 
 	readAnimation = function (name, map, skeletonData)
 		local timelines = {}
 		local duration = 0
 
+		local slotsMap = map["slots"]
+		if slotsMap then
+			for slotName,timelineMap in pairs(slotsMap) do
+				local slotIndex = skeletonData.slotNameIndices[slotName]
+
+				for timelineName,values in pairs(timelineMap) do
+					if timelineName == "color" then
+						local timeline = Animation.ColorTimeline.new()
+						timeline.slotIndex = slotIndex
+
+						local frameIndex = 0
+						for i,valueMap in ipairs(values) do
+							local color = valueMap["color"]
+							timeline:setFrame(
+								frameIndex, valueMap["time"], 
+								tonumber(color:sub(1, 2), 16) / 255,
+								tonumber(color:sub(3, 4), 16) / 255,
+								tonumber(color:sub(5, 6), 16) / 255,
+								tonumber(color:sub(7, 8), 16) / 255
+							)
+							readCurve(timeline, frameIndex, valueMap)
+							frameIndex = frameIndex + 1
+						end
+						table.insert(timelines, timeline)
+						duration = math.max(duration, timeline:getDuration())
+
+					elseif timelineName == "attachment" then
+						local timeline = Animation.AttachmentTimeline.new()
+						timeline.slotName = slotName
+
+						local frameIndex = 0
+						for i,valueMap in ipairs(values) do
+							local attachmentName = valueMap["name"]
+							if not attachmentName then attachmentName = nil end
+							timeline:setFrame(frameIndex, valueMap["time"], attachmentName)
+							frameIndex = frameIndex + 1
+						end
+						table.insert(timelines, timeline)
+						duration = math.max(duration, timeline:getDuration())
+
+					else
+						error("Invalid frame type for a slot: " .. timelineName .. " (" .. slotName .. ")")
+					end
+				end
+			end
+		end
+
 		local bonesMap = map["bones"]
 		if bonesMap then
 			for boneName,timelineMap in pairs(bonesMap) do
@@ -199,12 +343,11 @@ function SkeletonJson.new (attachmentLoader)
 						local timeline = Animation.RotateTimeline.new()
 						timeline.boneIndex = boneIndex
 
-						local keyframeIndex = 0
+						local frameIndex = 0
 						for i,valueMap in ipairs(values) do
-							local time = valueMap["time"]
-							timeline:setFrame(keyframeIndex, time, valueMap["angle"])
-							readCurve(timeline, keyframeIndex, valueMap)
-							keyframeIndex = keyframeIndex + 1
+							timeline:setFrame(frameIndex, valueMap["time"], valueMap["angle"])
+							readCurve(timeline, frameIndex, valueMap)
+							frameIndex = frameIndex + 1
 						end
 						table.insert(timelines, timeline)
 						duration = math.max(duration, timeline:getDuration())
@@ -220,14 +363,13 @@ function SkeletonJson.new (attachmentLoader)
 						end
 						timeline.boneIndex = boneIndex
 
-						local keyframeIndex = 0
+						local frameIndex = 0
 						for i,valueMap in ipairs(values) do
-							local time = valueMap["time"]
 							local x = (valueMap["x"] or 0) * timelineScale
 							local y = (valueMap["y"] or 0) * timelineScale
-							timeline:setFrame(keyframeIndex, time, x, y)
-							readCurve(timeline, keyframeIndex, valueMap)
-							keyframeIndex = keyframeIndex + 1
+							timeline:setFrame(frameIndex, valueMap["time"], x, y)
+							readCurve(timeline, frameIndex, valueMap)
+							frameIndex = frameIndex + 1
 						end
 						table.insert(timelines, timeline)
 						duration = math.max(duration, timeline:getDuration())
@@ -239,73 +381,71 @@ function SkeletonJson.new (attachmentLoader)
 			end
 		end
 
-		local slotsMap = map["slots"]
-		if slotsMap then
-			for slotName,timelineMap in pairs(slotsMap) do
-				local slotIndex = skeletonData.slotNameIndices[slotName]
-
-				for timelineName,values in pairs(timelineMap) do
-					if timelineName == "color" then
-						local timeline = Animation.ColorTimeline.new()
+		local ffd = map["ffd"]
+		if ffd then
+			for skinName,slotMap in pairs(ffd) do
+				local skin = skeletonData.findSkin(skinName)
+				for slotName,meshMap in pairs(slotMap) do
+					local slotIndex = skeletonData.findSlotIndex(slotName)
+					for meshName,values in pairs(meshMap) do
+						local timeline = Animation.FfdTimeline.new()
+						local attachment = skin:getAttachment(slotIndex, meshName)
+						if not attachment then error("FFD attachment not found: " .. meshName) end
 						timeline.slotIndex = slotIndex
+						timeline.attachment = attachment
 
-						local keyframeIndex = 0
-						for i,valueMap in ipairs(values) do
-							local time = valueMap["time"]
-							local color = valueMap["color"]
-							timeline:setFrame(
-								keyframeIndex, time, 
-								tonumber(color:sub(1, 2), 16) / 255,
-								tonumber(color:sub(3, 4), 16) / 255,
-								tonumber(color:sub(5, 6), 16) / 255,
-								tonumber(color:sub(7, 8), 16) / 255
-							)
-							readCurve(timeline, keyframeIndex, valueMap)
-							keyframeIndex = keyframeIndex + 1
+						local isMesh = attachment.type == AttachmentType.mesh
+						local vertexCount
+						if isMesh then
+							vertexCount = attachment.vertices.length
+						else
+							vertexCount = attachment.weights.length / 3 * 2
 						end
-						table.insert(timelines, timeline)
-						duration = math.max(duration, timeline:getDuration())
-
-					elseif timelineName == "attachment" then
-						local timeline = Animation.AttachmentTimeline.new()
-						timeline.slotName = slotName
 
 						local frameIndex = 0
 						for i,valueMap in ipairs(values) do
-							local time = valueMap["time"]
-							local attachmentName = valueMap["name"]
-							if not attachmentName then attachmentName = nil end
-							timeline:setFrame(frameIndex, time, attachmentName)
+							local vertices
+							if not valueMap["vertices"] then
+								if isMesh then
+									vertices = attachment.vertices
+								else
+									vertices = {}
+									vertices.length = vertexCount
+								end
+							else
+								local verticesValue = valueMap["vertices"]
+								local vertices = {}
+								local start = valueMap["offset"] or 0
+								if scale == 1 then
+									for ii = 1, #verticesValue do
+										vertices[ii + start] = verticesValue[ii]
+									end
+								else
+									for ii = 1, #verticesValue do
+										vertices[ii + start] = verticesValue[ii] * scale
+									end
+								end
+								if isMesh then
+									local meshVertices = attachment.vertices
+									for ii = 1, vertexCount do
+										vertices[ii] = vertices[ii] + meshVertices[ii]
+									end
+								elseif #verticesValue < vertexCount then
+									vertices[vertexCount] = 0
+								end
+							end
+
+							timeline:setFrame(frameIndex, valueMap["time"], vertices)
+							readCurve(timeline, frameIndex, valueMap)
 							frameIndex = frameIndex + 1
 						end
 						table.insert(timelines, timeline)
 						duration = math.max(duration, timeline:getDuration())
-
-					else
-						error("Invalid frame type for a slot: " .. timelineName .. " (" .. slotName .. ")")
 					end
 				end
 			end
 		end
 
-		local events = map["events"]
-		if events then
-			local timeline = Animation.EventTimeline.new(#events)
-			local frameIndex = 0
-			for i,eventMap in ipairs(events) do
-				local eventData = skeletonData:findEvent(eventMap["name"])
-				if not eventData then error("Event not found: " .. eventMap["name"]) end
-				local event = Event.new(eventData)
-				event.intValue = eventMap["int"] or eventData.intValue
-				event.floatValue = eventMap["float"] or eventData.floatValue
-				event.stringValue = eventMap["string"] or eventData.stringValue
-				timeline:setFrame(frameIndex, eventMap["time"], event)
-				frameIndex = frameIndex + 1
-			end
-			table.insert(timelines, timeline)
-			duration = math.max(duration, timeline:getDuration())
-		end
-
 		local drawOrderValues = map["draworder"]
 		if drawOrderValues then
 			local timeline = Animation.DrawOrderTimeline.new(#drawOrderValues)
@@ -353,6 +493,36 @@ function SkeletonJson.new (attachmentLoader)
 			duration = math.max(duration, timeline:getDuration())
 		end
 
+		local events = map["events"]
+		if events then
+			local timeline = Animation.EventTimeline.new(#events)
+			local frameIndex = 0
+			for i,eventMap in ipairs(events) do
+				local eventData = skeletonData:findEvent(eventMap["name"])
+				if not eventData then error("Event not found: " .. eventMap["name"]) end
+				local event = Event.new(eventData)
+				if eventMap["int"] ~= nil then
+					event.intValue = eventMap["int"]
+				else
+					event.intValue = eventData.intValue
+				end
+				if eventMap["float"] ~= nil then
+					event.floatValue = eventMap["float"]
+				else
+					event.floatValue = eventData.floatValue
+				end
+				if eventMap["string"] ~= nil then
+					event.stringValue = eventMap["string"]
+				else
+					event.stringValue = eventData.stringValue
+				end
+				timeline:setFrame(frameIndex, eventMap["time"], event)
+				frameIndex = frameIndex + 1
+			end
+			table.insert(timelines, timeline)
+			duration = math.max(duration, timeline:getDuration())
+		end
+
 		table.insert(skeletonData.animations, Animation.new(name, timelines, duration))
 	end
 

+ 131 - 0
spine-lua/SkinnedMeshAttachment.lua

@@ -0,0 +1,131 @@
+-------------------------------------------------------------------------------
+-- Spine Runtimes Software License
+-- Version 2.1
+-- 
+-- Copyright (c) 2013, Esoteric Software
+-- All rights reserved.
+-- 
+-- You are granted a perpetual, non-exclusive, non-sublicensable and
+-- non-transferable license to install, execute and perform the Spine Runtimes
+-- Software (the "Software") solely for internal use. Without the written
+-- permission of Esoteric Software (typically granted by licensing Spine), you
+-- may not (a) modify, translate, adapt or otherwise create derivative works,
+-- improvements of the Software or develop new applications using the Software
+-- or (b) remove, delete, alter or obscure any trademarks or any copyright,
+-- trademark, patent or other intellectual property or proprietary rights
+-- notices on or in the Software, including any copy thereof. Redistributions
+-- in binary or source form must include this license and terms.
+-- 
+-- THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 ESOTERIC SOFTARE 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.
+-------------------------------------------------------------------------------
+
+local AttachmentType = require "spine-lua.AttachmentType"
+
+local SkinnedMeshAttachment = {}
+function SkinnedMeshAttachment.new (name)
+	if not name then error("name cannot be nil", 2) end
+	
+	local self = {
+		name = name,
+		type = AttachmentType.mesh,
+		bones = nil,
+		weights = nil,
+		uvs = nil,
+		regionUVs = nil,
+		triangles = nil,
+		hullLength = 0,
+		r = 1, g = 1, b = 1, a = 1,
+		path = nil,
+		rendererObject = nil,
+		regionU = 0, regionV = 0, regionU2 = 0, regionV2 = 0, regionRotate = false,
+		regionOffsetX = 0, regionOffsetY = 0,
+		regionWidth = 0, regionHeight = 0,
+		regionOriginalWidth = 0, regionOriginalHeight = 0,
+		edges = nil,
+		width = 0, height = 0
+	}
+
+	function self:updateUVs ()
+		local width, height = self.regionU2 - self.regionU, self.regionV2 - self.regionV
+		local n = #self.regionUVs
+		if not self.uvs or #self.uvs ~= n then
+			self.uvs = {}
+		end
+		if self.regionRotate then
+			for i = 1, n, 2 do
+				self.uvs[i] = self.regionU + self.regionUVs[i + 1] * width
+				self.uvs[i + 1] = self.regionV + height - self.regionUVs[i] * height
+			end
+		else
+			for i = 1, n, 2 do
+				self.uvs[i] = self.regionU + self.regionUVs[i] * width
+				self.uvs[i + 1] = self.regionV + self.regionUVs[i + 1] * height
+			end
+		end
+	end
+
+	function self:computeWorldVertices (x, y, slot, worldVertices)
+		local skeletonBones = slot.skeleton.bones
+		local weights = self.weights
+		local bones = self.bones
+
+		local w, v, b, f = 0, 0, 0, 0
+		local	n = bones.length
+		local wx, wy, bone, vx, vy, weight
+		if #slot.attachmentVertices == 0 then
+			while v < n do
+				wx = 0
+				wy = 0
+				local nn = bones[v] + v
+				v = v + 1
+				while v <= nn do
+					bone = skeletonBones[bones[v]]
+					vx = weights[b]
+					vy = weights[b + 1]
+					weight = weights[b + 2]
+					wx = wx + (vx * bone.m00 + vy * bone.m01 + bone.worldX) * weight
+					wy = wy + (vx * bone.m10 + vy * bone.m11 + bone.worldY) * weight
+					v = v + 1
+					b = b + 3
+				end
+				worldVertices[w] = wx + x
+				worldVertices[w + 1] = wy + y
+				w = w + 2
+			end
+		else
+			local ffd = slot.attachmentVertices
+			while v < n do
+				wx = 0
+				wy = 0
+				local nn = bones[v] + v
+				v = v + 1
+				while v <= nn do
+					bone = skeletonBones[bones[v]]
+					vx = weights[b] + ffd[f]
+					vy = weights[b + 1] + ffd[f + 1]
+					weight = weights[b + 2]
+					wx = wx + (vx * bone.m00 + vy * bone.m01 + bone.worldX) * weight
+					wy = wy + (vx * bone.m10 + vy * bone.m11 + bone.worldY) * weight
+					v = v + 1
+					b = b + 3
+					f = f + 2
+				end
+				worldVertices[w] = wx + x
+				worldVertices[w + 1] = wy + y
+				w = w + 2
+			end
+		end
+	end
+
+	return self
+end
+return MeshAttachment

+ 4 - 1
spine-lua/Slot.lua

@@ -38,7 +38,10 @@ function Slot.new (slotData, skeleton, bone)
 		data = slotData,
 		skeleton = skeleton,
 		bone = bone,
-		r = 1, g = 1, b = 1, a = 1
+		r = 1, g = 1, b = 1, a = 1,
+		attachment = nil,
+		attachmentTime = 0,
+		attachmentVertices = nil
 	}
 
 	function self:setColor (r, g, b, a)

+ 4 - 2
spine-lua/SlotData.lua

@@ -36,7 +36,9 @@ function SlotData.new (name, boneData)
 	local self = {
 		name = name,
 		boneData = boneData,
-		r = 1, g = 1, b = 1, a = 1
+		r = 1, g = 1, b = 1, a = 1,
+		attachmentName = nil,
+		additiveBlending = false
 	}
 
 	function self:setColor (r, g, b, a)
@@ -46,6 +48,6 @@ function SlotData.new (name, boneData)
 		self.a = a
 	end
 
-	return self;
+	return self
 end
 return SlotData