Source code for pyunity.loader

"""
Utility functions related to loading
and saving PyUnity meshes and scenes.

This will be imported as ``pyunity.Loader``.

"""

__all__ = ["ObjectInfo", "GetId", "Primitives", "GetImports", "SaveSceneToProject",
           "LoadProject", "SaveAllScenes", "LoadMesh", "SaveMesh", "SaveScene", "LoadObj", "SaveObj"]

from .meshes import Mesh
from .core import *
from .values import Material, Vector3, Quaternion, ImmutableStruct
from .scenes import SceneManager
from .files import Behaviour, Project, Scripts
from .render import Camera
from .audio import AudioSource, AudioListener, AudioClip
from .physics import AABBoxCollider, SphereCollider, Rigidbody  # , PhysicMaterial
from uuid import uuid4
import inspect
import json
import os
import shutil

[docs]def LoadObj(filename): """ Loads a .obj file to a PyUnity mesh. Parameters ---------- filename : str Name of file Returns ------- Mesh A mesh of the object file """ vertices = [] normals = [] faces = [] for line in open(filename, "r"): if line.startswith("#"): continue values = line.split() if not values: continue if values[0] == "v": v = Vector3(float(values[1]), float(values[3]), float(values[2])) vertices.append(v) elif values[0] == "f": face = [] for v in values[1:]: w = v.split("/") face.append(int(w[0]) - 1) face.reverse() faces.append(face) for face in faces: a = vertices[face[2]] - vertices[face[1]] b = vertices[face[0]] - vertices[face[1]] normal = a.cross(b).normalized() normals.append(normal) return Mesh(vertices, faces, normals)
[docs]def SaveObj(mesh, name, filePath=None): if filePath: directory = os.path.dirname(os.path.abspath(filePath)) else: directory = os.getcwd() os.makedirs(directory, exist_ok=True) with open(os.path.join(directory, name + ".obj"), "w+") as f: for vertex in mesh.verts: f.write(f"v {' '.join(map(str, round(vertex, 8)))}\n") for normal in mesh.normals: f.write(f"vn {' '.join(map(str, round(normal, 8)))}\n") for face in mesh.triangles: face = " ".join([f"{x + 1}//{x + 1}" for x in face]) f.write(f"f {face}\n")
[docs]def LoadMesh(filename): """ Loads a .mesh file generated by `SaveMesh`. It is optimized for faster loading. Parameters ---------- filename : str Name of file relative to the cwd Returns ------- Mesh Generated mesh """ with open(filename, "r") as f: lines = list(map(str.rstrip, f.read().rstrip().splitlines())) vertices = list(map(float, lines[0].split("/"))) vertices = [ Vector3(vertices[i], vertices[i + 1], vertices[i + 2]) for i in range(0, len(vertices), 3) ] faces = list(map(int, lines[1].split("/"))) faces = [ [faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3) ] normals = list(map(float, lines[2].split("/"))) normals = [ Vector3(normals[i], normals[i + 1], normals[i + 2]) for i in range(0, len(normals), 3) ] texcoords = list(map(float, lines[3].split("/"))) texcoords = [ [texcoords[i], texcoords[i + 1]] for i in range(0, len(texcoords), 2) ] return Mesh(vertices, faces, normals, texcoords)
[docs]def SaveMesh(mesh, name, filePath=None): """ Saves a mesh to a .mesh file for faster loading. Parameters ---------- mesh : Mesh Mesh to save name : str Name of the mesh filePath : str, optional Pass in `__file__` to save in directory of script, otherwise pass in the path of where you want to save the file. For example, if you want to save in C:\Downloads, then give "C:\Downloads\mesh.mesh". If not specified, then the mesh is saved in the cwd. """ if filePath: directory = os.path.dirname(os.path.abspath(filePath)) else: directory = os.getcwd() os.makedirs(directory, exist_ok=True) with open(os.path.join(directory, name + ".mesh"), "w+") as f: i = 0 for vertex in mesh.verts: i += 1 f.write(str(round(vertex.x, 8)) + "/") f.write(str(round(vertex.y, 8)) + "/") f.write(str(round(vertex.z, 8))) if i != len(mesh.verts): f.write("/") f.write("\n") i = 0 for triangle in mesh.triangles: i += 1 j = 0 for item in triangle: j += 1 f.write(str(item)) if i != len(mesh.triangles) or j != 3: f.write("/") f.write("\n") i = 0 for normal in mesh.normals: i += 1 f.write(str(round(normal.x, 8)) + "/") f.write(str(round(normal.y, 8)) + "/") f.write(str(round(normal.z, 8))) if i != len(mesh.normals): f.write("/") f.write("\n") i = 0 for texcoord in mesh.texcoords: i += 1 f.write(str(texcoord[0]) + "/") f.write(str(texcoord[1])) if i != len(mesh.texcoords): f.write("/") f.write("\n")
[docs]def GetImports(file): with open(file) as f: lines = f.read().rstrip().splitlines() imports = [] for line in lines: line = line.lstrip() if line.startswith("import") or (line.startswith("from") and " import " in line): imports.append(line) return "\n".join(imports) + "\n\n"
[docs]def SaveSceneToProject(scene, filePath=None, name=None): if filePath: directory = os.path.dirname(os.path.abspath(filePath)) else: directory = os.getcwd() if name is None: directory = os.path.join(directory, scene.name) else: directory = os.path.join(directory, name) os.makedirs(directory, exist_ok=True) project = Project(directory, scene.name) project.import_file(os.path.join("Scenes", scene.name + ".scene"), None) SaveScene(scene, project) return project
[docs]def SaveAllScenes(name, filePath=None): if filePath: directory = os.path.dirname(os.path.abspath(filePath)) else: directory = os.getcwd() directory = os.path.join(directory, name) os.makedirs(directory, exist_ok=True) project = Project(directory, name) for scene in SceneManager.scenesByIndex: SaveScene(scene, project) project.import_file(os.path.join( "Scenes", scene.name + ".scene"), None) project.write_project() return project
[docs]def GetId(ids, obj): id_ = id(obj) if id_ not in ids: ids[id_] = str(uuid4()) return ids[id_]
[docs]def SaveScene(scene, project): directory = project.path os.makedirs(os.path.join(directory, "Scenes"), exist_ok=True) f = open(os.path.join(directory, "Scenes", scene.name + ".scene"), "w+") f.write(f"Scene : {scene.id}\n" f" name: {json.dumps(scene.name)}\n") ids = scene.ids for gameObject in scene.gameObjects: f.write(f"GameObject : {GetId(ids, gameObject)}\n" f" name: {json.dumps(gameObject.name)}\n" f" tag: {gameObject.tag.tag}\n" f" transform: {GetId(ids, gameObject.transform)}\n") # 2nd pass (for components) for gameObject in scene.gameObjects: for component in gameObject.components: uuid = GetId(ids, component) if issubclass(type(component), Behaviour): name = type(component).__name__ + "(Behaviour)" file = os.path.join("Scripts", type( component).__name__ + ".py") path = os.path.join(directory, file) if file not in project.file_paths: os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w+") as f2: f2.write(GetImports(inspect.getfile(type(component)))) f2.write(inspect.getsource(type(component))) project.import_file(file, "Behaviour") else: name = type(component).__name__ + "(Component)" # lgtm [py/clear-text-storage-sensitive-data] f.write(f"{name} : {uuid}\n") f.write(f" gameObject: {ids[id(gameObject)]}\n") if isinstance(component, Behaviour): scriptId = project.file_paths[os.path.join( 'Scripts', type(component).__name__ + ".py")].uuid # lgtm [py/clear-text-storage-sensitive-data] f.write(f" _script: {scriptId}\n") for attr in component.saved: value = getattr(component, attr) if id(value) in ids: written = ids[id(value)] elif isinstance(value, Behaviour) and attr == "_script": continue elif isinstance(value, Mesh): if id(value) in ids: written = ids[id(value)] else: written = str(uuid4()) SaveMesh(value, gameObject.name, os.path.join( directory, "Meshes", gameObject.name + ".mesh")) project.import_file(os.path.join( "Meshes", gameObject.name + ".mesh"), "Mesh", written) ids[id(value)] = written elif isinstance(value, Material): if hasattr(component, "default"): written = "default" elif id(value) in ids: written = ids[id(value)] else: written = str(uuid4()) project.save_mat(value, gameObject.name) project.import_file(os.path.join( "Materials", gameObject.name + ".mat"), "Material", written) ids[id(value)] = written elif isinstance(value, AudioClip): if id(value) in ids: written = ids[id(value)] else: written = str(uuid4()) os.makedirs(os.path.join( directory, "Sounds"), exist_ok=True) shutil.copy(value.path, os.path.join(directory, "Sounds", os.path.basename(value.path))) project.import_file(os.path.join("Sounds", os.path.basename(value.path)), written) ids[id(value)] = written else: written = str(value) # lgtm [py/clear-text-storage-sensitive-data] f.write(f" {attr}: {written}\n") project.write_project()
[docs]class ObjectInfo: def __init__(self, uuid, type, attrs): self.uuid = uuid self.type = type self.attrs = attrs def __getattr__(self, attr): return self.attrs[attr]
components = { "Transform": Transform, "Camera": Camera, "Light": Light, "MeshRenderer": MeshRenderer, "AABBoxCollider": AABBoxCollider, "SphereCollider": SphereCollider, "Rigidbody": Rigidbody, "AudioSource": AudioSource, "AudioListener": AudioListener } """List of all components by name"""
[docs]def parse_string(string): if string.startswith("Vector3("): return True, Vector3(*list(map(float, string[8:-1].split(", ")))) if string.startswith("Quaternion("): return True, Quaternion(*list(map(float, string[11:-1].split(", ")))) if string in ["True", "False"]: return True, string == "True" if string == "None": return True, None if string.isdigit(): return True, int(string) try: return True, float(string) except (ValueError, OverflowError): pass try: return True, json.loads(string) except json.decoder.JSONDecodeError: pass if string.startswith("(") and string.endswith(")"): check, items = zip(*list(map(parse_string, string.split(", ")))) if all(check): return True, tuple(items) if string.startswith("[") and string.endswith("]"): check, items = zip(*list(map(parse_string, string[1:-1].split(", ")))) if all(check): return True, list(items) return False, None
[docs]def LoadProject(filePath): project = Project.from_folder(filePath) scenes = [value[1] for value in project.files.values() if value[0].type == "Scene"] for path in scenes: with open(os.path.join(project.path, path), "r") as f: lines = f.read().rstrip().splitlines() data = [] for line in lines: if not line.startswith(" "): data.append([line]) else: data[-1].append(line) infos = [] for info in data: type_, uuid = info[0].split(" : ") attrs = {attr: value for attr, value in map( lambda x: x[4:].split(": "), info[1:])} infos.append(ObjectInfo(uuid, type_, attrs)) gameObjectInfo = list(filter(lambda x: x.type == "GameObject", infos)) componentInfo = list(filter(lambda x: "(Component)" in x.type, infos)) behaviourInfo = list(filter(lambda x: "(Behaviour)" in x.type, infos)) scene_info = infos.pop(0) scene = SceneManager.AddBareScene(json.loads(scene_info.name)) scene.id = scene_info.uuid ids = {} gameObjects = [] for info in gameObjectInfo: gameObject = GameObject.BareObject(json.loads(info.name)) gameObjects.append(gameObject) gameObject.tag = Tag(int(info.tag)) ids[info.uuid] = gameObject for info in componentInfo: gameObject = ids[info.gameObject] del info.attrs["gameObject"] component = components[info.type[:-11]] component = gameObject.AddComponent(component) ids[info.uuid] = component for name, value in reversed(info.attrs.items()): if isinstance(component, MeshRenderer) and \ [name, value] == ["mat", "default"]: component.mat = MeshRenderer.DefaultMaterial continue check, obj = parse_string(value) if check: setattr(component, name, obj) elif value in ids: setattr(component, name, ids[value]) elif value in project.files: file = project.files[value][0] if file.type == "Material": obj = project.load_mat(file) elif file.type == "Mesh": obj = LoadMesh(os.path.join(project.path, file.path)) elif file.type == "Ogg": obj = AudioClip(os.path.join(project.path, file.path)) setattr(component, name, obj) script = Scripts.LoadScripts(os.path.join(filePath, "Scripts")) for info in behaviourInfo: gameObject = ids[info.gameObject] del info.attrs["gameObject"] behaviour = gameObject.AddComponent( getattr(script, info.type[:-11])) for name, value in reversed(info.attrs.items()): check, obj = parse_string(value) if check: setattr(behaviour, name, obj) elif value in ids: setattr(behaviour, name, ids[value]) elif value in project.files: file = project.files[value][0] if file.type == "Material": obj = project.load_mat(file) elif file.type == "Mesh": obj = LoadMesh(os.path.join(project.path, file.path)) elif file.type == "Ogg": obj = AudioClip(os.path.join(project.path, file.path)) setattr(behaviour, name, obj) for gameObject in gameObjects: scene.Add(gameObject) scene.mainCamera = scene.FindGameObjectsByName( "Main Camera")[0].GetComponent(Camera) scene.ids = ids return project
[docs]class Primitives(metaclass=ImmutableStruct): """ Primitive preloaded meshes. Do not instantiate this class. """ _names = ["cube", "quad", "double_quad", "sphere", "capsule", "cylinder"] __path = os.path.dirname(os.path.abspath(__file__)) cube = LoadMesh(os.path.join(__path, "primitives/cube.mesh")) quad = LoadMesh(os.path.join(__path, "primitives/quad.mesh")) double_quad = LoadMesh(os.path.join(__path, "primitives/double_quad.mesh")) sphere = LoadMesh(os.path.join(__path, "primitives/sphere.mesh")) capsule = LoadMesh(os.path.join(__path, "primitives/capsule.mesh")) cylinder = LoadMesh(os.path.join(__path, "primitives/cylinder.mesh"))