雰囲気で使うMedic

一年ぶりまして。
今年もAdventCalendarの季節がやってまいりました！
今年の[Maya Advent Calendar](https://qiita.com/advent-calendar/2020/maya)はというと例年と同様に大好評(?)のようで、安心しました(涙目)

さて、それでは今回はMedicという個人的に触ってるツールについてお話します。

## Medicについて
[Medic](https://github.com/sol-ansano-kim/medic/blob/master/README_ja.md)とは、Mayaで動作するオープンソースのデータチェックツールです。
Python、C++でテスト項目(Tester)を拡張でき、それらを一つのチェックリスト(Karte)として統合することでシーン全体または選択したノードに対して一括でデータチェックを走らせることができます。
(画像左：Karte画面、画像右：Tester画面)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212052/dd0684b0-ff66-a58e-dd76-1767a5c06929.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212052/4d10b964-ccc8-d678-862e-5a094a547993.png)

この記事では細かいことは置いておいて、**Tester**と**Karte**というMedicの中でも重要な2つの要素について触れていきます。

## Tester(テスター)
Testerはチェック項目本体です。
Testerは先述した通りPythonとC++で拡張できますが、私はC++ワカラナイのでここではPythonのTesterについて書きます。

Testerの書き方は基本的に[ここ](https://github.com/sol-ansano-kim/medic/wiki/Japaness#custom-tester)に書いてある情報しかありません。
(ググってみても第三者によるそれらしい情報のようなものは殆ど見つけられませんでした。)

これだけの情報だと少し心細さはありますが、Medicには最初からお手本となるような便利なTesterがそれなりのボリュームで用意されているため、それらのコードを読むことで大体の雰囲気を掴むことが出来ました。

実際のコードを見ていきましょう。


### example1: anyLayer.py ([code link](https://github.com/sol-ansano-kim/medic/blob/master/plugins/Tester/anyLayer.py))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212052/a7ad8d43-c4a1-78f2-bb27-b6639b178bfb.png)
これはシーン内にBaseAnimation, defaultRenderLayer, defaultLayerがある場合に検知するTesterです。
上画像の例では `layer1` が存在するためエラー表示になっています。

このTesterの[コード](https://github.com/sol-ansano-kim/medic/blob/master/plugins/Tester/anyLayer.py)を例に、それぞれのブロックで何が行われているか解説していきます。

#### AnyLayer(medic.PyTester)
```python
class AnyLayer(medic.PyTester):
    def __init__(self):
        super(AnyLayer, self).__init__()
    ...
```
Testerは必ず`medic.PyTester`を継承する必要があります。
このPyTesterの内容をオーバーライドすることで独自のTesterが出来上がるわけです。


#### Name
```python
def Name(self):
    return "AnyLayer"
```
GUIのTester一覧画面に表示される文字列を設定する関数です。

#### Description
```python
def Description(self):
    return "Check if any render, anim or display layer exists"
```
GUIに表示されるTesterの説明文を設定する関数です。

#### Match
```python
def Match(self, node):
    return node.object().hasFn(OpenMaya.MFn.kDisplayLayer) or node.object().hasFn(OpenMaya.MFn.kAnimLayer) or node.object().hasFn(OpenMaya.MFn.kRenderLayer)
```
ここではTesterの対象となるノードの条件を記載します。
このコードの例では`kDisplayLayer`, `kAnimLayer`, `kRenderLayer`がフィルタリングされます。

引数の`node`はMedic独自の`MdNode`というノードが入っていて、`node.object()`と書くことで`MObject`を呼び出し`hasFun`でノードのtypeを判定しています。

Medicではこの`node`がとても重要になってきます。
nodeの中身は詳しく知りたい方は[こちら](https://github.com/sol-ansano-kim/medic/blob/master/include/medic/node.h)のコードをご覧ください。

#### test
```python

def test(self, node):
    if node.name() not in ["BaseAnimation", "defaultRenderLayer", "defaultLayer"]:
        return medic.PyReport(node)

    return None
```
Matchでフィルタリングされたnodeはこのtestでデータチェックが行われます。
このコードの例では`node.name()`でノード名の文字列を取得し、その文字列が`"BaseAnimation", "defaultRenderLayer", "defaultLayer"`以外であれば`medic.PyReport(node)`を返します。
この「`medic.PyReport(node)`を返す」という部分がエラーを検出したということになり、検出されたノードはGUIのListViewに一覧されます。
エラーが検出されなかった場合は`None`を返します。

#### fix
```python
def IsFixable(self):
    return True

def fix(self, report, params):
    node = report.node()
    if node.dg().isFromReferencedFile():
        return False

    if node.dg().isLocked():
        node.dg().setLocked(False)

    cmds.delete(node.name())

    return True
```
Medicではtestで検出したエラーを修正(fix)するロジックを書くこともできます。
この処理はtestと違って実際のデータを弄ることになるため、もしfixを作成する場合は気を付けて書く必要があります。

コードの例ではまず`report.node()`でtestに引っかかったnodeを取得し→
そのnodeがリファレンスだった場合は処理を中断しFalseを返す、リファレンスでない場合は→
そのノード(レイヤー)がロックされているかどうかを調べ、Trueならロックを解除、Falseならそのまま→
`cmds.delete(node.name())`でノードを削除し→
fix処理が完了

といった処理内容になっています。

`IsFixble`はこのTesterがエラーを修正できる能力を持っているかどうかを表すためのもの(GUIのfixボタンをenableにするためのもの)なので、もしfixを書くならセットでIsFixbleをオーバーライドしてTrueを返しましょう。

#### Create
```python
def Create():
    return AnyLayer()
```
いまいち詳しいことは分かっていませんが、とりあえずCreateでこのTester本体のclassのインスタンスを返す必要があるようです。
深く考えず、おまじないだと思って忘れずに書きましょう。

### testの補足：GUIのListViewから問題のあるコンポーネントを選択できるようにする
「transformノードの名前がルールと合っているかどうか」を調べるようなTesterだった場合は問題のノードはどれか分かるだけで問題ないと思います。
しかし、「とあるメッシュの全頂点のウェイト値がルール通りかどうか(小数点第3位以下が切り捨てられてるか 等)」を調べるようなTesterの場合、問題のあるメッシュと合わせて問題のあるコンポーネント(頂点やエッジ、面など)も選択できた方が便利だと思います。

実はMedicではそういった操作もサポートしています。
しかし、標準のTester、具体的にはpythonで書かれたTesterにはそういった処理がどれにも書かれていません。

それではどこに書かれているのかというと、C++で書かれたTesterの中に答えが書かれています。

少しだけコードを覗いてみましょう。

### example2: nonManifoldVertex.cpp ([github link](https://github.com/sol-ansano-kim/medic/blob/master/plugins/Tester/nonManifoldVertex.cpp#L37-L66))

このTesterのコードの37~66行目に書かれている内容がその答えになります。
一応コードを解説すると、

```cpp
MItMeshVertex itvtx(node->getPath());
```
nodeの持つdagPathから`MItMeshVertex`イテレーターの`itvtx`を作成し、


```cpp
MFnSingleIndexedComponent comp;
MObject comp_obj = comp.create(MFn::kMeshVertComponent);
```
`MFnSingleIndexedComponent `型のcompと、
`comp.create(MFn::kMeshVertComponent)`で生成したMObjectをもったオブジェクト`comp_obj`を生成し、

```cpp
while (!itvtx.isDone())
{
    MIntArray faces;
    MIntArray edges;
    itvtx.getConnectedFaces(faces);
    itvtx.getConnectedEdges(edges);

    if ((edges.length() - faces.length()) > 1)
    {
        result = true;
        comp.addElement(itvtx.index());
    }

    itvtx.next();
}
```
全頂点をイテレーションして問題のある頂点がある場合はresultにtrueを代入すると同時に頂点のindexをcompに格納しています。

そして、resultがfalseであれば`return 0`を返し、tureであればMdReport(Python TesterのPyReportに相当)を返します。

```cpp
return new MdReport(node, comp_obj);
```
ここでMdReportの第2引数に注目すると、`comp_obj`が渡されているのが分かると思います。
察しの良い人はお気づきかと思いますが、**PyReportでも同様に第2引数に問題のあるコンポーネントのインデックスが格納されたMObjectを渡してあげるとGUIから問題のあるコンポーネントを選択できるようになります。**

少し難点なのは、コンポーネントのインデックスが格納されたMObjectを生成する必要がある都合上、
OpenMayaをある程度理解して使える必要があることかなと思います。
(恐らくcmdsやpymelだと膨大な頂点数を処理する必要がある場合に速度面がネックになるからだと思います。)

今までcmdsやpymelで慣れてきた人にとっては少しハードルがあるかもしれませんが、頑張りましょう。


### Testerまとめ
Testerとは要は`Match`でテストしたいノードをフィルタリングし、`test`で実際にテストして問題を検出し、必要に応じて`fix`で修正するという役割を持ったモジュールです。

Medicにおいてもっともキモとなる部分ですね。


## Karte(カルテ)
Karteは先ほど説明したTesterをひとまとめにするためのものです。
例えばモデリング工程であればモデリング工程用のTesterをひとまとめにし、セットアップ工程ではセットアップ工程用のTesterをひとまとめにするなど、そういった用途で使います。

Karteの作り方はTesterと比べると非常にシンプルです。

### example3: all.karte ([github link](https://github.com/sol-ansano-kim/medic/blob/master/plugins/Karte/all.karte))
これがall.karteの全文です。
名前から分かる通り全てのTesterを表示します。

```
{
    "Name": "All",
    "Description": "All testers",
    "Testers": ["*"],
}
```

jsonのような構文になっていて、これをもとに目的に合わせて内容を書き換えることでKarteを作成します。
例えば以下のようなKarteの場合、

```
{
    "Name": "Modeling",
    "Description": "Modeling testers",
    "Testers": [
        "NonManifoldEdge",
        "NonManifoldVertex",
        "NonUniqueName",
        "ZeroLengthEdge"
    ]
}
```

こういう感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212052/774fbbe9-db88-a5bb-340b-6c340640e2bd.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212052/f8c76a0f-5f8d-ebbc-556f-b242eff6c604.png)

### Karteまとめ
全てのプロジェクト、全ての工程で全てのTesterが必要になるかというとそういうわけではないと思います。
そういった場合にこのKarteを使用して、必要なTesterだけをまとめたチェックリストを作りましょう。


## まとめ
データチェックツールの類は各社で独自に用意されていることが多いと思います。
ただ、0から作るとなるとツールそのものを用意したりUIを用意したりそれらを更新するシステムを作ったりとそれなりにコストがかかると思います。

そこで、Medicはこれからチェックツールを用意しようとしている場合に検討する候補の一つとしておすすめです。

データチェックという非クリエイティブな作業をとことん自動化して、クリエイティブな作業に費やす時間を増やしたいですね！

明日の記事はUnPySideさんの記事です。お楽しみに！