2019年11月24日日曜日

Blenderのオブジェクト配置をUnityに(JSON)

Unityのアセットの配置をCSVで読み書きで単純な位置情報をCSVでやりとりしましたが
拡張性も足りなく、階層構造もないため実運用では少し物足りない感じになりました
そのまま適当に拡張してもよかったのですが、
改めて機能を整理したものを作成したので記事として残しておきます。
対応したかったものとして

  1. 階層構造でオブジェクトの管理をしやすく
  2. カメラやライトの設定も対応できるよう拡張性を持たせる
  3. (Unity側)アセットもフォルダで階層化した中身から取得したい

といった機能がありました
1 2のため、Unityの読み込み機能を使ったJSONで行うことにしました。
まずとりあえす Blender側のPyhtonです
前回と同じくUnity側にはBlenderで配置たオブジェクトと同名のアセットがあるのを前提にしています。
import bpy
from mathutils import Vector, Euler, Matrix, Quaternion
#''の間にCSVを書き出すパスを記述

file_path = r'C:\Users\user\Documents\test\AssetTest\Assets\AssetTest3.txt'

#座標軸を変換するためのマトリクス
convert_matrix = Matrix([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])

class objectInfo:
    def __init__(self):
        self.name = ""
        self.id = 0
        self.parent = 0
        self.type = ""
        self.position = {"x":0,"y":0,"z":0}
        self.rotation = {"x":0,"y":0,"z":0,"w":1}
        self.scale = {"x":1,"y":1,"z":1}
        self.datastring = ""
        self.bound_min = ""
        self.bound_max = ""
        self.childlist = []
    def __str__(self):
        return str(self.__dict__)
    def set_Info_name(self, obj_name):
        Info.name = obj_name.split(".")[0]
    def set_Info_mat(self, matrix_world, matrix_local):
        #座標軸の回転
        mat_w = convert_matrix @ matrix_world @ convert_matrix
        pos = ["{0:.6f}".format(f) for f in mat_w.translation]
        rot = ["{0:.6f}".format(f) for f in mat_w.to_quaternion()]
        siz = ["{0:.6f}".format(f) for f in matrix_local.to_scale()]
        self.position = {"x":pos[0],"y":pos[1],"z":pos[2]}
        self.rotation = {"w":rot[0],"x":rot[1],"y":rot[2],"z":rot[3]}
        self.scale = {"x":siz[0],"y":siz[2],"z":siz[1]}
    def to_json(self):
        #Unityに読ませるためにシングルクォーテーションをダブルクオーテーションに
        dict = str(self.__dict__)
        return (dict.replace("'",'"')) #"

#親になるオブジェクト
root = objectInfo()
#シーン名を親オブジェクト名として使用
root.name = bpy.context.scene.name
info_list = [root.to_json()]
#シーン内にあるオブジェクトを取得
objects = list(bpy.context.scene.objects)
for obj in objects:
    if obj.type == 'MESH':
        Info = objectInfo()
        Info.type = "MESH"
    elif obj.type == 'EMPTY':
        Info = objectInfo()
        Info.type = ""
    else : continue
    Info.set_Info_name(obj.name)
    #オブジェクトのidを取得
    Info.id = objects.index(obj) +1
    #親のidを取得
    if obj.parent == None:
        Info.parent = 0
    else:
        Info.parent = objects.index(obj.parent) +1
    #オブジェクトの情報から移動回転拡縮の情報を取得
    Info.set_Info_mat(obj.matrix_world, obj.matrix_local)
    info_list.append( Info.to_json() )
#書き出し
with open(file_path, "w") as f:
    f.write('\n'.join(info_list))
スクリプトの先頭に書かれたファイルパスにテキストファイルが書き出されます

続いてUnity側の読み込みスクリプト
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
public class CreatePrefabsJSON : MonoBehaviour
{
    // 読み込むファイルのAssetsより下のパス
    public string filePath = "/AssetTest3.txt";
    //plefabDirectryの末尾の/は不要
    public string plefabDirectry = "/Resources";
    void Start()
    {
        //Application.dataPathはプロジェクトデータのAssetフォルダまでのパス
        string path = Application.dataPath + filePath;
        objectInfo[] InfoList = looadJSON(path);
        //情報を元にオブジェクトの配置
        GameObject obj =  createPrefab(InfoList);
    }

    public GameObject createPrefab(objectInfo[] InfoList)
    {
        //親になるオブジェクトの作成
        string objName = InfoList[0].name;
        GameObject rootObj = new GameObject(objName);
        Dictionary objDic = LoadAssetPath(plefabDirectry);

        GameObject[] objects = new GameObject[InfoList.Length];
        objects[0] = rootObj;
        //テキスト情報を元にオブジェクトを配置
        for (int i = 1; i < InfoList.Length; i++)
        {
            objectInfo Info = InfoList[i];
            //オブジェクトのタイプがMESHになっているもののprefabを検索
            if (Info.type == "MESH" && objDic.ContainsKey(Info.name))
            {
                GameObject obj = (GameObject)PrefabUtility.InstantiatePrefab(objDic[Info.name]);
                obj.transform.localScale = Info.scale;
                obj.transform.position = Info.position;
                obj.transform.rotation = Info.rotation;
                objects[Info.id] = obj;
            }
            else
            {
                GameObject obj = new GameObject(Info.name);
                obj.transform.localScale = Info.scale;
                obj.transform.position = Info.position;
                obj.transform.rotation = Info.rotation;
                objects[Info.id] = obj;
            }   
        }
        //親子関係の設定
        for (int i = 1; i < InfoList.Length; i++)
        {
            GameObject obj = objects[i];
            GameObject parent = objects[InfoList[i].parent];
            obj.transform.parent = parent.transform;
            //Debug.Log(parent.name + ":"+ obj.name) ;
        }
        return (rootObj);
    }
    objectInfo[] looadJSON(string path)
    {
        List InfoList = new List();
        using (StreamReader reader = new StreamReader(path))
        {
            while (reader.Peek() >= 0)
            {
                objectInfo line = JsonUtility.FromJson(reader.ReadLine()); // 一行ずつ変換
                InfoList.Add(line);
            }
            reader.Close();
            return InfoList.ToArray();
        }
    }
    public Dictionary LoadAssetPath(string serch_path = "/Resources")
    {
        //指定パス以下のGameObjectを取得してオブジェクト名の辞書で返す
        //エディッタのみで使用可能 (using UnityEditor;)
        //パスの記述をそろえるため
        serch_path = "Assets" + serch_path;
        //オブジェクト情報を収める辞書
        var objDic = new Dictionary();
        //条件に合うアセットの検索
        string[] guid_list = AssetDatabase.FindAssets("t:GameObject", new string[] { serch_path });
        for (int i = 0; i < guid_list.Length; i++)
        {
            //GUIDからパスの取得
            string assetPath = AssetDatabase.GUIDToAssetPath(guid_list[i]);
            GameObject obj = AssetDatabase.LoadAssetAtPath(assetPath);
            objDic.Add(obj.name, obj);
        }
        return objDic;
    }

    [System.Serializable]
    public struct objectInfo
    {
        public string name;
        public int id;
        public int parent;
        public string type;
        public Vector3 position;
        public Quaternion rotation;
        public Vector3 scale;
        public String datastring;
        public Vector3 bound_min;
        public Vector3 bound_max;
        public int[] childlist;
    }
}

書き出したテキストファイルをプロジェクトのAssetsフォルダ以下に置き
適当なシーンでスクリプトをGameObjectにアサインして
インスペクタで読み込むファイルのパスを指定してPlayモードに入るとシーンにオブジェクトが配置されるかと思います

インスペクタ上で指定するファイルパスはプロジェクトのAssetsから下のパスになります
冒頭に書いたように アセットは指定したフォルダの下のフォルダ内のものも全て名前で取得するようにしました
(デフォルトではResourcesフォルダの下のデータを取得するようにしています)
記事中のものでは位置情報のみですが他の情報を読み込む拡張ができるようにしています
最後にテストに使ったUnity側からPrefabのデータをJSONで書き出すスクリプトを置いておきます
using UnityEngine;
using UnityEditor;
using System;
using System.IO;

public class ActiveInfoJSON : MonoBehaviour
{
    public string filePath = "/saveJS.txt";
    // Start is called before the first frame update
    void Start()
    {
        //inspectorの表示されているオブジェクト1つを取得
        GameObject activeObj = (GameObject)Selection.activeObject;
        int object_id = 0;
        //取得したオブジェクト情報を収めるためのリスト
        objectInfo[] InfoList = new objectInfo[1];
        //ルートのオブジェクトの情報を取得
        var rootInfo = getInfo(activeObj);
        rootInfo.id = object_id;
        //子の情報を再帰的に取得
        int[] child_list = getChild(activeObj, ref InfoList,ref object_id);
        rootInfo.childlist = child_list;
        InfoList[0] = rootInfo;

        //Application.dataPathはプロジェクトデータのAssetフォルダまでのパス
        string path = Application.dataPath + filePath;
        saveJSON(path, InfoList);
    }

    objectInfo getInfo(GameObject obj)
    {
        var Info = new objectInfo();
        GameObject original = PrefabUtility.GetCorrespondingObjectFromOriginalSource(obj);
        if (original == null)
        { Info.name = obj.name; }
        else
        { Info.name = original.name; }
        Info.position = obj.transform.position;
        Info.rotation = obj.transform.rotation;
        Info.scale = obj.transform.localScale;
        //メッシュを持つ場合bboxを取得
        var objMesh = obj.GetComponent();
        if (objMesh != null )
        {
            Info.type = "MESH";
            Bounds bounds = objMesh.sharedMesh.bounds;
            Info.bound_max = bounds.max;
            Info.bound_min = bounds.min;
        }else
        {
            var light = obj.GetComponent();
            var camera = obj.GetComponent();
            if(light != null)
            {
                Info.type = "LIGHT";
                Info.datastring = string.Format("type:{0}", light.type );
                //Debug.Log(light.type);
                //Debug.Log(light.color);
                //Debug.Log(light.range);//ポイントライトの範囲
            }
            else if(camera != null)
            {
                Info.type = "CAMERA";
                //インスペクタでは表示されないもののUntyではFOVとアスペクトでカメラを設定
                Info.datastring = string.Format("FOV:{0}",camera.fieldOfView);
                //Debug.Log(camera.fieldOfView);
                //Debug.Log(camera.aspect);
            }  
        }
        return (Info);
    }
    int[] getChild(GameObject parent,ref objectInfo[] InfoList,ref int object_id)
    {
        int parent_id = object_id;
        //オブジェクトの子の数を取得
        int childCount = parent.transform.childCount;
        //オブジェクトの子のidを収めるリスト
        int[] child_list = new int[childCount];
        int offset = InfoList.Length;
        Array.Resize(ref InfoList, offset +childCount);
        for (int i = 0; i < childCount; i++)
        {
            object_id++;
            GameObject childObj = parent.transform.GetChild(i).gameObject;
            var childInfo = getInfo(childObj);
            childInfo.id = object_id;
            childInfo.parent = parent_id;
            child_list[i] = offset + i; 
            //再帰的に子の情報を取得
            int[] grandchild_list = getChild(childObj, ref InfoList, ref object_id);
            childInfo.childlist = grandchild_list;
            InfoList[offset + i] = childInfo;
        }
        return (child_list);
    }

    bool saveJSON(string path, objectInfo[] InfoList)
    {
        try
        {
            using (StreamWriter writer = new StreamWriter(path, false))
            {

                foreach (objectInfo Info in InfoList)
                {
                    writer.WriteLine(JsonUtility.ToJson(Info));
                }
                writer.Flush();
                writer.Close();
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            return false;
        }
        return true;
    }
    [System.Serializable]
    public struct objectInfo
    {
        public string name;
        public int id;
        public int parent;
        public string type;
        public Vector3 position;
        public Quaternion rotation;
        public Vector3 scale;
        public String datastring;
        public Vector3 bound_min;
        public Vector3 bound_max;
        public int[] childlist;
    }
}
個人的な覚え書き程度ですが、何かのお役に立てば幸いです

0 件のコメント:

コメントを投稿