285 lines
12 KiB
C#
285 lines
12 KiB
C#
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<Vector3> vertices;
|
|
public List<int> 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<int> 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<Vector3>();
|
|
objData.triangles = new List<int>();
|
|
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<Vector3> vertices = new List<Vector3>();
|
|
List<int> triangles = new List<int>();
|
|
|
|
//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<MeshFilter>();
|
|
//create a renderer object to will render the data of the meshfilter
|
|
MeshRenderer meshRenderer = spawnedObject.AddComponent<MeshRenderer>();
|
|
|
|
//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<MeshCollider>();
|
|
coll.sharedMesh = mesh;
|
|
coll.convex = true;
|
|
|
|
//add a rigidbody to the object so that raycast etc. can collide with it
|
|
Rigidbody rigid = spawnedObject.AddComponent<Rigidbody>();
|
|
rigid.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
|
rigid.useGravity = false;
|
|
rigid.isKinematic = true;
|
|
|
|
//add scripts to make the object draggable later
|
|
XRGrabInteractable interactableGrab = spawnedObject.AddComponent<XRGrabInteractable>();
|
|
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<ObjectGrabListener>();
|
|
|
|
//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<BlockRotAndMove>();
|
|
OBJScaler oBJScaler = spawnedObject.AddComponent<OBJScaler>();
|
|
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<InputDevice> mDevice = new List<InputDevice>();
|
|
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;
|
|
}
|
|
}
|