2017年04月

2017年04月05日

うにばな (シェーダへ配列データの転送) その4 JsonでMayaからUnityへ

 

 

今回は前回記事の関連事項となりますが Jsonデータを使用して各ツールでのデータのやりとり関するお話です。

 

すでにあるデータ形式に関しては必要以上に手を加えず サポート外のデータはJsonでやりとりするのが効率が良いという本家フォーラム(英語)のほうでアドバイスを読みまして おおきくはこのような理由が挙げられているようです。

  • Jsonは軽量で各種ゲームエンジン、DCCツール、WebGLなど ほとんどの開発ツールでサポートされているためやりとりに都合が良い
  • データ形式ごとにコンバータを作成する場合 バージョンアップなどのたびに手を入れ続けなければいけないため作業効率が良くない。
  • すでにあるデータ形式の改造を繰り返すことでツール間でデータの互換性が失われてしまうことがある。

 

 

そこで今回はサンプルとしてアンリアルエンジンで提供されているgridExport.mel pythonにリライトし Jsonで書き出しする機能を追加してみました。連番も対応しています。

Pythonを選択する理由は、メジャーなDCCツールではほとんどでPythonに対応されているためです。元がmelで記述されているため一旦pythonでリライトします。

 

 

 

【Python -Maya側】

gridExporter2

gridExportergridExporter3

 

 

『gridExportU.py』

# Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
# Export velocity grid data from a Maya fluid container
# Description: Writes out velocity grid data in a custom formatted ascii file


import pymel.core as pm
import maya.cmds as cmds
import json
from collections import OrderedDict


def gridExportU():
    # Check to see if the UI window already exists. If it does, it is deleted
    if pm.window('gridExport', exists=1):
        pm.deleteUI('gridExport', window=1)
    # Create new UI Window
    pm.window('gridExport', rtf=1, t="gridExport", widthHeight=(300, 300))
    pm.columnLayout('rootLayout')
    pm.frameLayout(marginHeight=5, labelVisible=False, marginWidth=5)
    pm.columnLayout('verticalSubframe')
    pm.setParent('..')
    pm.text(label="UE4 & UnityX Vector Field Exporter")
    pm.radioButtonGrp('myRadBtnGrp', numberOfRadioButtons=2, label="Export Mode",
                      select=1,
                      labelArray2=("Single Frame", "Sequence"),
                      onCommand1=lambda *args: pm.intFieldGrp(
                          'endFrame', edit=1, enable=0),
                      onCommand2=lambda *args: pm.intFieldGrp('endFrame', edit=1, enable=1))

    pm.radioButtonGrp('myRadBtnGrp1', numberOfRadioButtons=2, label="Export Type",
                      select=1,
                      labelArray2=("Fga", "Json"))

    pm.checkBoxGrp('isCached', v1=0, l="Cached Fluid?")
    pm.intFieldGrp('startFrame', l="Start Frame")
    pm.intFieldGrp('endFrame', v1=3, enable=0, l="End Frame")
    #pm.intFieldGrp('increment', v1=1, enable=0, l="Increment")
    pm.textFieldGrp('folderPath', text="C:\\", l="Path")
    pm.textFieldGrp('filename', text="vel", l="Filename prefix")
    pm.columnLayout('exportButton', adjustableColumn=True,
                    columnAttach=("both", 0))
    pm.button(c=lambda *args: iterateExport(), l="export")
    pm.showWindow('gridExport')


def iterateExport():

    startFrame = int(pm.intFieldGrp('startFrame', q=1, v1=1))
    endFrame = int(pm.intFieldGrp('endFrame', q=1, v1=1))
    #increment=int(pm.intFieldGrp('increment', q=1, v1=1))
    dataName = ["velocity"]
    n = startFrame
    sel = pm.ls(sl=1)
    if len(sel) > 1 or len(sel) < 1:
        print "ERROR: Please select a single fluid container \n"

    else:
        fluidShape = pm.listRelatives(sel[0], s=1)
        objectCheck = str(pm.objectType(fluidShape[0]))
        if objectCheck == "fluidShape":
            doit = int(pm.checkBoxGrp('isCached', q=1, v1=1))
            print doit
            if doit == 0:
                sceneCurTime = int(pm.currentTime(q=1))
                sceneMinTime = int(pm.playbackOptions(q=1, minTime=1))
                if sceneCurTime > startFrame:
                    pm.currentTime(sceneMinTime)
                    runupToStart(sceneMinTime, startFrame)

                elif sceneCurTime < startFrame:
                    runupToStart(sceneCurTime, startFrame)

            if pm.radioButtonGrp('myRadBtnGrp', q=1, select=1) == 1:
                pm.currentTime(n)
                folder = str(pm.textFieldGrp('folderPath', q=1, text=1))
                filename = str(pm.textFieldGrp('filename', q=1, text=1))
                filePath = folder + "\\" + filename + "." + str(n)
                # print "Wrote: " + filePath + "\n"

                if pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 1:
                    dataExport(dataName[0], filePath, fluidShape[0])
                elif pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 2:
                    dataExportJson(dataName[0], filePath, fluidShape[0])

            else:
                for n in range(startFrame, (endFrame + 1)):
                    pm.currentTime(n)
                    folder = str(pm.textFieldGrp('folderPath', q=1, text=1))
                    filename = str(pm.textFieldGrp('filename', q=1, text=1))
                    #filePath=folder + "" + filename + "." + str(n) + ".fga"
                    filePath = folder + "" + filename + "." + str(n)
                    # print "Wrote: " + filePath + "\n"
                    if pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 1:
                        dataExport(dataName[0], filePath, fluidShape[0])
                    elif pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 2:
                        dataExportJson(dataName[0], filePath, fluidShape[0])

        else:
            print "ERROR: Please select a fluid container \n"


def dataExport(dataName, filePath, myfluidShape):

    filePath += ".fga"
    voxCount = 0
    # Grab the Grid resolution
    res = map(int, pm.getAttr(myfluidShape + ".res"))
    # switch back to parent transform
    fluidShapeParent = pm.listRelatives(myfluidShape, p=1)
    # Grab the voxel container bounding box
    bb = pm.xform(fluidShapeParent[0], q=1, ws=1, bb=1)
    # create and open the output file in write mode
    fileId = open(filePath, "w")
    # Write voxel res
    fileId.write(("" + str(res[0]) + ","))
    fileId.write(("" + str(res[1]) + ","))
    fileId.write(("" + str(res[2]) + ","))
    # Write bounding Box info
    fileId.write(("" + str(bb[0]) + ","))
    fileId.write(("" + str(bb[1]) + ","))
    fileId.write(("" + str(bb[2]) + ","))
    fileId.write(("" + str(bb[3]) + ","))
    fileId.write(("" + str(bb[4]) + ","))
    fileId.write(("" + str(bb[5]) + ","))
    x = 0
    y = 0
    z = 0

    for z in range(0, res[2]):
        for y in range(0, res[1]):
            for x in range(0, res[0]):
                v = pm.getFluidAttr(xi=x, yi=y, zi=z, at=dataName)

                fileId.write(
                    (str(v[0]) + "," + str(v[1]) + "," + str(v[2]) + ","))

    fileId.close()


def runupToStart(baseframe, exportFirstFrame):

    i = 0
    for i in range(baseframe, exportFirstFrame):
        # print($i+"...\n")
        pm.currentTime(i)


def dataExportJson(dataName, filePath, myfluidShape):

    filePath += ".json"

    voxCount = 0

    # Grab the Grid resolution

    res = map(int, pm.getAttr(myfluidShape + ".res"))
    # switch back to parent transform
    fluidShapeParent = pm.listRelatives(myfluidShape, p=1)
    # Grab the voxel container bounding box
    bb = pm.xform(fluidShapeParent[0], q=1, ws=1, bb=1)
    # create and open the output file in write mode

    x = 0
    y = 0
    z = 0
    dict = []

    for iz in range(0, res[2]):
        for iy in range(0, res[1]):
            for ix in range(0, res[0]):
                v = pm.getFluidAttr(xi=x, yi=y, zi=z, at=dataName)
                myVelocity = {"x": v[0], "y": v[1], "z": v[2]}
                dict.append({"velocity": OrderedDict(
                    sorted(myVelocity.items()))})

    exportData = {
        'metadata': {
            'formatVersion': 1.0,
            'generatedBy': 'VectorFieldExporter'
        },
        "Data": {
            "Resolution": OrderedDict(sorted({"resX": res[0], "resY": res[1], "resZ": res[2]}.items())),
            "BBOX": {
                "Min": OrderedDict(sorted({"minX": bb[0], "minY": bb[1], "minZ": bb[2]}.items())),
                "Max": OrderedDict(sorted({"maxX": bb[3], "maxY": bb[4], "maxZ": bb[5]}.items()))
            }
        }, "DataArray": dict

    }

    writeJsonFile(exportData, filePath)


def writeJsonFile(dataToWrite, fileName):
    if ".json" not in fileName:
        fileName += ".json"

    print "> write to json file is seeing: {0}".format(fileName)

    with open(fileName, "w") as jsonFile:
        json.dump(dataToWrite, jsonFile, indent=4, separators=(',', ': '))

    print "Data was successfully written to {0}".format(fileName)

    return fileName


gridExportU()

 

GUI部分を除けばJsonのエクスポートをする部分とgetAttrを使用してシーンからデータを配列にコピーするだけなので、Maya以外に対応させるときは、使用するツールごとになんらかのエクスポーターを参考にしてJsonデータを書き出しする箇所を追記するだけで同様なスクリプトが書けると思います。

”import json” でJsonモジュールを読み込んで key[ ]とValue[ ]を別々に配列として登録した後に zip(key,value)で辞書型配列に登録するのが一般的な書き方のようなのですが、データの整列部分をはさんで今回は直接データを辞書型に変換しています。

通常の配列を辞書型の配列に変換すると要素の順番が保持されないため これを整列するためにOrderedDictモジュールをインポートしています。 これによってアルファベット順に辞書型配列内のデータを整列させて読みやすくなります ただしJsonをインポートするときには構造体のKeyを参照して読み込まれるためデータ順はどのようになっていても良いので この部分は無くてもかまいません。

  

OrderedDict(sorted({"resX": res[0], "resY": res[1], "resZ": res[2]}.items()))

 

”Python側”のexportDataが書き出しデータを整形するための配列となります。、カスタマイズしたい場合はこの配列内の記述を参考に、書き出されたデータの対応は”Python側” ”Json側"のスクリプトを参考にしてください

python(json)での表記は { }:辞書型配列 、[ ]:リスト配列型、():タプル型 となります。

Jsonファイルに書き出しをする部分は以下のように

 

with open(fileName, "w") as jsonFile:
        json.dump(dataToWrite, jsonFile, indent=4, separators=(',', ': '))

 

それぞれ 書き出しは json.dump( )   読み込みは json.loads( ) を使用します

参考:YoheiM.NET:[Python] JSONを扱う http://www.yoheim.net/blog.php?q=20150901

 

”Python側”

  exportData = {
        'metadata': {
            'formatVersion': 1.0,
            'generatedBy': 'VectorFieldExporter'
        },
        "Data": {
            "Resolution": OrderedDict(sorted({"resX": res[0], "resY": res[1], "resZ": res[2]}.items())),
            "BBOX": {
                "Min": OrderedDict(sorted({"minX": bb[0], "minY": bb[1], "minZ": bb[2]}.items())),
                "Max": OrderedDict(sorted({"maxX": bb[3], "maxY": bb[4], "maxZ": bb[5]}.items()))
            }
        }, "DataArray": dict

    }

”Json側"

{
    "Data": {
        "Resolution": {
            "resX": 30,
            "resY": 30,
            "resZ": 30
        },
        "BBOX": {
            "Max": {
                "maxX": 5.0,
                "maxY": 5.0,
                "maxZ": 5.0
            },
            "Min": {
                "minX": -5.0,
                "minY": -5.0,
                "minZ": -5.0
            }
        }
    },
    "DataArray": [
        {
            "velocity": {
                "x": -0.0008264329517260194,
                "y": 0.03183583915233612,
                "z": 0.014935646206140518
            }
        },
        {
            "velocity": {
                "x": -0.0008264329517260194,
                "y": 0.03183583915233612,
                "z": 0.014935646206140518
            }

 

   …………

 

],
    "metadata": {
        "formatVersion": 1.0,
        "generatedBy": "VectorFieldExporter"
    }
}

 

 

【C# Unity側】

『 SerializeBuffer.cs 』

using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;


[System.Serializable]
public class HeaderData
{
    public Vector3 Res;
    public Vector3 Max;
    public Vector3 Min;
}

[System.Serializable]
public class TransformData
{
    public Vector3 velocity;
}

[System.Serializable]
public class HeaderCollection
{
    public HeaderData[] HeaderArray;
}

[System.Serializable]
public class TransformCollection
{
    public TransformData[] DataArray;
}


public class SerializeBuffer : MonoBehaviour
{  
    public int texWidth = 1024;
    public int texHeight = 1024;


    public Vector3 Resolution;
    public Vector3 BBOX_Min;
    public Vector3 BBOX_Max;

    
    public string _FilePath = "Assets/Resources/";
    public string _FileName = "TexturedBuffer.png";

   
    public HeaderCollection MyHeaderData;
    public TransformCollection MyData;
    public const int MaxFileNum = 10;

    TransformCollection[] MyDataList = new TransformCollection[ MaxFileNum]; 
  



    void Awake()
    { 
    
        MyHeaderData = new HeaderCollection();
        MyData = new TransformCollection();
  

    }

    public void ConvertData()
    {
       
        string JSONString;
   
        DirectoryInfo dir = new DirectoryInfo(_FilePath);
        FileInfo[] info = dir.GetFiles("*.json");

        var index = 0;

        foreach (FileInfo f in info)
        {
                  
            JSONString = File.ReadAllText(f.FullName); // FileInfo.Name  or FileInfo.FullName
            Debug.Log("LoadPath: " + f);

            MyHeaderData = JsonUtility.FromJson(JSONString);
            MyData = JsonUtility.FromJson(JSONString);
            MyDataList[index] = MyData;
               
            index++;
           
        }
           
        Texture2D texture = new Texture2D(texWidth, texHeight, TextureFormat.RGB24, false, false);
        texture.filterMode = FilterMode.Point;
        texture.wrapMode = TextureWrapMode.Clamp;

        texture.Apply();

        int mip = 0;
        int maxIndex = (int)Mathf.Ceil(MyData.DataArray.Length / texWidth) * texWidth;

        Color[] cols = new Color[3];
        cols = texture.GetPixels(mip);

        int offset = 0;

        for (int j = 0; j < info.Length; j++)
        { 
           
            offset = j * maxIndex;

            for (int i = 0; i < maxIndex; ++i)
            {
               
                if (i < MyData.DataArray.Length)
                {

                    cols[i + offset].r = MyDataList[j].DataArray[i].velocity.x;
                    cols[i + offset].g = MyDataList[j].DataArray[i].velocity.y;
                    cols[i + offset].b = MyDataList[j].DataArray[i].velocity.z;

                }
                else
                {
                    cols[i + offset].r = 0;
                    cols[i + offset].g = 0;
                    cols[i + offset].b = 0;
                  
                }
                            
            }

        }

        texture.SetPixels(cols, mip);
         
        string saveFilePath = Path.Combine(_FilePath, _FileName);
        byte[] bytes = texture.EncodeToPNG();
        File.WriteAllBytes(saveFilePath, bytes);

        DestroyObject(texture);
  

       
        Resolution = MyHeaderData.HeaderArray[0].Res;
        BBOX_Min = MyHeaderData.HeaderArray[0].Min;
        BBOX_Max = MyHeaderData.HeaderArray[0].Max;

        Debug.Log("Textured_Buffer Exported : " + saveFilePath);
        Debug.Log("Resolution:" + MyHeaderData.HeaderArray[0].Res);
        Debug.Log("BBOX Min:" + MyHeaderData.HeaderArray[0].Min);
        Debug.Log("BBOX Max:" + MyHeaderData.HeaderArray[0].Max);

        Debug.Log("\n Buffer Export Completed ! ");
    }


    //------------------------------------------
    void Update()
    {
         if (Input.GetKeyDown(KeyCode.Space))
        {
            ConvertData();
            return;
        }
    }

}

 

動作は単純で指定したフォルダ内にあるJsonファイルを全て読み込んでデータをテクスチャに変換するだけです。

使用方法はJsonファイルへのフォルダパスと書き出すテクスチャの名前を設定した後スペースキーを押してください Jsonファイルと同じフォルダパスにTextureBufferのPngデータが書き込まれます。

当初サブフォルダまでサーチする仕様でしたが、作業中にサブフォルダに一旦ファイルを避けておくような用途も考えてやめました。

コマ抜きやら順序入れ替えやらのオプション操作も考えましたがwindows上で必要なファイルだけを選ればいいでしょうしツールは単純な方が使い勝手が良いので必要なしと判断しました。

 

Jsonから読み込んだデータの内容は以下の通り

        Resolution (バッファの解像度) MyHeaderData.HeaderArray[ ].Res 
        BBOX_Min (バウンディングボックス最小座標) MyHeaderData.HeaderArray[ ].Min 
        BBOX_Max (バウンディングボックス最大座標) MyHeaderData.HeaderArray[ ].Max

        velocity.x (x要素)           MyDataList[ ].DataArray[ ].velocity.x; 
        velocity.y (y要素)           MyDataList[ ].DataArray[ ].velocity.y; 
        velocity.z (z要素)            MyDataList[ ].DataArray[ ].velocity.z;


DataArray[ ]内の要素がvelocity.X、velocity.Y、velocity.Z、でMyDataList[ ]がDataArray[ ]をJsonの数だけ格納されています。

MyHeaderDataは解像度は共通なため一つ分だけ存在します。

 

 

【テクスチャデータについて】

【TextureBuffer.png】

TexturedBuffer

 

テクスチャへのデータのパックはUnity側はGL系なのでUV座標のV方向がフリップしています そのため見た目で下から上に向かって配置されます。 シェーダ側でDirectXを使用する場合はV方向は反転されるためテクスチャは上下が反転した見た目となります。

Velocityのデータはインポートした時すでに0~1の長さに正規化されているので、Color型配列にそのまま代入してテクスチャに書き込んでいます。

1以上の範囲を取るデータの場合はな正規化してから代入してください。詳しくは 以前の配列の転送に関する記事を参考にしてください。

 

【ファイルパス操作について】

セーブファイル名のパスとファイル名の連結にPathクラスを使用しています。

 

Path.Combine(_FilePath, _FileName);

 

Pathクラスの標準関数はマニュアルと同じですが次の通り

  • Combine                                   2つのパスストリングを結合してファイルパスに変換。
  • GetDirectoryName                       ディレクトリパスを返す.
  • GetExtension                              ファイルの 拡張子を返す.
  • GetFileName                              拡張子を含めたファイルネームを返す.   
  • GetFileNameWithoutExtension    拡張子抜きのファイルネームを返す.

    Path.GetFileNameWithoutExtension("/Some/File/At/foo.extension"));
    Path.GetFileName("/Some/File/At/foo.extension"));
    Path.GetDirectoryName("/Path/To/A/File/foo.txt"));
    Path.GetExtension("/Some/File/At/foo.extension"));
    Path.Combine("/First/Path/To", "Some/File/At/foo.txt"));

Pathクラスのvalue

  • AltDirectorySeparatorChar       ディレクトリレベルを区切るための代替文字。 (Read Only) '/' Windows 、 '/'  macOS
  • DirectorySeparatorChar           ディレクトリレベルを区切るために使用されるデフォルトの文字。 (Read Only) '\' Windows 、 '/'  macOS


 

 

【その他】

"maya-json-exporter" https://github.com/Jam3 https://github.com/Jam3/maya-json-export

Ttree.js など MayaのJsonエクスポータはいくつか公開されていますので参考にすると良いかと思います。

melとPython両方に対応されていますが、pythonがOpenMayaで記述されている箇所があり、非プログラマには若干ハードルが高いかもしれないです。 今回の解説をPython+Pymelだけで書いた理由でもあります 他プラットフォームへの移植もめんどうですし。

データが大きく 処理時間がかかりすぎるようであればOpenMayaかC++で書き換えする必要があるかもしれませんが、データをGetする以外は特に重たい処理をしているわけでもないので、わりと速度が気になるほどではないような気がします。

現在すでに手が入っている部分はそのままでよいかと思いますが ちょっとデータが必要だけどコンバータを書くのがめんどくさいとか言う場合に手軽にデータを送ることができるので便利かもしれません。

 

シェーダ側での配列の扱いについては、次回以降 余裕があれば、ぼちぼち書いていく 予定。

 

ではまた



akinow at 09:50|PermalinkComments(0)TrackBack(0) Clip to Evernote Unity3d | シリーズ講座