😃

Unityで使うRBF補間:滑らかなアニメーションとAIの実装

に公開

はじめに

RBF(Radial Basis Function:放射基底関数)は、点群データから滑らかな連続関数を生成する補間手法です。本記事では、UnityでRBFを実装し、アニメーション補間やAI・パスファインディングに応用する方法を解説します。

RBFが解決する問題

ゲーム開発では、以下のような「離散的なデータから連続的な値を得る」場面に頻繁に遭遇します。

  • アニメーションブレンディング: 複数のキーフレーム間を滑らかに補間したい
  • AIの意思決定: 複数の判断基準から最適な行動を選択したい
  • 影響範囲の計算: 複数の音源や光源の影響を合成したい

従来の線形補間やスプライン補間と異なり、RBFは多次元空間での補間が得意で、かつ局所的な影響範囲を制御できる点が特徴です。

RBFの基本原理

数学的定義

RBFは、各データ点 x_i からの距離に基づいて影響を計算します。

f(x) = Σ(i=1 to n) w_i * φ(||x - x_i||)
  • x: 補間したい位置
  • x_i: i番目のデータ点
  • w_i: i番目の重み係数
  • φ(r): 影響関数(距離 r を受け取り、影響の強さを返す)
  • ||x - x_i||: 点間の距離(ユークリッド距離)

影響関数の種類

RBFでは、距離をどのように影響に変換するかを決める影響関数を選択します。

1. ガウス関数(Gaussian)

φ(r) = exp(-ε²r²)

特徴: 指数関数的に減衰。遠くの点の影響をほぼ無視できる。

用途: 局所的な変化を表現したい場合(近くのキーフレームのみ影響)

Unity実装例:

public static float Gaussian(float distance, float epsilon = 1.0f)
{
    return Mathf.Exp(-Mathf.Pow(epsilon * distance, 2));
}

2. 逆多重二次型(Inverse Multiquadric, IMQ)

φ(r) = 1 / sqrt(1 + (εr)²)

特徴: 1/r 的に減衰。物理的な音や光の減衰に近い自然な減衰曲線。

用途: 局所と大域のバランスが取れた補間(アニメーションブレンド、AIの意思決定)

Unity実装例:

public static float InverseMultiquadric(float distance, float epsilon = 1.0f)
{
    return 1.0f / Mathf.Sqrt(1.0f + Mathf.Pow(epsilon * distance, 2));
}

3. マルチクアドリック(Multiquadric)

φ(r) = sqrt(1 + (εr)²)

特徴: 距離が大きくなっても減衰しない。すべての点が影響し合う。

用途: 大域的な影響を表現したい場合(広範囲の地形生成など)

Unity実装例:

public static float Multiquadric(float distance, float epsilon = 1.0f)
{
    return Mathf.Sqrt(1.0f + Mathf.Pow(epsilon * distance, 2));
}

形状パラメータ ε の役割

形状パラメータ ε(epsilon)は、影響の広がり方を制御します。

  • ε が大きい: 影響範囲が狭い(急速に減衰)
  • ε が小さい: 影響範囲が広い(緩やかに減衰)

実用上は、データの分布やスケールに応じて ε = 0.52.0 の範囲で調整することが多いです。

Unity実装:基本的なRBFクラス

以下は、Unityで使える基本的なRBF補間クラスです。

using UnityEngine;

public class RBFInterpolator
{
    // データ点の位置
    private Vector3[] centers;

    // 各データ点の重み係数
    private float[] weights;

    // 形状パラメータ
    private float epsilon;

    // 影響関数の種類
    public enum BasisFunction
    {
        Gaussian,
        InverseMultiquadric,
        Multiquadric
    }

    private BasisFunction basisFunc;

    public RBFInterpolator(Vector3[] dataCenters, float[] dataValues,
                          float shapeParameter = 1.0f,
                          BasisFunction basis = BasisFunction.InverseMultiquadric)
    {
        centers = dataCenters;
        epsilon = shapeParameter;
        basisFunc = basis;

        // 重み係数を計算(連立方程式を解く)
        ComputeWeights(dataValues);
    }

    // 任意の位置での補間値を計算
    public float Evaluate(Vector3 position)
    {
        float result = 0f;

        for (int i = 0; i < centers.Length; i++)
        {
            float distance = Vector3.Distance(position, centers[i]);
            float influence = EvaluateBasisFunction(distance);
            result += weights[i] * influence;
        }

        return result;
    }

    // 影響関数の評価
    private float EvaluateBasisFunction(float r)
    {
        switch (basisFunc)
        {
            case BasisFunction.Gaussian:
                return Mathf.Exp(-Mathf.Pow(epsilon * r, 2));

            case BasisFunction.InverseMultiquadric:
                return 1.0f / Mathf.Sqrt(1.0f + Mathf.Pow(epsilon * r, 2));

            case BasisFunction.Multiquadric:
                return Mathf.Sqrt(1.0f + Mathf.Pow(epsilon * r, 2));

            default:
                return 1.0f;
        }
    }

    // 重み係数を計算(簡易実装:疑似逆行列を使用)
    private void ComputeWeights(float[] values)
    {
        int n = centers.Length;
        float[,] matrix = new float[n, n];

        // RBF行列を構築
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                float distance = Vector3.Distance(centers[i], centers[j]);
                matrix[i, j] = EvaluateBasisFunction(distance);
            }
        }

        // 連立方程式を解く(ここでは簡易的にガウスの消去法を使用)
        weights = SolveLinearSystem(matrix, values);
    }

    // 連立方程式の求解(簡易実装)
    private float[] SolveLinearSystem(float[,] A, float[] b)
    {
        int n = b.Length;
        float[] x = new float[n];

        // ガウスの消去法による実装
        // (実用では Math.NET Numerics などのライブラリ使用を推奨)

        for (int i = 0; i < n; i++)
        {
            // ピボット選択
            int maxRow = i;
            for (int k = i + 1; k < n; k++)
            {
                if (Mathf.Abs(A[k, i]) > Mathf.Abs(A[maxRow, i]))
                    maxRow = k;
            }

            // 行の交換
            for (int k = i; k < n; k++)
            {
                float tmp = A[maxRow, k];
                A[maxRow, k] = A[i, k];
                A[i, k] = tmp;
            }
            float tmpB = b[maxRow];
            b[maxRow] = b[i];
            b[i] = tmpB;

            // 前進消去
            for (int k = i + 1; k < n; k++)
            {
                float factor = A[k, i] / A[i, i];
                b[k] -= factor * b[i];
                for (int j = i; j < n; j++)
                {
                    A[k, j] -= factor * A[i, j];
                }
            }
        }

        // 後退代入
        for (int i = n - 1; i >= 0; i--)
        {
            x[i] = b[i];
            for (int j = i + 1; j < n; j++)
            {
                x[i] -= A[i, j] * x[j];
            }
            x[i] /= A[i, i];
        }

        return x;
    }
}

応用例1:アニメーションブレンディング

RBFを使って、複数のアニメーションクリップを滑らかにブレンドします。

実装例:2Dモーションブレンディング

using UnityEngine;

public class RBFAnimationBlender : MonoBehaviour
{
    [System.Serializable]
    public class AnimationPoint
    {
        public AnimationClip clip;
        public Vector2 position; // 2Dブレンド空間上の位置(例: 速度 x 方向)
    }

    [SerializeField] private AnimationPoint[] animationPoints;
    [SerializeField] private Animator animator;
    [SerializeField] private float epsilon = 1.0f;

    private RBFInterpolator2D rbfInterpolator;

    void Start()
    {
        // アニメーションの位置と重みを設定
        Vector2[] positions = new Vector2[animationPoints.Length];
        float[] weights = new float[animationPoints.Length];

        for (int i = 0; i < animationPoints.Length; i++)
        {
            positions[i] = animationPoints[i].position;
            weights[i] = 1.0f; // 各アニメーションの基準値
        }

        rbfInterpolator = new RBFInterpolator2D(positions, weights, epsilon);
    }

    // 入力パラメータから各アニメーションの重みを計算
    public float[] GetBlendWeights(Vector2 input)
    {
        float[] blendWeights = new float[animationPoints.Length];
        float totalWeight = 0f;

        for (int i = 0; i < animationPoints.Length; i++)
        {
            float distance = Vector2.Distance(input, animationPoints[i].position);
            blendWeights[i] = rbfInterpolator.EvaluateBasis(distance);
            totalWeight += blendWeights[i];
        }

        // 正規化
        for (int i = 0; i < blendWeights.Length; i++)
        {
            blendWeights[i] /= totalWeight;
        }

        return blendWeights;
    }

    void Update()
    {
        // 入力例:移動速度と方向からブレンド位置を決定
        Vector2 inputVector = new Vector2(
            Input.GetAxis("Horizontal"),
            Input.GetAxis("Vertical")
        );

        float[] weights = GetBlendWeights(inputVector);

        // Animatorのブレンドツリーに重みを適用
        for (int i = 0; i < animationPoints.Length; i++)
        {
            animator.SetFloat($"Weight{i}", weights[i]);
        }
    }
}

// 2D版のRBF補間クラス
public class RBFInterpolator2D
{
    private Vector2[] centers;
    private float[] weights;
    private float epsilon;

    public RBFInterpolator2D(Vector2[] dataCenters, float[] dataValues, float shapeParameter)
    {
        centers = dataCenters;
        epsilon = shapeParameter;
        weights = dataValues; // 簡易版:重み計算をスキップ
    }

    public float EvaluateBasis(float distance)
    {
        // IMQを使用
        return 1.0f / Mathf.Sqrt(1.0f + Mathf.Pow(epsilon * distance, 2));
    }
}

使用例

  1. Unityエディタで AnimationPoint を4つ設定

    • 「停止」(0, 0)
    • 「前進」(0, 1)
    • 「左移動」(-1, 0)
    • 「右移動」(1, 0)
  2. 入力に応じて、これらのアニメーションが滑らかにブレンドされます

応用例2:AI意思決定とパスファインディング

RBFを使って、複数の要因から最適な行動を選択するAIを実装します。

実装例:敵AIの行動選択

using UnityEngine;

public class RBFEnemyAI : MonoBehaviour
{
    [System.Serializable]
    public class DecisionPoint
    {
        public string actionName;
        public Vector2 condition; // (プレイヤーとの距離, HP残量)
        public float priority;    // この条件での行動の優先度
    }

    [SerializeField] private DecisionPoint[] decisionPoints;
    [SerializeField] private Transform player;
    [SerializeField] private float maxHealth = 100f;
    [SerializeField] private float epsilon = 0.5f;

    private float currentHealth;
    private RBFInterpolator2D decisionRBF;

    void Start()
    {
        currentHealth = maxHealth;

        Vector2[] positions = new Vector2[decisionPoints.Length];
        float[] priorities = new float[decisionPoints.Length];

        for (int i = 0; i < decisionPoints.Length; i++)
        {
            positions[i] = decisionPoints[i].condition;
            priorities[i] = decisionPoints[i].priority;
        }

        decisionRBF = new RBFInterpolator2D(positions, priorities, epsilon);
    }

    void Update()
    {
        string bestAction = DecideAction();
        ExecuteAction(bestAction);
    }

    // 現在の状況から最適な行動を決定
    private string DecideAction()
    {
        // 現在の状態ベクトル
        float distanceToPlayer = Vector3.Distance(transform.position, player.position);
        float healthRatio = currentHealth / maxHealth;
        Vector2 currentState = new Vector2(distanceToPlayer, healthRatio);

        // 各行動の評価値を計算
        float maxEvaluation = float.MinValue;
        string bestAction = "";

        foreach (var point in decisionPoints)
        {
            float distance = Vector2.Distance(currentState, point.condition);
            float influence = decisionRBF.EvaluateBasis(distance);
            float evaluation = point.priority * influence;

            if (evaluation > maxEvaluation)
            {
                maxEvaluation = evaluation;
                bestAction = point.actionName;
            }
        }

        return bestAction;
    }

    private void ExecuteAction(string action)
    {
        switch (action)
        {
            case "Attack":
                // 攻撃ロジック
                Debug.Log("Attacking player");
                break;

            case "Retreat":
                // 後退ロジック
                Debug.Log("Retreating from player");
                Vector3 retreatDirection = (transform.position - player.position).normalized;
                transform.position += retreatDirection * 2f * Time.deltaTime;
                break;

            case "Patrol":
                // パトロールロジック
                Debug.Log("Patrolling");
                break;

            case "Heal":
                // 回復ロジック
                Debug.Log("Healing");
                currentHealth = Mathf.Min(currentHealth + 10f * Time.deltaTime, maxHealth);
                break;
        }
    }

    public void TakeDamage(float damage)
    {
        currentHealth = Mathf.Max(0, currentHealth - damage);
    }
}

使用例:DecisionPointの設定

- Attack:  condition(3.0, 0.7),  priority 1.0  // 近距離 & HP高い → 攻撃
- Retreat: condition(2.0, 0.2),  priority 1.0  // 近距離 & HP低い → 後退
- Patrol:  condition(10.0, 0.8), priority 0.5  // 遠距離 & HP高い → パトロール
- Heal:    condition(8.0, 0.3),  priority 0.8  // 遠距離 & HP低い → 回復

この設定により、敵AIは距離とHPに応じて滑らかに行動を切り替えます。

応用:影響マップによるパスファインディング

RBFを使って、複数の要因を考慮した影響マップを生成できます。

using UnityEngine;

public class RBFInfluenceMap : MonoBehaviour
{
    [System.Serializable]
    public class InfluenceSource
    {
        public Vector3 position;
        public float strength;  // 正の値:誘引、負の値:反発
        public InfluenceType type;
    }

    public enum InfluenceType
    {
        Attraction,  // 目的地など
        Repulsion,   // 敵や障害物など
        Neutral      // 中立的な影響
    }

    [SerializeField] private InfluenceSource[] sources;
    [SerializeField] private float epsilon = 1.0f;

    // 指定位置での影響度を計算
    public float GetInfluence(Vector3 position)
    {
        float totalInfluence = 0f;

        foreach (var source in sources)
        {
            float distance = Vector3.Distance(position, source.position);

            // IMQを使用
            float influence = 1.0f / Mathf.Sqrt(1.0f + Mathf.Pow(epsilon * distance, 2));

            totalInfluence += source.strength * influence;
        }

        return totalInfluence;
    }

    // グリッド上で最も影響度の高い方向を取得
    public Vector3 GetBestDirection(Vector3 currentPosition, float stepSize = 1.0f)
    {
        Vector3 bestDirection = Vector3.zero;
        float bestInfluence = float.MinValue;

        // 8方向を評価
        Vector3[] directions = new Vector3[]
        {
            Vector3.forward, Vector3.back, Vector3.left, Vector3.right,
            new Vector3(1, 0, 1).normalized, new Vector3(-1, 0, 1).normalized,
            new Vector3(1, 0, -1).normalized, new Vector3(-1, 0, -1).normalized
        };

        foreach (var dir in directions)
        {
            Vector3 testPosition = currentPosition + dir * stepSize;
            float influence = GetInfluence(testPosition);

            if (influence > bestInfluence)
            {
                bestInfluence = influence;
                bestDirection = dir;
            }
        }

        return bestDirection;
    }

    // デバッグ用:影響マップの可視化
    void OnDrawGizmos()
    {
        if (sources == null) return;

        foreach (var source in sources)
        {
            Gizmos.color = source.strength > 0 ? Color.green : Color.red;
            Gizmos.DrawWireSphere(source.position, 1.0f);
        }
    }
}

影響マップの使用例

public class RBFPathfindingAgent : MonoBehaviour
{
    [SerializeField] private RBFInfluenceMap influenceMap;
    [SerializeField] private float moveSpeed = 5f;

    void Update()
    {
        // 影響マップに基づいて移動方向を決定
        Vector3 direction = influenceMap.GetBestDirection(transform.position);

        if (direction != Vector3.zero)
        {
            transform.position += direction * moveSpeed * Time.deltaTime;
        }
    }
}

パフォーマンスの最適化

1. 影響範囲の制限

ガウス関数などの局所的な影響関数を使う場合、一定距離以上離れた点の計算をスキップできます。

public float Evaluate(Vector3 position, float cutoffDistance = 10f)
{
    float result = 0f;

    for (int i = 0; i < centers.Length; i++)
    {
        float distance = Vector3.Distance(position, centers[i]);

        // カットオフ距離を超えたらスキップ
        if (distance > cutoffDistance)
            continue;

        float influence = EvaluateBasisFunction(distance);
        result += weights[i] * influence;
    }

    return result;
}

2. 空間分割による高速化

多数のデータ点がある場合、空間分割(グリッド、Octreeなど)を使って近傍探索を高速化します。

// 簡易的なグリッドベースの最適化
public class SpatialGrid
{
    private Dictionary<Vector3Int, List<int>> grid;
    private Vector3[] centers;
    private float cellSize;

    public SpatialGrid(Vector3[] dataCenters, float gridCellSize)
    {
        centers = dataCenters;
        cellSize = gridCellSize;
        grid = new Dictionary<Vector3Int, List<int>>();

        // グリッドにデータ点を登録
        for (int i = 0; i < centers.Length; i++)
        {
            Vector3Int cell = GetCellIndex(centers[i]);
            if (!grid.ContainsKey(cell))
                grid[cell] = new List<int>();
            grid[cell].Add(i);
        }
    }

    private Vector3Int GetCellIndex(Vector3 position)
    {
        return new Vector3Int(
            Mathf.FloorToInt(position.x / cellSize),
            Mathf.FloorToInt(position.y / cellSize),
            Mathf.FloorToInt(position.z / cellSize)
        );
    }

    public List<int> GetNearbyIndices(Vector3 position, int searchRadius = 1)
    {
        List<int> nearbyIndices = new List<int>();
        Vector3Int centerCell = GetCellIndex(position);

        // 周囲のセルを検索
        for (int x = -searchRadius; x <= searchRadius; x++)
        {
            for (int y = -searchRadius; y <= searchRadius; y++)
            {
                for (int z = -searchRadius; z <= searchRadius; z++)
                {
                    Vector3Int cell = centerCell + new Vector3Int(x, y, z);
                    if (grid.ContainsKey(cell))
                        nearbyIndices.AddRange(grid[cell]);
                }
            }
        }

        return nearbyIndices;
    }
}

3. 事前計算とキャッシュ

影響マップなど、頻繁に参照される値は事前計算してテクスチャやバッファに保存します。

public class PrecomputedInfluenceMap
{
    private float[,,] influenceGrid;
    private Vector3 gridOrigin;
    private Vector3Int gridSize;
    private float cellSize;

    public void Precompute(RBFInterpolator rbf, Vector3 origin, Vector3Int size, float cell)
    {
        gridOrigin = origin;
        gridSize = size;
        cellSize = cell;
        influenceGrid = new float[size.x, size.y, size.z];

        for (int x = 0; x < size.x; x++)
        {
            for (int y = 0; y < size.y; y++)
            {
                for (int z = 0; z < size.z; z++)
                {
                    Vector3 worldPos = origin + new Vector3(x, y, z) * cellSize;
                    influenceGrid[x, y, z] = rbf.Evaluate(worldPos);
                }
            }
        }
    }

    public float GetInfluence(Vector3 position)
    {
        Vector3 localPos = (position - gridOrigin) / cellSize;
        Vector3Int gridPos = new Vector3Int(
            Mathf.Clamp(Mathf.RoundToInt(localPos.x), 0, gridSize.x - 1),
            Mathf.Clamp(Mathf.RoundToInt(localPos.y), 0, gridSize.y - 1),
            Mathf.Clamp(Mathf.RoundToInt(localPos.z), 0, gridSize.z - 1)
        );

        return influenceGrid[gridPos.x, gridPos.y, gridPos.z];
    }
}

まとめ

RBFは、Unityでの多次元補間・影響範囲計算に強力なツールです。

主な利点

  1. 多次元対応: アニメーションブレンディングやAI意思決定など、複数のパラメータを扱える
  2. 滑らかな補間: 線形補間より自然な曲線を生成
  3. 局所制御: 影響関数により、局所的/大域的な影響を柔軟に制御

影響関数の選択指針

  • ガウス関数: 局所的な変化を重視(近くのキーフレームのみ影響)
  • IMQ: バランス重視(自然な減衰曲線)
  • マルチクアドリック: 大域的な影響を重視(全体の整合性)

パフォーマンス考慮

  • データ点が少ない(< 20点): そのまま使用可能
  • データ点が中規模(20-100点): カットオフ距離を設定
  • データ点が多数(> 100点): 空間分割や事前計算を検討

RBFを活用することで、より洗練されたアニメーションとAI挙動を実装できます。

参考実装

本記事のコードは以下のGitHubリポジトリで公開予定です。

  • 完全なRBFInterpolatorクラス
  • アニメーションブレンディングのサンプルシーン
  • AI意思決定のデモ
  • 影響マップのビジュアライゼーション

さらに学ぶために

  • 線形代数: RBFの数学的背景を理解するため
  • 数値計算: より高速な連立方程式ソルバー(Math.NET Numerics等)
  • 空間データ構造: Octree、kd-tree等の高速化手法

Discussion