softIKをanim curveノードで実現する。

こちらは [Maya Advent Calendar](https://qiita.com/advent-calendar/2018/maya)2018 12月11日の記事です。
だいぶ期日過ぎた投稿になりますが、まだ枠が空いておりましたので、僭越ながら投稿いたします。

---
### まえがき
以前Twitter経由で「[Mayaのノードで数学の演算を実現する](http://philosy.com/blog/2018/12/21/maya%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%81%A7%E6%95%B0%E5%AD%A6%E3%81%AE%E6%BC%94%E7%AE%97%E3%82%92%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8B/)」という記事を拝見いたしました。
この中でanimcurveノードを使った、疑似的な数学の演算手法が紹介されており、私自身も同様手法を用いてsoftIKのリグを実装しました。
正直自分の実装方法については、個人的には「こりゃ強引だよなぁ…」などと思って、自信が持てなかったのですが、同様のアプローチをされている方がいたということに、内心ほっとしたというか、勇気づけられたといいますか。

そこで、こちらの記事では私が取ったsoftIKの実装方法について、簡単に紹介したいと思います。

---
## 設計

softIKの一番のポイントは、
IKを操作すると…
- IKハンドルが、起点からある一定距離以内であれば、通常通りの挙動を取る
- ある一定距離を超えると、IKが伸びきる距離に近似する挙動を取る
ということ。
この「近似挙動をとる」って時点でlogとか三角関数とかのグラフを使いたいマンになるわけですが。

グラフ形状を計算すると、
途中までは
***y=ｘ***
途中からある定数の直線
***y=K***
に近似するグラフになるわけです。
つまり、***y=Xとy=Kの二つの接線を持つグラフ***、となるわけです。

#### 問題点
しかし、Mayaには残念ながらデフォルトで三角関数やlogを計算してくれる計算ノードがありません。
（melにはあるけど。）
Plugin等を作成して計算させてもよいのですが、どんな環境でもすんなり開けるようにしたかったので、Mayaデフォルトのノードで構築することにしました。さてどうしよう。

#### 解決策
端的に考えて、
**y=Xとy=Kの二つの接線を持つグラフ**
という点を考えると、spline曲線が使えそうです。
そこで、animcurveノードで何とかできないか、模索してみました。
[Autodeskのドキュメント](http://help.autodesk.com/cloudhelp/2018/JPN/Maya-Tech-Docs/Nodes/animCurve.html)によると、animcurveノードには以下の種類があります。

|Name|Input Type|Output　Type|
|:--:|:--:|:--:|
|animCurveTL|time|distance|
|animCurveTA|time|angle|
|animCurveTT|time|time|
|animCurveTU|time|double|
|animCurveUL|double|distance|
|animCurveUA|double|angle|
|animCurveUT|double|time|
|animCurveUU|double|double|

上記の表から、input・output両方ともdoubleで受け付けているanimCurveUUノードが使えそうです。
（ドリブンキーで設定されるanimCurveノードがこれにあたりますね。）


補完がかかる範囲について少し整理すると、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/7773d6ce-0619-b7bc-193a-35d6c88ceb8d.png)
こんな感じ。
上記の、「補完がかかる範囲」のところで、このanimCurveで代用した近似グラフを使ってやれば、何とかなりそうです。

## 実装
では、上記の設計を基に実装してみます。
### scene構成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/f93353b6-0250-ff49-8d85-05932ec54d09.png)

- 簡単なIK挙動での実装をテストするため、単純な2ボーンIK用のジョイント構造を用意。
- 距離の起点を取得するためのRootノードに各々リグノードを格納
- softIK補完がかかっていない、デフォルトの距離を取得するためのbaseTrnsノードを作成
- softIK補完をかけるノード、softTrnsを作成。
- baseTrnsとコントローラー、softTrnsをikHandleにコンストレイン接続

### 各種リグノードの作成
softIKの各種情報を格納するため、networkNodeを作成し、addAttrで必要なアトリビュートを追加。

```.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division

import maya.cmds as mc
import maya.api.OpenMaya as om2

nt = mc.createNode('network',n='softIK_info')
mc.addAttr(nt,ln='ikMaxLength',at='float')
mc.addAttr(nt,ln='startLengthSoftCorrection',at='float')
```
|name|説明
|:--:|:--:
|ikMaxLength|距離の最大値を格納
|startLengthSoftCorrection|softIK補完を開始する距離を格納

次に、IK設定時のbaseTrnsまでの距離を最大値として、MaxLengthを取得
とりあえず、softIK補完の開始距離を80％くらいからと仮定し、startLengthSoftCorrectionも設定しちゃいます。

```.py
#get max Distance
rootPoint = mc.xform('root',q=True,rp=True,ws=True,wd=True)
currentPoint = mc.xform('baseTrns',q=True,rp=True,ws=True,wd=True)

rootVector = om2.MVector(rootPoint)
currentVector = om2.MVector(currentPoint)

distanceVector = rootVector-currentVector
maxDistance = distanceVector.length()
softStartLength = maxDistance*0.8

mc.setAttr('%s.ikMaxLength'%nt,maxDistance)
mc.setAttr('%s.startLengthSoftCorrection'%nt,softStartLength)
```
便宜上、ここでは距離計算にopenMayaからvectorを持ってきて使っています。
vector便利。

ここまでのノード構造は
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/0e68da3e-245b-93cc-734e-1696d7c31693.png)
まだこんな感じ。

さて、softIK補完の計算ノードを構築しましょう。
baseTrnsノードのMatrixから、rootからの距離をdistanceBetweenNodeで取得し、各種計算ノードで、補完スタート地点からどれくらい進んでいるかを計算します。

```.py
#culculate soft pct
distNode = mc.shadingNode('distanceBetween',au=True,n='currentLength')
mc.connectAttr('baseTrns.matrix','%s.inMatrix2'%distNode,f=True)

culLength = mc.shadingNode('plusMinusAverage',au=True,n='culLength')
mc.setAttr('%s.operation'%culLength,2)
mc.connectAttr('%s.distance'%distNode,'%s.input2D[0].input2Dx'%culLength)
mc.connectAttr('%s.startLengthSoftCorrection'%nt,'%s.input2D[1].input2Dx'%culLength)
mc.connectAttr('%s.ikMaxLength'%nt,'%s.input2D[0].input2Dy'%culLength)
mc.connectAttr('%s.startLengthSoftCorrection'%nt,'%s.input2D[1].input2Dy'%culLength)

culSoftPct = mc.shadingNode('multiplyDivide',au=True,n='culSoftPct')
mc.setAttr('%s.operation'%culSoftPct,2)
mc.connectAttr('%s.output2Dx'%culLength,'%s.input1X'%culSoftPct)
mc.connectAttr('%s.output2Dy'%culLength,'%s.input2X'%culSoftPct)
```
◇ここが混乱ポイントなのですが。
設計時の表でも示しましたが、全体の距離のうち、補完がかかる範囲は、「補完スタート地点」から「MaxLength」までです。　そのため、その範囲をどれくらい進んでいるのかを計算する必要があるわけです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/25be38d1-d9a6-6a85-eaaf-a530192304ff.png)
ここまでのノード構造は
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/7cfccf0a-c02d-56c8-1175-c50485c05340.png)
こんな感じになります。

#### animCurveUUを使ったグラフ制御
さて、ここからcurveAnimUUノードを使って、softPctに対してsoftIK補完のグラフを作成します。
softIK補完の計算結果を格納するノードを作成し、そこにanimCurveUUノードを接続します。

```.py
#create animCurveNode
culSoftLength = mc.shadingNode('multDoubleLinear',au=True,n='culSoftLength')
cv = mc.createNode('animCurveUU',n='softCorrectionCurve')
mc.connectAttr('%s.outputX'%culSoftPct,'%s.input'%cv)
mc.connectAttr('%s.output'%cv,'%s.input1'%culSoftLength)
mc.connectAttr('%s.output2Dy'%culLength,'%s.input2'%culSoftLength)
```
※なお作成したculSoftLengthノードには、animCurveノードで計算したsoftIK補完後の数値が0～1で返されます。　そのため、元のスケールに戻すため、上記コードでは、補完がかかる範囲を何％進んだかを計算する際に使用した、分母の値を掛けています。


#### グラフカーブの形状を修正
ひとまず、y=xの形でグラフカーブを作成し、その時のindex0のkeyframeのtangentAngleを取得しておきます。
ここでは仮に、x=2の時にy=1に接する事にして、index1のkeyframeを1から2へ変更し、あらかじめ取得しておいたy=xのtangentAngleをinAngleに代入することによってy=xの接線を設定します。

```.py
mc.setDrivenKeyframe(culSoftLength,at=('input1'),cd='%s.outputX'%culSoftPct,
    dv=0.0,v=0.0,itt='clamped',ott='clamped')
mc.setDrivenKeyframe(culSoftLength,at=('input1'),cd='%s.outputX'%culSoftPct,
    dv=1.0,v=1.0,itt='clamped',ott='clamped')
angle = mc.keyTangent(cv,q=True,index=(0,0),inAngle=True)[0]
mc.keyframe(cv,index=(1,1),floatChange=2)
mc.keyTangent(cv,index=(1,1),itt='flat',ott='flat')
mc.keyTangent(cv,index=(0,0),inAngle=angle)
mc.setAttr('%s.preInfinity'%cv,1)
mc.setAttr('%s.postInfinity'%cv,1)
```
あと、preInfinity、postInfinityの設定も。

ここまでのノード構造は
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/63397b6f-1a31-4e3f-ac27-3a67a9476bed.png)
こんな感じになってきました。

#### 求めた長さの整理
さて、以上で求められる補完済みの長さとはあくまで…
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/01215ff2-e524-bcbd-87c8-deae0cf46833.png)

↑ここなので、最終的に求めたい長さを取得するには、もう少し料理が必要です。

まず、
***（補完がかかった部分の長さ）+（補完がかかるまでの長さ）＝（補完後のIKの長さ）***
つぎに、
***（補完後のIKの長さ）/（補完前のIKの長さ）＝（補完割合）***
このようにして、softIKの補完割合を求めてしまえば、現時点のbaseTrnsから補完後のsoftTrnsの位置を計算できるようになります。

```.py
#cul softLength
culAllSoftLength = mc.shadingNode('addDoubleLinear',au=True,n='culAllSoftLength')
mc.connectAttr('%s.output'%culSoftLength,'%s.input1'%culAllSoftLength)
mc.connectAttr('%s.startLengthSoftCorrection'%nt,'%s.input2'%culAllSoftLength)
culSoftLengthPct = mc.shadingNode('multiplyDivide',au=True,n='culSoftLengthPct')
mc.setAttr('%s.operation'%culSoftLengthPct,2)
mc.connectAttr('%s.output'%culAllSoftLength,'%s.input1X'%culSoftLengthPct)
mc.connectAttr('%s.distance'%distNode,'%s.input2X'%culSoftLengthPct)

softedTrns = mc.shadingNode('multiplyDivide',au=True,n='softedTrns')
mc.connectAttr('%s.outputX'%culSoftLengthPct,'%s.input1X'%softedTrns)
mc.connectAttr('%s.outputX'%culSoftLengthPct,'%s.input1Y'%softedTrns)
mc.connectAttr('%s.outputX'%culSoftLengthPct,'%s.input1Z'%softedTrns)
mc.connectAttr('baseTrns.translateX','%s.input2X'%softedTrns)
mc.connectAttr('baseTrns.translateY','%s.input2Y'%softedTrns)
mc.connectAttr('baseTrns.translateZ','%s.input2Z'%softedTrns)
```
ここまでのノード構造は
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/701b1c87-666a-e839-6e11-c0bc072ed64e.png)
あともう少しですね。

#### conditionNodeで挙動をスイッチ
softIK補完が開始されていないときは、
baseTrns==softTrns
という位置関係が保たれているので、currentLengthとstartLengthSoftCorrectionの値を使ってconditionNodeで計算を切り替えるようにします。

```.py
#createCondition
cond = mc.shadingNode('condition',au=True,n='checkCurrentDistance')
mc.setAttr('%s.operation'%cond,2)
mc.connectAttr('%s.distance'%distNode,'%s.firstTerm'%cond)
mc.connectAttr('%s.startLengthSoftCorrection'%nt,'%s.secondTerm'%cond)
mc.connectAttr('baseTrns.translateX','%s.colorIfFalseR'%cond)
mc.connectAttr('baseTrns.translateY','%s.colorIfFalseG'%cond)
mc.connectAttr('baseTrns.translateZ','%s.colorIfFalseB'%cond)
mc.connectAttr('%s.outputX'%softedTrns,'%s.colorIfTrueR'%cond)
mc.connectAttr('%s.outputY'%softedTrns,'%s.colorIfTrueG'%cond)
mc.connectAttr('%s.outputZ'%softedTrns,'%s.colorIfTrueB'%cond)
```

さいごに、conditionNodeとsoftTrnsノードを接続してやれば…

```.py
#connect softIK
mc.connectAttr('%s.outColorR'%cond,'softTrns.translateX')
mc.connectAttr('%s.outColorG'%cond,'softTrns.translateY')
mc.connectAttr('%s.outColorB'%cond,'softTrns.translateZ')
```

![録画_2018_12_25_02_59_45_285.gif](https://qiita-image-store.s3.amazonaws.com/0/211727/8b94794d-f245-cf07-a058-64354a99be8b.gif)
ハラショー。

最終的なノード構造は
![image.png](https://qiita-image-store.s3.amazonaws.com/0/211727/de1a1282-93dc-cb4d-34d0-d42d9c8198f6.png)
こんな感じになります。

---
以上の実装に
- 補完の開始距離
- 補完の強さ
…などを調整可能になるよう、追加実装を施してあげれば、アニメーターさんが使いやすくなるのではないでしょうか。