mirror of
https://github.com/godotengine/godot-website.git
synced 2026-01-06 14:09:58 +03:00
79 lines
44 KiB
HTML
79 lines
44 KiB
HTML
<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=author content="Godot Engine"><meta name=description content="We have reworked the skeleton bone update process to add SkeletonModifier3D for modifying the Skeleton."><script defer data-domain=godotengine.org src=https://plausible.godot.foundation/js/script.file-downloads.outbound-links.js></script><meta property="og:site_name" content="Godot Engine"><meta property="og:url" content="https://godotengine.org/article/design-of-the-skeleton-modifier-3d/"><meta property="og:type" content="website"><meta property="og:description" content="We have reworked the skeleton bone update process to add SkeletonModifier3D for modifying the Skeleton."><meta property="og:image" content="https://godotengine.org/storage/blog/covers/design-of-the-skeleton-modifier-3d.webp"><meta name=twitter:card content="summary_large_image"><meta property="twitter:domain" content="godotengine.org"><meta property="twitter:url" content="https://godotengine.org/article/design-of-the-skeleton-modifier-3d/"><meta property="og:title" content="Design of the Skeleton Modifier 3D – Godot Engine"><title>Design of the Skeleton Modifier 3D – Godot Engine</title>
|
||
<link rel=alternate type=application/rss+xml title="Godot News" href=/rss.xml><link rel=alternate type=application/json title="Godot News" href=/rss.json><link rel=alternate type=application/atom+xml title="Godot News" href=/atom.xml><link rel=icon href=/assets/favicon.png sizes=any><link rel=icon href=/assets/favicon.svg type=image/svg+xml><link rel=stylesheet href=/assets/css/main.css?121><link rel=stylesheet href=/assets/css/header.css?1><link rel=stylesheet href=/assets/css/tobii.min.css><link rel=preload as=font href=/assets/fonts/Montserrat-Italic-VariableFont_wght.woff2 crossorigin><link rel=preload as=font href=/assets/fonts/Montserrat-VariableFont_wght.woff2 crossorigin><link rel=me href=https://mastodon.gamedev.place/@godotengine><input type=checkbox id=nav_toggle_cb><header class="flex column"><div class="container flex align-center"><div id=nav_head><a href=/ id=logo-link><img class=nav-logo src=/assets/logo.svg width=136 height=48 alt="Godot Engine">
|
||
<img class="nav-logo dark-logo" src=/assets/logo_dark.svg width=136 height=48 alt="Godot Engine"></a><div class=mobile-links><span class="fund mobile"><a href=https://fund.godotengine.org><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width:13px;fill:#fff;margin-right:4px"><path d="M47.6 300.4 228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6.0 115.2.0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg> Donate</a></span>
|
||
<label for=nav_toggle_cb id=nav_toggle_btn><img src=/assets/icons/hamburger.svg width=24 height=24 alt="Main menu"></label></div></div><nav id=nav><ul class=left><li><a href=/features/>Features</a><li><a href=/showcase/>Showcase</a><li><a href=/blog/>Blog</a><li><a href=/community/>Community</a><li><a href=https://godotengine.org/asset-library/asset>Assets</a></ul><ul class=right><li><a href=/download/windows/ class=set-os-download-url>Download</a><li><a href=https://docs.godotengine.org>Docs</a><li><a href=https://docs.godotengine.org/en/stable/contributing/how_to_contribute.html>Contribute</a><li class="fund desktop"><a href=https://fund.godotengine.org><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width:13px;fill:#fff;margin-right:4px;top:1px;position:relative"><path d="M47.6 300.4 228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6.0 115.2.0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg> Donate</a></ul></nav></div></header><script>document.addEventListener("click",function(e){const t=document.querySelector(".language-selector");if(!t)return;t.contains(e.target)||t.classList.remove("open")});function setLanguagePreference(e,t){e.preventDefault();const s=t.getAttribute("data-lang-path"),o=t.getAttribute("data-lang"),n=new Date;n.setDate(n.getDate()+365),document.cookie=`preferred_language=${o}; expires=${n.toUTCString()}; path=/; SameSite=Lax`,window.location.href=s}</script><main><style>body{background-color:var(--background-color)}h1{margin-bottom:8px;margin-top:32px}:not(pre)>code{background:var(--code-background-color);padding:1px 4px;font-size:.95em;border-radius:3px}pre{background:var(--codeblock-background-color);color:var(--codeblock-color)}pre code{display:block;overflow-x:auto;padding:.5em}.date-big{line-height:2;margin-left:32px}article{background-color:var(--base-color);box-shadow:0 3px 2px rgba(0,0,0,.15)}figure{margin:0}figure img{margin:0}article img,article video{max-width:100%;height:auto;display:block;margin:auto;margin-top:16px;margin-bottom:16px}article h1{margin-top:64px}article h2,article h3,article h4{margin-top:42px}.article-info{display:flex;flex-direction:column;gap:8px}.article-metadata{display:flex;gap:24px;align-items:center;font-family:var(--header-font-family);margin-bottom:12px}@media(max-width:900px){.article-metadata{flex-direction:column;align-items:flex-start;gap:16px}}.article-author{color:var(--base-color-text-subtitle-date);font-weight:700;font-size:18px;flex-grow:1;display:flex;gap:12px;align-items:center}.article-author .avatar{border-radius:100%;margin:0;background:0 0}.article-author .by{color:var(--base-color-text-subtitle)}.article-metadata .date{color:var(--base-color-text-subtitle-date)}.article-metadata .date.post-recent-highlight{color:var(--post-recent-highlight-color);opacity:.8}.article-metadata .date.post-recent-highlight::after{font-size:80%;content:"NEW";border:2px solid var(--post-recent-highlight-color);padding:2px 3px;margin-left:8px}.tag.active{filter:saturate(.75)}@media screen and (min-width:900px){article .content{width:70%;margin:auto}}@media(max-width:900px){body{background-color:var(--base-color)}article{background-color:initial;box-shadow:none}article img:first-child,article video:first-child{max-width:100%}}.blog-navigation{display:grid;grid-template-columns:1fr 1fr;padding-top:30px;padding-bottom:60px}.blog-navigation .next{text-align:right}@media(max-width:900px){.blog-navigation{grid-template-columns:1fr;gap:20px;border-top:1px solid var(--code-background-color)}.blog-navigation .next{text-align:left}}.blog-navigation span{opacity:.6;font-weight:700;margin-bottom:5px;display:block}.blog-navigation a{display:inline-block;text-decoration:none;color:inherit;opacity:.6;transition:opacity .2s}.blog-navigation a:hover{opacity:1}</style><link rel=stylesheet href=/assets/css/highlight.obsidian.min.css><div class=container><article class=padded><div class="content article-container"><figure class=article-cover><img src=/storage/blog/covers/design-of-the-skeleton-modifier-3d.webp title alt=" " class=rounded-lg style=width:100%;height:auto;background-color:initial></figure><div class=article-info><h1>Design of the Skeleton Modifier 3D</h1><div class=article-metadata><div class=article-author><span>By: </span><img class=avatar width=25 height=25 src=/assets/images/authors/tokage.png alt="Silc Renew" loading=lazy>
|
||
<span class=by>Silc Renew</span></div><span class=date data-post-date="2024-08-12 10:00:00 +0000">12 August 2024</span></div><div class=tags><a href=/blog/progress-report><div class="tag active">Progress Report</div></a></div></div><div class=article-body><style>article .content img{background-color:initial}</style><p>In Godot 4.3 we are adding a new node called <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>. It is used to animate <code class="language-plaintext highlighter-rouge">Skeleton3D</code>s outside of <code class="language-plaintext highlighter-rouge">AnimationMixer</code> and is now the base class for several existing nodes.<p>As part of this we have deprecated (but not removed) some of the pose override functionality in <code class="language-plaintext highlighter-rouge">Skeleton3D</code> including:<ul><li><code class="language-plaintext highlighter-rouge">set_bone_global_pose_override()</code><li><code class="language-plaintext highlighter-rouge">get_bone_global_pose_override()</code><li><code class="language-plaintext highlighter-rouge">get_bone_global_pose_no_override()</code><li><code class="language-plaintext highlighter-rouge">clear_bones_global_pose_override()</code></ul><h1 id=did-the-pose-override-design-have-problems>Did the pose override design have problems?</h1><p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/mod.webp alt=modification><p>Previously, we recommended using the property <code class="language-plaintext highlighter-rouge">global_pose_override</code> when modifying the bones. This was useful because the original pose was kept separately, so blend values could be set, and bones could be modified without changing the property in <code class="language-plaintext highlighter-rouge">.tscn</code> file. However, the more complex people’s demands for Godot 3D became, the less it covered the use cases and became outdated.<p>The main problem is the fact that “the processing order between <code class="language-plaintext highlighter-rouge">Skeleton3D</code> and <code class="language-plaintext highlighter-rouge">AnimationMixer</code> is changed depending on the <code class="language-plaintext highlighter-rouge">SceneTree</code> structure`.<p><strong>For example, it means that the following two scenes will have different results:</strong><p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/different_process_orders.webp alt="different process orders"><p>If there is a modifier such as IK or physical bone, in most cases, it needs to be applied to the result of the played animation. So they need to be processed after the <code class="language-plaintext highlighter-rouge">AnimationMixer</code>.<p>In the old skeleton modifier design with bone pose override you must place those modifiers below the <code class="language-plaintext highlighter-rouge">AnimationMixer</code>. However as scene trees become more complex, it becomes difficult to keep track of the processing order. Also the scene might be imported from glTF which cannot be edited without localization, so managing node order becomes tedious.<p>Moreover, if multiple nodes use bone pose override, it breaks the modified result.<p><strong>Let’s imagine a case in which bone modification is performed in the following order:</strong><div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>AnimationMixer -> ModifierA -> ModifierB
|
||
</code></pre></div></div><p>Keep in mind that both <code class="language-plaintext highlighter-rouge">ModifierA</code> and <code class="language-plaintext highlighter-rouge">ModifierB</code> need to get the bone pose that was processed immediately before.<p>The <code class="language-plaintext highlighter-rouge">AnimationMixer</code> does not use <code class="language-plaintext highlighter-rouge">set_bone_global_pose_override()</code>, so it transforms the original pose as <code class="language-plaintext highlighter-rouge">set_bone_pose_rotation()</code>. This means that the input to <code class="language-plaintext highlighter-rouge">ModifierA</code> must be retrieved from the original pose with <code class="language-plaintext highlighter-rouge">get_bone_global_pose_no_override()</code> and the output must be retreived from the override with <code class="language-plaintext highlighter-rouge">get_bone_global_pose_override()</code>. In this case, if <code class="language-plaintext highlighter-rouge">ModiferB</code> wants to consider the output of <code class="language-plaintext highlighter-rouge">ModiferA</code>, both the input and output of <code class="language-plaintext highlighter-rouge">ModifierB</code> must be the override with <code class="language-plaintext highlighter-rouge">get_bone_global_pose_override()</code>.<p>Then, can the order of <code class="language-plaintext highlighter-rouge">ModifierA</code> and <code class="language-plaintext highlighter-rouge">ModifierB</code> be interchanged?<p>–The answer is “NO”.<p>Because <code class="language-plaintext highlighter-rouge">ModifierB</code>’s input is now <code class="language-plaintext highlighter-rouge">get_bone_global_pose_override()</code> which is different from <code class="language-plaintext highlighter-rouge">get_bone_global_pose_no_override()</code>, so <code class="language-plaintext highlighter-rouge">ModifierB</code> cannot get the original pose set by the <code class="language-plaintext highlighter-rouge">AnimationMixer</code>.<p>As I described above, the override design was very weak in terms of process ordering.<h1 id=how-does-the-new-skeleton-design-work-with-skeletonmodifier3d>How does the new skeleton design work with SkeletonModifier3D?</h1><p><code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> is designed to modify bones in the <code class="language-plaintext highlighter-rouge">_process_modification()</code> virtual method. This means that if you want to develop a custom <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>, you will need to modify the bones within that method.<p><code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> does not execute modifications by itself, but is executed by the parent of <code class="language-plaintext highlighter-rouge">Skeleton3D</code>. By placing <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> as a child of <code class="language-plaintext highlighter-rouge">Skeleton3D</code>, they are registered in <code class="language-plaintext highlighter-rouge">Skeleton3D</code>, and the process is executed only once per frame in the <code class="language-plaintext highlighter-rouge">Skeleton3D</code> update process. Then, <strong>the processing order between modifiers is guaranteed to be the same as the order of the children in <code class="language-plaintext highlighter-rouge">Skeleton3D</code>’s child list</strong>.<p>Since <code class="language-plaintext highlighter-rouge">AnimationMixer</code> is applied before the <code class="language-plaintext highlighter-rouge">Skeleton3D</code> update process, <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> is guaranteed to run after <code class="language-plaintext highlighter-rouge">AnimationMixer</code>. Also, they do not require <code class="language-plaintext highlighter-rouge">bone_pose_global_override</code>; This removes any confusion as to whether we should use override or not.<p><strong>Here is a SkeletonModifier3D sequence diagram:</strong><p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/skeleton_modifier_process.webp alt="skeleton modifier process"><p>Dirty flag resolution may be performed several times per frame, but the update process is a deferred call and is performed only once per frame.<p>At the beginning of the update process, it stores the pose before the modification process temporarily. When the modification process is complete and applied to the skin, the pose is rolled back to the temporarily stored pose. This performs the role of the past <code class="language-plaintext highlighter-rouge">bone_pose_global_override</code> which stored the override pose separate from the original pose.<p>By the way, you may want to get the pose after the modification, or you may wonder why the modifier in the later part cannot enter the original pose when there are multiple modifiers.<p>We have added some signals for cases where you need to retrieve the pose at each point in time, so you can use them.<ul><li>AnimationMixer: mixer_applied<ul><li>Notifies when the blending result related have been applied to the target objects</ul><li>SkeletonModifier3D: modification_processed<ul><li>Notifies when the modification have been finished</ul><li>Skeleton3D: skeleton_updated<ul><li>Emitted when the final pose has been calculated will be applied to the skin in the update process</ul></ul><p>Also, note that this process depends on the <code class="language-plaintext highlighter-rouge">Skeleton3D.modifier_callback_mode_process</code> property.<p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/modifier_callback_mode_process.webp alt="modifier callback mode process property"><p>For example, in a use case that the node uses the physics process outside of <code class="language-plaintext highlighter-rouge">Skeleton3D</code> and it affects <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>, the property must be set to <code class="language-plaintext highlighter-rouge">Physics</code>.<p>Finally, now we can say that <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> does not make it impossible to do anything that was possible in the past.<h1 id=how-to-make-a-custom-skeletonmodifier3d>How to make a custom SkeletonModifier3D?</h1><p><code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> is a virtual class, so you can’t add it as stand alone node to a scene.<p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/add_skeleton_modifier.webp alt="add skeleton modifier"><p>Then, how do we create a custom <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>? Let’s try to create a simple custom <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> that points the Y-axis of a bone to a specific coordinate.<h2 id=1-create-a-script>1. Create a script</h2><p>Create a blank gdscript file that extends <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>. At this time, register the custom <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> you created with the <code class="language-plaintext highlighter-rouge">class_name</code> declaration so that it can be added to the scene dock.<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=k>class_name</span> <span class=n>CustomModifier</span>
|
||
<span class=k>extends</span> <span class=n>SkeletonModifier3D</span>
|
||
</code></pre></div></div><p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/register_custom_modifier.webp alt="register custom modifier"><h2 id=2-add-some-declarations-and-properties>2. Add some declarations and properties</h2><p>If necessary, add a property to set the bone by declaring <code class="language-plaintext highlighter-rouge">@export_enum</code> and set the <code class="language-plaintext highlighter-rouge">Skeleton3D</code> bone names as a hint in <code class="language-plaintext highlighter-rouge">_validate_property()</code>. You also need to declare <code class="language-plaintext highlighter-rouge">@tool</code> if you want to select it in the editor.<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=err>@</span><span class=k>tool</span>
|
||
|
||
<span class=k>class_name</span> <span class=n>CustomModifier</span>
|
||
<span class=k>extends</span> <span class=n>SkeletonModifier3D</span>
|
||
|
||
<span class=err>@</span><span class=k>export</span> <span class=k>var</span> <span class=n>target_coordinate</span><span class=p>:</span> <span class=kt>Vector3</span> <span class=o>=</span> <span class=kt>Vector3</span><span class=p>(</span><span class=mi>0</span><span class=p>,</span> <span class=mi>0</span><span class=p>,</span> <span class=mi>0</span><span class=p>)</span>
|
||
<span class=err>@</span><span class=n>export_enum</span><span class=p>(</span><span class=s2>" "</span><span class=p>)</span> <span class=k>var</span> <span class=n>bone</span><span class=p>:</span> <span class=kt>String</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_validate_property</span><span class=p>(</span><span class=n>property</span><span class=p>:</span> <span class=kt>Dictionary</span><span class=p>)</span> <span class=o>-></span> <span class=n>void</span><span class=p>:</span>
|
||
<span class=k>if</span> <span class=n>property</span><span class=o>.</span><span class=n>name</span> <span class=o>==</span> <span class=s2>"bone"</span><span class=p>:</span>
|
||
<span class=k>var</span> <span class=n>skeleton</span><span class=p>:</span> <span class=n>Skeleton3D</span> <span class=o>=</span> <span class=n>get_skeleton</span><span class=p>()</span>
|
||
<span class=k>if</span> <span class=n>skeleton</span><span class=p>:</span>
|
||
<span class=n>property</span><span class=o>.</span><span class=n>hint</span> <span class=o>=</span> <span class=n>PROPERTY_HINT_ENUM</span>
|
||
<span class=n>property</span><span class=o>.</span><span class=n>hint_string</span> <span class=o>=</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>get_concatenated_bone_names</span><span class=p>()</span>
|
||
</code></pre></div></div><p>The <code class="language-plaintext highlighter-rouge">@tool</code> declaration is also required for previewing modifications by <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>, so you can consider it is required basically.<p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/bones_enum.webp alt="bones enum"><h2 id=3-coding-calculations-of-the-modification-in-_process_modification>3. Coding calculations of the modification in <code class="language-plaintext highlighter-rouge">_process_modification()</code></h2><div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=err>@</span><span class=k>tool</span>
|
||
|
||
<span class=k>class_name</span> <span class=n>CustomModifier</span>
|
||
<span class=k>extends</span> <span class=n>SkeletonModifier3D</span>
|
||
|
||
<span class=err>@</span><span class=k>export</span> <span class=k>var</span> <span class=n>target_coordinate</span><span class=p>:</span> <span class=kt>Vector3</span> <span class=o>=</span> <span class=kt>Vector3</span><span class=p>(</span><span class=mi>0</span><span class=p>,</span> <span class=mi>0</span><span class=p>,</span> <span class=mi>0</span><span class=p>)</span>
|
||
<span class=err>@</span><span class=n>export_enum</span><span class=p>(</span><span class=s2>" "</span><span class=p>)</span> <span class=k>var</span> <span class=n>bone</span><span class=p>:</span> <span class=kt>String</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_validate_property</span><span class=p>(</span><span class=n>property</span><span class=p>:</span> <span class=kt>Dictionary</span><span class=p>)</span> <span class=o>-></span> <span class=n>void</span><span class=p>:</span>
|
||
<span class=k>if</span> <span class=n>property</span><span class=o>.</span><span class=n>name</span> <span class=o>==</span> <span class=s2>"bone"</span><span class=p>:</span>
|
||
<span class=k>var</span> <span class=n>skeleton</span><span class=p>:</span> <span class=n>Skeleton3D</span> <span class=o>=</span> <span class=n>get_skeleton</span><span class=p>()</span>
|
||
<span class=k>if</span> <span class=n>skeleton</span><span class=p>:</span>
|
||
<span class=n>property</span><span class=o>.</span><span class=n>hint</span> <span class=o>=</span> <span class=n>PROPERTY_HINT_ENUM</span>
|
||
<span class=n>property</span><span class=o>.</span><span class=n>hint_string</span> <span class=o>=</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>get_concatenated_bone_names</span><span class=p>()</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_process_modification</span><span class=p>()</span> <span class=o>-></span> <span class=n>void</span><span class=p>:</span>
|
||
<span class=k>var</span> <span class=n>skeleton</span><span class=p>:</span> <span class=n>Skeleton3D</span> <span class=o>=</span> <span class=n>get_skeleton</span><span class=p>()</span>
|
||
<span class=k>if</span> <span class=o>!</span><span class=n>skeleton</span><span class=p>:</span>
|
||
<span class=k>return</span> <span class=c1># Never happen, but for the safety.</span>
|
||
<span class=k>var</span> <span class=n>bone_idx</span><span class=p>:</span> <span class=kt>int</span> <span class=o>=</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>find_bone</span><span class=p>(</span><span class=n>bone</span><span class=p>)</span>
|
||
<span class=k>var</span> <span class=n>parent_idx</span><span class=p>:</span> <span class=kt>int</span> <span class=o>=</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>get_bone_parent</span><span class=p>(</span><span class=n>bone_idx</span><span class=p>)</span>
|
||
<span class=k>var</span> <span class=n>pose</span><span class=p>:</span> <span class=n>Transform3D</span> <span class=o>=</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>global_transform</span> <span class=o>*</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>get_bone_global_pose</span><span class=p>(</span><span class=n>bone_idx</span><span class=p>)</span>
|
||
<span class=k>var</span> <span class=n>looked_at</span><span class=p>:</span> <span class=n>Transform3D</span> <span class=o>=</span> <span class=n>_y_look_at</span><span class=p>(</span><span class=n>pose</span><span class=p>,</span> <span class=n>target_coordinate</span><span class=p>)</span>
|
||
<span class=n>skeleton</span><span class=o>.</span><span class=n>set_bone_global_pose</span><span class=p>(</span><span class=n>bone_idx</span><span class=p>,</span> <span class=n>Transform3D</span><span class=p>(</span><span class=n>looked_at</span><span class=o>.</span><span class=n>basis</span><span class=o>.</span><span class=n>orthonormalized</span><span class=p>(),</span> <span class=n>skeleton</span><span class=o>.</span><span class=n>get_bone_global_pose</span><span class=p>(</span><span class=n>bone_idx</span><span class=p>)</span><span class=o>.</span><span class=n>origin</span><span class=p>))</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_y_look_at</span><span class=p>(</span><span class=n>from</span><span class=p>:</span> <span class=n>Transform3D</span><span class=p>,</span> <span class=n>target</span><span class=p>:</span> <span class=kt>Vector3</span><span class=p>)</span> <span class=o>-></span> <span class=n>Transform3D</span><span class=p>:</span>
|
||
<span class=k>var</span> <span class=n>t_v</span><span class=p>:</span> <span class=kt>Vector3</span> <span class=o>=</span> <span class=n>target</span> <span class=o>-</span> <span class=n>from</span><span class=o>.</span><span class=n>origin</span>
|
||
<span class=k>var</span> <span class=n>v_y</span><span class=p>:</span> <span class=kt>Vector3</span> <span class=o>=</span> <span class=n>t_v</span><span class=o>.</span><span class=n>normalized</span><span class=p>()</span>
|
||
<span class=k>var</span> <span class=n>v_z</span><span class=p>:</span> <span class=kt>Vector3</span> <span class=o>=</span> <span class=n>from</span><span class=o>.</span><span class=n>basis</span><span class=o>.</span><span class=n>x</span><span class=o>.</span><span class=n>cross</span><span class=p>(</span><span class=n>v_y</span><span class=p>)</span>
|
||
<span class=n>v_z</span> <span class=o>=</span> <span class=n>v_z</span><span class=o>.</span><span class=n>normalized</span><span class=p>()</span>
|
||
<span class=k>var</span> <span class=n>v_x</span><span class=p>:</span> <span class=kt>Vector3</span> <span class=o>=</span> <span class=n>v_y</span><span class=o>.</span><span class=n>cross</span><span class=p>(</span><span class=n>v_z</span><span class=p>)</span>
|
||
<span class=n>from</span><span class=o>.</span><span class=n>basis</span> <span class=o>=</span> <span class=kt>Basis</span><span class=p>(</span><span class=n>v_x</span><span class=p>,</span> <span class=n>v_y</span><span class=p>,</span> <span class=n>v_z</span><span class=p>)</span>
|
||
<span class=k>return</span> <span class=n>from</span>
|
||
</code></pre></div></div><p><code class="language-plaintext highlighter-rouge">_process_modification()</code> is a virtual method called in the update process after the AnimationMixer has been applied, as described in the sequence diagram above. If you modify bones in it, it is guaranteed that the order in which the modifications are applied will match the order of <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> of the <code class="language-plaintext highlighter-rouge">Skeleton3D</code>’s child list.</p><video autoplay loop muted playsinline>
|
||
<source src=/storage/blog/design-of-the-skeleton-modifier-3d/custom_modifier.webm?1 type=video/webm></video><p>Note that the modification should always be applied to the bones at 100% amount. Because <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> has an <code class="language-plaintext highlighter-rouge">influence</code> property, the value of which is processed and interpolated by <code class="language-plaintext highlighter-rouge">Skeleton3D</code>. In other words, you do not need to write code to change the amount of modification applied; You should avoid implementing duplicate interpolation processes. However, if your custom <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> can specify multiple bones and you want to manage the amount separately for each bone, it makes sense that adding the amount properties for each bone to your custom modifier.<p>Finally, remember that this method will not be called if the parent is not a <code class="language-plaintext highlighter-rouge">Skeleton3D</code>.<h2 id=4-retrieve-modified-values-from-other-nodes>4. Retrieve modified values from other Nodes</h2><p>The modification by <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> is immediately discarded after it is applied to the skin, so it is not reflected in the bone pose of <code class="language-plaintext highlighter-rouge">Skeleton3D</code> during <code class="language-plaintext highlighter-rouge">_process()</code>.<p>If you need to retrieve the modificated pose values from other nodes, you must connect them to the appropriate signals.<p>For example, this is a <code class="language-plaintext highlighter-rouge">Label3D</code> which reflects the modification after the animation is applied and after all modifications are processed.<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=err>@</span><span class=k>tool</span>
|
||
|
||
<span class=k>extends</span> <span class=n>Label3D</span>
|
||
|
||
<span class=err>@</span><span class=k>onready</span> <span class=k>var</span> <span class=n>poses</span><span class=p>:</span> <span class=kt>Dictionary</span> <span class=o>=</span> <span class=p>{</span> <span class=s2>"animated_pose"</span><span class=p>:</span> <span class=s2>""</span><span class=p>,</span> <span class=s2>"modified_pose"</span><span class=p>:</span> <span class=s2>""</span> <span class=p>}</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_update_text</span><span class=p>()</span> <span class=o>-></span> <span class=n>void</span><span class=p>:</span>
|
||
<span class=n>text</span> <span class=o>=</span> <span class=s2>"animated_pose:"</span> <span class=o>+</span> <span class=nb>str</span><span class=p>(</span><span class=n>poses</span><span class=p>[</span><span class=s2>"animated_pose"</span><span class=p>])</span> <span class=o>+</span> <span class=s2>"</span><span class=se>\n</span><span class=s2>"</span> <span class=o>+</span> <span class=s2>"modified_pose:"</span> <span class=o>+</span> <span class=nb>str</span><span class=p>(</span><span class=n>poses</span><span class=p>[</span><span class=s2>"modified_pose"</span><span class=p>])</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_on_animation_player_mixer_applied</span><span class=p>()</span> <span class=o>-></span> <span class=n>void</span><span class=p>:</span>
|
||
<span class=n>poses</span><span class=p>[</span><span class=s2>"animated_pose"</span><span class=p>]</span> <span class=o>=</span> <span class=o>$</span><span class=s2>"../Armature/Skeleton3D"</span><span class=o>.</span><span class=n>get_bone_pose</span><span class=p>(</span><span class=mi>1</span><span class=p>)</span>
|
||
<span class=n>_update_text</span><span class=p>()</span>
|
||
|
||
<span class=k>func</span> <span class=nf>_on_skeleton_3d_skeleton_updated</span><span class=p>()</span> <span class=o>-></span> <span class=n>void</span><span class=p>:</span>
|
||
<span class=n>poses</span><span class=p>[</span><span class=s2>"modified_pose"</span><span class=p>]</span> <span class=o>=</span> <span class=o>$</span><span class=s2>"../Armature/Skeleton3D"</span><span class=o>.</span><span class=n>get_bone_pose</span><span class=p>(</span><span class=mi>1</span><span class=p>)</span>
|
||
<span class=n>_update_text</span><span class=p>()</span>
|
||
</code></pre></div></div><p>You can see the pose is different depending on the signal.<p><img src=/storage/blog/design-of-the-skeleton-modifier-3d/modified_pose.webp alt="modified pose"><h3 id=download>Download</h3><p><a href=/storage/blog/design-of-the-skeleton-modifier-3d/skeleton-modifier-3d-demo-project.zip>skeleton-modifier-3d-demo-project.zip</a><h1 id=do-i-always-need-to-create-a-custom-skeletonmodifier3d-when-modifying-a-skeleton3d-bone>Do I always need to create a custom SkeletonModifier3D when modifying a Skeleton3D bone?</h1><p>As explained above, the modification provided by <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> is temporary. So <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> would be appropriate for effectors and controllers as <strong>post FX</strong>.<p>If you want permanent modifications, i.e., if you want to develop something like a bone editor, then it makes sense that it is not a <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>. Also, in simple cases where it is guaranteed that no other <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> will be used in the scene, your judgment will prevail.<h1 id=what-kind-of-skeletonmodifier3d-nodes-are-included-in-godot-43>What kind of SkeletonModifier3D nodes are included in Godot 4.3?</h1><p>For now, Godot 4.3 will be containing only <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code> which is a migration of several existing nodes that have been in existence since 4.0.<p>But, there is good news! We are planning to add some built in <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>s in Godot 4.4, such as new IK, constraint, and springbone/jiggle.<p>If you are interested in developing your own effect using <code class="language-plaintext highlighter-rouge">SkeletonModifier3D</code>, feel free to make a proposal to include it in core.<h2 id=support>Support</h2><p>Godot is a non-profit, open source game engine developed by hundreds of contributors on their free time, as well as a handful of part or full-time developers hired thanks to <a href=https://fund.godotengine.org/>generous donations from the Godot community</a>. A big thank you to everyone who has contributed <a href=https://github.com/godotengine/godot/blob/master/AUTHORS.md>their time</a> or <a href=https://github.com/godotengine/godot/blob/master/DONORS.md>their financial support</a> to the project!<p>If you’d like to support the project financially and help us secure our future hires, you can do so using the <a href=https://fund.godotengine.org/>Godot Development Fund</a> platform managed by <a href=https://godot.foundation/>Godot Foundation</a>. There are also several <a href=/donate>alternative ways to donate</a> which you may find more suitable.</div></div></article><div class=blog-navigation><div class=previous><span>Previous</span>
|
||
<a rel=prev href=/article/release-candidate-godot-4-3-rc-3/>Release candidate: Godot 4.3 RC 3</a></div><div class=next><span>Next</span>
|
||
<a rel=next href=/article/godot-4-3-a-shared-effort/>Godot 4.3, a shared effort</a></div></div></div><link rel=stylesheet href=/assets/css/anchor-link.css?1><link rel=stylesheet href=/assets/css/article-cards.css?3><script src=/assets/js/anchor-link.js></script><script>document.addEventListener("DOMContentLoaded",()=>{window.applyAnchorLinks(".article-body"),document.querySelectorAll(".article-cover img, .article-body img").forEach(e=>{if(e.classList.contains("lightbox-ignore"))return;const t=document.createElement("a");t.href=e.src,t.classList.add("lightbox"),t.dataset.group="article",e.parentNode.appendChild(t),t.appendChild(e)})})</script></main><footer class=footer-global><div class=wrapper><div class=columns><div class=col><h2>Godot Engine</h2><ul><li><a class=set-os-download-url href=/download>Download</a><li><a href=https://docs.godotengine.org>Documentation</a><li><a href=/features/>Features</a><li><a href=https://editor.godotengine.org/releases/latest/>Web editor</a><li><a href=/download/archive/>Release archive</a><li><a href=https://github.com/godotengine>Source code</a></ul></div><div class=col><h2>Project</h2><ul><li><a href=/blog/>Blog</a><li><a href=/code-of-conduct/>Code of conduct</a><li><a href=/governance/>Governance</a><li><a href=/teams/>Teams</a><li><a href=/priorities/>Priorities</a><li><a href=/community/>Communities</a></ul></div><div class=col><h2>Resources</h2><ul><li><a href=https://godotengine.org/asset-library/asset>Asset library</a><li><a href=/press/>Press kit</a><li><a href=/showcase/>Showcase</a><li><a href=/education/>Education</a></ul></div><div class=col><h2>Foundation</h2><ul><li><a href=https://godot.foundation/>About</a><li><a href=https://fund.godotengine.org>Donate</a><li><a href=/license/>License</a><li><a href=/privacy-policy/>Privacy policy</a><li><a href=/contact/>Contact us</a></ul></div></div><hr><div class=credits-and-socials><p>© 2007-2025 Juan Linietsky, Ariel Manzur and <a href=https://github.com/godotengine/godot/blob/master/AUTHORS.md target=_blank rel=noopener>contributors</a>. Hosted by the <a href=https://godot.foundation/ target=_blank rel=noopener>Godot Foundation</a>. Website <a href=https://github.com/godotengine/godot-website target=_blank rel=noopener>source code on GitHub</a>.<div class=social><a href=https://github.com/godotengine target=_blank rel=noopener title=GitHub><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6.0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6.0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3.0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1.0-6.2-.3-40.4-.3-61.4.0.0-70 15-84.7-29.8.0.0-11.4-29.1-27.8-36.6.0.0-22.9-15.7 1.6-15.4.0.0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5.0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9.0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4.0 33.7-.3 75.4-.3 83.6.0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6.0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9.0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>
|
||
</a><a href=https://bsky.app/profile/godotengine.org target=_blank rel=noopener title=Bluesky><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M407.8 294.7c-3.3-.4-6.7-.8-10-1.3 3.4.4 6.7.9 10 1.3zM288 227.1C261.9 176.4 190.9 81.9 124.9 35.3 61.6-9.4 37.5-1.7 21.6 5.5 3.3 13.8.0 41.9.0 58.4S9.1 194 15 213.9c19.5 65.7 89.1 87.9 153.2 80.7 3.3-.5 6.6-.9 10-1.4-3.3.5-6.6 1-10 1.4-93.9 14-177.3 48.2-67.9 169.9C220.6 589.1 265.1 437.8 288 361.1c22.9 76.7 49.2 222.5 185.6 103.4 102.4-103.4 28.1-156-65.8-169.9-3.3-.4-6.7-.8-10-1.3 3.4.4 6.7.9 10 1.3 64.1 7.1 133.6-15.1 153.2-80.7C566.9 194 576 75 576 58.4s-3.3-44.7-21.6-52.9c-15.8-7.1-40-14.9-103.2 29.8C385.1 81.9 314.1 176.4 288 227.1z"/></svg>
|
||
</a><a href=https://mastodon.gamedev.place/@godotengine target=_blank rel=noopener title=Mastodon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5.0.0.0-63.7 28.5-63.7 125.7.0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5.0 01-.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6V190.1c0-49.7-64-51.6-64 6.9v62.5H201V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></svg>
|
||
</a><a href=https://discord.gg/godotengine target=_blank rel=noopener title=Discord><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M524.5 69.8a1.5 1.5.0 00-.8-.7A485.1 485.1.0 00404.1 32a1.8 1.8.0 00-1.9.9 337.5 337.5.0 00-14.9 30.6 447.8 447.8.0 00-134.4.0 309.5 309.5.0 00-15.1-30.6 1.9 1.9.0 00-1.9-.9A483.7 483.7.0 00116.1 69.1a1.7 1.7.0 00-.8.7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 00.8 1.4A487.7 487.7.0 00176 479.9a1.9 1.9.0 002.1-.7 348.2 348.2.0 0030-48.8 1.9 1.9.0 00-1-2.6 321.2 321.2.0 01-45.9-21.9 1.9 1.9.0 01-.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8.0 011.9-.3c96.2 43.9 200.4 43.9 295.5.0a1.8 1.8.0 011.9.2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9.0 01-.2 3.1 301.4 301.4.0 01-45.9 21.8 1.9 1.9.0 00-1 2.6 391.1 391.1.0 0030 48.8 1.9 1.9.0 002.1.7 486 486 0 00147.2-74.1 1.9 1.9.0 00.8-1.4c12.2-126.7-20.6-236.8-87-334.5zm-302 267.8c-29 0-52.8-26.6-52.8-59.2s23.4-59.3 52.8-59.3c29.7.0 53.3 26.8 52.8 59.2.0 32.7-23.4 59.3-52.8 59.3zm195.4.0c-29 0-52.8-26.6-52.8-59.2s23.3-59.3 52.8-59.3c29.7.0 53.3 26.8 52.8 59.2.0 32.7-23.2 59.3-52.8 59.3z"/></svg>
|
||
</a><a href=https://www.reddit.com/r/godot title=Reddit target=_blank rel=noopener><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256C0 114.6 114.6.0 256 0S512 114.6 512 256 397.4 512 256 512H37.1c-13.7.0-20.5-16.5-10.9-26.2L75 437C28.7 390.7.0 326.7.0 256zM349.6 153.6c23.6.0 42.7-19.1 42.7-42.7s-19.1-42.7-42.7-42.7c-20.6.0-37.8 14.6-41.8 34-34.5 3.7-61.4 33-61.4 68.4v.2c-37.5 1.6-71.8 12.3-99 29.1-10.1-7.8-22.8-12.5-36.5-12.5-33 0-59.8 26.8-59.8 59.8.0 24 14.1 44.6 34.4 54.1 2 69.4 77.6 125.2 170.6 125.2s168.7-55.9 170.6-125.3c20.2-9.6 34.1-30.2 34.1-54 0-33-26.8-59.8-59.8-59.8-13.7.0-26.3 4.6-36.4 12.4-27.4-17-62.1-27.7-1e2-29.1v-.2c0-25.4 18.9-46.5 43.4-49.9 4.4 18.8 21.3 32.8 41.5 32.8zM177.1 246.9c16.7.0 29.5 17.6 28.5 39.3s-13.5 29.6-30.3 29.6-31.4-8.8-30.4-30.5S160.3 247 177 247zm190.1 38.3c1 21.7-13.7 30.5-30.4 30.5s-29.3-7.9-30.3-29.6c-1-21.7 11.8-39.3 28.5-39.3s31.2 16.6 32.1 38.3zm-48.1 56.7c-10.3 24.6-34.6 41.9-63 41.9s-52.7-17.3-63-41.9c-1.2-2.9.8-6.2 3.9-6.5 18.4-1.9 38.3-2.9 59.1-2.9s40.7 1 59.1 2.9c3.1.3 5.1 3.6 3.9 6.5z"/></svg>
|
||
</a><a href=/rss.xml title=RSS target=_blank rel=noopener><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M64 32C28.7 32 0 60.7.0 96V416c0 35.3 28.7 64 64 64h320c35.3.0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24 137 0 248 111 248 248 0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-2e2-2e2-2e2-13.3.0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24 83.9.0 152 68.1 152 152 0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104-13.3.0-24-10.7-24-24zm0 120a32 32 0 1164 0 32 32 0 11-64 0z"/></svg></a></div></div></div></footer><script defer src=/assets/js/localize.js?5></script><script defer src=/assets/js/tobii.min.js></script><script defer src=/assets/js/highlight.min.js?1></script><script defer src=/assets/js/highlight.gdscript.min.js?1></script><script>document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("pre code").forEach(e=>{hljs.highlightBlock(e)}),document.querySelectorAll("[data-post-date]").forEach(e=>{Date.parse(e.dataset.postDate)>Date.now()-1e3*60*60*48&&e.classList.add("post-recent-highlight")}),new Tobii({zoom:!1});const e=document.querySelectorAll(".set-os-download-url");for(let n=0;n<e.length;n++){const s=e[n];let o="download";"version"in s.dataset&&s.dataset.version==="3"&&(o="download/3.x");let t="windows";navigator.platform.indexOf("Mac")!==-1?t="macos":navigator.userAgent.indexOf("Android")!==-1?t="android":navigator.platform.indexOf("Linux")!==-1&&(t="linux"),s.href=`/${o}/${t}/`}})</script> |