using System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; using System; using System.Globalization; using SimpleFileBrowser; using UnityEngine.XR; using UnityEngine.XR.Interaction.Toolkit; using System.Reflection; using UnityEngine.Android; using Unity.Netcode; using System.Linq; using Unity.VisualScripting; //this script provides functions to load obj files, build 3D-Meshes from vertices and triangles etc. //all objects do only exist on the local client, not in the network!!! //network behaviour is "simulated" by other script like ObjectSync //BECAUSE: netcode for gameobjects needs each network object as prefab... //!!!!-----> these objects can not be made prefabs before runtime because they are loaded at runtime XD public class OBJLoader : NetworkBehaviour { //reference to the current spawned gameobjet private GameObject spawnedObject; //culture info (for float parse) private NumberFormatInfo ci = CultureInfo.InvariantCulture.NumberFormat; [SerializeField] private Material defaultMat; [SerializeField] private ObjectSync objectSync; //data struct to store object data in one place public struct OBJData { public string objectName; public List vertices; public List triangles; } //convert a vector string to a vector3 object (originally made for obj fileformat, might be extended) private Vector3 ObjVector3StringToVector3(string vecString) { string[] lnSplit = vecString.Trim().Split(" "); return new Vector3(float.Parse(lnSplit[1], ci), float.Parse(lnSplit[2], ci), float.Parse(lnSplit[3], ci)); } //this function is a litte tricky //each face for rendering must be a tringle shape (3 vertices) //sometimes (because of some 3D software like blender) faces have more than 3 vertices //this functions cuts each complex shape (vertices count > 3) into nice and clean triangles private int[] GetIndexScheme(string[] lnSplit) { //create an index scheme (triangles) to spawn new vertices later int faceTrianglesCount = (lnSplit.Length - 1) - 2; int[] indexScheme = new int[((lnSplit.Length - 1) - 2)*3]; int arrIdx = 0; for (int i = 0; i < faceTrianglesCount; i++) { indexScheme[arrIdx++] = 0; indexScheme[arrIdx++] = i + 1; indexScheme[arrIdx++] = i + 2; } return indexScheme; } //this function can parse a triangle define string from obj file format und adds it to the triangle index list private void AddTrianglesLineToList(string line, ref List ls) { string[] lnSplit = line.Trim().Split(" "); int[] indexScheme = GetIndexScheme(lnSplit); foreach (int i in indexScheme) { ls.Add(int.Parse(lnSplit[i + 1].Trim().Split("/")[0]) - 1); } } //this function generates a perfect triangulated mesh data as output public OBJData buildObjData(Vector3[] vertices, int[] triangles) { OBJData objData = new OBJData(); objData.vertices = new List(); objData.triangles = new List(); Vector3[] objVertices = vertices.ToArray(); //vertices have been ordered to fit triangle index so the triangle index must just count up here (fixes normal calculation problem) int triangleIndex = 0; foreach (int i in triangles) { objData.vertices.Add(objVertices[i]); objData.triangles.Add(triangleIndex++); } return objData; } //this function reads the vertices and triangles from an obj file private void ReadObjDataFromFile(string path) { //handles SAF stuff on android (works on windows too of course) string[] lines = FileBrowserHelpers.ReadTextFromFile(path).Split("\n"); //try to get permission to access file on android Permission.RequestUserPermission(Permission.ExternalStorageRead); Permission.RequestUserPermission(Permission.ExternalStorageWrite); //list to temp store obj files vertex and triangle data List vertices = new List(); List triangles = new List(); //cut the obj file into pieces //string line = ""; foreach(string line in lines) { if (!(line.StartsWith("vt") || line.StartsWith("s") || line.StartsWith("use") || line.StartsWith("vn"))) { if (line.StartsWith("f")) { AddTrianglesLineToList(line, ref triangles); } else if (line.StartsWith("v")) { vertices.Add(ObjVector3StringToVector3(line)); } } } //calculate position to spawn object in front of the player GameObject mainCameraObj = Camera.main.gameObject; Vector3 positionVector = mainCameraObj.transform.position + mainCameraObj.transform.forward; positionVector.y = 1.5f; //to actually spawn the object ask the server to spawn it (of course spawn it on all other clients too) SpawnObjServerRPC(vertices.ToArray(), triangles.ToArray(), positionVector, new Vector3(1, 1, 1)); } //this function creates a unity mesh from objdata struct private Mesh CreateMeshFromObjData(OBJData objData) { Mesh mesh = new Mesh(); //set the vertices and triangles of the unity mesh mesh.vertices = objData.vertices.ToArray(); mesh.triangles = objData.triangles.ToArray(); //recalucate the normals of the mesh to provide correct lighting mesh.RecalculateNormals(); //for some reason inverted normals sometimes generate weird effects, so invert them again --> works XD for (int i = 0; i < mesh.normals.Length; i++) { mesh.normals[i] = -mesh.normals[i]; } return mesh; } //this function generates the final gameobject (should be only accessible by servers clientrpc call!!!) public GameObject CreateMesh(OBJData objData, Vector3 positionVector) { //call the function above to get a unity mesh object Mesh mesh = CreateMeshFromObjData(objData); //crate a new gameobject in the scene spawnedObject = new GameObject(objData.objectName); //can maybe set a parent of the spawned object (creates issues in position sync, so just put in the blank scene) //spawnedObject.transform.parent = gameObject.transform; //spawnedObject.transform.localPosition = Vector3.zero; //set the requested position to the gameobject spawnedObject.transform.position = positionVector; //create a meshfilter that will store the mesh object / data and material info MeshFilter meshFilter = spawnedObject.AddComponent(); //create a renderer object to will render the data of the meshfilter MeshRenderer meshRenderer = spawnedObject.AddComponent(); //set the meshfilters mesh to the generated mesh meshFilter.sharedMesh = mesh; //set the meshfilters material to a a default white material passed by serializefield meshRenderer.material = defaultMat; //add a collider to the object that uses the generated mesh as collission shape MeshCollider coll = spawnedObject.AddComponent(); coll.sharedMesh = mesh; coll.convex = true; //add a rigidbody to the object so that raycast etc. can collide with it Rigidbody rigid = spawnedObject.AddComponent(); rigid.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; rigid.useGravity = false; rigid.isKinematic = true; //add scripts to make the object draggable later XRGrabInteractable interactableGrab = spawnedObject.AddComponent(); InteractionLayerMask interactLayerMask = InteractionLayerMask.GetMask("Interactable"); interactableGrab.interactionLayers = interactLayerMask; interactableGrab.movementType = XRGrabInteractable.MovementType.VelocityTracking; //add object grab listener to the object (for the scale etc.) ObjectGrabListener objectGrabListener = spawnedObject.AddComponent(); //add the object to the list of spawned objects to keep track of it (for other scripts) objectSync.spawnedObjects.Add(spawnedObject); //get the index of the spawed objects (important for network syncs) objectGrabListener.index = objectSync.spawnedObjects.IndexOf(spawnedObject); BlockRotAndMove rotMoveBlocker = spawnedObject.AddComponent(); OBJScaler oBJScaler = spawnedObject.AddComponent(); return spawnedObject; } //will be called from the server to create the object on the local client //keep in mind that this function will only run on the client and does not have to any server ressources !!! //even if the variables are availabe --> the will be empty !!!!!!!! [ClientRpc] private void SpawnObjClientRPC(Vector3[] vertices, int[] triangles, Vector3 positionVector) { CreateMesh(buildObjData(vertices, triangles), positionVector); } //if you are the one that has loaded a object from a obj file, this function can be called to spawn the object //on your own and all others clients //all raw object data like vertices and triangles MUST be passed! //keep in mind that this function will only run on the server and will not have any access to clients ressources [ServerRpc(RequireOwnership = false)] private void SpawnObjServerRPC(Vector3[] vertices, int[] triangles, Vector3 positionVector, Vector3 scaleVector) { ObjectSync.OBJData backupObjData = new ObjectSync.OBJData(); backupObjData.vertices = vertices; backupObjData.triangles = triangles; backupObjData.positionVector = positionVector; backupObjData.rotation = new Quaternion(0, 0, 0, 0); backupObjData.scale = new Vector3(1, 1, 1); objectSync.objDataList.Add(backupObjData); //tell each client to spawn the object SpawnObjClientRPC(vertices, triangles, positionVector); } //this function opens the simple file browser public void OpenObjLoadingDialog() { //only allow opening obj files SimpleFileBrowser.FileBrowser.SetFilters(false, new FileBrowser.Filter("OBJ", ".obj")); //attach loader function to the "open" button of the file browser //place the filebrowser in the scene correctly SimpleFileBrowser.FileBrowser.ShowLoadDialog((path) => { ReadObjDataFromFile(path[0]); }, () => { }, FileBrowser.PickMode.Files, allowMultiSelection: false); GameObject fileExplorerObject = GameObject.Find("SimpleFileBrowserCanvas(Clone)"); GameObject mainCameraObj = Camera.main.gameObject; fileExplorerObject.transform.rotation = mainCameraObj.transform.rotation; Vector3 localRot = Quaternion.ToEulerAngles(fileExplorerObject.transform.localRotation); localRot.z = 0; localRot.x = 0; fileExplorerObject.transform.localRotation = Quaternion.EulerAngles(localRot); Vector3 positionVector = mainCameraObj.transform.position + mainCameraObj.transform.forward * 2 - fileExplorerObject.transform.right * 1.25f; positionVector.y = 0; fileExplorerObject.transform.position = positionVector; } private void Awake() { } private List mDevice = new List(); private bool lastDown = false; //depracted method to open the file browser (well still works, but is replaced by menu bars button) nice for test use private void Update() { mDevice.Clear(); InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Left, mDevice); bool primaryDown = false; if (mDevice.Count == 1) { InputDevice leftController = mDevice[0]; leftController.TryGetFeatureValue(CommonUsages.primaryButton, out primaryDown); } if (primaryDown && !lastDown || Input.GetKeyDown(KeyCode.P)) { OpenObjLoadingDialog(); } lastDown = primaryDown; } }