diff --git a/docs/en/workflow/usage_scripting.md b/docs/en/workflow/usage_scripting.md index 6da239c..1c6b145 100644 --- a/docs/en/workflow/usage_scripting.md +++ b/docs/en/workflow/usage_scripting.md @@ -142,7 +142,7 @@ 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 | @@ -150,6 +150,10 @@ Tracking is done with the dedicated **`SpriteStudioPartAttachment2D`** node. Pla | **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. @@ -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`. diff --git a/docs/ja/workflow/usage_scripting.md b/docs/ja/workflow/usage_scripting.md index c066b15..595f8b9 100644 --- a/docs/ja/workflow/usage_scripting.md +++ b/docs/ja/workflow/usage_scripting.md @@ -142,7 +142,7 @@ 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 で対象のローカル座標として書き込みます | @@ -150,6 +150,10 @@ func change_costume(): | **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 のみ・ボーン無し)メッシュはノードに忠実に追従するので、ドロップダウンにも残り、警告も出ません。 + ### スクリプトからの参照 ノードを置かずに、プレーヤへ直接パーツの姿勢を問い合わせることもできます。 diff --git a/ss_player/ss_internal_player.cpp b/ss_player/ss_internal_player.cpp index 1db839d..6c79ab1 100644 --- a/ss_player/ss_internal_player.cpp +++ b/ss_player/ss_internal_player.cpp @@ -341,16 +341,25 @@ void SsInternalPlayer::setSSABResource(const Ref& 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)) { @@ -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(); } diff --git a/ss_player/ss_internal_player.h b/ss_player/ss_internal_player.h index 6a2bb92..83d6f97 100644 --- a/ss_player/ss_internal_player.h +++ b/ss_player/ss_internal_player.h @@ -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; @@ -357,6 +365,10 @@ class SsInternalPlayer { // space the world matrices and `_parts_by_idx` use. HashMap _part_name_to_index; LocalVector _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 _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 _part_hidden; diff --git a/ss_player/ss_part_attachment_2d.cpp b/ss_player/ss_part_attachment_2d.cpp index 2f5c2fa..d5c1ba7 100644 --- a/ss_player/ss_part_attachment_2d.cpp +++ b/ss_player/ss_part_attachment_2d.cpp @@ -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); } } } @@ -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; } diff --git a/ss_player/ss_player_node_2d.cpp b/ss_player/ss_player_node_2d.cpp index fbc3c8c..f600c93 100644 --- a/ss_player/ss_player_node_2d.cpp +++ b/ss_player/ss_player_node_2d.cpp @@ -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(); @@ -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 ------------- diff --git a/ss_player/ss_player_node_2d.h b/ss_player/ss_player_node_2d.h index aed0cd4..b1dc2a3 100644 --- a/ss_player/ss_player_node_2d.h +++ b/ss_player/ss_player_node_2d.h @@ -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;