/// <summary> /// Loads the mesh with the given details level and converts from .ply format into Unity format. /// </summary> public static AsyncRequest <MeshData> LoadDetailedMeshDataFromDiskAsync(string avatarId, int detailsLevel) { var request = new AsyncRequest <MeshData>(AvatarSdkMgr.Str(Strings.LoadingFiles)); AvatarSdkMgr.SpawnCoroutine(LoadDetailedMeshDataFromDisk(avatarId, detailsLevel, request)); return(request); }
/// <summary> /// Read blendshapes from the avatar directory and add them to 3D head mesh. /// </summary> public static AsyncRequest <Mesh> AddBlendshapesAsync(string avatarId, Mesh mesh, int[] indexMap) { var request = new AsyncRequest <Mesh> (AvatarSdkMgr.Str(Strings.LoadingAnimations)); AvatarSdkMgr.SpawnCoroutine(AddBlendshapes(avatarId, mesh, indexMap, request)); return(request); }
/// <summary> /// Loads the avatar haircut files from disk into TexturedMesh object (parses .ply files too). /// </summary> /// <returns>Async request which gives complete haircut TexturedMesh object eventually.</returns> public static AsyncRequest <TexturedMesh> LoadHaircutFromDiskAsync(string avatarCode, string haircutId) { var request = new AsyncRequest <TexturedMesh> (AvatarSdkMgr.Str(Strings.LoadingHaircut)); AvatarSdkMgr.SpawnCoroutine(LoadHaircutFromDiskFunc(avatarCode, haircutId, request)); return(request); }
/// <summary> /// Loads the avatar head files from disk into TexturedMesh object (parses .ply file too). /// </summary> /// <param name="avatarCode">Avatar code</param> /// <param name="withBlendshapes">If True, blendshapes will be loaded and added to mesh.</param> /// <param name="detailsLevel">Indicates polygons count in mesh. 0 - highest resolution, 3 - lowest resolution.</param> public static AsyncRequest <TexturedMesh> LoadAvatarHeadFromDiskAsync(string avatarCode, bool withBlendshapes, int detailsLevel) { var request = new AsyncRequest <TexturedMesh> (AvatarSdkMgr.Str(Strings.LoadingAvatar)); AvatarSdkMgr.SpawnCoroutine(LoadAvatarHeadFromDisk(avatarCode, withBlendshapes, detailsLevel, request)); return(request); }
/// <summary> /// See LoadMeshDataFromDiskAsync. /// </summary> private static IEnumerator LoadMeshDataFromDisk(string avatarId, AsyncRequest <MeshData> request) { var meshBytesRequest = LoadAvatarFileAsync(avatarId, AvatarFile.MESH_PLY); yield return(request.AwaitSubrequest(meshBytesRequest, finalProgress: 0.5f)); if (request.IsError) { yield break; } var parsePlyTimer = new MeasureTime("Parse ply"); var parsePlyRequest = PlyToMeshDataAsync(meshBytesRequest.Result); yield return(request.AwaitSubrequest(parsePlyRequest, finalProgress: 1)); if (request.IsError) { yield break; } parsePlyTimer.Stop(); request.Result = parsePlyRequest.Result; request.IsDone = true; }
/// <summary> /// Helper method that automatically generates full path to file from file type and avatar id, and then calls /// SaveFileAsync. /// </summary> /// <param name="bytes">Binary file content.</param> /// <param name="code">Avatar code.</param> /// <param name="file">Avatar file type.</param> public static AsyncRequest <string> SaveAvatarFileAsync(byte[] bytes, string code, AvatarFile file) { try { var filename = AvatarSdkMgr.Storage().GetAvatarFilename(code, file); return(SaveFileAsync(bytes, filename)); } catch (Exception ex) { Debug.LogException(ex); var request = new AsyncRequest <string> (""); request.SetError(string.Format("Could not save {0}, reason: {1}", file, ex.Message)); return(request); } }
/// <summary> /// Loads the haircut file asynchronously. /// </summary> /// <param name="haircutId">Unique ID of a haircut.</param> /// <param name="file">File type (e.g. haircut texture).</param> public static AsyncRequest <byte[]> LoadHaircutFileAsync(string haircutId, HaircutFile file) { try { var filename = AvatarSdkMgr.Storage().GetHaircutFilename(haircutId, file); return(LoadFileAsync(filename)); } catch (Exception ex) { Debug.LogException(ex); var request = new AsyncRequest <byte[]> (); request.SetError(string.Format("Could not load {0}, reason: {1}", file, ex.Message)); return(request); } }
/// <summary> /// Loads the avatar haircut points file asynchronously. /// </summary> /// <param name="code">Avatar unique code.</param> /// <param name="haircutId">Unique ID of a haircut.</param> public static AsyncRequest <byte[]> LoadAvatarHaircutPointcloudFileAsync(string code, string haircutId) { try { var filename = AvatarSdkMgr.Storage().GetAvatarHaircutPointCloudFilename(code, haircutId); return(LoadFileAsync(filename)); } catch (Exception ex) { Debug.LogException(ex); var request = new AsyncRequest <byte[]> (); request.SetError(string.Format("Could not load haircut {0} point cloud, reason: {1}", haircutId, ex.Message)); return(request); } }
/// <summary> /// LoadAvatarHeadFromDiskAsync implementation. /// </summary> private static IEnumerator LoadAvatarHeadFromDisk( string avatarId, bool withBlendshapes, int detailsLevel, AsyncRequest <TexturedMesh> request ) { // loading two files simultaneously var meshDataRequest = LoadDetailedMeshDataFromDiskAsync(avatarId, detailsLevel); var textureBytesRequest = LoadAvatarFileAsync(avatarId, AvatarFile.TEXTURE); yield return(request.AwaitSubrequests(0.6f, meshDataRequest, textureBytesRequest)); if (request.IsError) { yield break; } MeshData meshData = meshDataRequest.Result; var parseTextureTimer = new MeasureTime("Parse texture data"); // at this point we have all data we need to generate a textured mesh var texturedMesh = new TexturedMesh { mesh = CreateMeshFromMeshData(meshData, "HeadMesh"), texture = new Texture2D(0, 0) }; // This actually blocks the main thread for a few frames, which is bad for VR. // To optimize: load jpg/png texture in C++ code in a separate thread and only SetPixels here in Unity. Should be faster. texturedMesh.texture.LoadImage(textureBytesRequest.Result); parseTextureTimer.Stop(); if (withBlendshapes) { // adding blendshapes... using (new MeasureTime("Add blendshapes")) { var addBlendshapesRequest = AddBlendshapesAsync(avatarId, texturedMesh.mesh, meshData.indexMap); yield return(request.AwaitSubrequest(addBlendshapesRequest, 1.0f)); if (addBlendshapesRequest.IsError) { Debug.LogError("Could not add blendshapes!"); } } } request.Result = texturedMesh; request.IsDone = true; }
/// <summary> /// Same as SaveAvatarFileAsync, but for haircut points, because they are unique for each avatar and should be stored in avatar folder. /// </summary> /// <param name="bytes">Binary file content.</param> /// <param name="code">Avatar unique code.</param> /// <param name="haircutId">Unique ID of a haircut.</param> public static AsyncRequest <string> SaveAvatarHaircutPointCloudZipFileAsync( byte[] bytes, string code, string haircutId ) { try { var filename = AvatarSdkMgr.Storage().GetAvatarHaircutPointCloudZipFilename(code, haircutId); return(SaveFileAsync(bytes, filename)); } catch (Exception ex) { Debug.LogException(ex); var request = new AsyncRequest <string> ("Saving file"); request.SetError(string.Format("Could not save point cloud zip, reason: {0}", ex.Message)); return(request); } }
/// <summary> /// LoadHaircutFromDiskAsync implementation. /// </summary> private static IEnumerator LoadHaircutFromDiskFunc( string avatarCode, string haircutId, AsyncRequest <TexturedMesh> request ) { var loadingTime = Time.realtimeSinceStartup; // start three async request in parallel var haircutTexture = LoadHaircutFileAsync(haircutId, HaircutFile.HAIRCUT_TEXTURE); var haircutMesh = LoadHaircutFileAsync(haircutId, HaircutFile.HAIRCUT_MESH_PLY); var haircutPoints = LoadAvatarHaircutPointcloudFileAsync(avatarCode, haircutId); // wait until mesh and points load yield return(request.AwaitSubrequests(0.4f, haircutMesh, haircutPoints)); if (request.IsError) { yield break; } // we can start another two subrequests, now parsing the ply files var parseHaircutPly = PlyToMeshDataAsync(haircutMesh.Result); var parseHaircutPoints = PlyToPointsAsync(haircutPoints.Result); // await everything else we need for the haircut yield return(request.AwaitSubrequests(0.95f, parseHaircutPly, parseHaircutPoints, haircutTexture)); if (request.IsError) { yield break; } // now we have all data we need to generate a textured mesh var haircutMeshData = ReplacePointCoords(parseHaircutPly.Result, parseHaircutPoints.Result); var texturedMesh = new TexturedMesh(); texturedMesh.mesh = CreateMeshFromMeshData(haircutMeshData, "HaircutMesh"); texturedMesh.texture = new Texture2D(0, 0); texturedMesh.texture.LoadImage(haircutTexture.Result); request.Result = texturedMesh; request.IsDone = true; Debug.LogFormat("Took {0} seconds to load a haircut", Time.realtimeSinceStartup - loadingTime); }
/// <summary> /// Unzips the file asynchronously. /// </summary> /// <param name="path">Absolute path to zip file.</param> /// <param name="location">Unzip location. If null, then files will be unzipped in the location of .zip file.</param> public static AsyncRequest <string> UnzipFileAsync(string path, string location = null) { if (string.IsNullOrEmpty(location)) { location = Path.GetDirectoryName(path); } AsyncRequest <string> request = null; Func <string> unzipFunc = () => { ZipUtils.Unzip(path, location); return(location); }; // unzip asynchronously in a separate thread request = new AsyncRequestThreaded <string> (() => unzipFunc(), AvatarSdkMgr.Str(Strings.UnzippingFile)); AvatarSdkMgr.SpawnCoroutine(request.Await()); return(request); }
/// <summary> /// Helper function that allows implementation of the "main" request as a sequence of async subrequests. /// Usage: yield return request.AwaitSubrequest(subrequest, finalProgress: 0.5f) /// </summary> /// <param name="subrequest">Subrequest - the async operation that is a part of "main" request.</param> /// <param name="finalProgress">Progress of main operation by the end of this sub-operation.</param> public IEnumerator AwaitSubrequests(float finalProgress, params AsyncRequest[] subrequests) { if (IsDone || IsError) { Debug.LogError("Cannot start subrequest for request that is already finished!"); yield break; } if (subrequests.Length == 0) { Debug.LogError("Cannot await on empty list of subrequests"); yield break; } if (finalProgress < Progress) { Debug.LogWarningFormat( "Cannot not rollback progress from {0} to {1}, progress can only move forward", Progress, finalProgress ); } float initialProgress = Progress; float progressShare = Math.Max(0, finalProgress - initialProgress); float progressSharePerSubrequest = progressShare / subrequests.Length; while (!IsDone) { float newProgress = initialProgress; int numUnfinished = subrequests.Length; bool currentSubrequestSet = false; foreach (var subrequest in subrequests) { if (subrequest.IsError) { SetError(string.Format("{0} failed, reason: {1}", State, subrequest.ErrorMessage)); Debug.LogWarning(ErrorMessage); break; } else { newProgress += progressSharePerSubrequest * subrequest.Progress; if (newProgress > 1.0001f) { Debug.LogWarningFormat("Progress for {0} is more than 1 ({1})!", State, newProgress); newProgress = 0.99f; } if (subrequest.IsDone) { --numUnfinished; } else if (!currentSubrequestSet) { CurrentSubrequest = subrequest; currentSubrequestSet = true; } } } if (!IsDone) { Progress = newProgress; } if (numUnfinished == 0) { break; } yield return(null); } }
/// <summary> /// Tiny wrapper around AwaitSubrequests for trivial case. /// </summary> public IEnumerator AwaitSubrequest(AsyncRequest request, float finalProgress) { return(AwaitSubrequests(finalProgress, request)); }
/// <summary> /// Read blendshapes from the avatar directory and add them to 3D head mesh. /// </summary> private static IEnumerator AddBlendshapes(string avatarId, Mesh mesh, int[] indexMap, AsyncRequest <Mesh> request) { var blendshapesDirs = AvatarSdkMgr.Storage().GetAvatarBlendshapesDirs(avatarId); var loadBlendshapesRequest = new AsyncRequestThreaded <Dictionary <string, Vector3[]> > ((r) => { var timer = new MeasureTime("Read all blendshapes"); var blendshapes = new Dictionary <string, Vector3[]> (); List <string> blendshapeFiles = new List <string>(); foreach (string dir in blendshapesDirs) { blendshapeFiles.AddRange(Directory.GetFiles(dir)); } var blendshapeReader = new BlendshapeReader(indexMap); for (int i = 0; i < blendshapeFiles.Count; ++i) { var blendshapePath = blendshapeFiles [i]; var filename = Path.GetFileName(blendshapePath); // crude parsing of filenames if (!filename.EndsWith(".bin")) { continue; } var tokens = filename.Split(new [] { ".bin" }, StringSplitOptions.None); if (tokens.Length != 2) { continue; } var blendshapeName = tokens [0]; blendshapes [blendshapeName] = blendshapeReader.ReadVerticesDeltas(blendshapePath); r.Progress = (float)i / blendshapeFiles.Count; } timer.Stop(); return(blendshapes); }, AvatarSdkMgr.Str(Strings.ParsingBlendshapes)); yield return(request.AwaitSubrequest(loadBlendshapesRequest, finalProgress: 0.9f)); if (request.IsError) { yield break; } var addBlendshapesTimer = DateTime.Now; float targetFps = 30.0f; int numBlendshapes = 0, loadedSinceLastPause = 0; var blendshapesDict = loadBlendshapesRequest.Result; foreach (var blendshape in blendshapesDict) { mesh.AddBlendShapeFrame(blendshape.Key, 100.0f, blendshape.Value, null, null); ++numBlendshapes; ++loadedSinceLastPause; if ((DateTime.Now - addBlendshapesTimer).TotalMilliseconds > 1000.0f / targetFps && loadedSinceLastPause >= 5) { // Debug.LogFormat ("Pause after {0} blendshapes to avoid blocking the main thread", numBlendshapes); yield return(null); addBlendshapesTimer = DateTime.Now; loadedSinceLastPause = 0; } } request.Result = mesh; request.IsDone = true; }
/// <summary> /// Loads a mesh with the given level of details. /// It takes faces and UV-coordinates from the template model, points coordinates from the avatar's model and merges them into a single model. /// </summary> private static IEnumerator LoadDetailedMeshDataFromDisk(string avatarId, int detailsLevel, AsyncRequest <MeshData> request) { if (detailsLevel < 0) { Debug.LogWarningFormat("Invalid details level parameter: {0}. Will be used value 0 (highest resolution).", detailsLevel); detailsLevel = 0; } if (detailsLevel > 4) { Debug.LogWarningFormat("Invalid details level parameter: {0}. Will be used value 3 (lowest resolution).", detailsLevel); detailsLevel = 4; } if (detailsLevel == 0) { yield return(LoadMeshDataFromDisk(avatarId, request)); } else { var meshBytesRequest = LoadAvatarFileAsync(avatarId, AvatarFile.MESH_PLY); yield return(request.AwaitSubrequest(meshBytesRequest, finalProgress: 0.3f)); if (request.IsError) { yield break; } string headTemplateFileName = string.Format("template_heads/head_lod_{0}", detailsLevel); var headTemplateRequest = Resources.LoadAsync(headTemplateFileName); yield return(headTemplateRequest); TextAsset templateHeadAsset = headTemplateRequest.asset as TextAsset; if (templateHeadAsset == null) { Debug.LogError("Unable to load template head!"); yield break; } var meshRequest = PlyToMeshDataAsync(templateHeadAsset.bytes); var pointsRequest = PlyToPointsAsync(meshBytesRequest.Result); yield return(request.AwaitSubrequests(0.95f, meshRequest, pointsRequest)); request.Result = ReplacePointCoords(meshRequest.Result, pointsRequest.Result); request.IsDone = true; } }