読者です 読者をやめる 読者になる 読者になる

Onoty3D

Unityに関するメモとか

SAFullBodyIKのヘルパーを作ってみたり

f:id:onoty3d:20150924194338p:plain

Unityでポーズを作成するとき、よく利用させてもらっているのが、Noraさん作の
SAFullBodyIKです。
Stereoarts Homepage
無料で公開されている、素晴らしいスクリプトです。

利用方法は、モデルにSAFullBodyIKをセットして、実行するだけ。
モデルの子に"FullBodyIK"というオブジェクトが追加されるので、Hierarchyビューで更にその子であるIK用の各種パーツを選んで、Sceneビューで動かす…といった仕組みです。
f:id:onoty3d:20150924193732g:plain
長らく利用させていただいているので、この操作になれてしまったのですが、ポーズの完成までHierarchyビューとSceneビューを往復することになります。
これが結構タイムロスになることもあります。

なので、Sceneビューだけでポーズが作れるよう、ヘルパーとなるスクリプトを作成しました。
仕組みとしては、Sceneビューで操作できるように、IK用の各種パーツにMeshを追加しているだけです。

f:id:onoty3d:20150924194634p:plain
利用方法はスクリプトをSAFullBodyIKが適用されたモデルにセットし、
IKScriptInstanceに、モデルに適用されているSAFullBodyIK
BaseMeshにセットしたいメッシュ、
Materialにそのメッシュに適用するマテリアルを指定するだけです。
※マテリアルは半透明なものを利用したほうが、元のモデルが隠れないので分かりやすいです。

実行すると、IK用の各種パーツに指定したメッシュが追加され、Sceneビューのみで操作が可能になります。
f:id:onoty3d:20150924193919g:plain
※なお、この時表示されるのはSAFullBodyIKでPositionEnabledまたはRotationEnabledのいずれかがONになっているパーツのみです。
また実行中にそれらを切り替えると、メッシュの表示/非表示も同期します。

ScaleFingerは指のメッシュ、Scaleはその他のメッシュの大きさを動的に変更できます。
またMeshVisibleで、全体のメッシュの表示/非表示が切り替えられます。
f:id:onoty3d:20150924194229g:plain

メッシュやマテリアル自体の動的な変更には対応していません(面倒だった)。
マテリアルのパラメータの動的変更(色を変えたりシェーダー変えたり)には対応しています。

using System.Linq;
using UnityEngine;

public class HelperForSAFullBodyIK : MonoBehaviour
{
    private class EffectorDetail
    {
        public SAFullBodyIK.Effector Effector { get; set; }

        public MeshFilter MeshFilter { get; set; }

        public MeshRenderer MeshRenderer { get; set; }
    }

    //SAFullBodyIKのInstance
    public SAFullBodyIK IKScriptInstance;

    //追加するメッシュ
    public Mesh BaseMesh;

    //メッシュに適用するマテリアル
    public Material Material;

    //メッシュのスケール
    public float Scale = 0.2f; //通常

    public float ScaleFinger = 0.1f; //指

    public bool MeshVisible = true;

    private Mesh _mesh;
    private Mesh _meshFinger;
    private Vector3[] _baseVertices;
    private EffectorDetail[] _effectorDetails;

    private float _latestScale;
    private float _latestScaleFinger;

    // Use this for initialization
    private void Start()
    {
        this._latestScale = this.Scale;
        this._latestScaleFinger = this.ScaleFinger;

        if (this.BaseMesh != null) this._baseVertices = this.BaseMesh.vertices;
    }

    // Update is called once per frame
    private void Update()
    {
        //SAFullBodyIKがセットされていなければ終わり
        if (this.IKScriptInstance == null) return;

        //SAFullBodyIKの諸々の準備が終わっていたら、初期処理
        if (this.IKScriptInstance._model != null
            && this.IKScriptInstance._model._effectors != null
            && this._effectorDetails == null)
        {
            //エフェクターを取得
            this._effectorDetails = this.IKScriptInstance._model._effectors.Select(x => new EffectorDetail { Effector = x }).ToArray();

            //メッシュのスケール初期値を取得
            this._latestScale = this.Scale;
            this._latestScaleFinger = this.ScaleFinger;

            //元となるメッシュのクローンを作成
            this.CloneMesh(ref this._mesh, this.BaseMesh);
            this.CloneMesh(ref this._meshFinger, this.BaseMesh);

            //メッシュのスケール変更
            this.RescaleMesh(this._mesh, this._latestScale);
            this.RescaleMesh(this._meshFinger, this._latestScaleFinger);

            for (int i = 0; i < this._effectorDetails.Length; i++)
            {
                var detail = this._effectorDetails[i];

                //MeshFilterの追加
                detail.MeshFilter = detail.Effector.transform.gameObject.AddComponent<MeshFilter>();
                if (detail.Effector._bone == null)
                {
                    detail.MeshFilter.mesh = this._mesh;
                }
                else
                {
                    if (detail.Effector._bone._boneType == SAFullBodyIK.BoneType.Finger)
                    {
                        detail.MeshFilter.mesh = this._meshFinger;
                    }
                    else
                    {
                        detail.MeshFilter.mesh = this._mesh;
                    }
                }

                //MeshRendererの追加
                detail.MeshRenderer = detail.Effector.transform.gameObject.AddComponent<MeshRenderer>();
                detail.MeshRenderer.material = this.Material;
                detail.MeshRenderer.enabled = this.MeshVisible && (detail.Effector.positionEnabled || detail.Effector.rotationEnabled);
            }
        }

        //まだエフェクターが取得できていなかったら終わり
        if (this._effectorDetails == null) return;

        //インスペクタ上などでScaleの値が変更されていたら、メッシュのScaleも変更
        if (this.Scale != this._latestScale)
        {
            this._latestScale = this.Scale;

            //元となるメッシュのクローンを作成
            this.CloneMesh(ref this._mesh, this.BaseMesh);

            //メッシュのスケール変更
            this.RescaleMesh(this._mesh, this._latestScale);
        }

        if (this.ScaleFinger != this._latestScaleFinger)
        {
            this._latestScaleFinger = this.ScaleFinger;

            //元となるメッシュのクローンを作成
            this.CloneMesh(ref this._meshFinger, this.BaseMesh);

            //メッシュのスケール変更
            this.RescaleMesh(this._meshFinger, this._latestScaleFinger);
        }

        //メッシュのスケールや表示の変更
        for (int i = 0; i < this._effectorDetails.Length; i++)
        {
            var detail = this._effectorDetails[i];
            if (detail.Effector._bone == null)
            {
                detail.MeshFilter.mesh = this._mesh;
            }
            else
            {
                if (detail.Effector._bone._boneType == SAFullBodyIK.BoneType.Finger)
                {
                    detail.MeshFilter.mesh = this._meshFinger;
                }
                else
                {
                    detail.MeshFilter.mesh = this._mesh;
                }
            }

            detail.MeshRenderer.enabled = this.MeshVisible && (detail.Effector.positionEnabled || detail.Effector.rotationEnabled);
        }
    }

    private void RescaleMesh(Mesh mesh, float scale)
    {
        //スケールの変更
        var vertices = new Vector3[this._baseVertices.Length];
        for (var i = 0; i < vertices.Length; i++)
        {
            vertices[i] = this._baseVertices[i] * scale;
        }

        //スケールの反映
        mesh.vertices = vertices;
        mesh.RecalculateBounds();
    }

    private void CloneMesh(ref Mesh to, Mesh from)
    {
        if (to != null)
        {
            Object.Destroy(to);
        }

        to = (Mesh)Object.Instantiate(this.BaseMesh);
    }
}

2015/09/25 ソース修正:Scale/ScaleFinger変更時にMeshを作り変える際、直前に利用していたMeshを明示的にDestroyするように。