/// <summary> /// Exports the given texture to the destination texture path and wires up the preview surface. /// </summary> /// <returns> /// Returns the path to the USD texture object. /// </returns> protected static string SetupTexture(Scene scene, string usdShaderPath, Material material, PreviewSurfaceSample surface, string destTexturePath, string textureName, string textureOutput) { #if UNITY_EDITOR var srcPath = UnityEditor.AssetDatabase.GetAssetPath(material.GetTexture(textureName)); srcPath = srcPath.Substring("Assets/".Length); srcPath = Application.dataPath + "/" + srcPath; var fileName = System.IO.Path.GetFileName(srcPath); var filePath = System.IO.Path.Combine(destTexturePath, fileName); System.IO.File.Copy(srcPath, filePath, overwrite: true); // Make file path baked into USD relative to scene file and use forward slashes. filePath = ImporterBase.MakeRelativePath(scene.FilePath, filePath); filePath = filePath.Replace("\\", "/"); var uvReader = new PrimvarReaderSample <Vector2>(); uvReader.varname.defaultValue = new TfToken("st"); scene.Write(usdShaderPath + "/uvReader", uvReader); var tex = new TextureReaderSample(filePath, usdShaderPath + "/uvReader.outputs:result"); scene.Write(usdShaderPath + "/" + textureName, tex); return(usdShaderPath + "/" + textureName + ".outputs:" + textureOutput); #else // Not supported at run-time, too many things can go wrong // (can't encode compressed textures, etc). throw new System.Exception("Not supported at run-time"); #endif }
/// <summary> /// Ensure each layer path is expressed in the USD sub layer stack of the given scene, /// creating the sublayer USD files if needed. /// </summary> public void SaveLayerStack(Scene scene, string[] layerStack) { if (scene == null) { throw new NullReferenceException("Null scene provided to SaveLayerStack"); } SdfSubLayerProxy subLayers = scene.Stage.GetRootLayer().GetSubLayerPaths(); for (int i = 0; i < m_layerStack.Length; i++) { string absoluteLayerPath = m_layerStack[i]; string relativeLayerPath = ImporterBase.MakeRelativePath(scene.FilePath, absoluteLayerPath); if (!System.IO.File.Exists(absoluteLayerPath)) { var newSubLayer = Scene.Create(absoluteLayerPath); SetupNewSubLayer(scene, newSubLayer); newSubLayer.Save(); newSubLayer.Close(); } if (subLayers.Count(relativeLayerPath) == 0) { subLayers.push_back(relativeLayerPath); } } scene.Save(); }
/// <summary> /// Copy mesh data from USD to Unity with the given import options. /// </summary> public static void BuildMesh(string path, MeshSample usdMesh, GeometrySubsets geomSubsets, GameObject go, SceneImportOptions options, bool isDynamic) { var mf = ImporterBase.GetOrAddComponent <MeshFilter>(go); var mr = ImporterBase.GetOrAddComponent <MeshRenderer>(go); if (mf.sharedMesh == null) { mf.sharedMesh = new Mesh { name = UniqueMeshName(go.name) }; } // We only check if a mesh is dynamic when scene.IsPopulatingAccessMask is True. It only happens when a playable is // created, potentially way after mesh creation. if (isDynamic) { mf.sharedMesh.MarkDynamic(); } BuildMesh_(path, usdMesh, mf.sharedMesh, geomSubsets, go, mr, options); }
/// <summary> /// Copy camera data from USD to Unity with the given import options. /// </summary> public static void BuildCamera(CameraSample usdCamera, GameObject go, SceneImportOptions options) { var cam = ImporterBase.GetOrAddComponent <Camera>(go); usdCamera.CopyToCamera(cam, setTransform: false); cam.nearClipPlane *= options.scale; cam.farClipPlane *= options.scale; }
/// <summary> /// Writes overrides over the given scene. The given scene is referenced into the override /// scene being exported. /// </summary> /// <param name="sceneInWhichToStoreTransforms"></param> public void ExportOverrides(Scene sceneInWhichToStoreTransforms) { var sceneToReference = this; var overs = sceneInWhichToStoreTransforms; if (overs == null) { return; } var baseLayer = sceneToReference.GetScene(); if (baseLayer == null) { throw new Exception("Could not open base layer: " + sceneToReference.usdFullPath); } overs.Time = baseLayer.Time; overs.StartTime = baseLayer.StartTime; overs.EndTime = baseLayer.EndTime; overs.WriteMode = Scene.WriteModes.Over; overs.UpAxis = baseLayer.UpAxis; try { SceneExporter.Export(sceneToReference.gameObject, overs, BasisTransformation.SlowAndSafe, exportUnvarying: false, zeroRootTransform: true); var rel = ImporterBase.MakeRelativePath(overs.FilePath, sceneToReference.usdFullPath); GetFirstPrim(overs).GetReferences().AddReference(rel, GetFirstPrim(baseLayer).GetPath()); } catch (System.Exception ex) { Debug.LogException(ex); return; } finally { if (overs != null) { overs.Save(); overs.Close(); } } }
/// <summary> /// Copy mesh data from USD to Unity with the given import options, setup for skinning. /// </summary> public static void BuildSkinnedMesh(string path, MeshSample usdMesh, GeometrySubsets geomSubsets, GameObject go, SceneImportOptions options) { var smr = ImporterBase.GetOrAddComponent <SkinnedMeshRenderer>(go); if (smr.sharedMesh == null) { smr.sharedMesh = new Mesh(); } BuildMesh_(path, usdMesh, smr.sharedMesh, geomSubsets, go, smr, options); }
/// <summary> /// Build the root of a scene under which more USD data will be imported. If the handedness /// is changed here, no subsequent changes are required below, however the root will contain /// a negative scale. /// </summary> public static void BuildSceneRoot(Scene scene, Transform root, SceneImportOptions options) { var stageRoot = root.GetComponent <UsdAsset>(); bool newStageRoot = false; if (stageRoot == null) { stageRoot = root.gameObject.AddComponent <UsdAsset>(); stageRoot.usdFullPath = scene.FilePath; newStageRoot = true; ImporterBase.MoveComponentFirst(stageRoot); stageRoot.OptionsToState(options); } if (newStageRoot || options.changeHandedness != stageRoot.LastHandedness || options.scale != stageRoot.LastScale || options.forceRebuild) { var localScale = root.transform.localScale; var localRotation = root.transform.localRotation; if (options.forceRebuild) { localScale = Vector3.one; localRotation = Quaternion.identity; } else if (!newStageRoot) { // Undo the previous transforms. UndoRootTransform(scene, stageRoot, ref localScale, ref localRotation); } stageRoot.LastScale = options.scale; stageRoot.LastHandedness = options.changeHandedness; // Handle configurable up-axis (Y or Z). float invert = options.changeHandedness == BasisTransformation.FastWithNegativeScale ? -1 : 1; if (scene.UpAxis == Scene.UpAxes.Z) { localRotation *= Quaternion.AngleAxis(invert * 90, Vector3.right); } if (options.changeHandedness == BasisTransformation.FastWithNegativeScale) { // Convert from right-handed (USD) to left-handed (Unity). if (scene.UpAxis == Scene.UpAxes.Z) { localScale.y *= -1; } else { localScale.z *= -1; } } if (Mathf.Abs(options.scale - 1.0f) > 0.0001) { // Unilaterally setting the scale here is a little wrong, since it will stomp the root // object scale if set in Unity. localScale *= options.scale; } root.transform.localScale = localScale; root.transform.localRotation = localRotation; } }
/// <summary> /// Rebuilds the USD scene as Unity GameObjects, with a limited budget per update. /// </summary> public static IEnumerator BuildScene(Scene scene, GameObject root, SceneImportOptions importOptions, PrimMap primMap, float targetFrameMilliseconds, bool composingSubtree) { var timer = new System.Diagnostics.Stopwatch(); var usdPrimRoot = new pxr.SdfPath(importOptions.usdRootPath); // Setting an arbitrary fudge factor of 20% is very non-scientific, however it's better than // nothing. The correct way to hit a deadline is to predict how long each iteration actually // takes and then return early if the estimated time is over budget. float targetTime = targetFrameMilliseconds * .8f; timer.Start(); // Reconstruct the USD hierarchy as Unity GameObjects. // A PrimMap is returned for tracking the USD <-> Unity mapping. Profiler.BeginSample("USD: Build Hierarchy"); if (importOptions.importHierarchy || importOptions.forceRebuild) { // When a USD file is fully RE-imported, all exsiting USD data must be removed. The old // assumption was that the root would never have much more than the UsdAsset component // itself, however it's now clear that the root may also have meaningful USD data added // too. // // TODO(jcowles): This feels like a workaround. What we really want here is an "undo" // process for changes made to the root GameObject. For example, to clean up non-USD // components which may have been added (e.g. what if a mesh is imported to the root? // currently the MeshRenderer etc will remain after re-import). RemoveComponent <UsdAssemblyRoot>(root); RemoveComponent <UsdVariantSet>(root); RemoveComponent <UsdModelRoot>(root); RemoveComponent <UsdLayerStack>(root); RemoveComponent <UsdPayload>(root); RemoveComponent <UsdPrimSource>(root); primMap.Clear(); HierarchyBuilder.BuildGameObjects(scene, root, usdPrimRoot, scene.Find(usdPrimRoot.ToString(), "UsdSchemaBase"), primMap, importOptions); } Profiler.EndSample(); if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } Profiler.BeginSample("USD: Post Process Hierarchy"); foreach (var processor in root.GetComponents <IImportPostProcessHierarchy>()) { try { processor.PostProcessHierarchy(primMap, importOptions); } catch (System.Exception ex) { Debug.LogException(ex); } } Profiler.EndSample(); if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } // // Pre-process UsdSkelRoots. // var skelRoots = new List <pxr.UsdSkelRoot>(); if (importOptions.importSkinning) { Profiler.BeginSample("USD: Process UsdSkelRoots"); foreach (var path in primMap.SkelRoots) { try { var skelRootPrim = scene.GetPrimAtPath(path); if (!skelRootPrim) { Debug.LogWarning("SkelRoot prim not found: " + path); continue; } var skelRoot = new pxr.UsdSkelRoot(skelRootPrim); if (!skelRoot) { Debug.LogWarning("SkelRoot prim not SkelRoot type: " + path); continue; } skelRoots.Add(skelRoot); GameObject go = primMap[path]; ImporterBase.GetOrAddComponent <Animator>(go, true); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error pre-processing SkelRoot <" + path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } Profiler.EndSample(); } // // Import known prim types. // // Materials. Profiler.BeginSample("USD: Build Materials"); if (importOptions.ShouldBindMaterials) { foreach (var pathAndSample in scene.ReadAll <MaterialSample>(primMap.Materials)) { try { var mat = MaterialImporter.BuildMaterial(scene, pathAndSample.path, pathAndSample.sample, importOptions); if (mat != null) { importOptions.materialMap[pathAndSample.path] = mat; } } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing material <" + pathAndSample.path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } } Profiler.EndSample(); // // Start threads. // ReadAllJob <XformSample> readXforms; if (importOptions.importTransforms) { readXforms = new ReadAllJob <XformSample>(scene, primMap.Xforms); #if UNITY_2018_1_OR_NEWER readXforms.Schedule(primMap.Xforms.Length, 4); #else readXforms.Run(); #endif } if (importOptions.importMeshes) { ActiveMeshImporter.BeginReading(scene, primMap); } #if UNITY_2018_1_OR_NEWER JobHandle.ScheduleBatchedJobs(); #endif // Xforms. // // Note that we are specifically filtering on XformSample, not Xformable, this way only // Xforms are processed to avoid doing that work redundantly. if (importOptions.importTransforms) { Profiler.BeginSample("USD: Build Xforms"); foreach (var pathAndSample in readXforms) { try { if (pathAndSample.path == usdPrimRoot) { // Never read the xform from the USD root, that will be authored in Unity. continue; } GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing xform <" + pathAndSample.path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } foreach (var pathAndSample in scene.ReadAll <XformSample>(primMap.SkelRoots)) { try { if (pathAndSample.path == usdPrimRoot) { // Never read the xform from the USD root, that will be authored in Unity. continue; } GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing xform <" + pathAndSample.path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } if (importOptions.importSkinning) { foreach (var pathAndSample in scene.ReadAll <XformSample>(primMap.Skeletons)) { try { if (pathAndSample.path == usdPrimRoot) { // Never read the xform from the USD root, that will be authored in Unity. continue; } GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing xform <" + pathAndSample.path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } } Profiler.EndSample(); } // Meshes. if (importOptions.importMeshes) { Profiler.BeginSample("USD: Build Meshes"); IEnumerator it = ActiveMeshImporter.Import(scene, primMap, importOptions); while (it.MoveNext()) { if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } Profiler.EndSample(); // Cubes. Profiler.BeginSample("USD: Build Cubes"); foreach (var pathAndSample in scene.ReadAll <CubeSample>(primMap.Cubes)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); CubeImporter.BuildCube(pathAndSample.sample, go, importOptions); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing cube <" + pathAndSample.path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } Profiler.EndSample(); } // Cameras. if (importOptions.importCameras) { Profiler.BeginSample("USD: Cameras"); foreach (var pathAndSample in scene.ReadAll <CameraSample>(primMap.Cameras)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); // The camera has many value-type parameters that need to be handled correctly when not // not animated. For now, only the camera transform will animate, until this is fixed. if (scene.AccessMask == null || scene.IsPopulatingAccessMask) { CameraImporter.BuildCamera(pathAndSample.sample, go, importOptions); } } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing camera <" + pathAndSample.path + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } Profiler.EndSample(); } // Build out masters for instancing. Profiler.BeginSample("USD: Build Instances"); foreach (var masterRootPath in primMap.GetMasterRootPaths()) { try { Transform masterRootXf = primMap[masterRootPath].transform; // Transforms if (importOptions.importTransforms) { Profiler.BeginSample("USD: Build Xforms"); foreach (var pathAndSample in scene.ReadAll <XformSample>(masterRootPath)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing xform <" + pathAndSample.path + ">", ex)); } } foreach (var pathAndSample in scene.ReadAll <XformSample>(masterRootPath)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing xform <" + pathAndSample.path + ">", ex)); } } foreach (var pathAndSample in scene.ReadAll <XformSample>(primMap.Skeletons)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing xform <" + pathAndSample.path + ">", ex)); } } Profiler.EndSample(); } // Meshes. if (importOptions.importMeshes) { Profiler.BeginSample("USD: Build Meshes"); foreach (var pathAndSample in scene.ReadAll <MeshSample>(masterRootPath)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); var subsets = MeshImporter.ReadGeomSubsets(scene, pathAndSample.path); MeshImporter.BuildMesh(pathAndSample.path, pathAndSample.sample, subsets, go, importOptions); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing mesh <" + pathAndSample.path + ">", ex)); } } Profiler.EndSample(); // Cubes. Profiler.BeginSample("USD: Build Cubes"); foreach (var pathAndSample in scene.ReadAll <CubeSample>(masterRootPath)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); CubeImporter.BuildCube(pathAndSample.sample, go, importOptions); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing cube <" + pathAndSample.path + ">", ex)); } } Profiler.EndSample(); } // Cameras. if (importOptions.importCameras) { Profiler.BeginSample("USD: Build Cameras"); foreach (var pathAndSample in scene.ReadAll <CameraSample>(masterRootPath)) { try { GameObject go = primMap[pathAndSample.path]; NativeImporter.ImportObject(scene, go, scene.GetPrimAtPath(pathAndSample.path), importOptions); XformImporter.BuildXform(pathAndSample.path, pathAndSample.sample, go, importOptions, scene); CameraImporter.BuildCamera(pathAndSample.sample, go, importOptions); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing camera <" + pathAndSample.path + ">", ex)); } } Profiler.EndSample(); } } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing master <" + masterRootPath + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } // Instances. Profiler.EndSample(); // // Post-process dependencies: materials and bones. // Profiler.BeginSample("USD: Process Material Bindings"); try { // TODO: Currently ProcessMaterialBindings runs too long and will go over budget for any // large scene. However, pulling the loop into this code feels wrong in terms of // responsibilities. // Process all material bindings in a single vectorized request. MaterialImporter.ProcessMaterialBindings(scene, importOptions); } catch (System.Exception ex) { Debug.LogException(new ImportException("Failed in ProcessMaterialBindings", ex)); } Profiler.EndSample(); if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } // // SkinnedMesh bone bindings. // if (importOptions.importSkinning) { Profiler.BeginSample("USD: Build Skeletons"); var skeletonSamples = new Dictionary <pxr.SdfPath, SkeletonSample>(); foreach (var skelRoot in skelRoots) { try { var bindings = new pxr.UsdSkelBindingVector(); if (!primMap.SkelBindings.TryGetValue(skelRoot.GetPath(), out bindings)) { Debug.LogWarning("No bindings found skelRoot: " + skelRoot.GetPath()); } if (bindings.Count == 0) { Debug.LogWarning("No bindings found skelRoot: " + skelRoot.GetPath()); } foreach (var skelBinding in bindings) { // The SkelRoot will likely have a skeleton binding, but it's inherited, so the bound // skeleton isn't actually known until it's queried from the binding. Still, we would // like not to reprocess skeletons redundantly, so skeletons are cached into a // dictionary. Profiler.BeginSample("Build Bind Transforms"); var skelPath = skelBinding.GetSkeleton().GetPath(); SkeletonSample skelSample = null; if (!skeletonSamples.TryGetValue(skelPath, out skelSample)) { skelSample = new SkeletonSample(); Profiler.BeginSample("Read Skeleton"); scene.Read(skelPath, skelSample); Profiler.EndSample(); skeletonSamples.Add(skelPath, skelSample); // Unity uses the inverse bindTransform, since that's actually what's needed for // skinning. Do that once here, so each skinned mesh doesn't need to do it // redundantly. SkeletonImporter.BuildBindTransforms(skelPath, skelSample, importOptions); var bindXforms = new pxr.VtMatrix4dArray(); var prim = scene.GetPrimAtPath(skelPath); var skel = new pxr.UsdSkelSkeleton(prim); Profiler.BeginSample("Get SkelQuery"); pxr.UsdSkelSkeletonQuery skelQuery = primMap.SkelCache.GetSkelQuery(skel); Profiler.EndSample(); Profiler.BeginSample("Get JointWorldBind Transforms"); if (!skelQuery.GetJointWorldBindTransforms(bindXforms)) { throw new ImportException("Failed to compute binding trnsforms for <" + skelPath + ">"); } Profiler.EndSample(); SkeletonImporter.BuildDebugBindTransforms(skelSample, primMap[skelPath], importOptions); } Profiler.EndSample(); if (importOptions.importSkinWeights) { // // Apply skinning weights to each skinned mesh. // Profiler.BeginSample("Apply Skin Weights"); foreach (var skinningQuery in skelBinding.GetSkinningTargetsAsVector()) { var meshPath = skinningQuery.GetPrim().GetPath(); try { var skelBindingSample = new SkelBindingSample(); var goMesh = primMap[meshPath]; scene.Read(meshPath, skelBindingSample); Profiler.BeginSample("Build Skinned Mesh"); SkeletonImporter.BuildSkinnedMesh( meshPath, skelPath, skelSample, skelBindingSample, goMesh, primMap, importOptions); Profiler.EndSample(); // In terms of performance, this is almost free. goMesh.GetComponent <SkinnedMeshRenderer>().rootBone = primMap[skelPath].transform.GetChild(0); } catch (System.Exception ex) { Debug.LogException(new ImportException("Error skinning mesh: " + meshPath, ex)); } } Profiler.EndSample(); } } } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing SkelRoot <" + skelRoot.GetPath() + ">", ex)); } } // foreach SkelRoot Profiler.EndSample(); if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } // // Bone transforms. // Profiler.BeginSample("USD: Pose Bones"); foreach (var pathAndSample in skeletonSamples) { var skelPath = pathAndSample.Key; try { var prim = scene.GetPrimAtPath(skelPath); var skel = new pxr.UsdSkelSkeleton(prim); pxr.UsdSkelSkeletonQuery skelQuery = primMap.SkelCache.GetSkelQuery(skel); var joints = skelQuery.GetJointOrder(); var restXforms = new pxr.VtMatrix4dArray(); var time = scene.Time.HasValue ? scene.Time.Value : pxr.UsdTimeCode.Default(); Profiler.BeginSample("Compute Joint Local Transforms"); if (!skelQuery.ComputeJointLocalTransforms(restXforms, time, atRest: false)) { throw new ImportException("Failed to compute bind trnsforms for <" + skelPath + ">"); } Profiler.EndSample(); Profiler.BeginSample("Build Bones"); for (int i = 0; i < joints.size(); i++) { var jointPath = scene.GetSdfPath(joints[i]); if (joints[i] == "/") { jointPath = skelPath; } else if (jointPath.IsAbsolutePath()) { Debug.LogException(new System.Exception("Unexpected absolute joint path: " + jointPath)); jointPath = new pxr.SdfPath(joints[i].ToString().TrimStart('/')); jointPath = skelPath.AppendPath(jointPath); } else { jointPath = skelPath.AppendPath(jointPath); } var goBone = primMap[jointPath]; Profiler.BeginSample("Convert Matrix"); var restXform = UnityTypeConverter.FromMatrix(restXforms[i]); Profiler.EndSample(); Profiler.BeginSample("Build Bone"); SkeletonImporter.BuildSkeletonBone(skelPath, goBone, restXform, joints, importOptions); Profiler.EndSample(); } Profiler.EndSample(); } catch (System.Exception ex) { Debug.LogException( new ImportException("Error processing SkelRoot <" + skelPath + ">", ex)); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } Profiler.EndSample(); } // // Apply instancing. // if (importOptions.importSceneInstances) { Profiler.BeginSample("USD: Build Scene-Instances"); try { // Build scene instances. InstanceImporter.BuildSceneInstances(primMap, importOptions); } catch (System.Exception ex) { Debug.LogException(new ImportException("Failed in BuildSceneInstances", ex)); } Profiler.EndSample(); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } // Build point instances. if (importOptions.importPointInstances) { Profiler.BeginSample("USD: Build Point-Instances"); // TODO: right now all point instancer data is read, but we only need prototypes and indices. foreach (var pathAndSample in scene.ReadAll <PointInstancerSample>()) { try { GameObject instancerGo = primMap[pathAndSample.path]; // Now build the point instances. InstanceImporter.BuildPointInstances(scene, primMap, pathAndSample.path, pathAndSample.sample, instancerGo, importOptions); } catch (System.Exception ex) { Debug.LogError("Error processing point instancer <" + pathAndSample.path + ">: " + ex.Message); } if (ShouldYield(targetTime, timer)) { yield return(null); ResetTimer(timer); } } Profiler.EndSample(); } // // Apply root transform corrections. // Profiler.BeginSample("USD: Build Root Transforms"); if (!composingSubtree) { if (!root) { // There is no single root, // Apply root transform corrections to all imported root prims. foreach (KeyValuePair <pxr.SdfPath, GameObject> kvp in primMap) { if (kvp.Key.IsRootPrimPath() && kvp.Value != null) { // The root object at which the USD scene will be reconstructed. // It may need a Z-up to Y-up conversion and a right- to left-handed change of basis. XformImporter.BuildSceneRoot(scene, kvp.Value.transform, importOptions); } } } else { // There is only one root, apply a single transform correction. XformImporter.BuildSceneRoot(scene, root.transform, importOptions); } } Profiler.EndSample(); Profiler.BeginSample("USD: Post Process Components"); foreach (var processor in root.GetComponents <IImportPostProcessComponents>()) { try { processor.PostProcessComponents(primMap, importOptions); } catch (System.Exception ex) { Debug.LogException(ex); } } Profiler.EndSample(); }
/// <summary> /// Copy sphere data from USD to Unity with the given import options. /// </summary> /// <param name="skinnedMesh"> /// Whether the Cube to build is skinned or not. This will allow to determine which Renderer to create /// on the GameObject (MeshRenderer or SkinnedMeshRenderer). Default value is false (not skinned). /// </param> public static void BuildSphere(SphereSample usdSphere, GameObject go, SceneImportOptions options, bool skinnedMesh = false) { Material mat = null; var sphereGo = GameObject.CreatePrimitive(PrimitiveType.Sphere); var unityMesh = sphereGo.GetComponent <MeshFilter>().sharedMesh; GameObject.DestroyImmediate(sphereGo); // Because Unity only handle a sphere with a default size, the custom size of it is define by the localScale // transform. This also need to be taken into account while computing the Unity extent of the mesh (see bellow). // This is doable because xformable data are always handled before mesh data, so go.transform already // contains any transform of the geometry. float size = (float)usdSphere.radius * 2; go.transform.localScale = go.transform.localScale * size; bool changeHandedness = options.changeHandedness == BasisTransformation.SlowAndSafe; bool hasBounds = usdSphere.extent.size.x > 0 || usdSphere.extent.size.y > 0 || usdSphere.extent.size.z > 0; if (ShouldImport(options.meshOptions.boundingBox) && hasBounds) { if (changeHandedness) { usdSphere.extent.center = UnityTypeConverter.ChangeBasis(usdSphere.extent.center); // Divide the extent by the size of the cube. A custom size of the extent is define by // the localScale transform (see above). usdSphere.extent.extents = UnityTypeConverter.ChangeBasis(usdSphere.extent.extents) / size; } unityMesh.bounds = usdSphere.extent; } else if (ShouldCompute(options.meshOptions.boundingBox)) { unityMesh.RecalculateBounds(); } if (usdSphere.colors != null && ShouldImport(options.meshOptions.color)) { // NOTE: The following color conversion assumes PlayerSettings.ColorSpace == Linear. // For best performance, convert color space to linear off-line and skip conversion. if (usdSphere.colors.Length == 1) { // Constant color can just be set on the material. mat = options.materialMap.InstantiateSolidColor(usdSphere.colors[0].gamma); } else { // TODO: Improve logging by adding the path to the sphere prim. This would require that SphereSample // (and SampleBase class in general) allow to get the UsdPrim back and it's path in the stage. Debug.LogWarning( "Only constant color are supported for sphere: (can't handle " + usdSphere.colors.Length + " color values)" ); } } if (mat == null) { mat = options.materialMap.InstantiateSolidColor(Color.white); } // Create Unity mesh. // TODO: This code is a duplicate of the CubeImporter code. It requires refactoring. Renderer renderer; if (skinnedMesh) { SkinnedMeshRenderer skinnedRenderer = ImporterBase.GetOrAddComponent <SkinnedMeshRenderer>(go); if (skinnedRenderer.sharedMesh == null) { skinnedRenderer.sharedMesh = Mesh.Instantiate(unityMesh); } renderer = skinnedRenderer; } else { renderer = ImporterBase.GetOrAddComponent <MeshRenderer>(go); MeshFilter meshFilter = ImporterBase.GetOrAddComponent <MeshFilter>(go); if (meshFilter.sharedMesh == null) { meshFilter.sharedMesh = Mesh.Instantiate(unityMesh); } } if (unityMesh.subMeshCount == 1) { renderer.sharedMaterial = mat; } else { var mats = new Material[unityMesh.subMeshCount]; for (int i = 0; i < mats.Length; i++) { mats[i] = mat; } renderer.sharedMaterials = mats; } }
public static void BuildSkinnedMesh(string meshPath, string skelPath, SkeletonSample skeleton, SkelBindingSample meshBinding, GameObject go, PrimMap primMap, SceneImportOptions options) { string[] joints = meshBinding.joints; // WARNING: Do not mutate skeleton values. string[] skelJoints = skeleton.joints; bool isConstant = meshBinding.jointWeights.interpolation == PrimvarInterpolation.Constant; if (joints == null || joints.Length == 0) { if (skelJoints == null || skelJoints.Length == 0) { throw new Exception("Joints array empty: " + meshPath); } else { joints = skelJoints; } } // The mesh renderer must already exist, since hte mesh also must already exist. var smr = go.GetComponent <SkinnedMeshRenderer>(); if (!smr) { throw new Exception("Error importing " + meshPath + " SkinnnedMeshRenderer not present on GameObject"); } var mesh = smr.sharedMesh; var geomXf = meshBinding.geomBindTransform.value; // If the joints list is a different length than the bind transforms, then this is likely // a mesh using a subset of the total bones in the skeleton and the bindTransforms must be // reconstructed. var bindPoses = skeleton.bindTransforms; if (!JointsMatch(skeleton.joints, joints)) { var boneToPose = new Dictionary <string, Matrix4x4>(); bindPoses = new Matrix4x4[joints.Length]; for (int i = 0; i < skelJoints.Length; i++) { boneToPose[skelJoints[i]] = skeleton.bindTransforms[i]; } for (int i = 0; i < joints.Length; i++) { bindPoses[i] = boneToPose[joints[i]]; } } // When geomXf is identity, we can take a shortcut and just use the exact skeleton bindPoses. if (!ImporterBase.ApproximatelyEqual(geomXf, Matrix4x4.identity)) { // Note that the bind poses were transformed when the skeleton was imported, but the // geomBindTransform is per-mesh, so it must be transformed here so it is in the same space // as the bind pose. XformImporter.ImportXform(ref geomXf, options); // Make a copy only if we haven't already copied the bind poses earlier. if (bindPoses == skeleton.bindTransforms) { var newBindPoses = new Matrix4x4[skeleton.bindTransforms.Length]; Array.Copy(bindPoses, newBindPoses, bindPoses.Length); bindPoses = newBindPoses; } // Concatenate the geometry bind transform with the skeleton bind poses. for (int i = 0; i < bindPoses.Length; i++) { // The geometry transform should be applied to the points before any other transform, // hence the right hand multiply here. bindPoses[i] = bindPoses[i] * geomXf; } } mesh.bindposes = bindPoses; var bones = new Transform[joints.Length]; var sdfSkelPath = new SdfPath(skelPath); for (int i = 0; i < joints.Length; i++) { var jointPath = new SdfPath(joints[i]); if (joints[i] == "/") { jointPath = sdfSkelPath; } else if (jointPath.IsAbsolutePath()) { Debug.LogException(new Exception("Unexpected absolute joint path: " + jointPath)); jointPath = new SdfPath(joints[i].TrimStart('/')); jointPath = sdfSkelPath.AppendPath(jointPath); } else { jointPath = sdfSkelPath.AppendPath(jointPath); } var jointGo = primMap[jointPath]; if (!jointGo) { Debug.LogError("Error importing " + meshPath + " " + "Joint not found: " + joints[i]); continue; } bones[i] = jointGo.transform; } smr.bones = bones; int[] indices = meshBinding.jointIndices.value; float[] weights = meshBinding.jointWeights.value; // Unity 2019 supports many-bone rigs, older versions of Unity only support four bones. #if UNITY_2019 var bonesPerVertex = new NativeArray <byte>(mesh.vertexCount, Allocator.Persistent); var boneWeights1 = new NativeArray <BoneWeight1>(mesh.vertexCount * meshBinding.jointWeights.elementSize, Allocator.Persistent); for (int i = 0; i < mesh.vertexCount; i++) { int unityIndex = i * meshBinding.jointWeights.elementSize; int usdIndex = isConstant ? 0 : unityIndex; bonesPerVertex[i] = (byte)meshBinding.jointWeights.elementSize; for (int wi = 0; wi < meshBinding.jointWeights.elementSize; wi++) { var bw = boneWeights1[unityIndex + wi]; bw.boneIndex = indices[usdIndex + wi]; bw.weight = weights[usdIndex + wi]; boneWeights1[unityIndex + wi] = bw; } } mesh.SetBoneWeights(bonesPerVertex, boneWeights1); bonesPerVertex.Dispose(); boneWeights1.Dispose(); #else var boneWeights = new BoneWeight[mesh.vertexCount]; for (int i = 0; i < boneWeights.Length; i++) { // When interpolation is constant, the base usdIndex should always be zero. // When non-constant, the offset is the index times the number of weights per vertex. int usdIndex = isConstant ? 0 : i * meshBinding.jointWeights.elementSize; var boneWeight = boneWeights[i]; if (usdIndex >= indices.Length) { Debug.Log("UsdIndex out of bounds: " + usdIndex + " indices.Length: " + indices.Length + " boneWeights.Length: " + boneWeights.Length + " mesh: " + meshPath); } boneWeight.boneIndex0 = indices[usdIndex]; boneWeight.weight0 = weights[usdIndex]; if (meshBinding.jointIndices.elementSize >= 2) { boneWeight.boneIndex1 = indices[usdIndex + 1]; boneWeight.weight1 = weights[usdIndex + 1]; } if (meshBinding.jointIndices.elementSize >= 3) { boneWeight.boneIndex2 = indices[usdIndex + 2]; boneWeight.weight2 = weights[usdIndex + 2]; } if (meshBinding.jointIndices.elementSize >= 4) { boneWeight.boneIndex3 = indices[usdIndex + 3]; boneWeight.weight3 = weights[usdIndex + 3]; } // If weights are less than 1, Unity will not automatically renormalize. // If weights are greater than 1, Unity will renormalize. // Only normalize when less than one to make it easier to diff bone weights which were // round-tripped and were being normalized by Unity. float sum = boneWeight.weight0 + boneWeight.weight1 + boneWeight.weight2 + boneWeight.weight3; if (sum < 1) { boneWeight.weight0 /= sum; boneWeight.weight1 /= sum; boneWeight.weight2 /= sum; boneWeight.weight3 /= sum; } boneWeights[i] = boneWeight; } mesh.boneWeights = boneWeights; #endif }
public static void BuildSkinnedMesh(string meshPath, string skelPath, SkeletonSample skeleton, UsdSkelSkinningQuery skinningQuery, GameObject go, PrimMap primMap, SceneImportOptions options) { // The mesh renderer must already exist, since hte mesh also must already exist. var smr = go.GetComponent <SkinnedMeshRenderer>(); if (!smr) { throw new Exception( "Error importing " + meshPath + " SkinnnedMeshRenderer not present on GameObject" ); } // Get and validate the joint weights and indices informations. UsdGeomPrimvar jointWeights = skinningQuery.GetJointWeightsPrimvar(); UsdGeomPrimvar jointIndices = skinningQuery.GetJointIndicesPrimvar(); if (!jointWeights.IsDefined() || !jointIndices.IsDefined()) { throw new Exception("Joints information (indices and/or weights) are missing for: " + meshPath); } // TODO: Both indices and weights attributes can be animated. It's not handled yet. // TODO: Having something that convert a UsdGeomPrimvar into a PrimvarSample could help simplify this code. int[] indices = IntrinsicTypeConverter.FromVtArray((VtIntArray)jointIndices.GetAttr().Get()); int indicesElementSize = jointIndices.GetElementSize(); pxr.TfToken indicesInterpolation = jointIndices.GetInterpolation(); if (indices.Length == 0 || indicesElementSize == 0 || indices.Length % indicesElementSize != 0 || !pxr.UsdGeomPrimvar.IsValidInterpolation(indicesInterpolation)) { throw new Exception("Joint indices information are invalid or empty for: " + meshPath); } float[] weights = IntrinsicTypeConverter.FromVtArray((VtFloatArray)jointWeights.GetAttr().Get()); int weightsElementSize = jointWeights.GetElementSize(); pxr.TfToken weightsInterpolation = jointWeights.GetInterpolation(); if (weights.Length == 0 || weightsElementSize == 0 || weights.Length % weightsElementSize != 0 || !pxr.UsdGeomPrimvar.IsValidInterpolation(weightsInterpolation)) { throw new Exception("Joints weights information are invalid or empty for: " + meshPath); } // Get and validate the local list of joints. VtTokenArray jointsAttr = new VtTokenArray(); skinningQuery.GetJointOrder(jointsAttr); // If jointsAttr wasn't define, GetJointOrder return an empty array and FromVtArray as well. string[] joints = IntrinsicTypeConverter.FromVtArray(jointsAttr); // WARNING: Do not mutate skeleton values. string[] skelJoints = skeleton.joints; if (joints == null || joints.Length == 0) { if (skelJoints == null || skelJoints.Length == 0) { throw new Exception("Joints array empty: " + meshPath); } else { joints = skelJoints; } } var mesh = smr.sharedMesh; // TODO: bind transform attribute can be animated. It's not handled yet. Matrix4x4 geomXf = UnityTypeConverter.FromMatrix(skinningQuery.GetGeomBindTransform()); // If the joints list is a different length than the bind transforms, then this is likely // a mesh using a subset of the total bones in the skeleton and the bindTransforms must be // reconstructed. var bindPoses = skeleton.bindTransforms; if (!JointsMatch(skeleton.joints, joints)) { var boneToPose = new Dictionary <string, Matrix4x4>(); bindPoses = new Matrix4x4[joints.Length]; for (int i = 0; i < skelJoints.Length; i++) { boneToPose[skelJoints[i]] = skeleton.bindTransforms[i]; } for (int i = 0; i < joints.Length; i++) { bindPoses[i] = boneToPose[joints[i]]; } } // When geomXf is identity, we can take a shortcut and just use the exact skeleton bindPoses. if (!ImporterBase.ApproximatelyEqual(geomXf, Matrix4x4.identity)) { // Note that the bind poses were transformed when the skeleton was imported, but the // geomBindTransform is per-mesh, so it must be transformed here so it is in the same space // as the bind pose. XformImporter.ImportXform(ref geomXf, options); // Make a copy only if we haven't already copied the bind poses earlier. if (bindPoses == skeleton.bindTransforms) { var newBindPoses = new Matrix4x4[skeleton.bindTransforms.Length]; Array.Copy(bindPoses, newBindPoses, bindPoses.Length); bindPoses = newBindPoses; } // Concatenate the geometry bind transform with the skeleton bind poses. for (int i = 0; i < bindPoses.Length; i++) { // The geometry transform should be applied to the points before any other transform, // hence the right hand multiply here. bindPoses[i] = bindPoses[i] * geomXf; } } mesh.bindposes = bindPoses; var bones = new Transform[joints.Length]; var sdfSkelPath = new SdfPath(skelPath); for (int i = 0; i < joints.Length; i++) { var jointPath = new SdfPath(joints[i]); if (joints[i] == "/") { jointPath = sdfSkelPath; } else if (jointPath.IsAbsolutePath()) { Debug.LogException(new Exception("Unexpected absolute joint path: " + jointPath)); jointPath = new SdfPath(joints[i].TrimStart('/')); jointPath = sdfSkelPath.AppendPath(jointPath); } else { jointPath = sdfSkelPath.AppendPath(jointPath); } var jointGo = primMap[jointPath]; if (!jointGo) { Debug.LogError("Error importing " + meshPath + " " + "Joint not found: " + joints[i]); continue; } bones[i] = jointGo.transform; } smr.bones = bones; bool isConstant = weightsInterpolation.GetString() == pxr.UsdGeomTokens.constant; // Unity 2019 supports many-bone rigs, older versions of Unity only support four bones. #if UNITY_2019 var bonesPerVertex = new NativeArray <byte>(mesh.vertexCount, Allocator.Persistent); var boneWeights1 = new NativeArray <BoneWeight1>(mesh.vertexCount * weightsElementSize, Allocator.Persistent); for (int i = 0; i < mesh.vertexCount; i++) { int unityIndex = i * weightsElementSize; int usdIndex = isConstant ? 0 : unityIndex; bonesPerVertex[i] = (byte)weightsElementSize; for (int wi = 0; wi < weightsElementSize; wi++) { var bw = boneWeights1[unityIndex + wi]; bw.boneIndex = indices[usdIndex + wi]; bw.weight = weights[usdIndex + wi]; boneWeights1[unityIndex + wi] = bw; } } // TODO: Investigate if bone weights should be normalized before this line. mesh.SetBoneWeights(bonesPerVertex, boneWeights1); bonesPerVertex.Dispose(); boneWeights1.Dispose(); #else var boneWeights = new BoneWeight[mesh.vertexCount]; for (int i = 0; i < boneWeights.Length; i++) { // When interpolation is constant, the base usdIndex should always be zero. // When non-constant, the offset is the index times the number of weights per vertex. int usdIndex = isConstant ? 0 : i * weightsElementSize; var boneWeight = boneWeights[i]; if (usdIndex >= indices.Length) { Debug.Log("UsdIndex out of bounds: " + usdIndex + " indices.Length: " + indices.Length + " boneWeights.Length: " + boneWeights.Length + " mesh: " + meshPath); } boneWeight.boneIndex0 = indices[usdIndex]; boneWeight.weight0 = weights[usdIndex]; if (indicesElementSize >= 2) { boneWeight.boneIndex1 = indices[usdIndex + 1]; boneWeight.weight1 = weights[usdIndex + 1]; } if (indicesElementSize >= 3) { boneWeight.boneIndex2 = indices[usdIndex + 2]; boneWeight.weight2 = weights[usdIndex + 2]; } if (indicesElementSize >= 4) { boneWeight.boneIndex3 = indices[usdIndex + 3]; boneWeight.weight3 = weights[usdIndex + 3]; } // If weights are less than 1, Unity will not automatically renormalize. // If weights are greater than 1, Unity will renormalize. // Only normalize when less than one to make it easier to diff bone weights which were // round-tripped and were being normalized by Unity. float sum = boneWeight.weight0 + boneWeight.weight1 + boneWeight.weight2 + boneWeight.weight3; if (sum < 1) { boneWeight.weight0 /= sum; boneWeight.weight1 /= sum; boneWeight.weight2 /= sum; boneWeight.weight3 /= sum; } boneWeights[i] = boneWeight; } mesh.boneWeights = boneWeights; #endif }
/// <summary> /// Copy cube data from USD to Unity with the given import options. /// </summary> /// <param name="skinnedMesh"> /// Whether the Cube to build is skinned or not. This will allow to determine which Renderer to create /// on the GameObject (MeshRenderer or SkinnedMeshRenderer). Default value is false (not skinned). /// </param> public static void BuildCube(CubeSample usdCube, GameObject go, SceneImportOptions options, bool skinnedMesh = false) { Material mat = null; var cubeGo = GameObject.CreatePrimitive(PrimitiveType.Cube); var unityMesh = cubeGo.GetComponent <MeshFilter>().sharedMesh; GameObject.DestroyImmediate(cubeGo); // Because Unity only handle a cube with a default size, the custom size of it is define by the localScale // transform. This also need to be taken into account while computing the Unity extent of the mesh (see bellow). // This is doable because xformable data are always handled before mesh data, so go.transform already // contains any transform of the geometry. float size = (float)usdCube.size; go.transform.localScale = go.transform.localScale * size; bool changeHandedness = options.changeHandedness == BasisTransformation.SlowAndSafe; bool hasBounds = usdCube.extent.size.x > 0 || usdCube.extent.size.y > 0 || usdCube.extent.size.z > 0; if (ShouldImport(options.meshOptions.boundingBox) && hasBounds) { if (changeHandedness) { usdCube.extent.center = UnityTypeConverter.ChangeBasis(usdCube.extent.center); // Divide the extent by the size of the cube. A custom size of the extent is define by // the localScale transform (see above). usdCube.extent.extents = UnityTypeConverter.ChangeBasis(usdCube.extent.extents) / size; } unityMesh.bounds = usdCube.extent; } else if (ShouldCompute(options.meshOptions.boundingBox)) { unityMesh.RecalculateBounds(); } if (usdCube.colors != null && ShouldImport(options.meshOptions.color)) { // NOTE: The following color conversion assumes PlayerSettings.ColorSpace == Linear. // For best performance, convert color space to linear off-line and skip conversion. if (usdCube.colors.Length == 1) { // Constant color can just be set on the material. mat = options.materialMap.InstantiateSolidColor(usdCube.colors[0].gamma); Debug.Log("constant colors assigned"); } else if (usdCube.colors.Length == 6) { // Uniform colors to verts. // Note that USD cubes have 6 uniform colors and Unity cube mesh has 24 (6*4) // TODO: move the conversion to C++ and use the color management API. Debug.Log(unityMesh.vertexCount); for (int i = 0; i < usdCube.colors.Length; i++) { usdCube.colors[i] = usdCube.colors[i]; } var unityColors = new Color[24]; // Front:0, Back:1, Top:2, Bottom:3, Right:4, Left:5 unityColors[0] = usdCube.colors[0]; // front bottom right unityColors[1] = usdCube.colors[0]; // front bottom left unityColors[2] = usdCube.colors[0]; // front top right unityColors[3] = usdCube.colors[0]; // front top left unityColors[4] = usdCube.colors[2]; // top back right unityColors[5] = usdCube.colors[2]; // top back left unityColors[6] = usdCube.colors[1]; // back bottom right unityColors[7] = usdCube.colors[1]; // back bottom left unityColors[8] = usdCube.colors[2]; // top front right unityColors[9] = usdCube.colors[2]; // top front left unityColors[10] = usdCube.colors[1]; // back top right unityColors[11] = usdCube.colors[1]; // back top left unityColors[12] = usdCube.colors[3]; // Bottom back right unityColors[13] = usdCube.colors[3]; // Bottom front right unityColors[14] = usdCube.colors[3]; // Bottom front left unityColors[15] = usdCube.colors[3]; // Bottom back left unityColors[16] = usdCube.colors[5]; // left front bottom unityColors[17] = usdCube.colors[5]; // left front top unityColors[18] = usdCube.colors[5]; // left back top unityColors[19] = usdCube.colors[5]; // left back bottom unityColors[20] = usdCube.colors[4]; // right back bottom unityColors[21] = usdCube.colors[4]; // right back top unityColors[22] = usdCube.colors[4]; // right front top unityColors[23] = usdCube.colors[4]; // right front bottom unityMesh.colors = unityColors; } else if (usdCube.colors.Length == 24) { // Face varying colors to verts. // Note that USD cubes have 24 face varying colors and Unity cube mesh has 24 (6*4) // TODO: move the conversion to C++ and use the color management API. Debug.Log(unityMesh.vertexCount); for (int i = 0; i < usdCube.colors.Length; i++) { usdCube.colors[i] = usdCube.colors[i]; } // USD order: front, back, top, bottom, right, left var unityColors = new Color[24]; unityColors[0] = usdCube.colors[3]; // front bottom right unityColors[1] = usdCube.colors[2]; // front bottom left unityColors[2] = usdCube.colors[0]; // front top right unityColors[3] = usdCube.colors[1]; // front top left unityColors[4] = usdCube.colors[8 + 1]; // top back right unityColors[5] = usdCube.colors[8 + 2]; // top back left unityColors[6] = usdCube.colors[4 + 3]; // back bottom right unityColors[7] = usdCube.colors[4 + 0]; // back bottom left unityColors[8] = usdCube.colors[8 + 0]; // top front right unityColors[9] = usdCube.colors[8 + 3]; // top front left unityColors[10] = usdCube.colors[4 + 2]; // back top right unityColors[11] = usdCube.colors[4 + 1]; // back top left unityColors[12] = usdCube.colors[12 + 1]; // Bottom back right unityColors[13] = usdCube.colors[12 + 2]; // Bottom front right unityColors[14] = usdCube.colors[12 + 3]; // Bottom front left unityColors[15] = usdCube.colors[12 + 0]; // Bottom back left unityColors[16] = usdCube.colors[20 + 1]; // left front bottom unityColors[17] = usdCube.colors[20 + 2]; // left front top unityColors[18] = usdCube.colors[20 + 3]; // left back top unityColors[19] = usdCube.colors[20 + 0]; // left back bottom unityColors[20] = usdCube.colors[16 + 2]; // right back bottom unityColors[21] = usdCube.colors[16 + 3]; // right back top unityColors[22] = usdCube.colors[16 + 0]; // right front top unityColors[23] = usdCube.colors[16 + 1]; // right front bottom unityMesh.colors = unityColors; } else if (usdCube.colors.Length == 8) { // Vertex colors map on to verts. // Note that USD cubes have 8 verts but Unity cube mesh has 24 (6*4) // TODO: move the conversion to C++ and use the color management API. Debug.Log(unityMesh.vertexCount); for (int i = 0; i < usdCube.colors.Length; i++) { usdCube.colors[i] = usdCube.colors[i]; } // USD order: front (top-right -> ccw) // back (bottom-left -> ccw (from back perspective)) var unityColors = new Color[24]; unityColors[0] = usdCube.colors[3]; // front bottom right unityColors[1] = usdCube.colors[2]; // front bottom left unityColors[2] = usdCube.colors[0]; // front top right unityColors[3] = usdCube.colors[1]; // front top left unityColors[4] = usdCube.colors[6]; // top back right unityColors[5] = usdCube.colors[5]; // top back left unityColors[6] = usdCube.colors[7]; // back bottom right unityColors[7] = usdCube.colors[4]; // back bottom left unityColors[8] = usdCube.colors[0]; // top front right unityColors[9] = usdCube.colors[1]; // top front left unityColors[10] = usdCube.colors[6]; // back top right unityColors[11] = usdCube.colors[5]; // back top left unityColors[12] = usdCube.colors[7]; // Bottom back right unityColors[13] = usdCube.colors[3]; // Bottom front right unityColors[14] = usdCube.colors[2]; // Bottom front left unityColors[15] = usdCube.colors[4]; // Bottom back left unityColors[16] = usdCube.colors[2]; // left front bottom unityColors[17] = usdCube.colors[1]; // left front top unityColors[18] = usdCube.colors[5]; // left back top unityColors[19] = usdCube.colors[4]; // left back bottom unityColors[20] = usdCube.colors[7]; // right back bottom unityColors[21] = usdCube.colors[6]; // right back top unityColors[22] = usdCube.colors[0]; // right front top unityColors[23] = usdCube.colors[3]; // right front bottom unityMesh.colors = unityColors; Debug.Log("vertex colors assigned"); } else { // FaceVarying and uniform both require breaking up the mesh and are not yet handled in // this example. Debug.LogWarning("Uniform (color per face) and FaceVarying (color per vert per face) " + "display color not supported in this example"); } } if (mat == null) { mat = options.materialMap.InstantiateSolidColor(Color.white); } // Create Unity mesh. Renderer renderer; if (skinnedMesh) { SkinnedMeshRenderer skinnedRenderer = ImporterBase.GetOrAddComponent <SkinnedMeshRenderer>(go); if (skinnedRenderer.sharedMesh == null) { skinnedRenderer.sharedMesh = Mesh.Instantiate(unityMesh); } renderer = skinnedRenderer; } else { renderer = ImporterBase.GetOrAddComponent <MeshRenderer>(go); MeshFilter meshFilter = ImporterBase.GetOrAddComponent <MeshFilter>(go); if (meshFilter.sharedMesh == null) { meshFilter.sharedMesh = Mesh.Instantiate(unityMesh); } } if (unityMesh.subMeshCount == 1) { renderer.sharedMaterial = mat; } else { var mats = new Material[unityMesh.subMeshCount]; for (int i = 0; i < mats.Length; i++) { mats[i] = mat; } renderer.sharedMaterials = mats; } }
/// <summary> /// Exports the given texture to the destination texture path and wires up the preview surface. /// </summary> /// <returns> /// Returns the path to the USD texture object. /// </returns> protected static string SetupTexture(Scene scene, string usdShaderPath, Material material, PreviewSurfaceSample surface, Vector4 scale, string destTexturePath, string textureName, string textureOutput, ConversionType conversionType = ConversionType.None) { // We have to handle multiple cases here: // - file exists on disk // - file is a supported format => can be directly copied // - file is not in a supported format => need to blit / export // - file is only in memory // - a Texture2D // - a Texture // - a RenderTexture // - needs special care if marked as Normal Map // (can probably only be detected in an Editor context, and heuristically at runtime) // => need to blit / export // - file is not supported at all (or not yet) // - a 3D texture // => needs to be ignored, log Warning bool textureIsExported = false; string filePath = null; string fileName = null; var srcTexture2d = material.GetTexture(textureName); bool needsConversion = false; switch (conversionType) { case ConversionType.None: break; case ConversionType.UnpackNormal: #if UNITY_EDITOR if (UnityEditor.AssetDatabase.Contains(srcTexture2d)) { // normal needs to be converted if the one on disk isn't really a normal map // (e.g. created from greyscale) UnityEditor.TextureImporter importer = (UnityEditor.TextureImporter)UnityEditor.AssetImporter.GetAtPath( UnityEditor.AssetDatabase.GetAssetPath(srcTexture2d)); if (importer.textureType != UnityEditor.TextureImporterType.NormalMap) { Debug.LogWarning("Texture " + textureName + " is set as NormalMap but isn't marked as such", srcTexture2d); } UnityEditor.TextureImporterSettings dst = new UnityEditor.TextureImporterSettings(); importer.ReadTextureSettings(dst); // if this NormalMap is created from greyscale we will export the NormalMap from memory. if (dst.convertToNormalMap) { needsConversion = true; break; } } #endif break; default: needsConversion = true; break; } #if UNITY_EDITOR // only export from disk if there's no need to do any type of data conversion here if (!needsConversion) { var srcPath = UnityEditor.AssetDatabase.GetAssetPath(srcTexture2d); if (!string.IsNullOrEmpty(srcPath)) { #if UNITY_2019_2_OR_GREATER // Since textures might be inside of packages for various reasons we should support that. // Usually this would just be "Path.GetFullPath(srcPath)", but USD export messes with the CWD (Working Directory) // and so we have to do a bit more path wrangling here. if (srcPath.StartsWith("Packages")) { var pi = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(srcPath); srcPath = pi.resolvedPath + srcPath.Substring(("Packages/" + pi.name).Length); } #endif if (srcPath.StartsWith("Assets")) { srcPath = Application.dataPath + "/" + srcPath.Substring("Assets/".Length); } fileName = System.IO.Path.GetFileName(srcPath); filePath = System.IO.Path.Combine(destTexturePath, fileName); if (System.IO.File.Exists(srcPath)) { // USDZ officially only supports png / jpg / jpeg // https://graphics.pixar.com/usd/docs/Usdz-File-Format-Specification.html var ext = System.IO.Path.GetExtension(srcPath).ToLowerInvariant(); if (ext == ".png" || ext == ".jpg" || ext == ".jpeg") { System.IO.File.Copy(srcPath, filePath, overwrite: true); if (System.IO.File.Exists(filePath)) { textureIsExported = true; } } } } } #endif if (!textureIsExported) { // Since this is a texture we can't directly export from disk, we need to blit it and output it as PNG. // To avoid collisions, e.g. with multiple different textures named the same, each texture gets a pseudo-random name. // This will also avoid collisions when exporting multiple models to the same folder, e.g. with a a RenderTexture called "RT" // in each of them that might look different between exports. // TODO Future work could, if necessary, generate a texture content hash to avoid exporting identical textures multiple times // (Unity's content hash isn't reliable for some types of textures unfortunately, e.g. RTs) #if UNITY_EDITOR if (srcTexture2d is Texture2D) { fileName = srcTexture2d.name + "_" + srcTexture2d.imageContentsHash.ToString(); } else { fileName = srcTexture2d.name + "_" + Random.Range(10000000, 99999999).ToString(); } #else fileName = srcTexture2d.name + "_" + Random.Range(10000000, 99999999).ToString(); #endif filePath = System.IO.Path.Combine(destTexturePath, fileName + ".png"); // TODO extra care has to be taken of Normal Maps etc., since these are in a converted format in memory (for example 16 bit AG instead of 8 bit RGBA, depending on platform) // An example of this conversion in a shader is in Khronos' UnityGLTF implementation. // Basically, the blit has do be done with the right unlit conversion shader to get a proper "file-based" tangent space normal map back // Blit the texture and get it back to CPU // Note: Can't use RenderTexture.GetTemporary because that doesn't properly clear alpha channel bool preserveLinear = false; switch (conversionType) { case ConversionType.UnpackNormal: preserveLinear = true; break; } var rt = new RenderTexture(srcTexture2d.width, srcTexture2d.height, 0, RenderTextureFormat.ARGB32, preserveLinear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.Default); var resultTex2d = new Texture2D(srcTexture2d.width, srcTexture2d.height, TextureFormat.ARGB32, true, preserveLinear ? true : false); var activeRT = RenderTexture.active; try { RenderTexture.active = rt; GL.Clear(true, true, Color.clear); // conversion material if (_metalGlossChannelSwapMaterial == null) { _metalGlossChannelSwapMaterial = new Material(Shader.Find("Hidden/USD/ChannelCombiner")); } if (_normalChannelMaterial == null) { _normalChannelMaterial = new Material(Shader.Find("Hidden/USD/NormalChannel")); } _metalGlossChannelSwapMaterial.SetTexture("_R", srcTexture2d); _metalGlossChannelSwapMaterial.SetTexture("_G", srcTexture2d); _metalGlossChannelSwapMaterial.SetTexture("_B", srcTexture2d); _metalGlossChannelSwapMaterial.SetTexture("_A", srcTexture2d); switch (conversionType) { case ConversionType.None: Graphics.Blit(srcTexture2d, rt); break; case ConversionType.SwapRASmoothnessToBGRoughness: _metalGlossChannelSwapMaterial.SetVector("_Invert", new Vector4(0, 1, 0, 1)); // invert resulting g channel, make sure alpha is 1 _metalGlossChannelSwapMaterial.SetVector("_RScale", new Vector4(0, 0, 0, 0)); _metalGlossChannelSwapMaterial.SetVector("_GScale", new Vector4(0, 0, 0, 1)); // use a channel from _G texture for resulting g _metalGlossChannelSwapMaterial.SetVector("_BScale", new Vector4(1, 0, 0, 0)); // use r channel from _B texture for resulting b _metalGlossChannelSwapMaterial.SetVector("_AScale", new Vector4(0, 0, 0, 0)); Graphics.Blit(srcTexture2d, rt, _metalGlossChannelSwapMaterial); break; case ConversionType.InvertAlpha: _metalGlossChannelSwapMaterial.SetVector("_Invert", new Vector4(0, 0, 0, 1)); // invert alpha result _metalGlossChannelSwapMaterial.SetVector("_RScale", new Vector4(1, 0, 0, 0)); // use all color channels as-is _metalGlossChannelSwapMaterial.SetVector("_GScale", new Vector4(0, 1, 0, 0)); _metalGlossChannelSwapMaterial.SetVector("_BScale", new Vector4(0, 0, 1, 0)); _metalGlossChannelSwapMaterial.SetVector("_AScale", new Vector4(0, 0, 0, 1)); Graphics.Blit(srcTexture2d, rt, _metalGlossChannelSwapMaterial); break; case ConversionType.MaskMapToORM: // Input is RGBA (Metallic, Occlusion, Detail, Smoothness) // Output is RGB1 (Occlusion, Roughness = 1 - Smoothness, Metallic, 1) _metalGlossChannelSwapMaterial.SetVector("_Invert", new Vector4(0, 1, 0, 1)); // smoothness to roughness, solid alpha _metalGlossChannelSwapMaterial.SetVector("_RScale", new Vector4(0, 1, 0, 0)); _metalGlossChannelSwapMaterial.SetVector("_GScale", new Vector4(0, 0, 0, 1)); _metalGlossChannelSwapMaterial.SetVector("_BScale", new Vector4(1, 0, 0, 0)); _metalGlossChannelSwapMaterial.SetVector("_AScale", new Vector4(0, 0, 0, 0)); Graphics.Blit(srcTexture2d, rt, _metalGlossChannelSwapMaterial); break; case ConversionType.UnpackNormal: Graphics.Blit(srcTexture2d, rt, _normalChannelMaterial); break; } resultTex2d.ReadPixels(new Rect(0, 0, srcTexture2d.width, srcTexture2d.height), 0, 0); resultTex2d.Apply(); System.IO.File.WriteAllBytes(filePath, resultTex2d.EncodeToPNG()); if (System.IO.File.Exists(filePath)) { textureIsExported = true; } } finally { RenderTexture.active = activeRT; rt.Release(); GameObject.DestroyImmediate(rt); GameObject.DestroyImmediate(resultTex2d); } } if (!textureIsExported) { var tmpTex2d = new Texture2D(1, 1, TextureFormat.ARGB32, true); try { tmpTex2d.SetPixel(0, 0, Color.white); tmpTex2d.Apply(); System.IO.File.WriteAllBytes(filePath, tmpTex2d.EncodeToPNG()); if (System.IO.File.Exists(filePath)) { textureIsExported = true; } } finally { GameObject.DestroyImmediate(tmpTex2d); } } if (textureIsExported) { // Make file path baked into USD relative to scene file and use forward slashes. filePath = ImporterBase.MakeRelativePath(scene.FilePath, filePath); filePath = filePath.Replace("\\", "/"); var uvReader = new PrimvarReaderSample <Vector2>(); uvReader.varname.defaultValue = new TfToken("st"); scene.Write(usdShaderPath + "/uvReader", uvReader); var usdTexReader = new TextureReaderSample(filePath, usdShaderPath + "/uvReader.outputs:result"); usdTexReader.wrapS = new Connectable <TextureReaderSample.WrapMode>( TextureReaderSample.GetWrapMode(srcTexture2d.wrapModeU)); usdTexReader.wrapT = new Connectable <TextureReaderSample.WrapMode>( TextureReaderSample.GetWrapMode(srcTexture2d.wrapModeV)); if (scale != Vector4.one) { usdTexReader.scale = new Connectable <Vector4>(scale); } // usdTexReader.isSRGB = new Connectable<TextureReaderSample.SRGBMode>(TextureReaderSample.SRGBMode.Auto); scene.Write(usdShaderPath + "/" + textureName, usdTexReader); return(usdShaderPath + "/" + textureName + ".outputs:" + textureOutput); } else { Debug.LogError( "Texture wasn't exported: " + srcTexture2d.name + " (" + textureName + " from material " + material, srcTexture2d); return(null); } }
public static void ExportUsdz(string usdzFilePath, GameObject root) { // Setup a temp directory for zipping up files. string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var tmpUsdName = Path.GetFileNameWithoutExtension(usdzFilePath); var di = Directory.CreateDirectory(tempDirectory); var tmpUsdFilePath = Path.Combine(tempDirectory, tmpUsdName + ".usdc"); var curDir = Directory.GetCurrentDirectory(); var supportedExtensions = new System.Collections.Generic.HashSet <string>(); supportedExtensions.Add(".usd"); supportedExtensions.Add(".usda"); supportedExtensions.Add(".usdc"); supportedExtensions.Add(".jpg"); supportedExtensions.Add(".jpeg"); supportedExtensions.Add(".jpe"); supportedExtensions.Add(".jif"); supportedExtensions.Add(".jfif"); supportedExtensions.Add(".jfi"); supportedExtensions.Add(".png"); // Create the temp .usd scene, into which the data will be exported. var scene = InitForSave(tmpUsdFilePath); var localScale = root.transform.localScale; try { try { // USDZ is in centimeters. root.transform.localScale = localScale * 100; // Set the current working directory to the USDZ directory so the paths in USD // will be relative. Directory.SetCurrentDirectory(tempDirectory); // Export the temp scene. SceneExporter.Export(root, scene, BasisTransformation.SlowAndSafe, // Required by ARKit exportUnvarying: true, zeroRootTransform: false, exportMaterials: true); } finally { // Undo temp scale. root.transform.localScale = localScale; // Flush any in-flight edits and release the scene so the file can be deleted. scene.Save(); scene.Close(); scene = null; } // Copy resulting files into the USDZ archive. var filesToArchive = new pxr.StdStringVector(); // According to the USDZ spec, the first file in the archive must be the primary USD file. filesToArchive.Add(tmpUsdFilePath); foreach (var fileInfo in di.GetFiles()) { if (fileInfo.Name.ToLower() == Path.GetFileName(tmpUsdFilePath).ToLower()) { continue; } var relPath = ImporterBase.MakeRelativePath(tmpUsdFilePath, fileInfo.FullName); var ext = Path.GetExtension(relPath).ToLower(); if (!supportedExtensions.Contains(ext)) { Debug.LogWarning("Unsupported file type in USDZ: " + relPath); continue; } filesToArchive.Add(relPath); } // Write the USDZ file. pxr.UsdCs.WriteUsdZip(usdzFilePath, filesToArchive); } finally { // Clean up temp files. Directory.SetCurrentDirectory(curDir); di.Delete(recursive: true); } }