- はじめに
- Mayaでカスタムプロパティにキーを打ってエクスポートする
- UnityでFBXをインポートする
- 値を反映する
- PlayableAPIを使用する場合を考える
- PlayableAPIを使ってもうちょっと柔軟に反映する方法を考える
- おわり
はじめに
Unity2017.2からは各種DCCツールで設定したカスタムプロパティをFBXからImport出来るようになりました。
Animated Custom Properties
Various DCCs (e.g. Maya and 3DSMax) support adding custom properties (or attributes) to objects:
These will appear in the Animation Window as Animator properties, just like additional curves created from imported clips:
You can then use a MonoBehaviour to drive other Component properties, or use an AssetPostprocessor to bind your curves directly to any Component.
https://blogs.unity3d.com/2017/10/12/unity-2017-2-is-now-available/
尚、この記事における利用環境などは以下の通り。
- Unity: 2017.3.0p3
- 使用モデル: ユニティちゃんトゥーンシェーダー2.0同梱のSDユニティちゃん
- Maya: 2018sp2
Mayaでカスタムプロパティにキーを打ってエクスポートする
というわけで早速やっていきます。
まずはMayaで適当なカスタムプロパティ(Extra Attributes)にキーを打っていきましょう。
今回は特定のマテリアルのアトリビュートを書き換えるようなものを考えます。
具体的にはユニティちゃんのこのパーツのハイライトのアトリビュートを変えてみようと思います。
| def copy_animation_to_custom_attribute(source_attr, target_node, target_attr): | |
| u"""アニメーションを指定したアトリビュートにコピーする | |
| Args: | |
| source_attr (unicode): コピー元のアトリビュート | |
| target_node (unicode): コピー先のノード | |
| target_attr (unicode): コピー先のアトリビュート名 | |
| """ | |
| keyframes = cmds.keyframe(source_attr, q=True) | |
| key_values = cmds.keyframe(source_attr, q=True, vc=True) | |
| attr = target_node + "." + target_attr | |
| if cmds.attributeQuery(target_attr, node=target_node, exists=True): | |
| cmds.deleteAttr(attr) | |
| cmds.addAttr(target_node, shortName=target_attr, at="float") | |
| for i in xrange(0, len(keyframes)): | |
| cmds.setAttr(attr, key_values[i]) | |
| cmds.setKeyframe(attr, t=keyframes[i]) | |
| source_attr = "file2.alphaGain" | |
| target_node = "Root_sphere" | |
| target_attr = "custom_highcolor_power" | |
| copy_animation_to_custom_attribute(source_attr, target_node, target_attr) |
こんな感じのコードを用意してスクリプトエディタなどで実行します。
引数は要件に応じて書き換えてください。
今回は変化が分かりやすいようにMaterialのcolorに割り当てたファイルノード(file2)のAlphaGainにキーを打っておいて、これをRoot_sphereノードにcustom_highcolor_powerという名前でコピーしています。
これをFBXエクスポートして準備完了です。
UnityでFBXをインポートする
上のFBXをUnityでインポートしたのち、ImportSettingで「Animated Custom Properties」にチェックを入れて「Apply」を実行します。
これをAnimationウィンドウで見てみるとこのようになっています。
どうやらちゃんとインポートされているようです。やったぜ。
値を反映する
さて、これを果たしてどうやって反映すればいいのか…!
Unityのブログには以下のように書いてあります。
MonoBehaviourを使って他のコンポーネントプロパティを制御することができますし、どのコンポーネントにも直接カーブをバインドするのにAssetPostprocessorを使うこともできます。
https://blogs.unity3d.com/jp/2017/10/12/unity-2017-2-is-now-available/
お、おう。
Animationウィンドウをよく見てみるとAnimator.〜と書いてあります。
というわけで、正解はこう。
AnimatorControllerを作成、Stateを追加してMotionにインポートしたAnimationClipを指定したのち、Mayaで作ったアトリビュートと同じ名前、同じ型のパラメーターを追加してやればおっけーです。
深刻なドキュメント不足。
ただこれだけだとパラメーターが受け取れただけでUnityちゃんの頭のアレには何も起きないので、Materialに値を渡してやりましょう。
| sing UnityEngine; | |
| public class SyncCustomPropertiesAnimator : MonoBehaviour | |
| { | |
| [SerializeField] | |
| SkinnedMeshRenderer _renderer; | |
| Animator animator; | |
| Material _targetMaterial; | |
| void Awake () | |
| { | |
| animator = GetComponent<Animator> (); | |
| _targetMaterial = _renderer.material; | |
| } | |
| void Update () | |
| { | |
| _targetMaterial.SetFloat ("_HighColor_Power", animator.GetFloat ("custom_highcolor_power")); | |
| } | |
| } |
_rendererにはinspectorでMesh_SD_unitychan以下の_headを指定しています。
いざ実行…!
ヤッター。反映できたよー。
PlayableAPIを使用する場合を考える
AnimatorControllerを使える環境ならこれでよいですが、PlayableAPIで制御する場合はパラメーターが使えない(自信ない)気がするので、これだと困ってしまいます。
となると、AnimationClipのカーブを書き換えたいなーとなるわけです。
まずはUnity2017.2の新機能を眺めてみましょう。
Asset Import: Added AssetPostprocessor.OnPostprocessGameObjectWithAnimatedUserProperties(GameObject go, EditorCurveBinding[] bindings) and void AssetPostprocessor.OnPostprocessAnimation(GameObject root,AnimationClip clip).
https://unity3d.com/jp/unity/whats-new/unity-2017.2.0
AssetPostprocessorにOnPostprocessGameObjectWithAnimatedUserPropertiesとOnPostprocessAnimationが追加されているようです。
今回はOnPostprocessAnimationを使ってみます。
| using System.Collections; | |
| using System.Collections.Generic; | |
| using UnityEngine; | |
| using UnityEditor; | |
| using System.IO; | |
| public class MotionImporter : AssetPostprocessor | |
| { | |
| private const string _CUSTOM_PREFIX = "custom_"; | |
| void OnPreprocessAnimation () | |
| { | |
| var modelImporter = assetImporter as ModelImporter; | |
| modelImporter.clipAnimations = modelImporter.defaultClipAnimations; | |
| modelImporter.importAnimatedCustomProperties = true; | |
| } | |
| List<string> targets = new List<string> (); | |
| void OnPostprocessGameObjectWithUserProperties ( | |
| GameObject go, | |
| string[] propNames, | |
| System.Object[] values) | |
| { | |
| Debug.Log ("OnProperties:" + assetPath); | |
| for (int i = 0; i < propNames.Length; i++) { | |
| if (propNames [i].StartsWith (_CUSTOM_PREFIX) && targets.Contains (assetPath) == false) { | |
| targets.Add (assetPath); | |
| Debug.Log ("Add:" + assetPath); | |
| } | |
| } | |
| } | |
| void OnPostprocessGameObjectWithAnimatedUserProperties (GameObject go, EditorCurveBinding[] bindings) | |
| { | |
| Debug.Log ("[test animated user properties]" + go.name); | |
| foreach (var b in bindings) { | |
| Debug.Log (b.propertyName); | |
| } | |
| } | |
| void OnPostprocessAnimation (GameObject root, AnimationClip clip) | |
| { | |
| if (Path.GetExtension (assetPath) != ".fbx") { | |
| Debug.Log (Path.GetExtension (assetPath)); | |
| return; | |
| } | |
| if (!targets.Contains (assetPath)) { | |
| Debug.Log ("not contain:" + assetPath); | |
| return; | |
| } | |
| Debug.Log ("[post anim]" + root.name + "," + clip.name); | |
| var file_name = Path.GetFileNameWithoutExtension (assetPath); | |
| var path = "Assets/Resources/" + file_name + ".anim"; | |
| var animclip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path); | |
| bool isNew = false; | |
| if (animclip == null) { | |
| isNew = true; | |
| animclip = new AnimationClip (); | |
| } | |
| animclip.ClearCurves (); | |
| EditorCurveBinding target_curve_binding = new EditorCurveBinding (); | |
| foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { | |
| if (binding.propertyName == "custom_highcolor_power") { | |
| target_curve_binding = binding; | |
| continue; | |
| } | |
| AnimationCurve curve = AnimationUtility.GetEditorCurve (clip, binding); | |
| AnimationUtility.SetEditorCurve (animclip, binding, curve); | |
| } | |
| if (target_curve_binding.path != null) { | |
| AnimationCurve curve = AnimationUtility.GetEditorCurve (clip, target_curve_binding); | |
| EditorCurveBinding curveBinding = new EditorCurveBinding (); | |
| curveBinding.path = "Mesh_SD_unitychan/_head"; | |
| curveBinding.type = typeof(SkinnedMeshRenderer); | |
| curveBinding.propertyName = "material._HighColor_Power"; | |
| AnimationUtility.SetEditorCurve (animclip, curveBinding, curve); | |
| } | |
| if (isNew) { | |
| AssetDatabase.CreateAsset ( | |
| animclip, | |
| AssetDatabase.GenerateUniqueAssetPath (path) | |
| ); | |
| } | |
| EditorUtility.SetDirty (animclip); | |
| AssetDatabase.SaveAssets (); | |
| Debug.Log ("AnimationClip created:" + file_name); | |
| } | |
| } |
もともとあったメソッドだとAnimationClipを取得するのがなかなかダルかったのですが、OnPostprocessAnimationは第二引数にAnimationClipが入ってくるのがよいですね。
まずはOnPostprocessGameObjectWithUserPropertiesでユーザー定義のアトリビュートのあるものをフックできるので、これで名前が一致するものをフィルタリングしてアセットパスをtargetsフィールドに格納しておきます。
次にOnPostprocessAnimationでこのtargetsに含まれるものがきたら対象のカーブのEditorCurveBindingを書き換え、AnimationClipとして保存しているような感じです。
curveBinding.path = "Mesh_SD_unitychan/_head"; curveBinding.type = typeof(SkinnedMeshRenderer); curveBinding.propertyName = "material._HighColor_Power";
pathはルートからの相対パス、typeは対象のクラス、propertyNameはそのまま格納するプロパティの名前を指定します。
目的のオブジェクトに対してそれぞれ何を指定したらいいか分からない場合、そのオブジェクトに対して新規でAnimationClipを作成して実際にAdd Propertyしてキーを打ってみて、生成された.animファイルをテキストとして開いてみるとよいです。
あとはこのAnimationClipをPlayableAPIを使って再生してやればおっけーです。
| using UnityEngine; | |
| using UnityEngine.Playables; | |
| using UnityEngine.Animations; | |
| public class SyncCustomPropertiesPlayable : MonoBehaviour | |
| { | |
| Animator animator; | |
| PlayableGraph graph; | |
| CustomProperties _customProperties; | |
| void Awake () | |
| { | |
| animator = GetComponent<Animator> (); | |
| graph = PlayableGraph.Create (); | |
| _customProperties = GetComponent<CustomProperties> (); | |
| } | |
| void Start () | |
| { | |
| var clip = Resources.Load<AnimationClip> ("custom_properties_playable"); | |
| if (clip == null) { | |
| Debug.Log ("null"); | |
| } else { | |
| Debug.Log ("not null"); | |
| } | |
| var clipPlayable = AnimationClipPlayable.Create (graph, clip); | |
| var output = AnimationPlayableOutput.Create (graph, "output", GetComponent<Animator> ()); | |
| output.SetSourcePlayable (clipPlayable); | |
| graph.Play (); | |
| } | |
| } |
シンプル。
ただ、これだと割と構造が厳格に決まってないと厳しいのと、使い回しがきかなさそうな感じがします。
PlayableAPIを使ってもうちょっと柔軟に反映する方法を考える
CurveBindingのtypeは何もビルトインされているクラスじゃないと駄目なわけではないので、自前で値を保持するクラスを作ってそいつを指定する、というのも可能です。
具体的には以下のようにします。
| using UnityEngine; | |
| public class CustomProperties : MonoBehaviour | |
| { | |
| [SerializeField] | |
| private float _highcolor_power = 1f; | |
| public float highcolorPower{ get { return this._highcolor_power; } } | |
| } |
| using UnityEngine; | |
| using UnityEngine.Playables; | |
| using UnityEngine.Animations; | |
| public class SyncCustomProperties : MonoBehaviour | |
| { | |
| [SerializeField] | |
| SkinnedMeshRenderer _renderer; | |
| Animator animator; | |
| Material _targetMaterial; | |
| PlayableGraph graph; | |
| CustomProperties _customProperties; | |
| void Awake () | |
| { | |
| animator = GetComponent<Animator> (); | |
| _targetMaterial = _renderer.material; | |
| graph = PlayableGraph.Create (); | |
| _customProperties = GetComponent<CustomProperties> (); | |
| } | |
| void Start () | |
| { | |
| var clip = Resources.Load<AnimationClip> ("custom_properties"); | |
| if (clip == null) { | |
| Debug.Log ("null"); | |
| } else { | |
| Debug.Log ("not null"); | |
| } | |
| var clipPlayable = AnimationClipPlayable.Create (graph, clip); | |
| var output = AnimationPlayableOutput.Create (graph, "output", GetComponent<Animator> ()); | |
| output.SetSourcePlayable (clipPlayable); | |
| graph.Play (); | |
| } | |
| void Update () | |
| { | |
| _targetMaterial.SetFloat ("_HighColor_Power", _customProperties.highcolorPower); | |
| } | |
| } |
あとはこれに合わせてModelImporterのcurveBindingまわりの処理を以下のように変えてやります。
curveBinding.path = ""; curveBinding.type = typeof(CustomProperties); curveBinding.propertyName = "_highcolor_power";
Inspectorはこんな感じ。
これでCurveBindingのpathは空文字でよく、さらに値の反映先も自由に決められるので、AnimatorControllerで制御するときと大きく変わらない感じで管理できそうです。やったぜ。
おわり
OnPostprocessGameObjectWithAnimatedUserPropertiesは2回目のインポートのタイミングで確かにカスタムプロパティのEditorCurveBindingが処理に入ってくるんですけど、いまいち何に使えばよいか分からず。
事例が色々出てくることに期待しつつ、気が向いたらもうちょっと追ってみたいところ。
カスタムプロパティがサポートされたのは大変喜ばしいんですけど国内も国外も資料なさすぎてつらみがすごい…。
この作品はユニティちゃんライセンス条項の元に提供されています