| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.IO; |
| | 4 | | using System.Linq; |
| | 5 | | using System.Net.Http; |
| | 6 | | using System.Text.RegularExpressions; |
| | 7 | | using Unity.EditorCoroutines.Editor; |
| | 8 | | using UnityEditor; |
| | 9 | | using UnityEngine; |
| | 10 | | using UnityEngine.Networking; |
| | 11 | | using UnityGLTF; |
| | 12 | | using UnityGLTF.Cache; |
| | 13 | | using GLTF; |
| | 14 | | using GLTF.Schema; |
| | 15 | |
|
| | 16 | | namespace DCL.ABConverter |
| | 17 | | { |
| | 18 | | public class Core |
| | 19 | | { |
| | 20 | | public enum ErrorCodes |
| | 21 | | { |
| | 22 | | SUCCESS, |
| | 23 | | UNDEFINED, |
| | 24 | | SCENE_LIST_NULL, |
| | 25 | | ASSET_BUNDLE_BUILD_FAIL, |
| | 26 | | SOME_ASSET_BUNDLES_SKIPPED |
| | 27 | | } |
| | 28 | |
|
| | 29 | | public class State |
| | 30 | | { |
| | 31 | | public enum Step |
| | 32 | | { |
| | 33 | | IDLE, |
| | 34 | | DUMPING_ASSETS, |
| | 35 | | BUILDING_ASSET_BUNDLES, |
| | 36 | | FINISHED, |
| | 37 | | } |
| | 38 | |
|
| 0 | 39 | | public Step step { get; internal set; } |
| 0 | 40 | | public ErrorCodes lastErrorCode { get; internal set; } |
| | 41 | | } |
| | 42 | |
|
| 0 | 43 | | public readonly State state = new State(); |
| | 44 | |
|
| | 45 | | private const string MAIN_SHADER_AB_NAME = "MainShader_Delete_Me"; |
| | 46 | | private const float MAX_TEXTURE_SIZE = 512f; |
| | 47 | |
|
| | 48 | | internal readonly string finalDownloadedPath; |
| | 49 | | internal readonly string finalDownloadedAssetDbPath; |
| 0 | 50 | | public Dictionary<string, string> hashLowercaseToHashProper = new Dictionary<string, string>(); |
| 0 | 51 | | internal bool generateAssetBundles = true; |
| | 52 | |
|
| | 53 | | public Client.Settings settings; |
| | 54 | |
|
| | 55 | | private float startTime; |
| | 56 | | private int totalAssets; |
| | 57 | | private int skippedAssets; |
| | 58 | |
|
| | 59 | | private Environment env; |
| 0 | 60 | | private static Logger log = new Logger("ABConverter.Core"); |
| | 61 | | private string logBuffer; |
| | 62 | |
|
| 0 | 63 | | public Core(Environment env, Client.Settings settings = null) |
| | 64 | | { |
| 0 | 65 | | this.env = env; |
| | 66 | |
|
| 0 | 67 | | this.settings = settings?.Clone() ?? new Client.Settings(); |
| | 68 | |
|
| 0 | 69 | | finalDownloadedPath = PathUtils.FixDirectorySeparator(Config.DOWNLOADED_PATH_ROOT + Config.DASH); |
| 0 | 70 | | finalDownloadedAssetDbPath = PathUtils.FixDirectorySeparator(Config.ASSET_BUNDLES_PATH_ROOT + Config.DASH); |
| | 71 | |
|
| 0 | 72 | | if (Utils.ParseOption(Config.CLI_SET_CUSTOM_OUTPUT_ROOT_PATH, 1, out string[] outputPath)) |
| | 73 | | { |
| 0 | 74 | | finalDownloadedAssetDbPath = System.IO.Path.Combine(Directory.GetCurrentDirectory(), outputPath[0] + "/" |
| | 75 | |
|
| 0 | 76 | | Debug.Log($"ABConverter Core: -output PATH param found, setting final ABPath as '{finalDownloadedAssetDb |
| 0 | 77 | | } |
| | 78 | | else |
| | 79 | | { |
| 0 | 80 | | Debug.Log($"ABConverter Core: -output PATH param NOT found, setting final ABPath as '{finalDownloadedAss |
| | 81 | | } |
| | 82 | |
|
| 0 | 83 | | settings.finalAssetBundlePath = finalDownloadedAssetDbPath; |
| | 84 | |
|
| 0 | 85 | | log.verboseEnabled = this.settings.verbose; |
| | 86 | |
|
| 0 | 87 | | state.step = State.Step.IDLE; |
| 0 | 88 | | } |
| | 89 | |
|
| | 90 | | /// <summary> |
| | 91 | | /// Generate asset bundles using a MappingPair list. |
| | 92 | | /// |
| | 93 | | /// This method will try to dump GLTF models, textures and buffers from the given mapping pair list, |
| | 94 | | /// tag them for asset bundle building and finally build them. |
| | 95 | | /// |
| | 96 | | /// If the GLTF have external references of any texture/buffer of the same list, the references will be |
| | 97 | | /// resolved correctly on the GLTF importer and then correctly converted to Asset Bundles references. |
| | 98 | | /// |
| | 99 | | /// Shader assets will be stripped from the generated bundles. |
| | 100 | | /// </summary> |
| | 101 | | /// <param name="rawContents">A list detailing assets to be dumped</param> |
| | 102 | | /// <param name="OnFinish">End callback with the proper ErrorCode</param> |
| | 103 | | public void Convert(ContentServerUtils.MappingPair[] rawContents, Action<ErrorCodes> OnFinish = null) |
| | 104 | | { |
| 0 | 105 | | OnFinish -= CleanAndExit; |
| 0 | 106 | | OnFinish += CleanAndExit; |
| | 107 | |
|
| 0 | 108 | | startTime = Time.realtimeSinceStartup; |
| | 109 | |
|
| 0 | 110 | | log.Info($"Conversion start... free space in disk: {PathUtils.GetFreeSpace()}"); |
| | 111 | |
|
| 0 | 112 | | InitializeDirectoryPaths(settings.clearDirectoriesOnStart); |
| 0 | 113 | | PopulateLowercaseMappings(rawContents); |
| | 114 | |
|
| 0 | 115 | | float timer = Time.realtimeSinceStartup; |
| 0 | 116 | | bool shouldGenerateAssetBundles = generateAssetBundles; |
| 0 | 117 | | bool assetsAlreadyDumped = false; |
| | 118 | |
|
| | 119 | | //TODO(Brian): Use async-await instead of Application.update |
| | 120 | | void UpdateLoop() |
| | 121 | | { |
| | 122 | | try |
| | 123 | | { |
| | 124 | | //NOTE(Brian): We have to check this because the ImportAsset for GLTFs is not synchronous, and must |
| | 125 | | // after the import asset finished. Therefore, we have to make sure those calls finished |
| 0 | 126 | | if (!GLTFImporter.finishedImporting && Time.realtimeSinceStartup - timer < 60) |
| 0 | 127 | | return; |
| | 128 | |
|
| 0 | 129 | | env.assetDatabase.Refresh(); |
| | 130 | |
|
| 0 | 131 | | if (!assetsAlreadyDumped) |
| | 132 | | { |
| 0 | 133 | | state.step = State.Step.DUMPING_ASSETS; |
| 0 | 134 | | shouldGenerateAssetBundles |= DumpAssets(rawContents); |
| 0 | 135 | | assetsAlreadyDumped = true; |
| 0 | 136 | | timer = Time.realtimeSinceStartup; |
| | 137 | |
|
| 0 | 138 | | if (settings.dumpOnly) |
| 0 | 139 | | shouldGenerateAssetBundles = false; |
| | 140 | |
|
| | 141 | | //NOTE(Brian): return in order to wait for GLTFImporter.finishedImporting flag, as it will set a |
| 0 | 142 | | return; |
| | 143 | | } |
| | 144 | |
|
| 0 | 145 | | EditorApplication.update -= UpdateLoop; |
| | 146 | |
|
| 0 | 147 | | if (shouldGenerateAssetBundles && generateAssetBundles) |
| | 148 | | { |
| | 149 | | AssetBundleManifest manifest; |
| | 150 | |
|
| 0 | 151 | | state.step = State.Step.BUILDING_ASSET_BUNDLES; |
| | 152 | |
|
| 0 | 153 | | if (BuildAssetBundles(out manifest)) |
| | 154 | | { |
| 0 | 155 | | CleanAssetBundleFolder(manifest.GetAllAssetBundles()); |
| | 156 | |
|
| 0 | 157 | | state.lastErrorCode = ErrorCodes.SUCCESS; |
| 0 | 158 | | state.step = State.Step.FINISHED; |
| 0 | 159 | | } |
| | 160 | | else |
| | 161 | | { |
| 0 | 162 | | state.lastErrorCode = ErrorCodes.ASSET_BUNDLE_BUILD_FAIL; |
| 0 | 163 | | state.step = State.Step.FINISHED; |
| | 164 | | } |
| 0 | 165 | | } |
| | 166 | | else |
| | 167 | | { |
| 0 | 168 | | state.lastErrorCode = ErrorCodes.SUCCESS; |
| 0 | 169 | | state.step = State.Step.FINISHED; |
| | 170 | | } |
| 0 | 171 | | } |
| 0 | 172 | | catch (Exception e) |
| | 173 | | { |
| 0 | 174 | | log.Error(e.Message + "\n" + e.StackTrace); |
| 0 | 175 | | state.lastErrorCode = ErrorCodes.UNDEFINED; |
| 0 | 176 | | state.step = State.Step.FINISHED; |
| 0 | 177 | | EditorApplication.update -= UpdateLoop; |
| 0 | 178 | | } |
| | 179 | |
|
| 0 | 180 | | if (generateAssetBundles) |
| | 181 | | { |
| 0 | 182 | | EditorCoroutineUtility.StartCoroutineOwnerless(VisualTests.TestConvertedAssets( |
| | 183 | | env: env, |
| | 184 | | OnFinish: (skippedAssetsCount) => |
| | 185 | | { |
| 0 | 186 | | ProcessSkippedAssets(skippedAssetsCount); |
| | 187 | |
|
| 0 | 188 | | OnFinish?.Invoke(state.lastErrorCode); |
| 0 | 189 | | })); |
| 0 | 190 | | } |
| | 191 | | else |
| | 192 | | { |
| 0 | 193 | | OnFinish?.Invoke(state.lastErrorCode); |
| | 194 | | } |
| 0 | 195 | | } |
| | 196 | |
|
| 0 | 197 | | EditorApplication.update += UpdateLoop; |
| 0 | 198 | | } |
| | 199 | |
|
| | 200 | | public void ConvertDumpedAssets(Action<ErrorCodes> OnFinish = null) |
| | 201 | | { |
| 0 | 202 | | if (!BuildAssetBundles(out AssetBundleManifest manifest)) |
| 0 | 203 | | return; |
| | 204 | |
|
| 0 | 205 | | CleanAssetBundleFolder(manifest.GetAllAssetBundles()); |
| | 206 | |
|
| 0 | 207 | | EditorCoroutineUtility.StartCoroutineOwnerless(VisualTests.TestConvertedAssets( |
| | 208 | | env: env, |
| | 209 | | OnFinish: (skippedAssetsCount) => |
| | 210 | | { |
| 0 | 211 | | ProcessSkippedAssets(skippedAssetsCount); |
| | 212 | |
|
| 0 | 213 | | OnFinish?.Invoke(state.lastErrorCode); |
| 0 | 214 | | })); |
| 0 | 215 | | } |
| | 216 | |
|
| | 217 | | private void ProcessSkippedAssets(int skippedAssetsCount) |
| | 218 | | { |
| 0 | 219 | | if (skippedAssetsCount <= 0) |
| 0 | 220 | | return; |
| | 221 | |
|
| 0 | 222 | | skippedAssets = skippedAssetsCount; |
| | 223 | |
|
| 0 | 224 | | if (skippedAssets >= totalAssets) |
| 0 | 225 | | state.lastErrorCode = ErrorCodes.ASSET_BUNDLE_BUILD_FAIL; |
| | 226 | | else |
| 0 | 227 | | state.lastErrorCode = ErrorCodes.SOME_ASSET_BUNDLES_SKIPPED; |
| 0 | 228 | | } |
| | 229 | |
|
| | 230 | | /// <summary> |
| | 231 | | /// Parses a GLTF and populates a List<ContentServerUtils.MappingPair> with its dependencies |
| | 232 | | /// </summary> |
| | 233 | | /// <param name="assetHash">The asset's content server hash</param> |
| | 234 | | /// <param name="assetFilename">The asset's content server file name</param> |
| | 235 | | /// <param name="sceneCid">The asset scene ID</param> |
| | 236 | | /// <param name="mappingPairsList">The reference of a list where the dependency mapping pairs will be added</par |
| | 237 | | public void GetAssetDependenciesMappingPairs(string assetHash, string assetFilename, string sceneCid, ref List< |
| | 238 | | { |
| | 239 | | // 1. Get all dependencies |
| 0 | 240 | | List<AssetPath> gltfPaths = ABConverter.Utils.GetPathsFromPairs(finalDownloadedPath, new [] |
| | 241 | | { |
| | 242 | | new ContentServerUtils.MappingPair |
| | 243 | | { |
| | 244 | | file = assetFilename, |
| | 245 | | hash = assetHash |
| | 246 | | } |
| | 247 | | }, Config.gltfExtensions); |
| | 248 | |
|
| | 249 | | // Disable editor assets auto-import temporarily to avoid Unity trying to import the GLTF on its own, when w |
| 0 | 250 | | AssetDatabase.StartAssetEditing(); |
| | 251 | |
|
| 0 | 252 | | string path = DownloadAsset(gltfPaths[0]); |
| | 253 | |
|
| 0 | 254 | | if (string.IsNullOrEmpty(path)) |
| | 255 | | { |
| 0 | 256 | | log.Error("Core - GetAssetDependenciesMappingPairs() - Invalid target asset data! aborting dependencies |
| 0 | 257 | | return; |
| | 258 | | } |
| | 259 | |
|
| 0 | 260 | | log.Info($"Core - GetAssetDependenciesMappingPairs() -> path: {path}," + |
| | 261 | | $"\n file: {gltfPaths[0].file}," + |
| | 262 | | $"\n hash: {gltfPaths[0].hash}," + |
| | 263 | | $"\n pair: {gltfPaths[0].pair}, " + |
| | 264 | | $"\n basePath: {gltfPaths[0].basePath}," + |
| | 265 | | $"\n finalPath: {gltfPaths[0].finalPath}," + |
| | 266 | | $"\n finalMetaPath: {gltfPaths[0].finalMetaPath}"); |
| | 267 | |
|
| | 268 | | // 2. Search for dependencies hashes in scene mappings and add them to the collection |
| 0 | 269 | | using (var stream = File.OpenRead(path)) |
| | 270 | | { |
| | 271 | | GLTFRoot gLTFRoot; |
| 0 | 272 | | GLTFParser.ParseJson(stream, out gLTFRoot); |
| | 273 | |
|
| 0 | 274 | | ContentServerUtils.MappingsAPIData parcelInfoApiData = ABConverter.Utils.GetSceneMappingsData(env.webReq |
| | 275 | |
|
| 0 | 276 | | if (gLTFRoot.Buffers != null) |
| | 277 | | { |
| 0 | 278 | | foreach (var asset in gLTFRoot.Buffers) |
| | 279 | | { |
| 0 | 280 | | if (string.IsNullOrEmpty(asset.Uri)) |
| | 281 | | continue; |
| | 282 | |
|
| 0 | 283 | | mappingPairsList.AddRange(parcelInfoApiData.data[0].content.contents.Where(x => x.file.Contains( |
| | 284 | |
|
| 0 | 285 | | log.Info("Core - GetAssetDependenciesMappingPairs - Buffers -> Searching for... uri: " + asset.U |
| | 286 | | } |
| | 287 | | } |
| | 288 | |
|
| 0 | 289 | | if (gLTFRoot.Images != null) |
| | 290 | | { |
| 0 | 291 | | foreach (var asset in gLTFRoot.Images) |
| | 292 | | { |
| 0 | 293 | | if (string.IsNullOrEmpty(asset.Uri)) |
| | 294 | | continue; |
| 0 | 295 | | mappingPairsList.AddRange(parcelInfoApiData.data[0].content.contents.Where(x => x.file.Contains( |
| | 296 | |
|
| 0 | 297 | | log.Info("Core - GetAssetDependenciesMappingPairs - Images -> uri: " + asset.Uri + " -> name: " |
| | 298 | | } |
| | 299 | | } |
| 0 | 300 | | } |
| | 301 | |
|
| | 302 | | // 3. Remove temporary GLTF file and re-enable editor assets auto-import |
| 0 | 303 | | File.Delete(path); |
| 0 | 304 | | AssetDatabase.StopAssetEditing(); |
| 0 | 305 | | } |
| | 306 | |
|
| | 307 | | /// <summary> |
| | 308 | | /// Dump all assets and tag them for asset bundle building. |
| | 309 | | /// </summary> |
| | 310 | | /// <param name="rawContents">An array containing all the assets to be dumped.</param> |
| | 311 | | /// <returns>true if succeeded</returns> |
| | 312 | | private bool DumpAssets(ContentServerUtils.MappingPair[] rawContents) |
| | 313 | | { |
| 0 | 314 | | List<AssetPath> gltfPaths = ABConverter.Utils.GetPathsFromPairs(finalDownloadedPath, rawContents, Config.glt |
| 0 | 315 | | List<AssetPath> bufferPaths = ABConverter.Utils.GetPathsFromPairs(finalDownloadedPath, rawContents, Config.b |
| 0 | 316 | | List<AssetPath> texturePaths = ABConverter.Utils.GetPathsFromPairs(finalDownloadedPath, rawContents, Config. |
| | 317 | |
|
| 0 | 318 | | List<AssetPath> assetsToMark = new List<AssetPath>(); |
| | 319 | |
|
| 0 | 320 | | if (!FilterDumpList(ref gltfPaths)) |
| 0 | 321 | | return false; |
| | 322 | |
|
| | 323 | | //NOTE(Brian): Prepare textures and buffers. We should prepare all the dependencies in this phase. |
| 0 | 324 | | assetsToMark.AddRange(DumpImportableAssets(texturePaths)); |
| 0 | 325 | | DumpRawAssets(bufferPaths); |
| | 326 | |
|
| 0 | 327 | | GLTFImporter.OnGLTFRootIsConstructed -= ABConverter.Utils.FixGltfRootInvalidUriCharacters; |
| 0 | 328 | | GLTFImporter.OnGLTFRootIsConstructed += ABConverter.Utils.FixGltfRootInvalidUriCharacters; |
| | 329 | |
|
| 0 | 330 | | foreach (var gltfPath in gltfPaths) |
| | 331 | | { |
| 0 | 332 | | assetsToMark.Add(DumpGltf(gltfPath, texturePaths, bufferPaths)); |
| | 333 | | } |
| | 334 | |
|
| 0 | 335 | | env.assetDatabase.Refresh(); |
| 0 | 336 | | env.assetDatabase.SaveAssets(); |
| | 337 | |
|
| 0 | 338 | | MarkAllAssetBundles(assetsToMark); |
| 0 | 339 | | MarkShaderAssetBundle(); |
| | 340 | |
|
| 0 | 341 | | return true; |
| | 342 | | } |
| | 343 | |
|
| | 344 | | /// <summary> |
| | 345 | | /// Trims off existing asset bundles from the given AssetPath array, |
| | 346 | | /// if none exists and shouldAbortBecauseAllBundlesExist is true, it will return false. |
| | 347 | | /// </summary> |
| | 348 | | /// <param name="gltfPaths">paths to be checked for existence</param> |
| | 349 | | /// <returns>false if all paths are already converted to asset bundles, true if the conversion makes sense</retu |
| | 350 | | internal bool FilterDumpList(ref List<AssetPath> gltfPaths) |
| | 351 | | { |
| | 352 | | bool shouldBuildAssetBundles; |
| | 353 | |
|
| 0 | 354 | | totalAssets += gltfPaths.Count; |
| | 355 | |
|
| 0 | 356 | | if (settings.skipAlreadyBuiltBundles) |
| | 357 | | { |
| 0 | 358 | | int gltfCount = gltfPaths.Count; |
| | 359 | |
|
| 0 | 360 | | gltfPaths = gltfPaths.Where( |
| | 361 | | assetPath => |
| 0 | 362 | | !env.file.Exists(settings.finalAssetBundlePath + assetPath.hash)) |
| | 363 | | .ToList(); |
| | 364 | |
|
| 0 | 365 | | int skippedCount = gltfCount - gltfPaths.Count; |
| 0 | 366 | | skippedAssets += skippedCount; |
| 0 | 367 | | shouldBuildAssetBundles = gltfPaths.Count == 0; |
| 0 | 368 | | } |
| | 369 | | else |
| | 370 | | { |
| 0 | 371 | | shouldBuildAssetBundles = false; |
| | 372 | | } |
| | 373 | |
|
| 0 | 374 | | if (shouldBuildAssetBundles) |
| | 375 | | { |
| 0 | 376 | | log.Info("All assets in this scene were already generated!. Skipping."); |
| 0 | 377 | | return false; |
| | 378 | | } |
| | 379 | |
|
| 0 | 380 | | return true; |
| | 381 | | } |
| | 382 | |
|
| | 383 | | /// <summary> |
| | 384 | | /// Dump a single gltf asset injecting the proper external references |
| | 385 | | /// </summary> |
| | 386 | | /// <param name="gltfPath">GLTF to be dumped</param> |
| | 387 | | /// <param name="texturePaths">array with texture dependencies</param> |
| | 388 | | /// <param name="bufferPaths">array with buffer dependencies</param> |
| | 389 | | /// <returns>gltf AssetPath if dump succeeded, null if don't</returns> |
| | 390 | | internal AssetPath DumpGltf(AssetPath gltfPath, List<AssetPath> texturePaths, List<AssetPath> bufferPaths) |
| | 391 | | { |
| 0 | 392 | | List<Stream> streamsToDispose = new List<Stream>(); |
| | 393 | |
|
| 0 | 394 | | PersistentAssetCache.ImageCacheByUri.Clear(); |
| 0 | 395 | | PersistentAssetCache.StreamCacheByUri.Clear(); |
| | 396 | |
|
| 0 | 397 | | log.Verbose("Start injecting stuff into " + gltfPath.hash); |
| | 398 | |
|
| | 399 | | //NOTE(Brian): Prepare gltfs gathering its dependencies first and filling the importer's static cache. |
| 0 | 400 | | foreach (var texturePath in texturePaths) |
| | 401 | | { |
| 0 | 402 | | log.Verbose("Injecting texture... " + texturePath.hash + " -> " + texturePath.finalPath); |
| 0 | 403 | | RetrieveAndInjectTexture(gltfPath, texturePath); |
| | 404 | | } |
| | 405 | |
|
| 0 | 406 | | foreach (var bufferPath in bufferPaths) |
| | 407 | | { |
| 0 | 408 | | log.Verbose("Injecting buffer... " + bufferPath.hash + " -> " + bufferPath.finalPath); |
| 0 | 409 | | RetrieveAndInjectBuffer(gltfPath, bufferPath); // TODO: this adds buffers that will be used in the futur |
| | 410 | | } |
| | 411 | |
|
| 0 | 412 | | log.Verbose("About to load " + gltfPath.hash); |
| | 413 | |
|
| | 414 | | //NOTE(Brian): Load the gLTF after the dependencies are injected. |
| | 415 | | // The GLTFImporter will use the PersistentAssetCache to resolve them. |
| 0 | 416 | | string path = DownloadAsset(gltfPath); |
| | 417 | |
|
| 0 | 418 | | if (path != null) |
| | 419 | | { |
| 0 | 420 | | env.assetDatabase.Refresh(); |
| 0 | 421 | | env.assetDatabase.SaveAssets(); |
| | 422 | | } |
| | 423 | |
|
| 0 | 424 | | log.Verbose("End load " + gltfPath.hash); |
| | 425 | |
|
| 0 | 426 | | foreach (var streamDataKvp in PersistentAssetCache.StreamCacheByUri) |
| | 427 | | { |
| 0 | 428 | | if (streamDataKvp.Value.stream != null) |
| 0 | 429 | | streamsToDispose.Add(streamDataKvp.Value.stream); |
| | 430 | | } |
| | 431 | |
|
| 0 | 432 | | foreach (var s in streamsToDispose) |
| | 433 | | { |
| 0 | 434 | | s.Dispose(); |
| | 435 | | } |
| | 436 | |
|
| 0 | 437 | | return path != null ? gltfPath : null; |
| | 438 | | } |
| | 439 | |
|
| | 440 | | /// <summary> |
| | 441 | | /// Download assets and put them in the working folder. |
| | 442 | | /// </summary> |
| | 443 | | /// <param name="bufferPaths">AssetPath list containing all the desired paths to be dumped</param> |
| | 444 | | /// <returns>List of the successfully dumped assets.</returns> |
| | 445 | | internal List<AssetPath> DumpRawAssets(List<AssetPath> bufferPaths) |
| | 446 | | { |
| 0 | 447 | | List<AssetPath> result = new List<AssetPath>(bufferPaths); |
| | 448 | |
|
| 0 | 449 | | if (bufferPaths.Count == 0 || bufferPaths == null) |
| 0 | 450 | | return result; |
| | 451 | |
|
| 0 | 452 | | foreach (var assetPath in bufferPaths) |
| | 453 | | { |
| 0 | 454 | | if (env.file.Exists(assetPath.finalPath)) |
| | 455 | | continue; |
| | 456 | |
|
| 0 | 457 | | var finalDlPath = DownloadAsset(assetPath); |
| | 458 | |
|
| 0 | 459 | | if (string.IsNullOrEmpty(finalDlPath)) |
| | 460 | | { |
| 0 | 461 | | result.Remove(assetPath); |
| 0 | 462 | | log.Error("Failed to get buffer dependencies! failing asset: " + assetPath.hash); |
| | 463 | | } |
| | 464 | | } |
| | 465 | |
|
| 0 | 466 | | return result; |
| | 467 | | } |
| | 468 | |
|
| | 469 | | /// <summary> |
| | 470 | | /// This will dump all assets contained in the AssetPath list using the baseUrl + hash. |
| | 471 | | /// |
| | 472 | | /// After the assets are dumped, they will be imported using Unity's AssetDatabase and |
| | 473 | | /// their guids will be normalized using the asset's cid. |
| | 474 | | /// |
| | 475 | | /// The guid normalization will ensure the guids remain consistent and the same asset will |
| | 476 | | /// always have the asset guid. If we don't normalize the guids, Unity will chose a random one, |
| | 477 | | /// and this can break the Asset Bundles dependencies as they are resolved by guid. |
| | 478 | | /// </summary> |
| | 479 | | /// <param name="assetPaths">List of assetPaths to be dumped</param> |
| | 480 | | /// <returns>A list with assetPaths that were successfully dumped. This list will be empty if all dumps failed.< |
| | 481 | | internal List<AssetPath> DumpImportableAssets(List<AssetPath> assetPaths) |
| | 482 | | { |
| 0 | 483 | | List<AssetPath> result = new List<AssetPath>(assetPaths); |
| | 484 | |
|
| 0 | 485 | | foreach (var assetPath in assetPaths) |
| | 486 | | { |
| 0 | 487 | | if (env.file.Exists(assetPath.finalPath)) |
| | 488 | | continue; |
| | 489 | |
|
| | 490 | | //NOTE(Brian): try to get an AB before getting the original texture, so we bind the dependencies correct |
| 0 | 491 | | string fullPathToTag = DownloadAsset(assetPath); |
| | 492 | |
|
| 0 | 493 | | if (fullPathToTag == null) |
| | 494 | | { |
| 0 | 495 | | result.Remove(assetPath); |
| 0 | 496 | | log.Error("Failed to get texture dependencies! failing asset: " + assetPath.hash); |
| 0 | 497 | | continue; |
| | 498 | | } |
| | 499 | |
|
| 0 | 500 | | var importer = env.assetDatabase.GetImporterAtPath(assetPath.finalPath); |
| | 501 | |
|
| 0 | 502 | | if (importer is TextureImporter texImporter) |
| | 503 | | { |
| 0 | 504 | | texImporter.crunchedCompression = true; |
| 0 | 505 | | texImporter.textureCompression = TextureImporterCompression.CompressedHQ; |
| | 506 | |
|
| 0 | 507 | | ReduceTextureSizeIfNeeded(assetPath.hash + "/" + assetPath.hash + Path.GetExtension(assetPath.file), |
| 0 | 508 | | } |
| | 509 | | else |
| | 510 | | { |
| 0 | 511 | | env.assetDatabase.ImportAsset(assetPath.finalPath, ImportAssetOptions.ForceUpdate); |
| 0 | 512 | | env.assetDatabase.SaveAssets(); |
| | 513 | | } |
| | 514 | |
|
| 0 | 515 | | SetDeterministicAssetDatabaseGuid(assetPath); |
| | 516 | |
|
| 0 | 517 | | log.Verbose($"Dumping file -> {assetPath}"); |
| | 518 | | } |
| | 519 | |
|
| 0 | 520 | | return result; |
| | 521 | | } |
| | 522 | |
|
| | 523 | | private void ReduceTextureSizeIfNeeded(string texturePath, float maxSize) |
| | 524 | | { |
| 0 | 525 | | string finalTexturePath = finalDownloadedPath + texturePath; |
| | 526 | |
|
| 0 | 527 | | byte[] image = File.ReadAllBytes(finalTexturePath); |
| | 528 | |
|
| 0 | 529 | | var tmpTex = new Texture2D(1, 1); |
| | 530 | |
|
| 0 | 531 | | if (!ImageConversion.LoadImage(tmpTex, image)) |
| 0 | 532 | | return; |
| | 533 | |
|
| 0 | 534 | | float factor = 1.0f; |
| 0 | 535 | | int width = tmpTex.width; |
| 0 | 536 | | int height = tmpTex.height; |
| | 537 | |
|
| 0 | 538 | | float maxTextureSize = maxSize; |
| | 539 | |
|
| 0 | 540 | | if (width < maxTextureSize && height < maxTextureSize) |
| 0 | 541 | | return; |
| | 542 | |
|
| 0 | 543 | | if (width >= height) |
| | 544 | | { |
| 0 | 545 | | factor = (float)maxTextureSize / width; |
| 0 | 546 | | } |
| | 547 | | else |
| | 548 | | { |
| 0 | 549 | | factor = (float)maxTextureSize / height; |
| | 550 | | } |
| | 551 | |
|
| 0 | 552 | | Texture2D dstTex = TextureHelpers.Resize(tmpTex, (int)(width * factor), (int)(height * factor)); |
| 0 | 553 | | byte[] endTex = ImageConversion.EncodeToPNG(dstTex); |
| 0 | 554 | | UnityEngine.Object.DestroyImmediate(tmpTex); |
| | 555 | |
|
| 0 | 556 | | File.WriteAllBytes(finalTexturePath, endTex); |
| | 557 | |
|
| 0 | 558 | | AssetDatabase.ImportAsset(finalDownloadedAssetDbPath + texturePath, ImportAssetOptions.ForceUpdate); |
| 0 | 559 | | AssetDatabase.SaveAssets(); |
| 0 | 560 | | } |
| | 561 | |
|
| | 562 | | /// <summary> |
| | 563 | | /// This will download a single asset referenced by an AssetPath. |
| | 564 | | /// The download target is baseUrl + hash. |
| | 565 | | /// </summary> |
| | 566 | | /// <param name="assetPath">The AssetPath object referencing the asset to be downloaded</param> |
| | 567 | | /// <returns>The file output path. Null if download failed.</returns> |
| | 568 | | internal string DownloadAsset(AssetPath assetPath) |
| | 569 | | { |
| 0 | 570 | | string outputPath = assetPath.finalPath; |
| 0 | 571 | | string outputPathDir = Path.GetDirectoryName(outputPath); |
| 0 | 572 | | string finalUrl = settings.baseUrl + assetPath.hash; |
| | 573 | |
|
| 0 | 574 | | if (env.file.Exists(outputPath)) |
| | 575 | | { |
| 0 | 576 | | log.Verbose("Skipping already generated asset: " + outputPath); |
| 0 | 577 | | return outputPath; |
| | 578 | | } |
| | 579 | |
|
| 0 | 580 | | DownloadHandler downloadHandler = null; |
| | 581 | |
|
| | 582 | | try |
| | 583 | | { |
| 0 | 584 | | downloadHandler = env.webRequest.Get(finalUrl); |
| | 585 | |
|
| 0 | 586 | | if (downloadHandler == null) |
| | 587 | | { |
| 0 | 588 | | log.Error($"Download failed! {finalUrl} -- null DownloadHandler"); |
| 0 | 589 | | return null; |
| | 590 | | } |
| 0 | 591 | | } |
| 0 | 592 | | catch (HttpRequestException e) |
| | 593 | | { |
| 0 | 594 | | log.Error($"Download failed! {finalUrl} -- {e.Message}"); |
| 0 | 595 | | return null; |
| | 596 | | } |
| | 597 | |
|
| 0 | 598 | | byte[] assetData = downloadHandler.data; |
| 0 | 599 | | downloadHandler.Dispose(); |
| | 600 | |
|
| 0 | 601 | | log.Verbose($"Downloaded asset = {finalUrl} to {outputPath}"); |
| | 602 | |
|
| 0 | 603 | | if (!env.directory.Exists(outputPathDir)) |
| 0 | 604 | | env.directory.CreateDirectory(outputPathDir); |
| | 605 | |
|
| 0 | 606 | | env.file.WriteAllBytes(outputPath, assetData); |
| | 607 | |
|
| 0 | 608 | | env.assetDatabase.ImportAsset(outputPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.Imp |
| | 609 | |
|
| 0 | 610 | | return outputPath; |
| 0 | 611 | | } |
| | 612 | |
|
| | 613 | | /// <summary> |
| | 614 | | /// Load dumped textures and put them in PersistentAssetCache so the GLTFSceneImporter |
| | 615 | | /// can pick them up. |
| | 616 | | /// </summary> |
| | 617 | | /// <param name="gltfPath">GLTF path of the gltf that will pick up the references</param> |
| | 618 | | /// <param name="texturePath">Texture path of the texture to be injected</param> |
| | 619 | | internal void RetrieveAndInjectTexture(AssetPath gltfPath, AssetPath texturePath) |
| | 620 | | { |
| 0 | 621 | | string finalPath = texturePath.finalPath; |
| | 622 | |
|
| 0 | 623 | | if (!env.file.Exists(finalPath)) |
| 0 | 624 | | return; |
| | 625 | |
|
| 0 | 626 | | Texture2D t2d = env.assetDatabase.LoadAssetAtPath<Texture2D>(finalPath); |
| | 627 | |
|
| 0 | 628 | | if (t2d == null) |
| 0 | 629 | | return; |
| | 630 | |
|
| 0 | 631 | | string relativePath = ABConverter.PathUtils.GetRelativePathTo(gltfPath.file, texturePath.file); |
| | 632 | |
|
| | 633 | | //NOTE(Brian): This cache will be used by the GLTF importer when seeking textures. This way the importer wil |
| | 634 | | // consume the asset bundle dependencies instead of trying to create new textures. |
| 0 | 635 | | PersistentAssetCache.AddImage(relativePath, gltfPath.finalPath, t2d); |
| 0 | 636 | | } |
| | 637 | |
|
| | 638 | | /// <summary> |
| | 639 | | /// Load dumped buffers and put them in PersistentAssetCache so the GLTFSceneImporter |
| | 640 | | /// can pick them up. |
| | 641 | | /// </summary> |
| | 642 | | /// <param name="gltfPath">GLTF path of the gltf that will pick up the references</param> |
| | 643 | | /// <param name="bufferPath">Buffer path of the texture to be injected</param> |
| | 644 | | internal void RetrieveAndInjectBuffer(AssetPath gltfPath, AssetPath bufferPath) |
| | 645 | | { |
| 0 | 646 | | string finalPath = bufferPath.finalPath; |
| | 647 | |
|
| 0 | 648 | | if (!env.file.Exists(finalPath)) |
| 0 | 649 | | return; |
| | 650 | |
|
| 0 | 651 | | Stream stream = env.file.OpenRead(finalPath); |
| 0 | 652 | | string relativePath = ABConverter.PathUtils.GetRelativePathTo(gltfPath.file, bufferPath.file); |
| | 653 | |
|
| | 654 | | // NOTE(Brian): This cache will be used by the GLTF importer when seeking streams. This way the importer wil |
| | 655 | | // consume the asset bundle dependencies instead of trying to create new streams. |
| 0 | 656 | | PersistentAssetCache.AddBuffer(relativePath, gltfPath.finalPath, stream); |
| 0 | 657 | | } |
| | 658 | |
|
| | 659 | | /// <summary> |
| | 660 | | /// Mark all the given assetPaths to be built as asset bundles by Unity's BuildPipeline. |
| | 661 | | /// </summary> |
| | 662 | | /// <param name="assetPaths">The paths to be built.</param> |
| | 663 | | private void MarkAllAssetBundles(List<AssetPath> assetPaths) |
| | 664 | | { |
| 0 | 665 | | foreach (var assetPath in assetPaths) |
| | 666 | | { |
| 0 | 667 | | ABConverter.Utils.MarkFolderForAssetBundleBuild(assetPath.finalPath, assetPath.hash); |
| | 668 | | } |
| 0 | 669 | | } |
| | 670 | |
|
| | 671 | | /// <summary> |
| | 672 | | /// Build all marked paths as asset bundles using Unity's BuildPipeline and generate their .depmap files |
| | 673 | | /// </summary> |
| | 674 | | /// <param name="manifest">AssetBundleManifest generated by the build.</param> |
| | 675 | | /// <returns>true is build was successful</returns> |
| | 676 | | public virtual bool BuildAssetBundles(out AssetBundleManifest manifest) |
| | 677 | | { |
| 0 | 678 | | env.assetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate | Impor |
| 0 | 679 | | env.assetDatabase.SaveAssets(); |
| | 680 | |
|
| 0 | 681 | | env.assetDatabase.MoveAsset(finalDownloadedPath, Config.DOWNLOADED_PATH_ROOT); |
| | 682 | |
|
| 0 | 683 | | manifest = env.buildPipeline.BuildAssetBundles(settings.finalAssetBundlePath, BuildAssetBundleOptions.Uncomp |
| | 684 | |
|
| 0 | 685 | | if (manifest == null) |
| | 686 | | { |
| 0 | 687 | | log.Error("Error generating asset bundle!"); |
| 0 | 688 | | return false; |
| | 689 | | } |
| | 690 | |
|
| 0 | 691 | | DependencyMapBuilder.Generate(env.file, settings.finalAssetBundlePath, hashLowercaseToHashProper, manifest, |
| 0 | 692 | | logBuffer += $"Generating asset bundles at path: {settings.finalAssetBundlePath}\n"; |
| | 693 | |
|
| 0 | 694 | | string[] assetBundles = manifest.GetAllAssetBundles(); |
| | 695 | |
|
| 0 | 696 | | logBuffer += $"Total generated asset bundles: {assetBundles.Length}\n"; |
| | 697 | |
|
| 0 | 698 | | for (int i = 0; i < assetBundles.Length; i++) |
| | 699 | | { |
| 0 | 700 | | if (string.IsNullOrEmpty(assetBundles[i])) |
| | 701 | | continue; |
| | 702 | |
|
| 0 | 703 | | logBuffer += $"#{i} Generated asset bundle name: {assetBundles[i]}\n"; |
| | 704 | | } |
| | 705 | |
|
| 0 | 706 | | logBuffer += $"\nFree disk space after conv: {PathUtils.GetFreeSpace()}"; |
| 0 | 707 | | return true; |
| | 708 | | } |
| | 709 | |
|
| | 710 | | /// <summary> |
| | 711 | | /// Clean all working folders and end the batch process. |
| | 712 | | /// </summary> |
| | 713 | | /// <param name="errorCode">final errorCode of the conversion process</param> |
| | 714 | | private void CleanAndExit(ErrorCodes errorCode) |
| | 715 | | { |
| 0 | 716 | | float conversionTime = Time.realtimeSinceStartup - startTime; |
| 0 | 717 | | logBuffer = $"Conversion finished!. last error code = {errorCode}"; |
| | 718 | |
|
| 0 | 719 | | logBuffer += "\n"; |
| 0 | 720 | | logBuffer += $"Converted {totalAssets - skippedAssets} of {totalAssets}. (Skipped {skippedAssets})\n"; |
| 0 | 721 | | logBuffer += $"Total time: {conversionTime}"; |
| | 722 | |
|
| 0 | 723 | | if (totalAssets > 0) |
| | 724 | | { |
| 0 | 725 | | logBuffer += $"... Time per asset: {conversionTime / totalAssets}\n"; |
| | 726 | | } |
| | 727 | |
|
| 0 | 728 | | logBuffer += "\n"; |
| 0 | 729 | | logBuffer += logBuffer; |
| | 730 | |
|
| 0 | 731 | | log.Info(logBuffer); |
| | 732 | |
|
| 0 | 733 | | CleanupWorkingFolders(); |
| 0 | 734 | | Utils.Exit((int) errorCode); |
| 0 | 735 | | } |
| | 736 | |
|
| | 737 | | /// <summary> |
| | 738 | | /// in asset bundles, all dependencies are resolved by their guid (and not the AB hash nor CRC) |
| | 739 | | /// So to ensure dependencies are being kept in subsequent editor runs we normalize the asset guid using |
| | 740 | | /// the CID. |
| | 741 | | /// |
| | 742 | | /// This method: |
| | 743 | | /// - Looks for the meta file of the given assetPath. |
| | 744 | | /// - Changes the .meta guid using the assetPath's cid as seed. |
| | 745 | | /// - Does some file system gymnastics to make sure the new guid is imported to our AssetDatabase. |
| | 746 | | /// </summary> |
| | 747 | | /// <param name="assetPath">AssetPath of the target asset to modify</param> |
| | 748 | | private void SetDeterministicAssetDatabaseGuid(AssetPath assetPath) |
| | 749 | | { |
| 0 | 750 | | string metaPath = env.assetDatabase.GetTextMetaFilePathFromAssetPath(assetPath.finalPath); |
| | 751 | |
|
| 0 | 752 | | env.assetDatabase.ReleaseCachedFileHandles(); |
| | 753 | |
|
| 0 | 754 | | string metaContent = env.file.ReadAllText(metaPath); |
| 0 | 755 | | string guid = ABConverter.Utils.CidToGuid(assetPath.hash); |
| 0 | 756 | | string newMetaContent = Regex.Replace(metaContent, @"guid: \w+?\n", $"guid: {guid}\n"); |
| | 757 | |
|
| | 758 | | //NOTE(Brian): We must do this hack in order to the new guid to be added to the AssetDatabase. |
| | 759 | | // on windows, an AssetImporter.SaveAndReimport call makes the trick, but this won't work |
| | 760 | | // on Unix based OSes for some reason. |
| 0 | 761 | | env.file.Delete(metaPath); |
| | 762 | |
|
| 0 | 763 | | env.file.Copy(assetPath.finalPath, finalDownloadedPath + "tmp"); |
| 0 | 764 | | env.assetDatabase.DeleteAsset(assetPath.finalPath); |
| 0 | 765 | | env.file.Delete(assetPath.finalPath); |
| | 766 | |
|
| 0 | 767 | | env.assetDatabase.Refresh(); |
| 0 | 768 | | env.assetDatabase.SaveAssets(); |
| | 769 | |
|
| 0 | 770 | | env.file.Copy(finalDownloadedPath + "tmp", assetPath.finalPath); |
| 0 | 771 | | env.file.WriteAllText(metaPath, newMetaContent); |
| 0 | 772 | | env.file.Delete(finalDownloadedPath + "tmp"); |
| | 773 | |
|
| 0 | 774 | | env.assetDatabase.Refresh(); |
| 0 | 775 | | env.assetDatabase.SaveAssets(); |
| 0 | 776 | | } |
| | 777 | |
|
| 0 | 778 | | internal void CleanAssetBundleFolder(string[] assetBundles) { ABConverter.Utils.CleanAssetBundleFolder(env.file, |
| | 779 | |
|
| | 780 | | internal void PopulateLowercaseMappings(ContentServerUtils.MappingPair[] pairs) |
| | 781 | | { |
| 0 | 782 | | foreach (var content in pairs) |
| | 783 | | { |
| 0 | 784 | | string hashLower = content.hash.ToLowerInvariant(); |
| | 785 | |
|
| 0 | 786 | | if (!hashLowercaseToHashProper.ContainsKey(hashLower)) |
| 0 | 787 | | hashLowercaseToHashProper.Add(hashLower, content.hash); |
| | 788 | | } |
| 0 | 789 | | } |
| | 790 | |
|
| | 791 | | /// <summary> |
| | 792 | | /// This method tags the main shader, so all the asset bundles don't contain repeated shader assets. |
| | 793 | | /// This way we save the big Shader.Parse and gpu compiling performance overhead and make |
| | 794 | | /// the bundles a bit lighter. |
| | 795 | | /// </summary> |
| | 796 | | private void MarkShaderAssetBundle() |
| | 797 | | { |
| | 798 | | //NOTE(Brian): The shader asset bundle that's going to be generated doesn't need to be really used, |
| | 799 | | // as we are going to use the embedded one, so we are going to just delete it after the |
| | 800 | | // generation ended. |
| 0 | 801 | | var mainShader = Shader.Find("DCL/Universal Render Pipeline/Lit"); |
| 0 | 802 | | ABConverter.Utils.MarkAssetForAssetBundleBuild(env.assetDatabase, mainShader, MAIN_SHADER_AB_NAME); |
| 0 | 803 | | } |
| | 804 | |
|
| | 805 | | internal virtual void InitializeDirectoryPaths(bool deleteIfExists) |
| | 806 | | { |
| 0 | 807 | | log.Info("Initializing directory -- " + finalDownloadedPath); |
| 0 | 808 | | env.directory.InitializeDirectory(finalDownloadedPath, deleteIfExists); |
| 0 | 809 | | log.Info("Initializing directory -- " + settings.finalAssetBundlePath); |
| 0 | 810 | | env.directory.InitializeDirectory(settings.finalAssetBundlePath, deleteIfExists); |
| 0 | 811 | | } |
| | 812 | |
|
| | 813 | | internal void CleanupWorkingFolders() |
| | 814 | | { |
| 0 | 815 | | env.file.Delete(settings.finalAssetBundlePath + Config.ASSET_BUNDLE_FOLDER_NAME); |
| 0 | 816 | | env.file.Delete(settings.finalAssetBundlePath + Config.ASSET_BUNDLE_FOLDER_NAME + ".manifest"); |
| | 817 | |
|
| 0 | 818 | | if (settings.deleteDownloadPathAfterFinished) |
| | 819 | | { |
| 0 | 820 | | env.directory.Delete(finalDownloadedPath); |
| 0 | 821 | | env.assetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); |
| | 822 | | } |
| 0 | 823 | | } |
| | 824 | | } |
| | 825 | | } |