using NLog; using Spine.Interfaces; using Spine.Interfaces.Attachments; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Spine { /// /// 命中测试等级枚举值 /// public enum HitTestLevel { None, Bounds, Meshes, Pixels } public static class SpineExtension { private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private static readonly SFML.Graphics.RenderTexture _renderTex; // XXX: 在此保留一个静态变量, 并且没有使用 Dispose 进行资源释放 static SpineExtension() { _renderTex = new(1, 1); _renderTex.SetActive(false); } /// /// 命中检测精确度等级 /// public static HitTestLevel HitTestLevel { get; set; } /// /// 命中测试时输出命中的插槽名称 /// public static bool LogHitSlots { get; set; } /// /// 获取当前状态包围盒 /// public static void GetBounds(this ISlot self, out float x, out float y, out float w, out float h) { float[] vertices = new float[8]; int verticesLength = 0; var attachment = self.Attachment; switch (attachment) { case IRegionAttachment: case IMeshAttachment: verticesLength = attachment.ComputeWorldVertices(self, ref vertices); break; default: break; } if (verticesLength > 0) { float minX = int.MaxValue; float minY = int.MaxValue; float maxX = int.MinValue; float maxY = int.MinValue; for (int ii = 0; ii + 1 < verticesLength; ii += 2) { float vx = vertices[ii]; float vy = vertices[ii + 1]; minX = Math.Min(minX, vx); minY = Math.Min(minY, vy); maxX = Math.Max(maxX, vx); maxY = Math.Max(maxY, vy); } x = minX; y = minY; w = maxX - minX; h = maxY - minY; } else { x = self.Bone.WorldX; y = self.Bone.WorldY; w = 0; h = 0; } } /// /// 获取当前状态包围盒 /// public static void GetBounds(this ISkeleton self, out float x, out float y, out float w, out float h) { float minX = int.MaxValue; float minY = int.MaxValue; float maxX = int.MinValue; float maxY = int.MinValue; foreach (var slot in self.IterDrawOrder()) { if (slot.A <= 0 || !slot.Bone.Active || slot.Disabled) continue; float[] vertices = new float[8]; int verticesLength = 0; var attachment = slot.Attachment; switch (attachment) { case IRegionAttachment: case IMeshAttachment: verticesLength = attachment.ComputeWorldVertices(slot, ref vertices); break; default: break; } if (verticesLength > 0) { for (int ii = 0; ii + 1 < verticesLength; ii += 2) { float vx = vertices[ii]; float vy = vertices[ii + 1]; minX = Math.Min(minX, vx); minY = Math.Min(minY, vy); maxX = Math.Max(maxX, vx); maxY = Math.Max(maxY, vy); } } else { var boneX = slot.Bone.WorldX; var boneY = slot.Bone.WorldY; minX = Math.Min(minX, boneX); minY = Math.Min(minY, boneY); maxX = Math.Max(maxX, boneX); maxY = Math.Max(maxY, boneY); } } if (minX >= int.MaxValue || minY >= int.MaxValue || maxX <= int.MinValue || maxY <= int.MinValue) { x = self.X; y = self.Y; w = 0; h = 0; } else { x = minX; y = minY; w = maxX - minX; h = maxY - minY; } } /// /// 命中测试, 当插槽全透明或者处于禁用或者骨骼处于未激活则无法命中 /// public static bool HitTest(this ISlot self, float x, float y) { if (self.A <= 0 || !self.Bone.Active || self.Disabled) return false; if (HitTestLevel == HitTestLevel.None || HitTestLevel == HitTestLevel.Bounds) { self.GetBounds(out var bx, out var by, out var bw, out var bh); return x >= bx && x <= bx + bw && y >= by && y <= by + bh; } else if (HitTestLevel == HitTestLevel.Meshes || HitTestLevel == HitTestLevel.Pixels) { float[] vertices = new float[8]; int[] triangles; float[] uvs; SFML.Graphics.Texture tex; switch (self.Attachment) { case IRegionAttachment regionAttachment: _ = regionAttachment.ComputeWorldVertices(self, ref vertices); triangles = regionAttachment.Triangles; uvs = regionAttachment.UVs; tex = regionAttachment.RendererObject; break; case IMeshAttachment meshAttachment: _ = meshAttachment.ComputeWorldVertices(self, ref vertices); triangles = meshAttachment.Triangles; uvs = meshAttachment.UVs; tex = meshAttachment.RendererObject; break; default: return false; } var trianglesLength = triangles.Length; for (int i = 0; i + 2 < trianglesLength; i += 3) { var idx0 = triangles[i] << 1; var idx1 = triangles[i + 1] << 1; var idx2 = triangles[i + 2] << 1; float x0 = vertices[idx0] - x, y0 = vertices[idx0 + 1] - y; float x1 = vertices[idx1] - x, y1 = vertices[idx1 + 1] - y; float x2 = vertices[idx2] - x, y2 = vertices[idx2 + 1] - y; float c0 = Cross(x0, y0, x1, y1); float c1 = Cross(x1, y1, x2, y2); float c2 = Cross(x2, y2, x0, y0); // 判断是否全部同号 (或为 0, 点在边上) if (c0 >= 0 && c1 >= 0 && c2 >= 0 || c0 <= 0 && c1 <= 0 && c2 <= 0) { if (HitTestLevel == HitTestLevel.Meshes) return true; float u0 = uvs[idx0], v0 = uvs[idx0 + 1]; float u1 = uvs[idx1], v1 = uvs[idx1 + 1]; float u2 = uvs[idx2], v2 = uvs[idx2 + 1]; float inv = 1 / (c0 + c1 + c2); float w0 = c1 * inv; float w1 = c2 * inv; float w2 = c0 * inv; float u = u0 * w0 + u1 * w1 + u2 * w2; float v = v0 * w0 + v1 * w1 + v2 * w2; var texW = tex.Size.X; var texH = tex.Size.Y; // 把要判断的那个像素点渲出来 using var view = _renderTex.GetView(); using var vertexArray = new SFML.Graphics.VertexArray(SFML.Graphics.PrimitiveType.Points, 1); vertexArray[0] = new(view.Center, new SFML.System.Vector2f(u * texW, v * texH)); // XXX: 此处 RenderTexture 不能临时用临时释放, 由于未知原因如果短时间快速创建释放 RenderTexture 可能让程序卡死 _renderTex.SetActive(true); _renderTex.Clear(SFML.Graphics.Color.Transparent); _renderTex.Draw(vertexArray, new(tex)); _renderTex.Display(); _renderTex.SetActive(false); using var img = _renderTex.Texture.CopyToImage(); var pixel = img.GetPixel(0, 0); return pixel.A > 0; } } return false; } else { throw new NotImplementedException(HitTestLevel.ToString()); } } /// /// 逐插槽的命中测试, 命中后会提前返回结果中止计算 /// public static bool HitTest(this ISkeleton self, float x, float y) { if (HitTestLevel == HitTestLevel.None) { self.GetBounds(out var bx, out var by, out var bw, out var bh); return x >= bx && x <= bx + bw && y >= by && y <= by + bh; } bool hit = false; string hitSlotName = ""; foreach (var st in self.IterDrawOrder().Reverse()) { if (st.HitTest(x, y)) { hit = true; hitSlotName = st.Name; break; } } if (hit && LogHitSlots) { _logger.Debug("Hit ({0}): [{1}]", self.Name, hitSlotName); } return hit; } /// /// 向量叉积 /// private static float Cross(float x0, float y0, float x1, float y1) => x0 * y1 - y0 * x1; } }