2019年11月1日金曜日

Unityのアセットの配置をCSVで読み書き

Unityのレベル(ゲームステージ)のアセット配置をする作業をすることになったのですが
Unity上での配置作業に煩わしさから、配置作業を他のソフトですることを考えました
他の方法を見つけられなかっただけで 車輪の再発明のようにも思いますが残しておきます

UnityのPrefabはプレハブを構成するアセットの位置や回転等の設定を記録したデータのようですが
ちょっとした配置変更や置き換えをしたいと思った場合に手間が多いように感じました
アセットの名前と位置情報を汎用性の高いフォーマットで書き出し、書き戻すこともできれば
表計算ソフトや3Dソフトで値を調整したりすることもできるはずです
今回は一番簡単なCSVを入出力に利用しました。
UnityはY-UP 左手系です。
位置はそのままの値で、回転はwxyzクォータニオンで取得・記録することにしました
書き出しスクリプトはこちら
using UnityEngine;
using UnityEditor;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class ActiveInfoCSV : MonoBehaviour
{
    public string filePath = "/Resources/Prefabs/save.txt";
    // Start is called before the first frame update
    void Start()
    {
        //Application.dataPathはプロジェクトデータのAssetフォルダまでのパス
        string path = Application.dataPath + filePath;
        //inspectorの表示されているオブジェクト1つを取得
        GameObject activeObj = (GameObject)Selection.activeObject;
        int childCount = activeObj.transform.childCount;
        string[][] infoList = new string[childCount + 1][];
        infoList[0] = getInfo(activeObj);
        for (int i = 0; i < childCount; i++)
        {
            GameObject childObj = activeObj.transform.GetChild(i).gameObject;
            infoList[i+1] = getInfo(childObj);
        }
        saveCSV(path, infoList);
    }
    string[] getInfo(GameObject Obj)
    {
        //オブジェクトの名前位置回転拡縮をリストで返す
        string[] info = new string[11];
        //プレハブのオリジナルのオブジェクトの名前を取得
        GameObject original = PrefabUtility.GetCorrespondingObjectFromOriginalSource(Obj);
        if (original == null)
        { info[0] = Obj.name; }
        else
        { info[0] = original.name; }
        info[1] = Obj.transform.position.x.ToString();
        info[2] = Obj.transform.position.y.ToString();
        info[3] = Obj.transform.position.z.ToString();
        info[4] = Obj.transform.rotation.x.ToString();
        info[5] = Obj.transform.rotation.y.ToString();
        info[6] = Obj.transform.rotation.z.ToString();
        info[7] = Obj.transform.rotation.w.ToString();
        info[8] = Obj.transform.localScale.x.ToString();
        info[9] = Obj.transform.localScale.y.ToString();
        info[10] = Obj.transform.localScale.z.ToString();
        
        return info;
    }
    bool saveCSV(string path, string[][] strList)
    {
        try
        {
            using (StreamWriter writer = new StreamWriter(path, false))
            {

                foreach (string[] str2 in strList)
                {
                    writer.WriteLine( string.Join(",",str2) );
                }
                writer.Flush();
                writer.Close();
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            return false;
        }
        return true;
    }
}
Projectビュー等で選択してアクティブになっているPrefabの情報を書き出します
空のシーンを作成してEmptyにスクリプトをアタッチした状態でプレハブを選択してPrayモードに入る等の操作で実行できると思います。
CSVはプロジェクト内のインスペクタで設定したパスに保存されます

Prefabと書き出したデータはこんな感じ

今回は1階層の入れ子のみの対応です
1行目に親となるPrefabの情報 以降にPrefab内にあるアセットの配置情報になります

続いて書き出したファイルのシーンへの読み込みです

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

public class CreatePrefabs2 : MonoBehaviour
{
    public string filePath = "/Resources/Prefabs/save.txt";
    public string plefabDirectry = "ASTEST";
    // Use this for initialization
    void Start()
    {
        //Application.dataPathはプロジェクトデータのAssetフォルダまでのパス
        string path = Application.dataPath + filePath;
        //csvの読み込み
        string[][] CSVdata = loadCSV(path);
        //prefabの生成
        CSVtoPrefab(CSVdata, plefabDirectry);
    }

    string[][] loadCSV(string path)
    {
        List strList = new List();
        //TextAsset csvFile = Resources.Load(path) as TextAsset;

        using (StreamReader reader = new StreamReader(path))
        {
            while (reader.Peek() >= 0)
            {
                string[] line = reader.ReadLine().Split(','); // 一行ずつ変換
                strList.Add(line);
            }
            reader.Close();
            return strList.ToArray();
        }
    }
    void CSVtoPrefab(string[][] CSVdata,string plefabDirectry)
    {
        string objName = "Empty";
        if (CSVdata[0][0] != null) { objName = CSVdata[0][0]; }
       
        GameObject newParent = new GameObject(objName); //親のエンプティの作成
        Vector3 pos = new Vector3();
        Quaternion rot = new Quaternion();
        string prefabsPath = "";
        for (int i = 1;i < CSVdata.Length; i++)
        {
            prefabsPath = plefabDirectry + "/" + CSVdata[i][0];
            pos = new Vector3(float.Parse(CSVdata[i][1]), float.Parse(CSVdata[i][2]), float.Parse(CSVdata[i][3]));
            rot = new Quaternion(float.Parse(CSVdata[i][4]), float.Parse(CSVdata[i][5]), float.Parse(CSVdata[i][6]), float.Parse(CSVdata[i][7]));
            GameObject obj = CreatePrefab(prefabsPath, pos, rot);
            obj.transform.parent = newParent.transform; 
        }
        //Debug.Log(CSVdata.Length);
    }
    GameObject CreatePrefab(string path, Vector3 pos, Quaternion rot)
    {
        GameObject prefabObj = (GameObject)Resources.Load(path);
        //GameObject prefab = (GameObject)Instantiate(prefabObj, pos, q);
        GameObject prefab = (GameObject)PrefabUtility.InstantiatePrefab(prefabObj);
        prefab.transform.position = pos;
        prefab.transform.rotation = rot;
        return prefab;
    }
}
同様に空のシーンで実行すると書き出しデータに従ってPrefabを作成します
名前を元にインスペクタで設定したフォルダ内のアセットを配置する仕様にしてあります
(生成されたオブジェクトをProjectにドラッグすることで新しいPrefabとして保存することもできます)

これによって、メインの作業者のプロジェクト内だけでなく
名前を統一したダミーのオブジェクトで配置をすることで元データを持っていない環境でも作業できることになります。
とはいえ、ここまでの機能では利点はほとんどないですね。

活用例としてBlenderでこのデータを入出力するスクリプトを書いてみました
まずは読み込み
import bpy
from mathutils import Vector, Euler, Matrix, Quaternion

scene = bpy.context.scene
convert_matrix = Matrix([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])
#''の間に設定のCSVのパスを記述
csv_path = r'C:\Users\user\Documents\test\AssetTest\Assets\Resources\Prefabs\save.txt'
f= open(csv_path).read()

def set_objects(data_list):
    if len(data_list) != 11: return()
    #serch Objects
    obj_name = data_list[0]
    t = [float(s) for s in data_list[1:]]
    mat_loc = convert_matrix @ Matrix.Translation(t[:3])
    mat_rot = Quaternion([t[6], t[3], t[4], t[5]]).to_matrix().to_4x4() @ convert_matrix
    mat_out = (mat_loc @  mat_rot)

    obj = bpy.data.objects.get(obj_name)
    if obj is None or obj.type == 'EMPTY':
        new_obj = bpy.data.objects.new(obj_name,None)
        new_obj.empty_display_size = 0.5
        new_obj.empty_display_type = 'CUBE'
    else:
        new_obj = bpy.data.objects.new(obj_name,obj.data)
    new_obj.matrix_world= mat_out
    scene.collection.objects.link(new_obj)

#一行1レイヤとして処理
lines = f.split('\n')
for line in lines[1:]:
    #戻り値を変数に設定
    data_list = line.split(',')
    set_objects(data_list)
Blender2.8用のスクリプトになります
テキストエディッタに読み込んで「スクリプト実行」をします

名前を元に.blendファイル内のオブジェクトを取得して CSVの通りに配置します
同名のオブジェクトがない場合はキューブ表示のエンプティを配置しています
Blenderはファイル内に複数のシーンを持てるので、配置用のオブジェクトを置いたシーンとスクリプト実行用の空のシーンを作っておくとよいかと思います

そして位置調整をしたデータをまたCSVを書き出せればフローができあがります
import bpy
import csv
from mathutils import Vector, Euler, Matrix, Quaternion
#''の間にCSVを書き出すパスを記述
file_path = r'C:\Users\user\Documents\test\AssetTest\Assets\Resources\Prefabs\save.txt'

obj_list = [["root",0,0,0,0,0,0,1,1,1,1]]
objects = bpy.context.scene.objects
for obj in objects:
    if obj.type == 'MESH':
        mat_r = obj.matrix_world
        m = [ [r[0], r[2], r[1],r[3]] for r in mat_r]
        mat_l = Matrix([m[0], m[2], m[1], m[3]])
        pos = mat_l.translation
        rot = mat_l.to_quaternion()
        siz = mat_l.to_scale()
        obj_data = list(pos) + [rot.x, rot.y, rot.z, rot.w] + list(siz)
        obj_tex = [obj.name.split(".")[0]] + ["{0:.8f}".format(f) for f in obj_data]
        obj_list.append(obj_tex)

with open(file_path, "w") as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerows(obj_list)

いかがでしょう。

0 件のコメント:

コメントを投稿