
始めに
趣味でPlayable APIについて調査や検証をしているのですが、どうしてもAnimatorのCulling Modeについて腑に落ちない部分がありました。
Geminiに相談しながら実際に動かして調査をしてみたので、その備忘録を残そうと思います。
動作環境はUnity6.3LTS(6000.3.2f1)です。
AnimatorのCulling Mode
アニメーションを制御するAnimatorにはCulling Modeが設定でき、カメラに映っている/いないに応じて再生を制御できます。この設定により、画面外でのモーション再生を抑制し無駄なリソースを削減できます。
Culling Modeで設定できるModeは以下の3つです。
| Mode | 説明 |
|---|---|
| Always Animate | 画面外でも常にアニメーションを再生 |
| Cull Update Transforms | 常にアニメーションは再生するが、画面外ではTransformへの書き込みを停止 |
| Cull Completely | 画面外ではアニメーションを完全に停止 |
簡単な検証
それぞれのCulling Modeについて、Unityで動作確認してみます。
- 適当なモーションを再生するようAnimatorを設定
- GameビューとSceneビューの両方で画面外へ移動
- Aniamtorビューからモーションの再生状態を確認
Always Animate
画面外へ移動しても常にAnimatorControllerが動き続けていることが分かります。

Cull Update Transforms
画面外へ移動しても常にAnimatorControllerは動作しますが、モデルのTransformへの反映は行われませんでした。

Cull Completely
画面外へ移動したとき、Animatorが一時停止していることが分かります。 また、再度画面内に移動したとき一時停止したところから再生されます。

PlayableGraph
PlayableGraphはPlayable APIが提供する機能で、再生可能なもの(アニメーション、音など)を木構造で定義し、ブレンドやミキシングを行い出力できます。
詳細な説明は省くので、公式ドキュメントなどを参照ください。
PlayableGraphとAnimatorについて
PlayableGraphでモーションを再生するとき、最終的にAnimatorへ出力されます。
実装としてはPlayableGraph内に AnimationPlayableOutput を作成し、そこにAnimatorを設定して再生したモーションをシーン上へ反映します。
var graph = PlayableGraph.Create("PlayableGraph"); // PlayableGraph作成 var output = AnimationPlayableOutput.Create(graph , "AnimOutput", animator); // animatorを出力先に設定 output.SetSourcePlayable(hogeAnimationPlayable); // 出力に何かしらアニメーションの出力を設定
Culling Modeへの疑問
AnimatorにはCulling Modeが存在しますが、PlayableGraphにCullingを設定するAPIもありません。
いくつか情報を調べるとPlayableGraphはAnimatorのCulling Modeを参照しないと見かけたため、実際にそうなのかを検証してみました。
PlayableGraphでAnimatorのCulling Modeを検証
PlayableGraphでAnimatorを再生し、Culling Modeによって再生状態に影響するか検証します。
PlayableGraphの状態はPlayableGraph Visualizerから確認し、また PlayableGraph.IsPlaying() をチェックします。
1つのPlayableを再生する
AnimationClipを再生するAnimationClipPlayableを一つだけ設定し検証してみます。
コードは以下の通りです。
AIが書いたコードです。 コード(折り畳み)
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
[RequireComponent(typeof(Animator))]
public class SimpleAnimationPlayableExample : MonoBehaviour
{
[SerializeField]
private AnimationClip _animationClip;
private Animator _animator;
private PlayableGraph _playableGraph;
// 表示用
private string _statusText = "";
private void Start()
{
_animator = GetComponent<Animator>();
// Playable Graphを生成(名前は任意)
_playableGraph = PlayableGraph.Create("Example Playable");
// 一つのAnimationを表すもの(= Playable)を作成
// Playableは必ずPlayable Graphに所属するので引数にPlayable Graphを渡す
var clipPlayable = AnimationClipPlayable.Create(_playableGraph, _animationClip);
// Playable Graphで計算されたアニメーションデータはAnimatorで使われてキャラなどが動く
// そのためにPlayable GraphとAnimatorを紐づけるのがPlayableOutput
var playableOutput = AnimationPlayableOutput.Create(_playableGraph, "Animation", _animator);
// 最終的に出力するアニメーションをPlayable Outputに登録
playableOutput.SetSourcePlayable(clipPlayable);
_playableGraph.Play();
}
private void Update()
{
// --- デバッグ表示用情報の更新 ---
var mode = _animator.cullingMode;
_statusText = $"<b>Culling Mode:</b> {mode}\n" +
$"<b>PlayableGraph Is Playing</b>: {_playableGraph.IsPlaying()}";
}
void OnGUI()
{
GUI.color = Color.black;
GUI.skin.label.fontSize = 24;
GUILayout.Label(_statusText);
}
private void OnDisable()
{
_playableGraph.Destroy();
}
}
結果
適当なモーションを再生して実行します。
Culling Mode: Cull Update Transforms
こちらはAnimatorの設定と同じく、GameObjectが画面外へ移動してもPlayableGraphは再生し続けますがTransform情報の更新は止まりました。

Culling Mode: Cull Completely
こちらは少しAnimatorの設定と異なります。
GameObjectが画面外へ移動しても、PlayableGraph.IsPlaying() はずっとtrueのままです。
しかし、PlayableGraph Visualizerを見るとAnimationClipPlayableが止まっていることが分かります。
これは AnimationPlayableOutput の入力をPauseして、AnimationClipPlayableが停止したようです。
つまり、Animatorの挙動と同様に画面外へ移動するとアニメーションが停止する結果となります。

(↑画面外へ移動したとき、AnimationClipノードの白縁が消えて再生が停止する)
複雑なPlayableGraphによるCull Completely
Cull CompletelyではAnimationPlayableOutputの入力となるPlayableを停止することが分かりました。
では、もう少し複雑なPlayableGraphではCull Completelyがどのような動作になるかを検証します。
作成するPlayableGraphを次のようにします。
- AnimationPlayableOutputにAnimationMixerPlayableを設定
- AnimationMixerPlayableに以下の2つを設定
- AnimationClipPlayable
- 適当なScriptPlayable

コードは以下の通りです。
AIが書いたコードなので、かなり雑でコメントが多いです。 コード(折り畳み)
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
[RequireComponent(typeof(Animator))]
public class PlayableMixingTest : MonoBehaviour
{
[Header("Settings")]
public AnimationClip animationClip; // テスト用のアニメーションクリップをセット
[Range(0f, 1f)] public float clipWeight = 1.0f; // クリップの強さ
[Range(0f, 1f)] public float rotationWeight = 1.0f; // 回転スクリプトの強さ
private PlayableGraph _graph;
private AnimationMixerPlayable _mixer;
private Animator _animator;
// 表示用
private string _statusText = "";
void Start()
{
_animator = GetComponent<Animator>();
// 1. Graph作成
_graph = PlayableGraph.Create("MixingGraph");
// 2. Mixer作成(入力ポートを2つ用意)
_mixer = AnimationMixerPlayable.Create(_graph, 2);
// 3. Output作成
var output = AnimationPlayableOutput.Create(_graph, "AnimOutput", _animator);
output.SetSourcePlayable(_mixer);
// --- Input A: AnimationClipPlayable (Port 0) ---
if (animationClip != null)
{
var clipPlayable = AnimationClipPlayable.Create(_graph, animationClip);
// Graphにつなぐ: clipPlayable -> mixerのPort 0
_graph.Connect(clipPlayable, 0, _mixer, 0);
}
// --- Input B: ScriptPlayable (Port 1) ---
var rotationPlayable = ScriptPlayable<RotationBehaviour>.Create(_graph);
var behaviour = rotationPlayable.GetBehaviour();
behaviour.TargetTransform = this.transform;
output.SetWeight(1.0f);
// Graphにつなぐ: rotationPlayable -> mixerのPort 1
_graph.Connect(rotationPlayable, 0, _mixer, 1);
// 4. 再生(Animatorのカリングに任せる)
_graph.Play();
}
void Update()
{
if (!_graph.IsValid()) return;
// Inspectorの値をMixerのウェイトに反映
_mixer.SetInputWeight(0, clipWeight);
_mixer.SetInputWeight(1, rotationWeight);
// --- デバッグ表示用情報の更新 ---
var mode = _animator.cullingMode;
// Mixer自体の時間(グラフ全体の進行状況)
double time = _mixer.GetTime();
_statusText = $"<b>Culling Mode:</b> {mode}\n" +
$"<b>Time:</b> {time:F2}\n" +
$"<b>Clip Weight:</b> {clipWeight:F2}\n" +
$"<b>Rotation Weight:</b> {rotationWeight:F2}\n" +
$"<b>PlayableGraph Is Playing</b>: {_graph.IsPlaying()}";
}
void OnGUI()
{
GUI.color = Color.black;
GUI.skin.label.fontSize = 24;
GUILayout.Label(_statusText);
}
void OnDisable()
{
if (_graph.IsValid()) _graph.Destroy();
}
// ---------------------------------------------------------
// Playable Behaviour
// ---------------------------------------------------------
class RotationBehaviour : PlayableBehaviour
{
public Transform TargetTransform;
// PrepareFrameは毎フレーム(カリングされていない間)呼ばれます
public override void PrepareFrame(Playable playable, FrameData info)
{
Debug.Log("RotationBehaviour PrepareFrame called");
if (TargetTransform == null) return;
// 重要: Mixerで設定されたWeight(重み)を取得する
// これにより、Inspectorのスライダーで回転の強さを制御できます
float weight = info.effectiveWeight;
// ウェイトが0なら処理しない(無駄な計算を省く)
if (weight <= 0f) return;
// 回転速度に weight を掛けることで、ブレンド具合を反映
// 例: Weightが0.5なら、回転速度も半分になる
float rotateSpeed = 180f * weight;
TargetTransform.Rotate(0, rotateSpeed * info.deltaTime, 0);
}
}
}
結果
GameObjetを画面外に出すと、AnimationPlayableOutputの入力となるAnimationMixerPlayableが停止しました。しかし、AnimationMixerPlayableの入力となるAnimationClipPlayableとScriptPlayableは停止せずPlayginのままになります。

次に ScriptPlayable.PrepareFrame() にDebug.Logを仕込み、毎フレーム動作をしているかをチェックします。
実行確認してGameObjectが画面外に移動すると、Debug.Logが停止することが分かりました。 ScriptPlayableがPlaying状態にもかかわらず ScriptPlayable.PrepareFrame() が実行されていません。

(↑ Transform上から、ScriptPlayableが行う回転が止まっていることも分かる)
これはPlayableGraphが再帰的にPlayableを実行するため、親となるAnimationMixerPlayableが停止してその子となるScriptPlayableも実行されませんでした。
つまり、Culling ModeがCull Completelyの場合、画面外ではPlayableGraphに含まれる全てのPlayableが停止することになります。
PlayableGraphを手動で更新する場合のCull Completely
最後に DirectorUpdateMode.Manual を設定し、 PlayableGraph.Evaluate() を毎フレーム実行して手動で更新する場合を検証します。
AIが書いたコードに少しだけ手を加えています。 コード(折り畳み)
Start() で _graph.SetTimeUpdateMode(DirectorUpdateMode.Manual) をして手動更新に設定Update() で _graph.Evaluate(Time.deltaTime) を実行して更新using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
[RequireComponent(typeof(Animator))]
public class PlayableMixingTest_Manual : MonoBehaviour
{
[Header("Settings")]
public AnimationClip animationClip; // テスト用のアニメーションクリップをセット
[Range(0f, 1f)] public float clipWeight = 1.0f; // クリップの強さ
[Range(0f, 1f)] public float rotationWeight = 1.0f; // 回転スクリプトの強さ
private PlayableGraph _graph;
private AnimationMixerPlayable _mixer;
private Animator _animator;
// 表示用
private string _statusText = "";
void Start()
{
_animator = GetComponent<Animator>();
// 1. Graph作成
_graph = PlayableGraph.Create("MixingGraph");
// 2. Mixer作成(入力ポートを2つ用意)
_mixer = AnimationMixerPlayable.Create(_graph, 2);
// 3. Output作成
var output = AnimationPlayableOutput.Create(_graph, "AnimOutput", _animator);
output.SetSourcePlayable(_mixer);
// --- Input A: AnimationClipPlayable (Port 0) ---
if (animationClip != null)
{
var clipPlayable = AnimationClipPlayable.Create(_graph, animationClip);
// Graphにつなぐ: clipPlayable -> mixerのPort 0
_graph.Connect(clipPlayable, 0, _mixer, 0);
}
// --- Input B: ScriptPlayable (Port 1) ---
var rotationPlayable = ScriptPlayable<RotationBehaviour>.Create(_graph);
var behaviour = rotationPlayable.GetBehaviour();
behaviour.TargetTransform = this.transform;
output.SetWeight(1.0f);
// Graphにつなぐ: rotationPlayable -> mixerのPort 1
_graph.Connect(rotationPlayable, 0, _mixer, 1);
// 手動更新モードに設定
_graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
// 4. 再生(Animatorのカリングに任せる)
_graph.Play();
}
void Update()
{
if (!_graph.IsValid()) return;
// Inspectorの値をMixerのウェイトに反映
_mixer.SetInputWeight(0, clipWeight);
_mixer.SetInputWeight(1, rotationWeight);
// 手動でPlayableGraphを更新
_graph.Evaluate(Time.deltaTime);
// --- デバッグ表示用情報の更新 ---
var mode = _animator.cullingMode;
// Mixer自体の時間(グラフ全体の進行状況)
double time = _mixer.GetTime();
_statusText = $"<b>Culling Mode:</b> {mode}\n" +
$"<b>Time:</b> {time:F2}\n" +
$"Director Update Mode: {_graph.GetTimeUpdateMode()}\n" +
$"<b>PlayableGraph Is Playing</b>: {_graph.IsPlaying()}";
}
void OnGUI()
{
GUI.color = Color.black;
GUI.skin.label.fontSize = 24;
GUILayout.Label(_statusText);
}
void OnDisable()
{
if (_graph.IsValid()) _graph.Destroy();
}
// ---------------------------------------------------------
// Playable Behaviour
// ---------------------------------------------------------
class RotationBehaviour : PlayableBehaviour
{
public Transform TargetTransform;
// PrepareFrameは毎フレーム(カリングされていない間)呼ばれます
public override void PrepareFrame(Playable playable, FrameData info)
{
Debug.Log("RotationBehaviour PrepareFrame called");
if (TargetTransform == null) return;
// 重要: Mixerで設定されたWeight(重み)を取得する
// これにより、Inspectorのスライダーで回転の強さを制御できます
float weight = info.effectiveWeight;
// ウェイトが0なら処理しない(無駄な計算を省く)
if (weight <= 0f) return;
// 回転速度に weight を掛けることで、ブレンド具合を反映
// 例: Weightが0.5なら、回転速度も半分になる
float rotateSpeed = 180f * weight;
TargetTransform.Rotate(0, rotateSpeed * info.deltaTime, 0);
}
}
}
結果
「複雑なPlayableGraphによるCull Completely」と同じように、GameObjectが画面外へ出るとPlayableGraph全体の動作が停止しました。

動作としては先ほどと同じく、
- PlayableGraphは再帰的にPlayableを実行する
- 画面外へ出ると、AnimationOutputPlayableの子となるPlayableが停止する
- Playableが停止すると、AnimationOutputPlayableの子で再帰実行が停止する
- 結果としてPlayableGraph全体が停止する
まとめ
結果をまとめますと、
- PlayableGraphでもAnimatorのCulling Modeを参照する
- Always Mode: 常に再生する
- Cull Update Transforms: 常にPlayableGraphは動作するが、画面外ではTransformへの書き込みは停止
- Cull Completely: 画面外ではPlayableGraphに含まれるPlayableは停止する
また、Cull CompletelyでのPlayableGraphの動作は以下の通りです。
- PlayableGraph自体は再生し続ける(常に
PlayableGraph.IsPlaying() == true) - AnimationOutputPlayableの子となるPlayableのみが停止する
- 再帰的にPlayableを走査する仕組みから実行が打ち切られて、全体のPlayable実行が停止する
- PlayableGraphを手動実行する場合(
DirectorUpdateMode.Manual)も変わらない
- PlayableGraphを手動実行する場合(
- 再帰的にPlayableを走査する仕組みから実行が打ち切られて、全体のPlayable実行が停止する
AnimatorのCulling Modeを参照しないなどの情報を見かけて自作する心配をしてましたが、その必要はなさそうです。 また時間があればPlayableGraphの調査やモーション制御を作る予定なので、いい感じまとまればブログを書くかもです。
使用したアセット
再生しているモーションは以下のアセットを使用しています。