Jianghu · 双锤起跳攻击(webm 透明视频)方案
Jianghu · 双锤起跳攻击方案
本地生图 + 本地 H3 出片 → 抠绿转 alpha → VP9 透明 webm → 前端播放与位移控制
2026-10-11 拟 · 状态:✅ 已实现(素材已出,可看效果页) · 验收页: 双锤攻击验收.html · 配套: 本地H3动作生成方案.html
✅ 10-11 晚已按本文档跑通,老大拍板的三项参数:
- 攻击判定 = 3 次(落地砸地 + 左横扫 + 右横扫各一次),非两次。
- 角色 = 现成锤客(hammerer_m 的立绘,不重新设计人物)。
- 动作名「震山锤」,速度加快,全程 ≈2.5 秒。
实测落到 拆两段才能做到(见 §4.3):起跳砸地 56 帧 2.333s + 横扫 22 帧 0.917s×2。 产出与踩到的坑见 §12 实测结果。
一、这次要做什么
1.1 需求原文(老大 2026-10-11)
1 动作设计:(a) 双锤攻击动作,整个动画过程原地起跳、无位移,不限帧数与时长。 (b) 流程:起跳(原地向上跳,双锤拖在身后)→ 顶点(最高处双锤朝上)→ 下落(双锤依然拖在上方)→ 落地(双锤砸向地面,然后左右各砸一次)。
2 特效:特效分开制作;双锤砸向地面时,表现土 + 金光四溅。
3 攻击判定:从起跳到落地,整个动画过程总共算两次攻击。
4 前端控制:攻击技能动作要做专门的前端控制 —— 角色起跳后要准确落到目标点(NPC/Boss 身前)。
5 位移与表现:播放从起跳到落下的横向位移,落地后两次攻击;动画精灵的位移由前端控制。先把这一条视频做出来,用 webm 格式保证背景透明。
6 特效处理:特效与动画分开,落地与两次攻击时才加,同样由前端控制。
1.2 拆成五件可验证的事
| # | 工作项 | 产物 | 验收方式 |
|---|---|---|---|
| W1 | 绿幕首帧(双锤角色 + 跳跃余量构图) | _gen_actions/_jump/..._first.png | 截图看:角色占屏高 ≈36%、脚底基线 86%、上方留白 ≥40% |
| W2 | H3 出片(4s 档 / 107 帧 / 24fps) | _jump_atk.mp4(绿幕) | 抽帧拼接触图:六阶段依次出现、无横向漂移、无烘入特效 |
| W3 | 抠绿 → 逐帧 alpha → VP9 透明 webm | hammer_jump_atk.webm(yuva420p) | 已实测:-c:v libvpx-vp9 解码 alpha 完整(含半透明) |
| W4 | 特效两条(土石 / 金光) | js_fx_dirt.webm + js_fx_gold.webm | 独立可播、背景透明、能与角色帧叠加不打架 |
| W5 | 前端接入(播放 + 位移 + 判定 + 特效叠加) | 改 src/idle/ 三个文件 | 无头截图三连:起跳中 / 落地砸 / 第二段;落点误差 ≤ 目标 ±2px |
✅ 三处已定稿(原为待确认项):
- 「三次攻击」 —— 落地砸地 = 第 1 次,左横扫 = 第 2 次,右横扫 = 第 3 次。
- 用现成锤客 ——
hammerer_m;原立绘只有一把锤,已程序化补出第二把(见 §12.1)。
- 震山锤 · 全程 ≈2.5 秒 —— 见 §4.3 的时长换算与拆段结论。
二、为什么要走 webm(与现有精灵表的关系)
项目现有角色动画全是精灵表:H3 出绿幕 mp4 → 抽帧抠绿 → 逐帧对齐 → 拼成一张 .webp + 一份 .json(帧数/fps/基线比例),前端在 canvas 上按帧序号画。
这条链跑了约 400 套动作,但它有三个前提,这次全部不成立:
| 前提 | 现有动作(idle/run/attack…) | 本次双锤起跳 | 结论 |
|---|---|---|---|
| 时长 | 0.7 ~ 2.0 s(最多 56 帧) | ≈ 4.46 s / 107 帧 | 帧数翻倍,拼表体积与内存都吃紧 |
| 画面纵向范围 | 角色占屏高 ~78%,几乎不上下动 | 起跳后整体位移 + 双锤上举,纵向跨度 ≈ 站姿的 2.3 倍 | 逐帧等宽 webp 要按最大跨度定画布 → 单帧高度暴涨,大量空白 |
| 播放控制 | 帧序号由 anim.js 算,对齐用 | 需要精确 seek 到某一个时刻(判定点、特效点) | 视频的 currentTime 天生就是时间轴,比帧序号更直接 |
✅ 本次已实测(2026-10-11,不是推测):VP9 + alpha 这条路在本机完全可用。
| 验证项 | Method | 结果 |
|---|---|---|
| 编码器可用 | 本机 ffmpeg 7.1 列编码器 | 有 libvpx-vp9 |
| alpha 是否真的编进去 | 8 帧 RGBA PNG(实心圆 + 半透明块)→ -pix_fmt yuva420p → webm → 解码回 PNG | 透明区 alpha=0(74.9% 像素)、半透明块 alpha=120 精确还原 |
| 浏览器能不能读 | Playwright Chromium 播 webm,ctx.drawImage(video) 后 getImageData | 角落 alpha=0、圆内 255、半透明 120 —— 逐像素一致 |
🔴 兼容性红线:Safari / iOS 不支持 VP9 alpha。桌面 Chrome / Edge 完全支持(含 drawImage)。 ⇒ 本方案必须有兜底路径:用同一批 .webm 导出低帧率精灵表(如 12fps × 54 帧)作为降级素材, 运行时用一条能力探测决定走哪条路(探测方法见 §9.1)。不能只有 webm 一份素材。
三、技术路线总览
1 立绘锤客 · 透明底 →
2 绿幕首帧竖构图 + 跳跃余量 →
3 H3 出片384×704 / 107 帧 / 24fps →
4 抽帧抠绿去绿溢 → RGBA PNG →
5 编码VP9 + yuva420p → webm →
6 前端canvas drawImage + 位移 + 判定
与现有链路的差别只在 456:4 少一步"对齐缩放/拼表"(视频不做逐帧对齐,几何靠首帧构图保证);5 从 webp 拼表换成 VP9 视频;6 从"按帧序号画"换成"按 currentTime 播 + 读 currentTime 触发判定"。
为什么4不做逐帧对齐:精灵表必须对齐,是因为它要把每一帧切成等宽小图再按同一基线摆,不对齐人物会抖。 视频是整幅连续播放,帧与帧天然连续;只要首帧构图把"脚底基线"和"横向中心"钉死,播放全程就不会漂。 少这一步同时去掉了"逐帧裁切 → 单帧尺寸不一致"这类最难查的问题。
四、动作设计与时序
4.1 六阶段分解(按 4s 档 / 107 帧 / 4.458s 折算)
| 阶段 | 时间窗 | 帧 | 动作描述(也是提示词要写清的六件事) | 前端职责 |
|---|---|---|---|---|
| 1 蓄势 | 0.00 – 0.45s | 0–10 | 双脚站定,微屈膝下沉,双锤自然垂在身侧 | 不动 |
| 2 起跳 | 0.45 – 1.35s | 11–32 | 原地垂直跃起,双锤拖在身后下方,身体后倾 | 横向位移开始(easeOut) |
| 3 顶点 | 1.35 – 1.85s | 33–44 | 最高点短暂滞空,双锤高举过头朝上 | 位移到达目标点 |
| 4 下落 | 1.85 – 2.60s | 45–62 | 身体下落,双锤依旧拖在上方(保持蓄力) | 保持落点 |
| 5 落地砸 | 2.60 – 3.15s | 63–76 | 双脚落地,双锤正面砸向地面,身体前压 | 🔴 第 1 次攻击判定 + 特效 A |
| 6 左右连砸 | 3.15 – 4.46s | 77–106 | 起身,向左横扫一次、回身向右横扫一次 | 🔴 第 2 次攻击判定 + 特效 B/C |
上表的时间点是设计值(按比例分摊),用来定义"前端在什么时刻做什么"。 实际出片后必须以肉眼核对 + 抽帧定位校准,把六阶段真实帧号量出来回填(§9.2 的验收动作), 再据此改前端常量。不要反过来把设计值当真值。
4.2 三次攻击判定(已定稿)
| 判定 | 触发时刻 | 对应画面 | 命中范围 |
|---|---|---|---|
| 第 1 击 | slam 段 ≈77%(1.80s) | 双锤触地 + 土/金光炸开 | 前方单体(眼前的目标) |
| 第 2 击 | sweep 段 ≈50%(+0.46s) | 横扫过顶、锤头到左下方 | 前方 + 左向小范围 |
| 第 3 击 | sweep 镜像段 ≈50% | 同上(镜像,向右) | 前方 + 右向小范围 |
右横扫复用同一份 sweep 素材,前端水平镜像播放(canvas 一个 scale(-1,1))—— 省一半素材,两个方向还天然对称。
4.3 时长档位换算(H3 硬约束:帧数必须落在 17k+5 上)
| 目标秒数 | 吸附后帧数 | 实际时长 @24fps | 评价 |
|---|---|---|---|
| 2.3s | 56 | 2.333s | ✅ 全场最贴近"2.5 秒"的档位(差 -0.167s) |
| 2.5s | 73 | 3.042s | 反而更远(差 +0.542s)—— 网格是向上吸附的 |
| 0.7s | 22 | 0.917s | 横扫段用这档 |
🔴 实测结论:单段 56 帧装不下 6 拍,必须拆。
- v1(2.33s 单段 / 六拍并列写法):起跳段把双锤横举(不是拖身后)、结尾整个人横躺且扫出画面右边界。
- v2(改成 FIRST/SECOND… 时间顺序写法):转好,但空中占了 16/56 帧(29%),而"左右横扫"整段没出现。
- ⇒ H3 对"多拍连续动作"的粒度大约是 3–4 拍。拆成「起跳砸地(4 拍)」+「横扫(1 拍)」后,
两段都一次过(见 §12)。
总时长:2.333 + 0.917×2 = 4.17s(自然速度)。 要压到 2.5s 就把播放速率提到 ≈1.65× —— 验收页上有速度滑块,当场就能试出来该多快。
五、生成环节:画幅、首帧与提示词
5.1 画幅:必须竖构图(本条是本次最容易翻车的地方)
算一笔账。设角色站姿占屏高 h,脚底基线在屏高 b 处(自上而下)。顶点时双锤举过头顶, 相对脚底高度 ≈ 1.5h;跳跃高度按 0.8h 估。要求最坏情况不出上边界:
b - 0.8h - 1.5h ≥ 0 → h ≤ b / 2.3
| 方案 | 画幅 | h 取值 | 人物像素 | 结论 |
|---|---|---|---|---|
| 沿用现有方图 | 448 × 448 | b=0.92 → h ≤ 0.40 | ≈161px | ❌ 角色太小,一放大就糊 |
| 第一版(已作废) | 384 × 704 | b=0.86 → h ≤ 0.374 | ≈239px | ⚠️ 挂机条带够用,但技能动画要放大展示 ⇒ 不够 |
| 第二版 | 512 × 896 | b=0.94 → h=0.38 | ≈340px | ⚠️ 还是不够(老大:「像素质量还是不够高」) |
| 定稿 | 704 × 1248 | b=0.94 → h=0.38 | ≈474px(共 +98%) | ✅ 短边 704 ≤ 768 封顶;比例 1.773 ≈ 9:16 |
尺寸必须 32 的倍数、短边 ≤ 768(H3 硬约束):704 = 22×32 ✅ 1248 = 39×32 ✅(短边 704)。
💡 分辨率涨了,出片时间反而没涨:帧数是线性因子、像素数是面积因子。 704×1248(1.9 倍像素)但帧数从 56 降到 22(0.39 倍)⇒ 单段仍是 ~100s。 "分辨率"和"时长"可以同时加码,这是这次最关键的一个发现。
🔴 画幅是按"人物像素够不够"定的,不是"够不够放下跳跃"(2026-10-11 老大反馈: 「生成的视频过于偏小,导致人物的像素不够」)。跳跃空间只是上限约束,真正决定画幅的是 人物最终占多少像素 —— 技能动画常要放大展示,素材没余量就糊。 把基线从 0.86 下移到 0.94 是这里的关键一步:它把 h 的上限从 0.374 抬到 0.409, 才腾出"人物更大"的空间。代价是出片时间 ×1.7(95s / 段)。
⚠️ 现有全部素材都是方图,竖构图在 H3 上属首次。因此 W2 之前先做一次"竖构图空跑": 同一提示词先出 2s 档,确认模型在 9:16 下没有把人拉长/裁切,再上 4s 档。这一步只花一次出片时间。
5.2 首帧合成(复用 build_green_frame)
- 输入:锤客立绘必须是真透明底(BiRefNet 语义抠图产物)→ 走
alpha_mode合成分支。
这是老坑:白底立绘走 flood fill,被身体围住的白灌不进去,会被画进每一帧。
- 关键:跑完扫日志要看到
立绘自带透明底 → alpha 合成。没这行就等于走了白底分支,且不报错。
- 构图三参数:角色占屏高 34% · 脚底基线 86% · 横向居中。这三个值取代现有硬编码的
target_h = h0.78 / y = h - h0.08。
- 姿态:首帧就用"双脚站定、双锤垂在身侧"的基准站姿(对应阶段1),不要用举锤帧 —— 否则模型会把整段演成"从举锤开始"。
5.3 提示词策略:新写一条「垂直跳、水平不动」的约束
现有 TREADMILL 那段措辞是为循环动作写的 —— 它的目标是连上下都不动("staying in exactly the same place")。 直接拿来用会把起跳压掉。这次要区分两个方向:
| 方向 | 要求 | 措辞 |
|---|---|---|
| 水平 | 零位移 | never travels left or right across the frame; the horizontal centre stays locked |
| 垂直 | 明确要有跳跃 | leaps straight UP off the ground in place, feet clearly leave the ground at the peak, then lands back on exactly the same spot |
另外三段必须写死、不能省(都是本项目踩过坑的措辞):
- NO_FX 全段保留 —— 特效必须零烘入。"土/金光"一个字都不能出现在角色提示词里(连
dust这个词都不行),
否则它会在脚下画土块,而抠图时无法把土和角色分开。
- 点掉地面与道具 —— 现有
LOCKED已经点了 floor/ground/platform/props。这次"落地/pounding the ground"天然会诱导模型画地面,
必须额外强调 the character lands on nothing, there is no floor at all, the green screen fills the whole frame。
- 朝右 ——
ALWAYS FACING TO THE RIGHT(与门人/主角基准一致;NPC 朝左才形成对战)。
- 双锤要可见、轮廓干净 —— 禁止武器的运动拖尾与辉光(
no glow around the weapon),否则挥砸瞬间锤子会糊成光团。
5.4 出片参数
| 参数 | 值 | 说明 |
|---|---|---|
| 分辨率 | 384 × 704 | megapixels=0.28 + aspect=9:16 |
| 时长 | 107 帧 / 24fps = 4.458s | seconds=4.0 → snap_length 吸附 |
| 步数 | 8(Turbo LoRA) | 与现有批量一致 |
| 种子 | 固定并记录 | 要能复现;调试期间锁种子,定稿后再试 2–3 个种子挑最好的 |
| 服务 | ComfyUI @ 127.0.0.1:8188 | --lowvram --disable-mmap;与 Z-Image 生图互斥排班 |
六、透明化与 webm 编码
6.1 抠绿:复用现有算法,但输出改成 RGBA 序列
抽帧 → 抠绿(gen_action_sheet.key_frame 的去绿溢 + 去白边那套)→ 直接存 RGBA PNG 序列, 不做 逐帧对齐、不做缩放裁剪。视频的每一帧保持首帧原始几何。
这一步的产物就是"最终画面",把它当"中间产物"随手丢掉会很难受 —— webm 是有损的,编码参数不满意时只能回到这一层重编。建议 PNG 序列落盘保留(约 107 张)。
6.2 编码命令
ffmpeg -y -framerate 24 -i frames/%04d.png \ -c:v libvpx-vp9 -pix_fmt yuva420p \ -auto-alt-ref 0 -b:v 0 -crf 30 \ -metadata:s:v:0 alpha_mode="1" \ out.webm
| 参数 | 作用 | 备注 |
|---|---|---|
| -pix_fmt yuva420p | 带 alpha 的像素格式 | 必需,缺了 alpha 会被直接丢弃 |
| -auto-alt-ref 0 | 关掉 alt-ref 帧 | VP9 alpha 的兼容稳妥值(实测不加也能保留 alpha,但按惯例关闭) |
| -metadata:s:v:0 alpha_mode="1" | 打上 alpha 标记 | 播放器据此启用透明合成 |
| -b:v 0 -crf 30 | 恒定质量模式 | crf 30 起步;体积超标再升到 34 |
🔴 验证/抽帧时必须显式指定 -c:v libvpx-vp9: ffmpeg -c:v libvpx-vp9 -i out.webm -frames:v 1 -pix_fmt rgba check.png 不加这一句,ffmpeg 会用自带的 vp9 解码器,alpha 全部变成 255(本次实测踩到:明明编码正确, 解出来却是一片不透明,差点误判成"编码把 alpha 弄丢了")。这是假警报的唯一来源,写进排查手册。
6.3 体积与加载预估
| 项 | 估算 | 依据 |
|---|---|---|
| 单条 webm | 1.5 – 4 MB | 384×704 / 107 帧 / crf 30,角色占比小、静止区多,压缩率高 |
| 特效两条 | 各 0.3 – 0.8 MB | 尺寸更小、时长更短 |
| 兜底精灵表 | ≈ 3 – 6 MB | 12fps × 54 帧,需实测 |
对照:现有首屏动作表已压到 5.51MB(按需加载后)。⇒ 这条视频不能进首屏,走"进入战斗/首次触发技能时才拉"的懒加载,与现有 _setKeyOf 分层加载同一套思路。
七、前端接入方案
7.1 渲染路径:走 canvas.drawImage(video),不走 DOM 叠加
| 方案 | 做法 | 评价 |
|---|---|---|
| A. 独立 DOM 叠在 canvas 上 | 绝对定位 + 手算坐标同步 | ❌ 不采用:条带要按 scrollX 滚动、有整体缩放与裁切,DOM 元素得手工同步 4 套参数;且 z-index 会与涂在 canvas 上的血条/飘字打架 |
| B. canvas drawImage(video) | 单位照旧在同一个 canvas 里画,只是"画一帧视频"代替"画精灵表第 n 帧" | ✅ 采用:位置/缩放/层级/裁剪全部沿用现有绘制链;已实测 alpha 逐像素保留 |
7.2 video 池(同屏多个单位的关键约束)
🔴 一个 <video> 元素同一时刻只有一个 currentTime —— 两个角色同时攻击时,用同一个 video 会让他们的动作完全同步(且互相拉扯进度)。 ⇒ 必须按单位分配:每个正在播技能的单位独占一个 video 实例。
- 池化:预建 N 个实例(N = 同屏最大同时施法数,建议 2–3),按需借出、播完归还。
- 素材复用:同一份 webm 可以被多个 video 元素加载(浏览器会共用 HTTP 缓存,不重复下载)。
- 视频属性统一:
muted playsinline preload="auto"——muted必须有,否则自动播放会被浏览器策略拦下。
- 未就绪兜底:
readyState 绝不画空(现有step()` 的既有原则)。
7.3 播放控制(三个已知坑,全部要写进代码注释)
| 动作 | 正确写法 | 为什么 |
|---|---|---|
| 开始 | v.currentTime = 0; v.play() | 一次性动作必须从头播 |
| 重播 | 等 seeked 事件后再 play(),或先 pause() → fastSeek(0) → play() | 刚 pause 就设 currentTime=0 立刻 play(),部分浏览器会忽略 seek 直接从旧位置续播 |
| 结束 | 监听 ended → pause() + 归还池 | 不 pause 会一直在Admin解码,白烧 GPU |
| 循环判定 | 用 currentTime 单调递增判上升沿 | 与 anim.js 的触发纪律一致:判上升沿,绝不判电平 |
7.4 位移控制:从"突进"改成"起跳带人"
现有 _stepAttackMoves() 的曲线是"0~30% 冲过去 → 贴身演招 → 动作后归位"。这次的语义不同:横向位移要发生在起跳那一段,落地后完全不动。
| 参数 | 现有(普通攻击) | 本次(起跳攻击) |
|---|---|---|
| 位移窗口 | 0 → 30% | 阶段1末 → 阶段3(顶点)到位,即 0 → 约 40% |
| 曲线 | easeOutCubic | easeOutQuad(起跳抛物线感更强) |
| 落点 | 目标 - ATK_STAND_GAP,夹在 ATK_MAX_TRAVEL 内 | 同左,但 必须允许覆盖更远的距离(这是"一次性突进型技能") |
| 归位 | 动作播完 → ATK_RETURN_TIME 退回 | 同左(沿用,不要改) |
🔴 位移量必须可信:NaN 会让角色在那几帧整个不画(现存坑,u.x/m.x 未初始化时是 undefined)。 所有参与位移计算的坐标,进公式前先过 Number.isFinite。落点误差验收:±2px 内。
7.5 攻击判定:挂在视频时间轴上
- 判定时刻定义成比例常量(
T1 = 0.64,T2 = 0.91),从素材实测帧号反推,不写绝对秒数。
- 触发方式:每帧读
v.currentTime / v.duration,跨过 T1/T2 时各触发一次(锁存 + 上升沿)。
- 结算走现有
_beginStrike()+_tickPending(dt)挂起机制,不在触发瞬间直接扣血 —— 保持"动作播到命中点才结算"的既有纪律。
- ⚠️
idleBattle.js的actionDur = { attack: 0.67, skill: 1.0 }是硬编码常量。
接入后必须改成从素材推导(视频 duration),否则战斗节奏与动画时长会对不上(俗称"怪都死了技能还在演")。
7.6 特效叠加
特效与角色彻底分家:角色 webm 里没有任何特效像素(提示词 NO_FX 保证)。前端在指定时刻把特效 webm 画在角色下层/上层:
| 特效 | 触发时刻 | 锚点 | 层级 |
|---|---|---|---|
| 土石迸溅 | 落地瞬间(T1) | 脚底 (x, baseY),横向居中 | 角色下层(土在脚边) |
| 金光四溅 | T1 / 左扫 / 右扫 三处 | 锤头位置(左扫时偏左、右扫时偏右) | 角色上层(光压在身上) |
特效的播放复用同一套 video 池与判定逻辑,只是"一个 unit 可以同时借 2 个实例(角色 + 特效)"。
7.7 改动范围(最小侵入)
| 文件 | 改动 | 规模 |
|---|---|---|
| src/idle/anim.js | 新增一种"视频动作"的时长/进度来源(duration 代替 frames/fps) | 小 |
| src/idle/idleBand.js | 新增 SkillVideo 小模块(池 + 播放 + 绘制 + 判定回调);_stepAttackMoves 增加"起跳型"曲线分支 | 中 |
| src/idle/idleBattle.js | actionDur 改为从素材推导;技能触发点改为读视频时间轴 | 小 |
| tools/ | 新增 gen_skill_video.py(首帧 / 出片 / 抠绿 / 编码 一条龙) | 中 |
不动:现有精灵表链路、actions.json 结构、按需加载分层 —— 视频素材作为平级的新通道接入,出问题可随时退回精灵表。
八、特效分层与生成
- 土石:碎土块 + 细沙向两侧抛洒,低饱和土黄褐,少量大块 + 大量碎屑;持续 ≈0.5s,前 0.15s 爆发。
- 金光:从锤头迸出的放射状金色光针 + 一圈快速扩散的光环,亮金到白的核心,持续 ≈0.4s。
- 生成方式:沿用
gen_fx_sheet.py的路子(黑底生成 → 抠黑转 alpha),但输出改 VP9 webm,与角色视频同规格。
- 素材独立:两条特效各自成文件,前端可单独调位置/时长/缩放 —— 这是"特效分开做"的落地含义。
九、验收清单
9.1 素材验收(每步都要截图/抽帧,不靠肉眼记忆)
| # | 检查项 | 判据 | 不过怎么办 |
|---|---|---|---|
| A1 | 首帧构图 | 角色占屏高 34%±3%、脚底基线 86%±2%、横向居中 | 调 target_h / y 重出首帧(不花 GPU) |
| A2 | 立绘真的走了 alpha 分支 | 日志出现 立绘自带透明底 → alpha 合成 | 检查立绘本身是否真透明(check_art.py) |
| A3 | 六阶段都在 | 抽 6 帧眼看:起跳/顶点举锤/下落/落地砸/左扫/右扫 | 改提示词重出 |
| A4 | 水平零漂移 | 逐帧测"非绿像素"的横向重心,极差 |
- F1 落点:技能释放后角色 x 与"Boss 身前 +
ATK_STAND_GAP"误差 ≤ 2px。
- F2 判定次数:一次完整技能 = 2 次伤害结算(在结算日志里数),不多不少。
- F3 时序:伤害发生在"锤砸到地"那一帧之后,不是技能开始就掉血。
- F4 特效:土/金光出现在 T1 与左右扫时刻;角色 webm 本身不含特效像素。
- F5 不画空:视频还没就绪时画精灵表兜底,任何一帧都不能出现"人消失"。
- F6 并发:两个单位同时施法,各自时间轴独立、不串帧。
- F7 降级:注入"不支持 VP9 alpha"的能力开关,确认能回退到精灵表且不报错。
能力探测建议:document.createElement('canvas').getContext('2d') 上探测 VP9 支持 (MediaSource.isTypeSupported('video/webm; codecs="vp9"'))+ UA 排除 Safari/iOS。 ⚠️ 探测要按"能不能透明"判,不能只判"能不能播" —— Safari 能播 VP9 但忽略 alpha, 结果会是黑底方块,比不播更难看。
十、风险与待确认
| 项 | 问题 | 影响 | 建议 |
|---|---|---|---|
| P0 | 两次攻击的分界未定 | 决定判定时刻表与前端常量,改一次返工一遍 | 本方案按"落地砸=1 + 左右合=2"设计,请老大拍板 |
| P0 | 触发器归属未定(技能?大招?连携?) | 4.46s 的时长会改变战斗节奏;挂在普通攻击上会明显变拖 | 建议挂"技能/大招",并临时把该单位其他技能冷却拉长做验证 |
| P1 | 竖构图首次尝试 | H3 在 9:16 上未验证(现全是方图) | 先 2s 空跑验形,再上 4s |
| P1 | 跳跃是否真的"原地" | 提示词层面最难保证的一项,模型偏爱做整体位移 | A4 逐帧量化横向重心,不达标就用固定种子多试几次 |
| P1 | Safari/iOS 无 VP9 alpha | 移动端会看到黑底方块 | 必须导出精灵表兜底 + 能力探测(§9.2) |
| P2 | 4s 档出片耗时未实测 | 影响迭代速度 | W2 先跑一次计时,超 2 分钟就考虑降到 3.5s 档 |
| P2 | 特效与角色的锚点对齐需手调 | 多次试错 | 做一个"钟表盘"调试页(复用现有 _dbg_shot_fx.mjs 思路) |
十一、执行步骤(确认后开跑)
| 步 | 做什么 | 产出 | 预计成本 |
|---|---|---|---|
| S1 | 确认 §10 的 P0 两项 + 选角色 | — | — |
| S2 | 建 tools/gen_skill_video.py:首帧合成(可 --reuse-video 只重跑后段)+ 抽帧抠绿 + VP9 编码 | 脚本 | 主要工作量 |
| S3 | 竖构图 2s 空跑(验形)→ 4s 正式出片 → 迭代提示词 | _jump_atk.mp4 + .webm | 每条 ≈1 分钟 GPU |
| S4 | 特效两条(土 / 金光) | 2 个 webm | 每条 ≈1 分钟 |
| S5 | 做一个自包含验收页:角色 webm + 两条特效,可叠、可拖位置、显示时间轴与判定点 | docs/双锤攻击验收.html | — |
| S6 | 前端接入(§7.7 三个文件)+ 无头截图验收 F1–F7 | 可跑效果 | — |
| S7 | 精灵表兜底导出 + 能力探测 | 降级素材 | — |
十二、实测结果(2026-10-11 已跑通)
12.1 产出素材
| 文件 | 规格 | 体积 | 说明 |
|---|---|---|---|
| _jump/art_2hammer.webp | 800×1251 RGBA | 148KB | 双锤立绘(补锤 + BiRefNet 抠图) |
| _jump/first_green.png | 704×1248 | 146KB | 绿幕首帧 · 人物 303×474 @ (200,699) · 上方留白 56% |
| _jump/slam.webm | 22 帧 / 0.917s / 704×1248 | 298KB | 起跳砸地(4 拍压紧)· 判定点 0.91 |
| _jump/sweep.webm | 22 帧 / 0.917s / 704×1248 | 254KB | 横扫(前端播两遍,第二遍镜像)· 只播到 68% |
| _jump/fx/fx_dirt.webm | 22 帧 / 0.917s / 448×448 | 119KB | 土石迸溅(收缩 86% / 羽化 22%) |
| _jump/fx/fx_gold.webm | 22 帧 / 0.917s / 448×448 | 212KB | 金光四溅(收缩 74% / 羽化 40%) |
| docs/双锤攻击验收.html | webm 全部内联 | 1.5MB | 自包含验收页,可直接外发 |
素材合计 ≈ 1.0MB,三段有效播放时长 2.16s(1× 速度,不需要前端加速)。
12.2 实测数据
| 项 | 实测值 | 对照 |
|---|---|---|
| 单段出片耗时 | 45–110s(slam 105s / sweep 45s) | 2s 方形档 ≈25s;512×896 是它的 ~1.7 倍 |
| 脚底水平位移 | sweep 18.9px ✅ / slam 40.6px ❌ | 判据 |
坑 1:alpha_mode 的值不能带引号 —— 最隐蔽的一个。
Python subprocess 传 list 不经过 shell,所以 'alpha_mode="1"' 会把引号原样交给 ffmpeg, 文件里就成了 ALPHA_MODE : "1"。 ffmpeg 自己照样能解出 alpha(所以常规解码验证查不出来), 而 Chromium 认不出这个值、直接把 alpha 丢掉 —— 作品里画出来是一整块绿底矩形。
更迷惑人的是:在 bash 里手敲同一条命令完全没事(shell 会先剥掉引号), 于是"命令行能跑、脚本跑就坏"。正解:ALPHA_META = "alpha_mode=1"; 验收加一关独立检查元数据格式(verify_alpha() 第 1 关)。
坑 2:抽帧/验证 webm 必须显式 -c:v libvpx-vp9。
不加就是用 ffmpeg 自带的 vp9 解码器,解出来的 alpha 全是 255 —— 明明是好的文件,看起来却像"编码把 alpha 丢了",是透明 webm 最常见的高频假警报。
坑 2b:-vf scale 会破坏 alpha(受控实验:缩放版 alphaZero 93% → 0.7%)。
坑 3:H3 出的特效会铺满整幅画布,直接贴上去是一条带硬边的方块光。
实测内容占比:土 87% / 金光 100%。光针和土块本身没问题,问题在 "内容的边界 = 画布的边界"。stage_fx_pad 三道后处理解决(都是确定性的,不花 GPU):
- 压底噪:视频压缩会把纯黑压出 5~20 的灰度,
alpha = 亮度时整幅都带一层极淡 alpha。
普通叠加看不出来,但金光走加法混合时这层底噪会整体提亮 ⇒ 方块形状直接被显出来。
- 收缩 74%:内容缩到中央,四周留纯透明。
- 羽化 40% + 平方曲线:⚠️ 这里是第二遍才对的 ——
第一遍用 26% 线性 ramp,而内容边缘落在画布 13% 处 ⇒ 13/26 = 0.5, 边缘还剩一半 alpha,屏幕上就是一条能看见的竖直方块边。 改成 40% + 平方后:(0.13/0.40)² ≈ 0.11,基本看不见了,而中心区仍是满 alpha。
另外金光在 canvas 上要 globalCompositeOperation = 'lighter':光叠在角色身上继续发光, 而不是把角色盖掉(土块是实体,保持 source-over,否则会糊成一片发亮的东西)。
坑 4:slam 段结尾模型会定格在落地蹲姿,占 25% 时长。 提示词明写 "then he pushes back up … the clip ends with him standing" 两版都没听; 改用 首尾帧约束(把首帧同时当尾帧传,逼它跳完落回原姿)也没压住。 ⇒ 最后走工程解:素材保留 56 帧,前端只播到 80% 就切下一段(play_to)。 跟模型纠缠不如在前端截断。
12.4 补第二把锤的做法(为什么没用图生图)
现成锤客立绘只有一把锤(职业设定写的是"双锤",画的是单锤)。先试了本地 Z-Image 图生图:
| denoise | 结果 |
|---|---|
| 0.65 | 武器没动(还是单锤),只重绘了背景与细节 |
| 0.86 | 还是单锤,而且人物发髻、风格已经漂移 |
⇒ 改程序化做法:从原立绘裁出「锤 + 柄」的矩形 → 按亮度阈值转 alpha → 水平镜像 → 以「柄顶端对齐另一只手心」贴上去 → 整张走 BiRefNet 变成透明底。 接缝虽然糙,但首帧里人物只有 239px 高、锤子约 40px,那个尺度下看不出来。
参数都在 gen_skill_video.py 顶部的 PATCH_* 常量里(换角色/换手位只改这几个数)。
12.5 老大第二轮反馈的调整(2026-10-11 晚)
| 反馈 | 改法 | 结果 |
|---|---|---|
| 「整个速度过慢,要提升到两倍速」 | 验收页默认播放速率 2.00×(滑块扩到 0.5~3.0) | 有效时长 3.70s ÷ 2 = 1.85s |
| 「人物像素不够,视频尺寸要加大」 | 画布 384×704 → 512×896;基线 0.86 → 0.94、人物占比 0.34 → 0.38 | 人物 239px → 340px(+42%);代价是出片 70s → 105s |
| 「特效太小、被切掉了」 | 特效画布 320 → 448;重出提示词改成"铺满整幅";再加 fxpad 收缩+羽化压掉方块边;绘制时裁到地面以上(被地面挡住比被画布边界硬切自然) | 见 §12.3 坑 3 |
12.6 第三轮(分辨率再加 / 素材本身要快 / 落地特效看不见)
| 反馈 | 改法 | 结果 |
|---|---|---|
| 「像素质量还是不够高,分辨率长宽再高一些」 | 512×896 → 704×1248 | 人物 340px → 474px(累计 +98%);因帧数同时降到 22,出片时间没涨 |
| 「渲染出来的速度本身就要有那么快,1 秒多一点完成,而不是 2 点几秒」 | seconds 2.0 → 0.7(56 帧 → 22 帧,H3 能演出动作的最短档) | slam 2.333s → 0.917s;三段合计 2.16s |
| 「落地的那个特效没有出来」 | 见下面那条坑 —— 判定点标错了两帧 | 特效现在在落地那一刻可见 |
🔴 坑 5:判定点比例必须抽帧数出来,不能凭感觉写。
上一版把 slam 的判定点写成 at=0.77 —— 而按 22 帧折算,第 17 帧 (0.77 × 22)人还在空中举锤;真正的"双锤触地"在第 20 帧(at=0.91)。 更巧的是切段点(play_to=0.80 = 第 17.6 帧)几乎压在判定点上 ⇒ 特效刚触发就被切走,表现就是老大说的「落地的那个特效没有出来」。
固定流程:抽帧 → 拼网格图 → 肉眼看"双锤触地"是第几帧 → 帧号 / 总帧数。 这一步不能省,也不能用设计值代替。
🔴 坑 6:土石的羽化不能照抄金光的参数。
土块是实体,收缩 74% + 羽化 40% 会把它削成一层几乎看不见的薄雾 (内容占比掉到 16%),再加上金光画在上面 ⇒ 土完全被盖掉。 ⇒ 分开配:土 86% / 22%(保住实体块)、金光 74% / 40%(光本来就要融边)。 另外绘制顺序是"先土后光":金光是加法混合,画在土上面才像"土里迸出金光"。
12.7 还没做 / 下一步
- 前端接入未做 —— 方案 §7 的设计还没落到
src/idle/三个文件;现在是"素材 + 验收页"阶段。
- 「左右各扫一次」只能靠镜像复用:22 帧里让模型连做两次扫,试了两版都只做一次、末尾还定格 7 帧
⇒ 现在用"单次扫素材 + 前端镜像播两遍"(两个方向天然对称)。
- 三段合计 2.16s,离"整个 1 秒多"还差 —— 6 个动作要塞进 1 秒多物理上做不到
(H3 最短有效档就是 0.917s)。要真压到 1 秒多,只能砍动作(例如去掉左右扫、只留起跳砸地)。
- slam 起跳仍有横向漂移(脚底极差 94px / 判据 28px,这一版更明显)—— 前端位移曲线可以盖住一部分。
- 精灵表兜底(Safari/iOS 无 VP9 alpha)还没导出。
十三、配音(2026-10-11 加)
老大:「在验收页面上要能听到这个动作的配音:会有掉下来的那一下,以及总共 3 次打击的响声。」
13.1 先说 H3 的音轨:有,但不能用
H3 是音视频联合生成(音频关不掉),所以每个 mp4 里都真的带一条 AAC 音轨 —— 实测 mean -14.1dB / max -0.5dB,不是静音。但把它导出来看波形就明白了:
- 它是整段连续的动作拟音(挥动的风声、衣料摩擦),波形是一层从头铺到尾的沙沙噪声
- 没有"打击"的起音瞬态(打击声的关键是那 5~120ms 的冲击前沿,它没有)
- 无法对齐判定点 —— 它是连续的一条,而我们需要在 3 个精确时刻各响一下
- 而且我们自己的提示词里写着
Silent, no audio,模型本来就不该配打击声
⇒ 打击声单独合成(tools/gen_sfx.py); H3 的原音轨先试过当"动作拟音层"(音量 0.38),后来直接停用 —— 见 §13.4。
13.2 合成的音效(4 层结构,不是随便调出来的)
| 音效 | 时长 | 层 | 作用 |
|---|---|---|---|
| 落地砸地 sfx_slam | 0.46s | 低频冲击 70→38Hz 扫频 · 衰减 0.16s | "咚"—— 大地被砸的闷响(这是"掉下来的那一下") |
| 宽带瞬态 150~1500Hz · 35ms | 锤面接触的"啪" | ||
| 金属余韵 1450/1524/2180Hz · 衰减 0.075s | 锤头的金属感(两音轻微失谐,纯音听着像电子音) | ||
| 土石碎屑:60 颗短脉冲(集中在前半段)+ 高通 · 衰减 0.10s | 土石飞溅的颗粒感(不是一层平噪声) | ||
| 锤击 sfx_hit / sfx_hit2 | 0.24s | 低频体 95→62Hz + 噪声瞬态 + 金属双音(衰减 0.05 / 0.018 / 0.055s) | 左扫 / 右扫各一声;hit2 音高 ×1.14、换 seed,两次扫听着不重复 |
所有音效都留了 3~4ms 的起音包络 —— 起音写 0 会"啪"一声爆音。
🔴 第二版:按"干脆"重调(老大 2026-10-11)
老大听完第一版(slam 0.90s / hit 0.42s)说 「稍微有点慢,拖得有点长了,不够干脆」。
病根是衰减时间太长(低频 0.30s / 金属 0.20s / 碎屑 0.22s)—— 四层的尾巴互相叠着,听感就是"轰——"拖一下。全部砍掉一半以上: 0.90 → 0.46s、0.42 → 0.24s(衰到 5% 只需 0.30s / 0.13s)。
⚠️ 关键:力度不靠长尾、靠起音那一下的幅度 —— 缩短的同时把低频分量从 0.95 提到 1.05, 所以听着不会变"轻",只是更"脆"。
13.3 触发时序与验收
- 打击音挂在判定点上(
hits[*].sfx),与特效同一时刻触发 —— 天然对齐,不用另算时间。
- 落地砸地用重音(含"掉下来"的低频冲击),左/右扫用两声音高不同的锤击。
sfx_slam0.90s 比 slam 段(0.917s)还长一点,会自然延续到下一段开头 —— 这是对的,
打击声本来就该有尾巴。
- 一个开关:打击音(关掉就只剩画面)。
- ⚠️ 音频都是 data URI 内联(页面自包含),但浏览器要求用户手势后才出声 ——
所以必须点「播放」,直接打开页面不会响。
✅ 无头实测(Chromium):5 个音频对象(3 音效 + 2 拟音)play() 全部成功、 currentTime 确实在推进;完整播放一遍后状态栏显示 「判定 3/3 · 特效 4 · 打击音 3」 —— 3 次打击音按判定点全部触发,无异常。
13.4 🔴 H3 的原音轨最终停用了(老大 2026-10-11)
第一版把 H3 原音轨当"动作拟音层"接了进去(音量 0.38,可开关)。老大听完的反馈是:
「这个只要音效,不要背景声。但是我现在发现有背景音乐,我只要打击的特效音效。」
原因就是 §13.1 说的那个性质:H3 音轨是整段连续的一层沙沙噪声(挥动风声 + 衣料摩擦), 它没有"一下一下"的起伏,听感上不像拟音、像一段一直响的背景音乐; 叠在打击声下面反而把打击的力度盖住了。
处理:VOICE_FILES = [](不内联、不播放),页面上的「动作拟音」开关也一并去掉。 音频素材仍留在 _jump/sfx/voice_*.m4a(各 12KB,以后想接回来随时可用)。
✅ 无头复验:sfxObj 只有 ["slam","hit","hit2"]、 voiceObj 为空、页面上没有拟音开关;三个打击音 play() 全部成功、 currentTime 推进;完整播放后 「判定 3/3 · 特效 4 · 打击音 3」。
相关文档:本地 H3 动作精灵生成方案 · 人物动作提示词-H3 · 技术架构总览
技能:~/.workbuddy/skills/sprite-action-forge(方法论通用版)