# Spine 粒子系统 · 开发总纲领 ## 一、终极目标(唯一目标) **在 Spine 编辑器内部新增一个"粒子窗口"**,用于调整 Spine 中**骨骼 / 插槽 / 附件对象的行为**: - 在编辑器里选中某个骨骼、插槽或附件,为它配置粒子发射行为; - 粒子的位置、旋转、缩放跟随该对象在动画中的实际运动; - 最终导出为**Spine 自身可以使用的动画数据**(与 Spine 的导出流程兼容); - 体验上要像 Spine 自带面板(权重 / 层级树 / 预览)一样,出现在编辑器窗口内部。 > 不是独立 app,不是外挂工具,不是浏览器页面。**必须是在 Spine 编辑器窗口内部的窗口/面板。** ## 二、硬性约束 1. **不破坏 Spine 原有功能**:不改动其原有面板、菜单、导出逻辑的行为; 2. **导出兼容**:新增功能产生的数据必须是 Spine 能识别/导入的形式(Spine JSON / .spine 工程); 3. **用户真实安装不被修改**:`/Applications/Spine.app` 与 `~/Library/Application Support/Spine` 保持原样; 所有试验在副本与隔离数据目录中进行。 ## 三、技术现实(已实测确认) 目标 App 是 Java + libGDX(Scene2D UI)+ LWJGL/GLFW/OpenAL 的桌面程序,且这份包被二次打包并加固: | 事实 | 影响 | | --- | --- | | `launcher.jar` 实际未被使用 | 改它无效 | | 启动类/编辑器类内嵌在 `libjvm`(约 49MB 数据段,加密) | 需要绕过其类来源 | | JVMTI agent 加载被裁(`-agentpath` 无效) | 常规 agent 方案不可用 | | classpath 注入无效(自定义类加载器 `s.LPt4`) | 需要 JNI 注入 | | 类被混淆(libGDX 核心类 → `s.*` 包) | 只能按结构/签名识别 | | 少量反射/JNI 操作稳定,**大量循环遍历会使其崩溃** | 必须"小步定向"操作 | | 改写编辑器内部对象可能导致 fatal error | 不能粗暴改内部状态 | | 面板没有运行期注册 API(只有布局类 `s.oxX`) | 无法"注册"新面板 | **已打通的能力**:通过 `DYLD_INSERT_LIBRARIES` + fishhook 重绑定 `_dlsym` 拦截 `JNI_CreateJavaVM`, 再用 JNI `DefineClass` 把我们的类注入 JVM 并执行;可枚举全部类(2365 个)、读取任意对象字段、 修改现有控件并让界面真实生效(工具栏按钮文字已验证)。 ## 四、路线与现状 | 路线 | 状态 | | --- | --- | | 官方插件 / 脚本 API | 不存在 | | JVMTI agent / javaagent | 被运行时裁掉,不可用 | | 改 `launcher.jar` / classpath | 无效(未被使用) | | 直接改编辑器内部对象加新控件(`addActor`) | **不渲染** | | 改写编辑器内部对象(面板标签等) | 会触发 fatal error,不可取 | | 在编辑器窗口内自绘(hook `glfwSwapBuffers` + OpenGL) | **实测失败**:即便保存/恢复 GL 状态并保留原有 swap 调用,仍会导致编辑器画面全黑。原因是该编辑器的呈现依赖自身 GL 管线状态,外挂式清屏/绘制会破坏其呈现。已回滚 | | 独立面板窗口 | 已实现(贴靠编辑器右侧),但**不符合终极目标**,仅作过渡 | ## 五、下一步(按优先级) 1. **窗口内集成的新方向**(旧的"外挂式 GL 自绘"已实测失败并回滚) - 方向 A:**hook 编辑器自身的绘制调用**(它用 libGDX/LWJGL 走 OpenGL),在它自己的绘制流程里插入我们的绘制, 而不是在帧末清屏自绘 —— 这样不会破坏它的呈现管线; - 方向 B:把编辑器界面**整帧渲染到离屏缓冲**再合成(复杂度高,暂不考虑); - 方向 C:改为扩展它自己的 UI 数据(面板集合是编译期固定的,需要更深的逆向)。 2. 面板内容先做占位(列表 + 参数条 + 按钮),确认"窗口内可见、可点击"; 3. 接入真实数据:读取 Spine 当前选中的骨骼/插槽/附件(可用已验证的对象读取能力), 把它的动画运动导出为粒子参数; 4. 导出链路:面板产出 Spine JSON → 用已验证的 `Spine --import` 流程导入编辑器。 ## 六、✅ 已打通:窗口内 UI 注入(AppKit 叠加方案) **方法**:我们的 dylib 运行在 Spine 进程内,直接用 **AppKit** 往编辑器窗口的 `contentView` 里 叠加一个原生 `NSView` 面板(真实 `NSButton` / `NSSlider` / `NSTextField`)。 - 不碰 OpenGL,因此**不会破坏编辑器渲染**(对比:帧末 GL 自绘会导致全黑,已废弃); - UI 位于编辑器窗口**内部**,可随窗口缩放(`autoresizingMask`),与编辑器面板观感一致; - 实测已在 Spine 编辑器窗口右侧成功显示「粒子系统」面板(标题 + 两个按钮 + 五条参数滑条)。 实现位置:`native-hook/ui.m`(AppKit 面板)+ `native-hook/hook.c`(JVM 建好后延迟注入)。 **下一步:与场景对象交互** 1. 在面板里显示"当前选中的骨骼 / 插槽 / 附件"——通过已验证的 JNI 反射能力读取编辑器当前选择; 2. 面板参数(发射速率等)改变时,写入我们自己的粒子数据结构; 3. 导出:产出 Spine JSON → 交给 `Spine --import` 变成编辑器可用的工程/动画数据。 ## 七、相关文档 ### 阶段二(本轮)· 面板路线 —— 停止「往它内部塞/掏」 阶段一证明了「在编辑器内部新增面板」在加固运行时上代价过高:`addActor` 不渲染、 改内部控件触发 fatal error、没有运行期注册 API。因此阶段二把力量全部放到**已经跑通的那条线**上: 1. **面板可拖动 / 可折叠**,不遮挡编辑器右侧面板 —— 已实现,并已挂进编辑器窗口(`native-hook/ui.m`); 2. **面板内自有粒子数据模型**(26 项参数、多发射器、JSON 落盘)—— 已实现(`panel_model.{h,m}`), 只动我们自己的结构,不碰编辑器对象; 3. **定向少量读取**当前选中的骨骼/插槽/附件 —— 通道与硬上限已实现(`selection.c`), 还差「编辑器类加载器」的抓取(当前读不到时面板会明说,目标名也可手填); 4. **导出**:面板产出 Spine JSON → `Spine --import` 生成 `.spine` —— 端到端已跑通 (`panel_import.m`,命令行与已验证的连接器一致)。 > ⚠️ 本轮还定位出阶段一遗留的一个坑:`defineClass1` 拦截 + `DefineClass` 打补丁 > (经典 dump 类字节码那套钩子)会让这套加固运行时在启动阶段 SIGSEGV。 > 它只是诊断功能,现已**默认关闭**(`SPINE_HOOK_DUMP_CLASSES=1` 才打开), > 关掉后编辑器启动正常、面板如期出现。 细节、实测证据与自测方法见 `阶段二-面板实现.md`。 ### 面板集合深挖进展(最新) - 面板内容类 `s.fOM`(基类 `s.Erk` → Group),面板标签页 `s.ILX`/`s.XDB`(ImageTextButton), 标签页里存的是**英文键名**(如 `"Outline"`),中文是本地化渲染出来的; - 尝试在运行期对象图里找"装着多个 `s.fOM` 的容器(Map/集合)"→ **未找到**: 说明面板不是放在一个可枚举的 Java 集合里,而是被**直接字段引用**(很可能在停靠管理器 `s.oxX` 或编辑器根对象里); - 下一步:定位 `s.oxX`(有 `_(Erk, boolean, boolean, float…)` 布局方法的类)的实例,dump 它的字段, 找出面板引用列表 → 判断能否在运行期扩展面板集合。 - 技术分析细节:`阶段一分析.md` - 注入器实现:`native-hook/hook.c` - 引导类(JNI 注入入口):`spine-lab/build/plugin-src/com/spineparticle/Boot.java`