Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/en/workflow/usage_scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,18 @@ Tracking is done with the dedicated **`SpriteStudioPartAttachment2D`** node. Pla

| Property | Description |
|---|---|
| **Part Name** (`part_name`) | The name of the part to follow (the PartData name in the `.ssab`). The inspector offers a dropdown populated from the asset's part names (still typable, for when the player cannot be resolved) |
| **Part Name** (`part_name`) | The name of the part to follow (the PartData name in the `.ssab`). The inspector offers a dropdown populated from the asset's part names (still typable, for when the player cannot be resolved). Skinned-mesh parts are left out of the dropdown — see the note below |
| **Follow Path** (`follow_path`) | The `SpriteStudioPlayer2D` to read from. Empty (default) uses the **nearest ancestor** player |
| **Remote Path** (`remote_path`) | The `Node2D` to drive. Empty (default) drives this node itself, and its children follow through scene-tree inheritance. Set it to push the pose to an external node instead (for assets that live outside the player's subtree) |
| **Use Global Coordinates** (`use_global_coordinates`) | ON (default) writes the pose in global coordinates, OFF in the target's local coordinates |
| **Update Position / Update Rotation** (`update_position` / `update_rotation`) | Reflect position / rotation (both ON by default) |
| **Update Scale** (`update_scale`) | Reflect scale (OFF by default) |
| **On Part Hidden** (`on_part_hidden`) | Behavior on frames where the part is hidden. `Follow Always` (keep following; default) / `Hide Target` (hide the target). From a script: `SpriteStudioPartAttachment2D.FOLLOW_ALWAYS` / `.HIDE_TARGET` |

> **Mesh (skinned) parts cannot be followed directly.** A bone-bound mesh part (a deforming part such as a hand or a face) has its vertices drawn by bone skinning, while the part's own transform stays at its setup node position — usually near the character root. So targeting such a part makes the follower snap near the root rather than onto the visible art. To follow the position of a mesh part, add a **NULL part (a bone-point / empty part)** at that spot in SpriteStudio and target that part's name instead; a bone (armature) or joint part works the same way.
>
> To steer away from the mistake, **the `part_name` dropdown omits skinned-mesh parts** — you can only reach one by typing its name in the field by hand (kept possible for the rare deliberate case). Whenever `part_name` does resolve to a skinned mesh, the node shows a **configuration warning** (the ⚠ next to it in the Scene dock) so the choice is never silent; `is_part_skinned_mesh(part_name)` reports the same thing from a script. Rigid (deform-only) meshes track their node faithfully, so they stay in the dropdown and raise no warning.

### Querying from a Script

Instead of placing a node, you can also ask the player for a part's pose directly.
Expand All @@ -175,6 +179,7 @@ func _on_frame_updated(frame_no: float):
| `find_part_index(part_name)` | Part index, or `-1` if the part is not in the asset |
| `get_part_transform(part_name)` | The part's transform for the current frame (a `Transform2D`, player-local, with `flip_h` / `flip_v` / `offset` already applied). Identity if the part is unknown |
| `is_part_hidden(part_name)` | Whether the part is hidden on the current frame. `false` if the part is unknown |
| `is_part_skinned_mesh(part_name)` | Whether the part is a bone-skinned mesh — a poor follow target (see the note above). `false` if the part is unknown or is a rigid (deform-only) mesh |
| signal `frame_updated(frame_no: float)` | Emitted right after the frame's part poses are finalized |

> When you only need the pose at a single moment (a projectile spawn point, for example) rather than continuous following, calling `get_part_transform()` directly is simpler than placing a `SpriteStudioPartAttachment2D`.
Expand Down
6 changes: 5 additions & 1 deletion docs/ja/workflow/usage_scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,18 @@ func change_costume():

| プロパティ | 説明 |
|---|---|
| **Part Name** (`part_name`) | 追従対象のパーツ名(`.ssab` の PartData 名)。インスペクタではアセットのパーツ名からドロップダウンで選べます(プレーヤを解決できない場面のために手入力も可) |
| **Part Name** (`part_name`) | 追従対象のパーツ名(`.ssab` の PartData 名)。インスペクタではアセットのパーツ名からドロップダウンで選べます(プレーヤを解決できない場面のために手入力も可)。スキンメッシュのパーツはドロップダウンには出ません(後述の注記を参照) |
| **Follow Path** (`follow_path`) | 追従元の `SpriteStudioPlayer2D`。空(既定)なら**最も近い祖先**のプレーヤを使います |
| **Remote Path** (`remote_path`) | 駆動する対象の `Node2D`。空(既定)なら自分自身を動かし、子はシーンツリーの継承で追従します。指定するとその外部ノードへ姿勢を書き込みます(プレーヤのサブツリーの外に置いた資産を追従させたい場合) |
| **Use Global Coordinates** (`use_global_coordinates`) | ON(既定)でグローバル座標として、OFF で対象のローカル座標として書き込みます |
| **Update Position / Update Rotation** (`update_position` / `update_rotation`) | 位置 / 回転を反映します(ともに既定 ON) |
| **Update Scale** (`update_scale`) | スケールを反映します(既定 OFF) |
| **On Part Hidden** (`on_part_hidden`) | パーツが hide のフレームでの挙動。`Follow Always`(追従を継続。既定)/ `Hide Target`(対象を非表示)。スクリプトからは `SpriteStudioPartAttachment2D.FOLLOW_ALWAYS` / `.HIDE_TARGET` |

> **メッシュ(スキン)パーツには直接追従できません。** ボーンにバインドされたメッシュパーツ(手・顔などの変形パーツ)は、頂点がボーンのスキニングで描かれる一方、パーツ自身の変換はセットアップ時のノード位置(多くはキャラクターのルート付近)に留まります。そのため、こうしたパーツを追従先に指定すると、対象は見た目の絵ではなくルート付近に吸い寄せられます。メッシュパーツの位置に追従させたいときは、SpriteStudio 側でその位置に **NULL パーツ(ボーンポイント/空パーツ)** を1つ置き、そのパーツ名を追従先に指定してください。ボーン(armature)やジョイントパーツを指定しても同様に追従できます。
>
> うっかり選択を防ぐため、**`part_name` のドロップダウンにはスキンメッシュのパーツを出していません** — 指定できるのはフィールドに名前を手入力した場合だけです(あえて選びたい稀なケースのために手入力は残しています)。手入力などで `part_name` がスキンメッシュに解決されたときは、ノードに**構成警告(configuration warning)**(シーンドックのノード横に付く⚠)が表示され、選択が黙って通ることはありません。スクリプトからは `is_part_skinned_mesh(part_name)` で同じ判定ができます。なお、リジッド(deform のみ・ボーン無し)メッシュはノードに忠実に追従するので、ドロップダウンにも残り、警告も出ません。

### スクリプトからの参照

ノードを置かずに、プレーヤへ直接パーツの姿勢を問い合わせることもできます。
Expand Down
16 changes: 16 additions & 0 deletions ss_player/ss_internal_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,16 +341,25 @@ void SsInternalPlayer::setSSABResource(const Ref<SSABResource>& ssabRes) {
void SsInternalPlayer::_rebuild_part_index_map() {
_part_name_to_index.clear();
_part_names.clear();
_part_skinned.clear();
if (_ssabRes.is_null()) return;
const ss::format::SsAnimeBinary* binary = _ssabRes->get_ss_anime_binary();
if (!binary || !binary->parts()) return;
auto parts = binary->parts();
const uint32_t n = parts->size();
_part_names.resize(n);
_part_skinned.resize(n);
for (uint32_t i = 0; i < n; i++) {
const auto* pd = parts->Get(i);
String nm = (pd && pd->name()) ? String::utf8(pd->name()->c_str()) : String();
_part_names[i] = nm;
// A mesh part is "skinned" when its binding carries influence bones; a
// rigid (deform-only) mesh has none and follows its node transform
// faithfully. Same test every sibling player uses (PartMeshBinding
// influence_bone length > 0), so the flag agrees across engines.
const auto* mb = pd ? pd->mesh_binding() : nullptr;
const auto* bones = mb ? mb->influence_bone() : nullptr;
_part_skinned[i] = (bones && bones->size() > 0) ? 1 : 0;
// First occurrence wins; duplicate part names are not expected within a
// single binary, but guard against clobbering a lower index just in case.
if (!nm.is_empty() && !_part_name_to_index.has(nm)) {
Expand Down Expand Up @@ -385,6 +394,13 @@ bool SsInternalPlayer::try_get_part_hidden(int p_part_index, bool& r_hidden) con
return true;
}

bool SsInternalPlayer::try_get_part_skinned_mesh(int p_part_index, bool& r_skinned) const {
r_skinned = false;
if (p_part_index < 0 || (uint32_t)p_part_index >= _part_skinned.size()) return false;
r_skinned = _part_skinned[p_part_index] != 0;
return true;
}

int SsInternalPlayer::get_part_count() const {
return (int)_part_names.size();
}
Expand Down
12 changes: 12 additions & 0 deletions ss_player/ss_internal_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ class SsInternalPlayer {
// Per-part hide flag for the current frame. False (with r_hidden=false)
// when the index is out of range.
bool try_get_part_hidden(int p_part_index, bool& r_hidden) const;
// Whether the part is a bone-skinned mesh (its mesh_binding has one or more
// influence bones). Such a part's vertices are drawn by skinning while its
// own node transform stays near the setup pose, so it is a poor follow
// target — the query lets the attachment node flag it. A binary property
// (like the part name), so it is answered from the cache built by
// `_rebuild_part_index_map`, not the per-frame state. False (with
// r_skinned=false) when the index is out of range.
bool try_get_part_skinned_mesh(int p_part_index, bool& r_skinned) const;
// Number of parts in the current binary, and the name of part i ("" if out
// of range). Used to populate the editor part-name dropdown.
int get_part_count() const;
Expand Down Expand Up @@ -357,6 +365,10 @@ class SsInternalPlayer {
// space the world matrices and `_parts_by_idx` use.
HashMap<String, int> _part_name_to_index;
LocalVector<String> _part_names;
// Per-part skinned-mesh flag, indexed by part_index. A binary property, so
// it is built once alongside _part_names in _rebuild_part_index_map (unlike
// the per-frame _part_hidden). 1 = bone-skinned mesh, 0 = anything else.
LocalVector<uint8_t> _part_skinned;
// Per-part hide flag for the current frame, indexed by part_index. Rebuilt
// each _drawAnimation; 0 (visible) for parts absent from the frame.
LocalVector<uint8_t> _part_hidden;
Expand Down
24 changes: 22 additions & 2 deletions ss_player/ss_part_attachment_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,23 @@ void SpriteStudioPartAttachment2D::_validate_property(PropertyInfo &p_property)
const SpriteStudioPlayer2D* player = _resolve_player();
if (player) {
PackedStringArray names = player->get_part_names();
// Keep skinned (bone-bound) mesh parts OUT of the dropdown: their
// vertices are drawn by skinning while the part's own transform stays
// near the setup pose, so they track poorly and picking one from the
// list is an easy mistake. Rigid (deform-only) meshes follow their
// node faithfully, so they stay. A name can still be typed by hand
// for the rare deliberate case — the field is ENUM_SUGGESTION, not
// ENUM — and the configuration warning then flags it.
PackedStringArray suggestions;
for (int i = 0; i < names.size(); i++) {
if (!player->is_part_skinned_mesh(names[i])) {
suggestions.push_back(names[i]);
}
}
// ENUM_SUGGESTION (not ENUM) so the field stays editable when the
// player isn't resolvable at edit time — never lock out a name.
p_property.hint = PROPERTY_HINT_ENUM_SUGGESTION;
p_property.hint_string = String(",").join(names);
p_property.hint_string = String(",").join(suggestions);
}
}
}
Expand Down Expand Up @@ -214,10 +227,17 @@ PackedStringArray SpriteStudioPartAttachment2D::_get_configuration_warnings() co
PackedStringArray SpriteStudioPartAttachment2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#endif
if (_resolve_player() == nullptr) {
SpriteStudioPlayer2D* player = _resolve_player();
if (player == nullptr) {
warnings.push_back(tr("No SpriteStudioPlayer2D found. Set \"follow_path\" or place this node under a SpriteStudioPlayer2D."));
} else if (_part_name.is_empty()) {
warnings.push_back(tr("Set \"part_name\" to the SpriteStudio part this node should follow."));
} else if (player->is_part_skinned_mesh(_part_name)) {
// Non-blocking: following still runs, but a bone-skinned mesh draws its
// vertices by skinning while its own transform stays near the setup pose
// (usually the character root), so the target snaps near the root rather
// than onto the visible art. Point at the NULL-part workaround.
warnings.push_back(tr("\"part_name\" is a skinned (bone-bound) mesh part. Its transform stays near the setup pose, so the follower will not sit on the visible art. Add a NULL part at that spot in SpriteStudio and follow that instead."));
}
return warnings;
}
9 changes: 9 additions & 0 deletions ss_player/ss_player_node_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ bool SpriteStudioPlayer2D::is_part_hidden(const String& part_name) const {
return hidden;
}

bool SpriteStudioPlayer2D::is_part_skinned_mesh(const String& part_name) const {
int idx = _internal->resolve_part_index(part_name);
if (idx < 0) return false;
bool skinned = false;
_internal->try_get_part_skinned_mesh(idx, skinned);
return skinned;
}

PackedStringArray SpriteStudioPlayer2D::get_part_names() const {
PackedStringArray names;
int count = _internal->get_part_count();
Expand Down Expand Up @@ -554,6 +562,7 @@ void SpriteStudioPlayer2D::_bind_methods() {
ClassDB::bind_method( D_METHOD( "find_part_index", "part_name" ), &SpriteStudioPlayer2D::find_part_index );
ClassDB::bind_method( D_METHOD( "get_part_transform", "part_name" ), &SpriteStudioPlayer2D::get_part_transform );
ClassDB::bind_method( D_METHOD( "is_part_hidden", "part_name" ), &SpriteStudioPlayer2D::is_part_hidden );
ClassDB::bind_method( D_METHOD( "is_part_skinned_mesh", "part_name" ), &SpriteStudioPlayer2D::is_part_skinned_mesh );
ClassDB::bind_method( D_METHOD( "get_part_names" ), &SpriteStudioPlayer2D::get_part_names );

// ---- Override Layer (Phase 2): per-part runtime overrides -------------
Expand Down
5 changes: 5 additions & 0 deletions ss_player/ss_player_node_2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ class SpriteStudioPlayer2D : public Node2D {
Transform2D get_part_transform(const String& part_name) const;
// True if the named part is hidden on the current frame. False when unknown.
bool is_part_hidden(const String& part_name) const;
// True if the named part is a bone-skinned mesh. Such a part's vertices are
// drawn by skinning while its own transform stays near the setup pose, so it
// is a poor follow target — the attachment node uses this to flag it. False
// when the part is unknown or is not a skinned mesh.
bool is_part_skinned_mesh(const String& part_name) const;
// All part names in the current binary (for the attachment's dropdown).
PackedStringArray get_part_names() const;

Expand Down
Loading