from github

This commit is contained in:
jonas
2024-10-29 17:16:17 +01:00
commit 537f1d77fb
1074 changed files with 303521 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
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;
}
}