惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
C
Check Point Blog
宝玉的分享
宝玉的分享
Attack and Defense Labs
Attack and Defense Labs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - Franky
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
罗磊的独立博客
S
SegmentFault 最新的问题
S
Secure Thoughts
T
Troy Hunt's Blog
J
Java Code Geeks
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
W
WeLiveSecurity
Help Net Security
Help Net Security
S
Security @ Cisco Blogs
T
Threatpost
Apple Machine Learning Research
Apple Machine Learning Research
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V2EX - 技术
V2EX - 技术
T
Tor Project blog
S
Security Affairs
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
腾讯CDC
博客园 - 司徒正美
The Last Watchdog
The Last Watchdog
N
News | PayPal Newsroom
博客园 - 聂微东
小众软件
小众软件
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
爱范儿
爱范儿
C
CERT Recently Published Vulnerability Notes
AI
AI
N
News and Events Feed by Topic
C
Cybersecurity and Infrastructure Security Agency CISA
O
OpenAI News
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 最新话题
T
Threat Research - Cisco Blogs
雷峰网
雷峰网
NISL@THU
NISL@THU
V
Visual Studio Blog

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog
spine_flutter 的重叠绘制问题 | Yesterday17's Blog
Yesterday17 · 2020-06-03 · via Yesterday17's Blog

大家好,好久不见,我是某昨。

最近依然是在写 Flutter 方面的内容,这次的需求是用 Flutter 渲染 Spine 动画。如果你不知道 Spine 的话可以尝试搜索一下,简单来说就是一套成熟的 2D?骨骼动画系统。

ToC

  • 先行者
    • spine_core
    • spine_flutter
  • 问题
    • 千手观音
    • 拼接处空白
  • 开始之前
    • 网格附件(mesh
    • Path
    • Paint
    • Canvas
  • 手怎么多了?
    • 修复 Debug 模式
    • 定位实际问题
  • 除此之外
  • 结语

先行者

在全文之前我需要声明一下,整个框架的移植并不是我完成的,而是基于了先行者的成果。总的来说是基于以下这两个库:

spine_core

https://github.com/jtakakura/spine_core

这个库负责的是 Spine 文本内容的解析,对应的是 spine-ts/core 的部分。Spine 需要解析的内容是 atlasjson 文件,并由此生成 Skeleton 对象。这部分的代码非常稳定,至少到目前位置没出现什么这个层面的问题,感谢。

这个库也发布在了 Dart Package[1]

spine_flutter

https://github.com/jtakakura/spine_flutter

这个库负责的就是渲染方面的内容了,对应的是 spine-ts/canvas 的部分。master 分支的代码逻辑很乱,绕来绕去看着令人头秃,还把 spine_core 的所有代码都附上了。dev2 分支就干净多了,整体逻辑很清楚,但缺点是有些部分没有完成。我们的开发就是基于这个分支的。

问题

在整个开发过程中,我们总共有两个问题:千手观音和拼接处空白。

千手观音

@KAAAsS
@KAAAsS

可以很明显地发现,めぐる 的手臂多出了好几根,手也多出了好几只。这就是这篇文章解决的主要问题了。

拼接处空白

这个问题其实最后也没有解决,要想解决这个问题以我现在的水平恐怕是做不到的吧(叹)。问题的根源在于浮点数精度,导致三角形的绘画必定中间会出现白色,就像下面这样:

仔细观察 めぐる 鼻子附近的部分,可以看到非常明显的淡灰色细线,这就是问题所在。如果我们开启了 Debug Paint,可以发现这部分其实就是三角形的交界部分:

问题的原因应该算是找到了,那该怎么修复呢?如果我找到了解决方案的话,那应该就是下一篇博客的事了吧(笑)

开始之前

在开始之前,我们首先需要了解一些背景知识。

网格附件(mesh

这个是我们这次渲染的主角。上面提到了三角形的问题,而三角形的出现也恰恰和 mesh 有着密不可分的关系。

引用官网的话[2],网格支持在图片内设置多边形,之后可操纵多边形的顶点,以有效的方式让图片弯曲和变形。简单来说,就是通过多边形的形变产生动画效果。具体的方式看下面这张图就足够了:

是不是看到了大量的三角形?mesh 需要绘制的也就是这些三角形。

Path

Path,也就是路径,简单来说就是类似画笔一样的存在。通过 Path 当前点的不断移动,我们可以通过历史轨迹形成一条当前点的移动路径

这个路径可以直接绘制出来——也就是绘制线条;也可以填充它的内部——也就是绘制图形;还可以在它上面进行 transform——施加形变。

Paint

Paint 是一个 Flutter 的概念,表示的是当前将要绘制到 canvas 上的样式。没错,不同于 JavaScript 的全局样式,Flutter 中有着更细致的样式划分。这也使得 Flutter 的渲染不需要像 JS 渲染那样需要 save/restore 那么多次上下文。

Canvas

这个大家应该都知道吧,我就不解释了(逃

手怎么多了?

修复 Debug 模式

遇到问题要做的第一件事就是定位问题的发生点。为了看起来方便,我这里首先修复了 debugRendering 的部分:

if (_debugRendering) {

final Path path = Path()

..moveTo(x0, y0)

..lineTo(x1, y1)

..lineTo(x2, y2)

..lineTo(x0, y0);

canvas.drawPath(

path,

Paint()

..style = PaintingStyle.stroke

..color = Colors.green

..strokeWidth = 1,

);

}

这段代码就算没有了解过 canvas 的读者应该也能看明白吧。首先是创建了一个 Path,通过三个点构成了一个三角形;然后在 canvas 上用绿色、宽度为 1 的 stroke 画出了这条 Path

补全了这部分代码之后,我们来看一下效果:

可以发现,手和手臂的部分明显出现了三角形的重叠。于是我们可以断定:是手多画了

定位实际问题

接下来的这一步是最耗时的,也是最令人无奈的。在不断的测试中,我发现有时候减少 Slot 的绘制会使结果正常。最后也正是这个实验结果促使我进行了一波插桩分析,并且发现了令人惊喜的线索:

多绘制的身体部分都存在 "color": "ffffff00" 这一条目。

当我得出这个结论的时候,其实问题就已经解决了。因为作者注释掉了和这个有关的某几行代码:

这里也可以看到很明显的移植迹象:没有找到对应语言中的替代品,因此暂时注释处理。但在这里的注释就要了老命了。这使得所有的 Slot 都以 100% 透明度进行绘制,导致本该因透明度而被隐藏的 Slot 也被绘制了出来。因此解决方案也很简单,只要让 Paint 带上这个透明度就行了。

至此,千手迷案也就告一段落了。

除此之外

除了解决上面这个问题,其实我还做了一些更改,不过和这个相比就微不足道了。

spine_flutter 默认是从 assets 中加载素材的。需要加载的素材上文也提到了:atlasjson 和图片。而我实际应用中是从网络上获取的,因此我增加了直接提供资源内容的素材加载方式。当存在对应的素材时,就不在本地查找了。

结语

整个过程其实持续的时间挺长的。因为不了解,因此花了大量的时间去理解源码的内容,并且经过了挺长时间的比对才最终确定是基于什么移植的。确定了这个其实之后就是比对源码的过程了,于是我又花了挺多的时间比对两份有差异的源码……

中间有想过直接放弃治疗用 Webview 好了,感谢 Flutter 的复杂性拯救了我(

目前修改过的源码已经丢到 GitHub 上了:

改了一下名字,看上去舒服一点(

由于原作者已经两年没更新了,而且也没有在 Dart Packages 上发布这个包,我也就不 PR 过去了(笑

嘛,就是这样(