summer-engine 1.5.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/{marketplace.json → .claude-plugin/marketplace.json} +3 -3
  2. package/.claude-plugin/plugin.json +52 -11
  3. package/.codex-plugin/plugin.json +69 -8
  4. package/.cursor-plugin/plugin.json +53 -12
  5. package/.factory-plugin/plugin.json +20 -0
  6. package/.opencode/INSTALL.md +75 -0
  7. package/.opencode/plugins/summer.js +9 -6
  8. package/AGENTS.md +51 -0
  9. package/CHANGELOG.md +31 -1
  10. package/CLAUDE.md +5 -0
  11. package/GEMINI.md +56 -0
  12. package/README.md +409 -80
  13. package/_persona/summer/SKILL.md +113 -0
  14. package/dist/bin/summer.js +0 -0
  15. package/dist/commands/assets.d.ts +2 -0
  16. package/dist/commands/assets.js +189 -0
  17. package/dist/commands/create.js +55 -17
  18. package/dist/commands/debug.d.ts +2 -0
  19. package/dist/commands/debug.js +127 -0
  20. package/dist/commands/install.js +2 -2
  21. package/dist/commands/list.js +24 -5
  22. package/dist/commands/project.d.ts +2 -0
  23. package/dist/commands/project.js +170 -0
  24. package/dist/commands/scene.d.ts +2 -0
  25. package/dist/commands/scene.js +290 -0
  26. package/dist/lib/banner.js +2 -2
  27. package/dist/lib/log-filters.d.ts +53 -0
  28. package/dist/lib/log-filters.js +130 -0
  29. package/dist/lib/plugin-manifests.test.d.ts +1 -0
  30. package/dist/lib/plugin-manifests.test.js +134 -0
  31. package/dist/lib/remote-templates.d.ts +25 -0
  32. package/dist/lib/remote-templates.js +86 -0
  33. package/dist/lib/skills-registry.d.ts +283 -3
  34. package/dist/lib/skills-registry.js +522 -2
  35. package/dist/lib/telemetry.d.ts +13 -0
  36. package/dist/lib/telemetry.js +63 -0
  37. package/dist/lib/with-engine.d.ts +14 -0
  38. package/dist/lib/with-engine.js +109 -0
  39. package/dist/mcp/tools/asset-tools.js +12 -13
  40. package/dist/mcp/tools/debug-tools.js +32 -9
  41. package/dist/mcp/tools/generate-tools.js +101 -0
  42. package/dist/mcp/tools/with-engine.js +4 -0
  43. package/gemini-extension.json +2 -2
  44. package/hooks/pre-commit-doctor.sh +0 -0
  45. package/hooks/run-hook.cmd +0 -0
  46. package/hooks/session-start.sh +0 -0
  47. package/package.json +11 -5
  48. package/skills/2d-assets/character-portrait/SKILL.md +196 -0
  49. package/skills/2d-assets/concept-art/SKILL.md +152 -0
  50. package/skills/2d-assets/pixel-art/SKILL.md +180 -0
  51. package/skills/2d-assets/skybox-panorama/SKILL.md +207 -0
  52. package/skills/2d-assets/sprite-sheet/SKILL.md +239 -0
  53. package/skills/2d-assets/tileable-texture/SKILL.md +205 -0
  54. package/skills/2d-assets/ui-graphics/SKILL.md +210 -0
  55. package/skills/3d-assets/character-model/SKILL.md +225 -0
  56. package/skills/3d-assets/environment-kit/SKILL.md +164 -0
  57. package/skills/3d-assets/organic-model/SKILL.md +192 -0
  58. package/skills/3d-assets/prop-model/SKILL.md +133 -0
  59. package/skills/3d-assets/vehicle-model/SKILL.md +202 -0
  60. package/skills/README.md +1 -1
  61. package/skills/_meta/skill-create/SKILL.md +1 -1
  62. package/skills/_meta/skill-improve/SKILL.md +1 -1
  63. package/skills/_meta/skill-test/SKILL.md +1 -1
  64. package/skills/_meta/using-summer/SKILL.md +130 -0
  65. package/skills/ai-and-npcs/design-npc/SKILL.md +3 -1
  66. package/skills/animation/animation-tree/SKILL.md +260 -0
  67. package/skills/animation/facial-and-lipsync/SKILL.md +282 -0
  68. package/skills/animation/generate-motion/SKILL.md +129 -0
  69. package/skills/animation/procedural-animation/SKILL.md +234 -0
  70. package/skills/animation/retarget/SKILL.md +172 -0
  71. package/skills/asset-pipeline/asset-strategy/SKILL.md +102 -212
  72. package/skills/audio/adaptive-music/SKILL.md +326 -0
  73. package/skills/audio/ambient-bed/SKILL.md +278 -0
  74. package/skills/audio/music-track/SKILL.md +268 -0
  75. package/skills/audio/sound-effect/SKILL.md +237 -0
  76. package/skills/audio/voice-line/SKILL.md +253 -0
  77. package/skills/catalog.yaml +266 -5
  78. package/skills/character-controllers/fps-controller/SKILL.md +1 -1
  79. package/skills/debugging/debug/SKILL.md +102 -82
  80. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
  81. package/skills/scene-and-project/browse-templates/SKILL.md +105 -0
  82. package/skills/scene-and-project/make-game/SKILL.md +159 -346
  83. package/skills/scene-and-project/new-project/SKILL.md +106 -0
  84. package/skills/scene-and-project/scene-composition/SKILL.md +1 -1
  85. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
  86. package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
  87. package/skills/video/animated-loop/SKILL.md +200 -0
  88. package/skills/video/cinematic-cutscene/SKILL.md +204 -0
  89. package/skills/video/trailer-shot/SKILL.md +203 -0
  90. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +28 -0
  91. package/skills/visual-effects/_building-blocks/noise-3d-fbm.gdshaderinc +45 -0
  92. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +51 -0
  93. package/skills/visual-effects/{vfx → game-feel}/SKILL.md +1 -1
  94. package/skills/visual-effects/recipes/dissolve/SKILL.md +349 -0
  95. package/skills/visual-effects/recipes/fire/SKILL.md +387 -0
  96. package/skills/visual-effects/recipes/hit-spark/SKILL.md +330 -0
  97. package/skills/visual-effects/recipes/lightning/SKILL.md +372 -0
  98. package/skills/visual-effects/recipes/magic-glow/SKILL.md +377 -0
  99. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +326 -0
  100. package/skills/visual-effects/recipes/smoke/SKILL.md +381 -0
  101. package/skills/visual-effects/recipes/water-ripple/SKILL.md +340 -0
@@ -0,0 +1,260 @@
1
+ ---
2
+ name: animation-tree
3
+ description: Use when the user has clips on a character and needs them to play in response to gameplay — idle/walk/run blend, attacks that interrupt locomotion, hit reactions that override everything. Designs and wires AnimationTree state machines and blend trees in Godot 4.5. Trigger on "AnimationTree", "state machine", "blend tree", "transition", "play animation when", "character keeps T-posing", "wire animations".
4
+ license: MIT
5
+ compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
+ category: animation
7
+ user-invocable: false
8
+ allowed-tools: Read Grep Edit Write summer_get_scene_tree summer_inspect_node summer_inspect_resource summer_add_node summer_set_prop summer_set_resource_property summer_save_scene summer_get_script_errors
9
+ paths: ["**/*.gd", "**/*.tscn", "**/*.tres"]
10
+ ---
11
+
12
+ # AnimationTree — Wire Clips Into Behavior
13
+
14
+ Generation gives you clips. The AnimationTree gives the character life. This skill is **not generative** — no FAL, no MCP AI calls. It composes existing clips on an `AnimationPlayer` into an `AnimationTree` with a `StateMachine` for distinct states (idle / attack / hit / dead) and `BlendSpace` nodes for continuous parameters (walk-run blend by speed). Done right, the character moves like a shipped game; done wrong, you get T-poses, snapping, or animations that "eat" each other.
15
+
16
+ The canonical Godot 4.5 stack:
17
+
18
+ ```
19
+ Character (CharacterBody3D)
20
+ ├── Skeleton3D (from the Meshy rig)
21
+ ├── AnimationPlayer # holds the raw clips (idle, walk, run, attack_sword, ...)
22
+ └── AnimationTree # references AnimationPlayer, drives behavior
23
+ ```
24
+
25
+ ## When to use this skill
26
+
27
+ - Character has 3+ clips and switching between them via `AnimationPlayer.play("walk")` is causing pops, T-poses, or "starts halfway through".
28
+ - Need a walk → run blend that's continuous, not boolean.
29
+ - Need attack to interrupt locomotion but only on the upper body, while feet keep walking.
30
+ - Need hit-react one-shot that returns to whatever state was playing before.
31
+ - Building any non-trivial NPC or player character past the prototype phase.
32
+
33
+ ## When NOT to use this skill
34
+
35
+ - Single-clip looping background prop (banner waving, water surface). Just `AnimationPlayer.play("loop")` is fine.
36
+ - Cinematic with hand-authored timing. Use `AnimationPlayer` directly with `play_with_capture`; AnimationTree is overkill.
37
+ - Character has zero clips — generate first via `summer:animation/generate-motion`.
38
+ - Procedural-only animation (no clips, all IK). That's `summer:animation/procedural-animation`.
39
+
40
+ ## Canonical state machine
41
+
42
+ This is the production-default for a humanoid character that can move, attack, take a hit, and die. Start here, prune what you don't need, extend with named states for specials.
43
+
44
+ ```
45
+ ┌─────────┐
46
+ ┌─────►│ Dead │ (terminal — no exit)
47
+ │ └─────────┘
48
+ │ ▲
49
+ │ │ on_death
50
+ ┌──────────┴───────────┴──────────┐
51
+ │ Hit │ (one-shot, returns to prev)
52
+ └──────────▲──────────────────────┘
53
+ │ on_damaged ┌──────────┐
54
+ ┌──────────┴──────────┐ │ Attack │ (one-shot)
55
+ │ Locomotion │ on_attack ──────►└──────────┘
56
+ │ (BlendSpace1D) │ ◄────── on_attack_done
57
+ │ idle ↔ walk ↔ run │
58
+ └─────────────────────┘
59
+ ```
60
+
61
+ - **Locomotion** is a single AnimationNodeBlendSpace1D parameterized by `move_speed` (0..max). Three points on the line: idle (0), walk (~3), run (~6). Crossfades for free.
62
+ - **Attack** is an AnimationNodeOneShot wrapping `attack_sword` (or whichever clip). Fires from the script via `parameters/attack/request = ONE_SHOT_REQUEST_FIRE`. Auto-returns to Locomotion when the clip ends.
63
+ - **Hit** same pattern, higher priority — interrupts attack. Fires on `damaged` signal.
64
+ - **Dead** is a transition-target with no exit. Trigger via `state_machine.travel("dead")`.
65
+
66
+ ## Steps via MCP
67
+
68
+ ### 1. Inspect the current character
69
+
70
+ ```
71
+ summer_get_scene_tree
72
+ summer_inspect_node "./World/Goblin"
73
+ ```
74
+
75
+ Confirm there's a `Skeleton3D` and an `AnimationPlayer` with at least `idle` + `walk` + `run` populated. If clips are missing, route to `summer:animation/generate-motion`.
76
+
77
+ ### 2. Add the AnimationTree
78
+
79
+ ```
80
+ summer_add_node(parent: "./World/Goblin", type: "AnimationTree", name: "AnimationTree")
81
+ summer_set_prop(path: "./World/Goblin/AnimationTree", key: "anim_player", value: "../AnimationPlayer")
82
+ summer_set_prop(path: "./World/Goblin/AnimationTree", key: "active", value: "true")
83
+ ```
84
+
85
+ `anim_player` must be a relative NodePath; `../AnimationPlayer` from a sibling. Don't use absolute paths — breaks scene reuse.
86
+
87
+ ### 3. Build the tree resource
88
+
89
+ The `tree_root` is a single `AnimationNodeStateMachine`. Inside it, a `Locomotion` node which is itself an `AnimationNodeBlendSpace1D`, plus `Attack`, `Hit`, `Dead` nodes.
90
+
91
+ This is more readable to edit by hand or in the Godot editor, then commit the `.tres`. The MCP tool `summer_set_resource_property` can build it node-by-node, but the tree-resource model is verbose enough that **writing the `.tres` and importing it is faster** for the canonical structure. Skeleton:
92
+
93
+ ```
94
+ [gd_resource type="AnimationNodeStateMachine" load_steps=8 format=3]
95
+
96
+ [sub_resource type="AnimationNodeBlendSpace1D" id="locomotion"]
97
+ blend_point_0/node = SubResource("idle_clip")
98
+ blend_point_0/pos = 0.0
99
+ blend_point_1/node = SubResource("walk_clip")
100
+ blend_point_1/pos = 3.0
101
+ blend_point_2/node = SubResource("run_clip")
102
+ blend_point_2/pos = 6.0
103
+ min_space = 0.0
104
+ max_space = 6.0
105
+
106
+ [sub_resource type="AnimationNodeOneShot" id="attack_oneshot"]
107
+ fadein_time = 0.05
108
+ fadeout_time = 0.10
109
+ mix_mode = 0 # 0=BLEND, 1=ADD
110
+
111
+ [sub_resource type="AnimationNodeOneShot" id="hit_oneshot"]
112
+ fadein_time = 0.0 # snap in — hit reactions are immediate
113
+ fadeout_time = 0.15
114
+
115
+ # ... AnimationNodeAnimation sub_resources point at clip names ...
116
+
117
+ [resource]
118
+ states/Locomotion/node = SubResource("locomotion")
119
+ states/Attack/node = SubResource("attack_oneshot")
120
+ states/Hit/node = SubResource("hit_oneshot")
121
+ states/Dead/node = SubResource("dead_clip")
122
+ transitions = [
123
+ "Locomotion", "Attack", SubResource("trans_to_attack"),
124
+ "Attack", "Locomotion", SubResource("trans_back"),
125
+ "Locomotion", "Hit", SubResource("trans_to_hit"),
126
+ "Attack", "Hit", SubResource("trans_to_hit"),
127
+ "Hit", "Locomotion", SubResource("trans_back"),
128
+ "Locomotion", "Dead", SubResource("trans_to_dead"),
129
+ "Attack", "Dead", SubResource("trans_to_dead"),
130
+ "Hit", "Dead", SubResource("trans_to_dead")
131
+ ]
132
+ graph_offset = Vector2(0, 0)
133
+ ```
134
+
135
+ Each `AnimationNodeStateMachineTransition` carries `xfade_time` (0.15 default), `switch_mode` (Immediate / Sync / At End), and `advance_mode` (Auto / Enabled / Disabled).
136
+
137
+ Ask before writing the `.tres`: "May I write `res://characters/goblin/animation_tree.tres` with the canonical 4-state machine + Locomotion blendspace?"
138
+
139
+ ### 4. Drive it from the controller script
140
+
141
+ ```gdscript
142
+ @onready var tree: AnimationTree = $AnimationTree
143
+ @onready var sm: AnimationNodeStateMachinePlayback = tree["parameters/playback"]
144
+
145
+ func _physics_process(delta: float) -> void:
146
+ var horiz_speed: float = Vector2(velocity.x, velocity.z).length()
147
+ tree.set("parameters/Locomotion/blend_position", horiz_speed)
148
+
149
+ func attack() -> void:
150
+ tree.set("parameters/Attack/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
151
+
152
+ func take_hit() -> void:
153
+ tree.set("parameters/Hit/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
154
+
155
+ func die() -> void:
156
+ sm.travel("Dead")
157
+ ```
158
+
159
+ `tree.set(...)` writes a parameter path. `sm.travel(...)` switches state.
160
+
161
+ ### 5. Connect signals
162
+
163
+ ```
164
+ summer_connect_signal(
165
+ source: "./World/Goblin",
166
+ signal: "damaged",
167
+ target: "./World/Goblin",
168
+ method: "take_hit"
169
+ )
170
+ ```
171
+
172
+ If the goblin has an HP component emitting `died`, connect that to `die`.
173
+
174
+ ### 6. Save & verify
175
+
176
+ ```
177
+ summer_save_scene
178
+ summer_get_script_errors
179
+ ```
180
+
181
+ Then `summer_play` and watch — the goblin should idle in place, blend up to walk/run as it moves, fire attack on input/AI, snap to hit on damage, end in Dead.
182
+
183
+ ## Confirmation gates
184
+
185
+ - **Before writing the `.tres`:** show the state list and transition table.
186
+ - **Before connecting signals:** show source → target → method.
187
+ - **Before pressing play:** state which behaviors should be visible (idle plays, walk crossfades, attack interrupts).
188
+
189
+ ## Reference card
190
+
191
+ ### Parameter paths cheat-sheet (Godot 4.5)
192
+
193
+ | Goal | Path |
194
+ |---|---|
195
+ | Set blendspace1D position | `parameters/Locomotion/blend_position` |
196
+ | Fire a OneShot | `parameters/<NodeName>/request` (set to `ONE_SHOT_REQUEST_FIRE`) |
197
+ | Abort a OneShot | `parameters/<NodeName>/request = ONE_SHOT_REQUEST_ABORT` |
198
+ | Read OneShot active state | `parameters/<NodeName>/active` |
199
+ | Get StateMachine playback | `parameters/playback` (returns `AnimationNodeStateMachinePlayback`) |
200
+ | Switch state | `state_machine.travel("StateName")` |
201
+ | Read current state | `state_machine.get_current_node()` |
202
+ | Force state without transition | `state_machine.start("StateName")` |
203
+
204
+ ### Transition `switch_mode` decision
205
+
206
+ - **Immediate (0):** snap to new state at xfade. Use for hit reactions, deaths.
207
+ - **Sync (1):** match current play position. Use for walk ↔ run (avoids foot-pop mid-stride).
208
+ - **At End (2):** finish current clip first. Use for attack-string combos (`attack_1` → `attack_2` only at end of `attack_1`).
209
+
210
+ ### Pitfalls
211
+
212
+ - **Character T-poses on play.** `AnimationTree.active = true` was missed. Set it in `_ready()` or via inspector.
213
+ - **Locomotion blendspace stuck on idle even when moving.** The `blend_position` parameter isn't being written — confirm `tree.set("parameters/Locomotion/blend_position", speed)` runs every frame in `_physics_process`. Also confirm the path matches your state name exactly (case-sensitive).
214
+ - **Walk-to-run pops.** Transition `switch_mode` is `Immediate`. Set to `Sync` so the cycle phase matches across the crossfade.
215
+ - **Attack one-shot fires every frame as long as input is held.** Use `Input.is_action_just_pressed`, not `is_action_pressed`. Or store a `_can_attack` flag with cooldown.
216
+ - **Hit reaction interrupts itself when hit twice in 100ms.** Set `fadein_time = 0.0` and gate from script: `if sm.get_current_node() == "Hit": return`.
217
+ - **Attack plays but locomotion is invisible (lower body T-pose).** OneShot's `mix_mode` is `BLEND` (full body). For upper-body-only attacks, set `mix_mode = ADD` and use an additive clip (recorded as the offset from idle), or split the rig with a SkeletonModification3D mask.
218
+ - **Dead state replays from start every time HP < 0.** Add a guard in the controller: `if is_dead: return`.
219
+
220
+ ### One-shot vs StateMachine — which to use
221
+
222
+ - **OneShot:** the action is a *temporary override* of an underlying state (attack while running, hit-react while attacking). The base keeps playing.
223
+ - **StateMachine state:** the action is a *replacement* (dead, stunned, ragdoll). Nothing else plays.
224
+ - Combat in 2026-feel games is almost always OneShot for attacks + StateMachine state for "dead". Don't model attacks as StateMachine states unless you need precise transition control between attack-string variants.
225
+
226
+ ### BlendSpace1D vs BlendSpace2D
227
+
228
+ - 1D for speed-only locomotion (idle / walk / run).
229
+ - 2D for omnidirectional movement (idle / forward / strafe-left / strafe-right / back). Eight or nine clips arranged on a unit circle. Necessary for top-down or strafing third-person; overkill for FPS hands.
230
+
231
+ ## Anti-patterns
232
+
233
+ - Skipping AnimationTree and calling `AnimationPlayer.play("walk")` from script. Works for one clip; breaks the moment you need a crossfade.
234
+ - Modeling every attack as a StateMachine state. The transition matrix explodes (N states × N transitions). Use one OneShot per attack and switch its inner clip via `parameters/Attack/animation`.
235
+ - Writing the `.tres` by hand without using sub_resource IDs — Godot loses references and the inspector is unhappy. Always use `[sub_resource id="..."]` blocks.
236
+ - Driving `blend_position` from raw input (`Input.get_axis(...)`). Drive from actual velocity — that way the blend matches the body's real motion, including knockback and slope-slide.
237
+
238
+ ## Edge cases
239
+
240
+ - **Player rotates 180° instantly while sprinting.** The blendspace doesn't know about turning — locomotion looks like the character moonwalks for one frame. Add a small additive turn-lean clip layered on top, or add a brief "turn" state with switch_mode=AtEnd.
241
+ - **Animation library uses root motion.** Set `root_motion_track` on the AnimationTree (path to the root bone), then in `_physics_process` use `tree.get_root_motion_position()` and apply to the CharacterBody3D. Don't combine with input-driven velocity — pick one.
242
+ - **Character has no AnimationPlayer.** Add one first. AnimationTree without anim_player target is a no-op.
243
+
244
+ ## Fallback (no MCP)
245
+
246
+ Open the scene in Godot 4.5 editor, add `AnimationTree` node, set `Anim Player` to the AnimationPlayer, set `Active = on`, double-click `Tree Root` and edit visually. Drag `AnimationNodeStateMachine` as the root, drop nodes inside, draw transitions by Shift-dragging. Save the scene. The visual editor produces the same `.tres` — slower but discoverable.
247
+
248
+ ## Handoff
249
+
250
+ - For NPC behavior firing these states (decide-when-to-attack), hand off to `summer:ai-and-npcs/design-npc`.
251
+ - For first-person hand animations on the player rig, the same pattern applies — hand off to `summer:character-controllers/fps-controller` for the state inputs.
252
+ - For procedural overlays on top of the tree (look-at the player while idling), hand off to `summer:animation/procedural-animation`.
253
+ - For lipsync layered on the face during dialogue states, hand off to `summer:animation/facial-and-lipsync`.
254
+
255
+ ## See also
256
+
257
+ - `summer:animation/generate-motion` — produce the clips this tree references.
258
+ - `summer:animation/retarget` — share one tree across many characters.
259
+ - `_shared/gd-style.md` — typed GDScript conventions in the controller snippets.
260
+ - `_shared/mcp-tools-reference.md` — `summer_connect_signal` and `summer_set_resource_property` rules.
@@ -0,0 +1,282 @@
1
+ ---
2
+ name: facial-and-lipsync
3
+ description: Use when the user has a voice-over (or wants one) and needs the character's mouth to actually move with the words — phoneme extraction from audio, mapping to viseme blendshapes, plus emotional facial expressions (smile, frown, surprise). Trigger on "lipsync", "lip sync", "talking head", "phonemes", "viseme", "facial animation", "blendshapes", "make him talk", "dialogue animation".
4
+ license: MIT
5
+ compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
+ category: animation
7
+ user-invocable: false
8
+ allowed-tools: Read Grep Edit Write summer_search_assets summer_inspect_resource summer_inspect_node summer_generate_audio summer_generate_motion summer_add_node summer_set_prop summer_set_resource_property summer_save_scene summer_get_script_errors
9
+ paths: ["**/*.gd", "**/*.tscn", "**/*.tres"]
10
+ ---
11
+
12
+ # Facial Animation & Lipsync
13
+
14
+ Half generative, half authored. The *generative* half: an audio file (TTS via `summer_generate_audio` with `capability: "text_to_speech"`) goes through a phoneme-extraction tool (Summer does NOT wrap one — you run it externally), out comes a viseme timeline — a list of `{ phoneme, start_time, duration }` triples. The *authored* half: the character's face mesh must have BlendShapes named for the standard viseme set, or the timeline has nothing to drive. Without both halves, you get a talking robot.
15
+
16
+ The 2026 production stack:
17
+
18
+ 1. **Audio in** — `.wav`/`.mp3` from `summer_generate_audio({capability: "text_to_speech", ...})` or imported VO.
19
+ 2. **Phoneme extraction** — done OUTSIDE Summer. Recommended: **Rhubarb Lip Sync** (open source, the industry standard for game-dev lipsync; outputs JSON with mouth-shape cues over time). Alternatives: gentle, allosaurus, or a Whisper-phoneme model on Replicate / Hugging Face. There is no `summer_*` MCP for this in the current engine.
20
+ 3. **Viseme mapping** — phonemes / Rhubarb cues → Oculus / Apple ARKit viseme set (15 standard shapes covers English).
21
+ 4. **BlendShape driver** — at runtime, lerp the mesh's BlendShape weights along the timeline, synced to audio playback.
22
+ 5. **Optional emotional layer** — separate BlendShape track for `smile`, `brow_raise`, `eye_squint` etc., authored in code or at clip-edit time.
23
+
24
+ ## When to use this skill
25
+
26
+ - NPC has voice lines and the mouth doesn't move.
27
+ - Cinematic with VO that needs lipsync.
28
+ - "Make him sing the lyrics."
29
+ - Animating expressions on a face that has BlendShapes (Meshy character heads ship with the ARKit-52 set by default).
30
+ - Reactive face: smile when player gives gold, scowl when attacked.
31
+
32
+ ## When NOT to use this skill
33
+
34
+ - Character has no face / no BlendShapes (helmeted soldier, robot, mascot). Hard-skip to body language via `summer:animation/generate-motion`.
35
+ - The dialogue is written but not yet voiced — generate audio first via `summer_generate_audio({capability: "text_to_speech", ...})`. Lipsync without audio has nothing to sync to.
36
+ - Pre-rendered cinematic from external DCC. Lipsync is in the rendered video, not the engine.
37
+
38
+ ## Steps
39
+
40
+ ### 1. Confirm the head has BlendShapes
41
+
42
+ ```
43
+ summer_inspect_node "./World/NPC/Head" # or wherever the head MeshInstance3D lives
44
+ ```
45
+
46
+ Look for `mesh.blend_shape_count` > 0 and a list of names. The Meshy ARKit-52 set (default for Meshy character heads) has names like `jawOpen`, `mouthClose`, `mouthFunnel`, `mouthPucker`, `mouthLeft`, `mouthRight`, `mouthSmile_L`, `mouthSmile_R`, `mouthFrown_L`, `mouthFrown_R`, `browInnerUp`, `browOuterUp_L`, `browDown_L`, `eyeBlink_L`, `eyeWide_L`, `eyeSquint_L` (mirrored on R).
47
+
48
+ If the head has zero BlendShapes, stop. Tell the user: "This head mesh has no BlendShapes — facial animation is impossible without re-meshing. Options: regenerate the character with the `face_blendshapes: arkit` option (`summer_generate_3d({ kind: \"image-to-3d\", options: { rig: true, face_blendshapes: \"arkit\" } })`), use an emotional body-language overlay instead, or hand off the character to a 3D artist for shape-key authoring." Don't proceed.
49
+
50
+ Note: the `face_blendshapes` option is a Meshy passthrough; if your target backend doesn't support it, the rig will use the default skeleton — adjust manually in the Meshy dashboard.
51
+
52
+ ### 2. Get the audio
53
+
54
+ If the user has VO already, use it. If not, generate via TTS. The `voiceId` is an ElevenLabs voice ID — the user has to pick one from the ElevenLabs voice library (https://elevenlabs.io/app/voice-library) or upload/clone their own. There is no "voice name" string; it's always an ID like `21m00Tcm4TlvDq8ikWAM` (Rachel) or a custom-cloned ID.
55
+
56
+ ```
57
+ summer_generate_audio({
58
+ capability: "text_to_speech",
59
+ text: "Welcome to the village, traveler.",
60
+ voiceId: "<elevenlabs_voice_id>"
61
+ })
62
+ // returns { jobId, ... } — poll with summer_check_job for the audio asset
63
+ ```
64
+
65
+ If the user hasn't picked a voice yet, stop and ask: "Which ElevenLabs voice ID should I use? You can browse the library at elevenlabs.io/app/voice-library and copy the ID, or paste a custom-clone ID from your account."
66
+
67
+ ### 3. Extract phonemes (the generative step — runs OUTSIDE Summer)
68
+
69
+ Summer does not wrap a phoneme-extraction MCP tool. Run it externally, then import the result. **Recommended: Rhubarb Lip Sync** — an open-source CLI built for game-dev lipsync. Mouth shapes A–H map cleanly to viseme shapes.
70
+
71
+ Install once:
72
+
73
+ ```
74
+ # macOS
75
+ brew install rhubarb-lip-sync
76
+ # Windows / Linux: download release from https://github.com/DanielSWolf/rhubarb-lip-sync
77
+ ```
78
+
79
+ Run per VO line:
80
+
81
+ ```
82
+ rhubarb -f json -o welcome_traveler.json welcome_traveler.wav
83
+ ```
84
+
85
+ Output JSON shape:
86
+
87
+ ```json
88
+ {
89
+ "metadata": { "duration": 2.34 },
90
+ "mouthCues": [
91
+ { "start": 0.00, "end": 0.08, "value": "B" },
92
+ { "start": 0.08, "end": 0.18, "value": "C" },
93
+ { "start": 0.18, "end": 0.25, "value": "D" },
94
+ { "start": 0.25, "end": 0.30, "value": "B" },
95
+ { "start": 0.30, "end": 0.42, "value": "A" },
96
+ { "start": 0.42, "end": 0.50, "value": "B" },
97
+ { "start": 2.30, "end": 2.34, "value": "X" }
98
+ ]
99
+ }
100
+ ```
101
+
102
+ Rhubarb mouth-shape → ARKit viseme cheat sheet (apply during the bake step):
103
+
104
+ | Rhubarb | ARKit viseme | Notes |
105
+ |---|---|---|
106
+ | A | `viseme_PP` | closed (P, B, M) |
107
+ | B | `viseme_kk` | slightly open, neutral |
108
+ | C | `viseme_E` | open, lips spread (EH, IH) |
109
+ | D | `viseme_aa` | wide open (AA, AH) |
110
+ | E | `viseme_O` | rounded (OW, ER) |
111
+ | F | `viseme_U` | small rounded (UW, OO) |
112
+ | G | `viseme_FF` | F, V (lower lip + teeth) |
113
+ | H | `viseme_RR` | L, R (tongue raised) |
114
+ | X | `viseme_sil` | silence / closed neutral |
115
+
116
+ **Manual fallback (no Rhubarb available):** for short lines you can hand-type a `mouthCues` array by listening to the clip and tagging vowel/consonant boundaries. Painful past ~3s of audio; use Rhubarb for anything longer.
117
+
118
+ **Cloud fallback:** a Whisper-phoneme model on Replicate or a Hugging Face inference endpoint will emit ARPAbet phonemes with timestamps. Then map ARPAbet → ARKit visemes via the table in the Reference card section. More accurate than Rhubarb on noisy audio; slower and costs cents per minute.
119
+
120
+ Cost: Rhubarb is free + ~5s CPU per 30s clip locally. Cloud fallback ~$0.02 / minute, ~5s wall-clock for a 30s clip.
121
+
122
+ ### 4. Persist the viseme track as an AnimationLibrary entry
123
+
124
+ Convert Rhubarb's `mouthCues` into a Godot Animation resource — one track per BlendShape, keyframes at each viseme transition. This makes lipsync replayable via the same AnimationPlayer/AnimationTree as body motion.
125
+
126
+ ```gdscript
127
+ # scripts/lipsync_baker.gd — run once per VO line at edit time
128
+ const RHUBARB_TO_VISEME := {
129
+ "A": "viseme_PP", "B": "viseme_kk", "C": "viseme_E",
130
+ "D": "viseme_aa", "E": "viseme_O", "F": "viseme_U",
131
+ "G": "viseme_FF", "H": "viseme_RR", "X": "viseme_sil",
132
+ }
133
+
134
+ static func bake_from_rhubarb(rhubarb_json_path: String, head_path: NodePath) -> Animation:
135
+ var f := FileAccess.open(rhubarb_json_path, FileAccess.READ)
136
+ var data: Dictionary = JSON.parse_string(f.get_as_text())
137
+ var cues: Array = data["mouthCues"]
138
+ var anim := Animation.new()
139
+ anim.length = float(data["metadata"]["duration"])
140
+ var visemes := ["viseme_aa", "viseme_E", "viseme_I", "viseme_O", "viseme_U",
141
+ "viseme_PP", "viseme_FF", "viseme_TH", "viseme_DD", "viseme_kk",
142
+ "viseme_CH", "viseme_SS", "viseme_nn", "viseme_RR", "viseme_sil"]
143
+ var tracks := {}
144
+ for v in visemes:
145
+ var idx := anim.add_track(Animation.TYPE_BLEND_SHAPE)
146
+ anim.track_set_path(idx, NodePath(str(head_path) + ":" + v))
147
+ tracks[v] = idx
148
+ # For each cue, set the active viseme to 1.0 and others to 0.0 at cue.start
149
+ for cue in cues:
150
+ var active_viseme: String = RHUBARB_TO_VISEME.get(cue["value"], "viseme_sil")
151
+ for v in visemes:
152
+ var weight: float = 1.0 if v == active_viseme else 0.0
153
+ anim.track_insert_key(tracks[v], float(cue["start"]), weight)
154
+ return anim
155
+ ```
156
+
157
+ Bake once, save into the character's AnimationLibrary as `dialogue_<line_id>`, and play via the AnimationTree.
158
+
159
+ If you used the Whisper/ARPAbet cloud fallback instead of Rhubarb, swap `bake_from_rhubarb` for a variant that consumes `{ phoneme, start, duration }` triples and applies the ARPAbet → viseme table from the Reference card.
160
+
161
+ ### 5. Wire into the AnimationTree
162
+
163
+ Add a OneShot node `Lipsync` that overlays the viseme animation as an *additive* layer over the base face. Fire from the dialogue system:
164
+
165
+ ```gdscript
166
+ @onready var tree: AnimationTree = $AnimationTree
167
+ @onready var audio: AudioStreamPlayer3D = $VoicePlayer
168
+
169
+ func say(line_id: String) -> void:
170
+ var clip_id := "dialogue_" + line_id
171
+ tree.set("parameters/Lipsync/animation", clip_id)
172
+ audio.stream = load("res://audio/" + line_id + ".ogg")
173
+ audio.play()
174
+ tree.set("parameters/Lipsync/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
175
+ ```
176
+
177
+ Sync is preserved as long as both fire on the same frame. ~16ms drift is the threshold of perception; AnimationTree + AudioStreamPlayer3D are both sample-accurate, so drift only happens if the engine hitches mid-line.
178
+
179
+ ### 6. Add the emotional layer (authored, not generative)
180
+
181
+ A second OneShot or persistent additive track for expressions. Key the relevant BlendShapes (`mouthSmile_L`, `mouthSmile_R`, `browInnerUp`, etc.) at design time:
182
+
183
+ ```gdscript
184
+ func smile(intensity: float) -> void:
185
+ var head: MeshInstance3D = $Head
186
+ head.set_blend_shape_value(head.find_blend_shape_by_name("mouthSmile_L"), intensity)
187
+ head.set_blend_shape_value(head.find_blend_shape_by_name("mouthSmile_R"), intensity)
188
+
189
+ func surprise(intensity: float) -> void:
190
+ var head: MeshInstance3D = $Head
191
+ head.set_blend_shape_value(head.find_blend_shape_by_name("browInnerUp"), intensity)
192
+ head.set_blend_shape_value(head.find_blend_shape_by_name("browOuterUp_L"), intensity * 0.7)
193
+ head.set_blend_shape_value(head.find_blend_shape_by_name("browOuterUp_R"), intensity * 0.7)
194
+ head.set_blend_shape_value(head.find_blend_shape_by_name("eyeWide_L"), intensity)
195
+ head.set_blend_shape_value(head.find_blend_shape_by_name("eyeWide_R"), intensity)
196
+ head.set_blend_shape_value(head.find_blend_shape_by_name("jawOpen"), intensity * 0.3)
197
+ ```
198
+
199
+ Driven from gameplay events; orthogonal to the lipsync layer.
200
+
201
+ ## Confirmation gates
202
+
203
+ - **Before extracting:** show audio length, est. cost, est. wait. Wait for OK.
204
+ - **Before baking the Animation resource:** confirm the head's BlendShape names match the standard set. If they're custom-named, ask for the mapping.
205
+ - **Before saving the scene:** confirm the AnimationTree changes (added Lipsync OneShot, library entry).
206
+
207
+ ## Reference card
208
+
209
+ ### ARKit-52 viseme subset (the 15 that matter for English lipsync)
210
+
211
+ | Viseme | Triggered by phonemes (ARPAbet) | Mouth shape |
212
+ |---|---|---|
213
+ | `viseme_sil` | silence | closed neutral |
214
+ | `viseme_PP` | P, B, M | lips pressed |
215
+ | `viseme_FF` | F, V | lower lip + upper teeth |
216
+ | `viseme_TH` | TH, DH | tongue tip + teeth |
217
+ | `viseme_DD` | T, D, N, S, Z | tongue + alveolar |
218
+ | `viseme_kk` | K, G, NG | back-tongue, mouth slightly open |
219
+ | `viseme_CH` | CH, JH, SH, ZH | lips rounded forward |
220
+ | `viseme_SS` | S, Z (sometimes split from DD) | teeth nearly closed |
221
+ | `viseme_nn` | N, L | tongue-tip + open mouth |
222
+ | `viseme_RR` | R, ER | mouth slightly rounded |
223
+ | `viseme_aa` | AA, AH, AE | wide open |
224
+ | `viseme_E` | EH, EY, IH | mid open + lips spread |
225
+ | `viseme_I` | IY, IH | narrow + spread |
226
+ | `viseme_O` | OW, AO | rounded |
227
+ | `viseme_U` | UW, UH | rounded + small |
228
+
229
+ ### Phoneme → viseme mapping (for hand-rolled extraction)
230
+
231
+ If the user has a CMUDict-style phoneme list and wants to map manually, this is the table to apply.
232
+
233
+ ### Pitfalls
234
+
235
+ - **Mouth never closes between words.** No `viseme_sil` keyframes at silence intervals. The bake step must scan the audio for silence (RMS below threshold for 100ms+) and insert sil keys, OR the phoneme extractor must emit silence markers. Rhubarb emits `X` cues for silence (mapped above to `viseme_sil`); cloud Whisper-phoneme models often don't — add a silence-detection pass if you go that route.
236
+ - **Lipsync drifts behind audio.** Audio playback latency on some platforms is 30–60ms. Either delay the audio start by 1 frame, or pre-shift the animation by the platform's known latency. On desktop Linux audio output can be 60ms behind; on Steam Deck ~20ms.
237
+ - **Visemes pop on/off.** Crossfade between viseme keyframes — set `Animation.TRACK_INTERPOLATION_LINEAR` (default) and ensure each viseme's weight ramps from previous to current. Default bake does this; if you wrote custom keyframes with NEAREST interp, switch.
238
+ - **Smile fights lipsync.** Both write to mouth BlendShapes. Solve by additive layering: lipsync layer outputs deltas from neutral, smile layer outputs deltas from neutral, sum them, clamp 0..1. ARKit shapes are sum-safe up to ~1.5; clamp prevents over-rotation.
239
+ - **Eyes look dead.** Lipsync is mouth-only; without blinks and saccades the face is uncanny. Add an idle blink track (every 4–8s, jittered) and a small saccade track (random eye movement up to 5°). Both can be one-shots fired by a `Timer`.
240
+ - **Phoneme extraction returns gibberish.** Audio is too noisy, mismatched language code, or compressed too aggressively. Re-export the source as 22kHz mono WAV before sending. Don't lipsync from a 64kbps MP3.
241
+ - **Custom rig has different BlendShape names.** The bake assumes ARKit-52. If the rig uses Preston Blair or a custom set, write a name-mapping `Dictionary` and apply it during bake. Only do this once per character.
242
+
243
+ ### Quality bar
244
+
245
+ - ~12 phonemes/sec is normal English speech; the bake produces ~25 keyframes/sec across all tracks. Below 60fps playback on weak hardware: enable `BlendShape track interpolation = NEAREST` (loses smoothness, gains 30% perf). Reserve for mobile / very crowded crowd scenes.
246
+ - Lipsync alone is ~70% of "alive". Add idle blink + idle micro-head-bob (`summer:animation/procedural-animation`) and you're at ~95%. The remaining 5% is brow articulation tied to dialogue sentiment, which is bespoke per scene.
247
+
248
+ ## Anti-patterns
249
+
250
+ - Driving BlendShape weights from `_process` with hand-coded curves. The Animation track is sample-accurate and re-uses the AnimationTree's interpolation; manual code drifts and stutters.
251
+ - Using a single "talking" loop instead of phoneme-driven lipsync. The 1990s look. Always extract phonemes; the cost is trivial.
252
+ - Forgetting to gate audio + animation on the same frame. If you `play()` audio in `_ready` and fire the OneShot in `_process`, you'll see ~16ms drift at line start. Both calls in the same function, same frame.
253
+ - Using lipsync for non-talking sounds (groans, screams). Lipsync needs phonemes; non-verbal vocals confuse the extractor. For grunts, drive `jawOpen` from the audio's RMS envelope instead — much simpler.
254
+
255
+ ## Edge cases
256
+
257
+ - **Multilingual VO.** Rhubarb supports English best; for other languages, pass `--recognizer phonetic` (language-agnostic, less accurate) or use a multilingual Whisper-phoneme model on Replicate / HF. Cross-language lipsync (extract as English on Spanish audio) gives ~70% accuracy — bad enough that subtitles are needed regardless.
258
+ - **Singing.** Phoneme extractor handles sustained vowels well, but consonant timing is loose. For sung dialogue, manually keyframe consonants and let extracted vowels fill in.
259
+ - **Aside / muttering at low volume.** Phoneme extractor needs > -40 dB. Boost the source clip before extracting if the line is intentionally quiet, then play it at the original volume in-engine.
260
+ - **Stylized character with no jaw bone (e.g., a cartoon ball).** No bone, but BlendShapes can still drive a "morph open" shape. Same pipeline; skip the jaw-bone-track and only animate BlendShapes.
261
+
262
+ ## Fallback (no Rhubarb)
263
+
264
+ If Rhubarb won't install on the user's platform, run a Whisper-phoneme model on Replicate (e.g., `cjwbw/whisper-phoneme`) or `aeneas-align` for forced alignment. Download the JSON, swap the bake function for one that consumes ARPAbet phonemes (table in Reference card). Same end result — replayable BlendShape Animation resource.
265
+
266
+ For projects that can't use any cloud or third-party tool, Godot 4.5's `AudioStreamGenerator` with hand-rolled vowel/consonant detection from RMS + zero-crossings gives ~50% accuracy — enough for a stylized character but not photoreal.
267
+
268
+ ## Handoff
269
+
270
+ - For voice generation upstream, `summer:audio/generate-voice` (which wraps `summer_generate_audio({capability: "text_to_speech", ...})`).
271
+ - For dialogue scripts and conversation flow, `summer:ai-and-npcs/design-npc`.
272
+ - For the AnimationTree this layer composes into, `summer:animation/animation-tree`.
273
+ - For idle blinks, saccades, and head-tracking that complement lipsync, `summer:animation/procedural-animation`.
274
+ - For full performance capture (face + body together), out of scope — see Meshy's mocap docs or external pipelines.
275
+
276
+ ## See also
277
+
278
+ - `summer:audio/generate-voice` — TTS upstream of this skill.
279
+ - `summer:animation/animation-tree` — wire the lipsync OneShot into the character's tree.
280
+ - `summer:animation/procedural-animation` — eye blinks, saccades, head idle.
281
+ - `_shared/mcp-tools-reference.md` — `summer_generate_audio` schema (TTS).
282
+ - Rhubarb Lip Sync — https://github.com/DanielSWolf/rhubarb-lip-sync (external tool; phoneme extraction).