Example #1
0
        /// <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();
        }
Example #2
0
 public static void FrameRateTest()
 {
     USD.NET.Scene scene = USD.NET.Scene.Create();
     scene.FrameRate = 30;
     AssertEqual(scene.FrameRate, 30);
     scene.Close();
 }
Example #3
0
        public static void PrintScene(USD.NET.Scene scene)
        {
            string layer;

            scene.Stage.ExportToString(out layer, addSourceFileComment: false);
            Console.WriteLine(layer);
        }
Example #4
0
 public static void StartEndTimeTest()
 {
     USD.NET.Scene scene = USD.NET.Scene.Create();
     scene.StartTime = 0;
     scene.EndTime   = 1;
     AssertEqual(scene.StartTime, 0);
     AssertEqual(scene.EndTime, 1);
     scene.Close();
 }
Example #5
0
        public string CreateTmpUsdFile(string fileName)
        {
            var filePath = System.IO.Path.Combine(UnityEngine.Application.dataPath, fileName);
            var scene    = Scene.Create(filePath);

            scene.Save();
            scene.Close();
            m_filesToDelete.Add(filePath);
            return(filePath);
        }
Example #6
0
        public void SetUp()
        {
            InitUsd.Initialize();
            var usdPath = Path.GetFullPath(AssetDatabase.GUIDToAssetPath(m_usdGUID));
            var stage   = pxr.UsdStage.Open(usdPath, pxr.UsdStage.InitialLoadSet.LoadNone);
            var scene   = Scene.Open(stage);

            m_usdRoot = ImportHelpers.ImportSceneAsGameObject(scene);
            scene.Close();
        }
Example #7
0
        /// <summary>
        /// Initialize a layer as a subLayer to be compatible with the parentLayer.
        /// </summary>
        private void SetupNewSubLayer(Scene parentScene, Scene subLayerScene)
        {
            if (parentScene == null)
            {
                throw new NullReferenceException("ParentScene is null");
            }

            subLayerScene.WriteMode = Scene.WriteModes.Over;
            subLayerScene.UpAxis    = parentScene.UpAxis;
        }
Example #8
0
        public void SetUp()
        {
            InitUsd.Initialize();
            var usdPath       = Path.GetFullPath(AssetDatabase.GUIDToAssetPath(m_usdGUID));
            var stage         = pxr.UsdStage.Open(usdPath, pxr.UsdStage.InitialLoadSet.LoadNone);
            var scene         = Scene.Open(stage);
            var importOptions = new SceneImportOptions();

            importOptions.materialImportMode = MaterialImportMode.ImportPreviewSurface;
            m_usdRoot = ImportHelpers.ImportSceneAsGameObject(scene, importOptions: importOptions);
            scene.Close();
        }
Example #9
0
        /// Authors a USD Xform prim at the given path with the given matrix transform.
        static void CreateXform(USD.NET.Scene scene, string path, Matrix4x4?xform = null)
        {
            var sample = new XformSample();

            if (xform.HasValue)
            {
                sample.transform = xform.Value;
            }
            else
            {
                sample.transform = Matrix4x4.identity;
            }
            scene.Write(path, sample);
        }
Example #10
0
        /// Authors the root "Sketch" object to be exported, with sketch metadta.
        static void AddSketchRoot(USD.NET.Scene scene, string path)
        {
            var sample = CreateSketchRoot();

            // Setup time to author default values, at no time sample.
            var oldTime = scene.Time;

            scene.Time = null;

            // Write the data.
            scene.Write(path, sample);

            // Restore the desired time.
            scene.Time = oldTime;
        }
Example #11
0
        private GameObject LoadUSD(Object usdObject, BasisTransformation changeHandedness = BasisTransformation.SlowAndSafeAsFBX)
        {
            InitUsd.Initialize();
            var usdPath       = Path.GetFullPath(AssetDatabase.GetAssetPath(usdObject));
            var stage         = pxr.UsdStage.Open(usdPath, pxr.UsdStage.InitialLoadSet.LoadNone);
            var scene         = Scene.Open(stage);
            var importOptions = new SceneImportOptions();

            importOptions.changeHandedness   = changeHandedness;
            importOptions.scale              = 0.01f;
            importOptions.materialImportMode = MaterialImportMode.ImportDisplayColor;
            var usdRoot = ImportHelpers.ImportSceneAsGameObject(scene, importOptions: importOptions);

            scene.Close();
            return(usdRoot);
        }
Example #12
0
        /// Authors USD Texture and PrimvarReader shader nodes for the given exportable material.
        ///
        /// Note that textureUris are stored as metadata and only the "Export Texture" is authored as a
        /// true texture in the shading network. This is due to the fact that the actual material textures
        /// are not currently exported with the USD file, but export textures are.
        ///
        /// Returns the texture path if a texture node was created, otherwise null.
        static string CreateAlphaTexture(USD.NET.Scene scene,
                                         string shaderPath,
                                         IExportableMaterial material)
        {
            // Currently, only export texture is previewed in USD.
            // Create an input parameter to read the texture, e.g. inputs:_MainTex.
            if (!material.HasExportTexture())
            {
                return(null);
            }

            string texFile = SanitizeIdentifier(material.DurableName)
                             + System.IO.Path.GetExtension(material.GetExportTextureFilename());

            // Establish paths in the USD scene.
            string texturePath = GetTexturePath(material, "MainTex", shaderPath);
            string primvarPath = GetPrimvarPath(material, "uv", texturePath);

            // Create the texture Prim.
            var texture = new ExportTextureSample();

            // Connect the texture to the file on disk.
            texture.file.defaultValue = new pxr.SdfAssetPath(texFile);
            texture.st.SetConnectedPath(primvarPath, "outputs:result");
            scene.Write(texturePath, texture);

            if (scene.GetPrimAtPath(new pxr.SdfPath(primvarPath)) == null)
            {
                if (material.VertexLayout.texcoord0.size == 2)
                {
                    var primvar = new PrimvarReader2fSample("uv");
                    scene.Write(primvarPath, primvar);
                }
                else if (material.VertexLayout.texcoord0.size == 3)
                {
                    var primvar = new PrimvarReader3fSample("uv");
                    scene.Write(primvarPath, primvar);
                }
                else if (material.VertexLayout.texcoord0.size == 4)
                {
                    var primvar = new PrimvarReader4fSample("uv");
                    scene.Write(primvarPath, primvar);
                }
            }

            return(texturePath);
        }
Example #13
0
        /// <summary>
        /// Writes overrides to the currently targeted subLayer.
        /// </summary>
        public void SaveToLayer()
        {
            var stageRoot = GetComponent <UsdAsset>();

            Scene subLayerScene = Scene.Create(m_targetLayer);

            if (subLayerScene == null)
            {
                throw new NullReferenceException("Could not create layer: " + m_targetLayer);
            }

            Scene rootScene = Scene.Open(stageRoot.usdFullPath);

            if (rootScene == null)
            {
                throw new NullReferenceException("Could not open base layer: " + stageRoot.usdFullPath);
            }

            SetupNewSubLayer(rootScene, subLayerScene);

            rootScene.Close();
            rootScene = null;

            try
            {
                SceneExporter.Export(stageRoot.gameObject,
                                     subLayerScene,
                                     stageRoot.m_changeHandedness,
                                     exportUnvarying: false,
                                     zeroRootTransform: false);
            }
            catch (Exception ex)
            {
                Debug.LogException(ex);
                return;
            }
            finally
            {
                if (subLayerScene != null)
                {
                    subLayerScene.Save();
                    subLayerScene.Close();
                    subLayerScene = null;
                }
            }
        }
            public UnityEngine.Matrix4x4[] ComputeInstanceMatrices(USD.NET.Scene scene, string primPath)
            {
                var prim   = scene.GetPrimAtPath(primPath);
                var pi     = new pxr.UsdGeomPointInstancer(prim);
                var xforms = new pxr.VtMatrix4dArray();

                pi.ComputeInstanceTransformsAtTime(xforms, scene.Time == null ? pxr.UsdTimeCode.Default() : scene.Time, 0);

                // Slow, but works.
                var matrices = new UnityEngine.Matrix4x4[xforms.size()];

                for (int i = 0; i < xforms.size(); i++)
                {
                    matrices[i] = UnityTypeConverter.FromMatrix(xforms[i]);
                }
                return(matrices);
            }
Example #15
0
        public void SetUp()
        {
            var fbxPath = AssetDatabase.GUIDToAssetPath(fbxGUID);
            var asset   = AssetDatabase.LoadAssetAtPath <GameObject>(fbxPath);

            fbxRoot = PrefabUtility.InstantiatePrefab(asset) as GameObject;

            InitUsd.Initialize();
            var usdPath       = Path.GetFullPath(AssetDatabase.GUIDToAssetPath(usdGUID));
            var stage         = pxr.UsdStage.Open(usdPath, pxr.UsdStage.InitialLoadSet.LoadNone);
            var scene         = Scene.Open(stage);
            var importOptions = new SceneImportOptions();

            importOptions.changeHandedness   = BasisTransformation.SlowAndSafeAsFBX;
            importOptions.scale              = 0.01f;
            importOptions.materialImportMode = MaterialImportMode.ImportDisplayColor;
            usdRoot = USD.UsdMenu.ImportSceneAsGameObject(scene, importOptions);
            scene.Close();
        }
Example #16
0
        public static GameObject Import(
            USD.NET.Scene scene,
            GameObject rootObj,
            Dictionary <SdfPath, GameObject> objectMap,
            UpdateMask mask,
            out List <string> warnings,
            List <string> pathsToUpdate = null)
        {
            // TODO: generalize this to avoid having to dig down into USD for sparse reads.
            TfToken brushToken             = new pxr.TfToken("brush");
            TfToken faceVertexIndicesToken = new pxr.TfToken("faceVertexIndices");

            warnings = new List <string>();

            // Would be nice to find a way to kick this off automatically.
            // Redundant calls are ignored.
            if (!InitUsd.Initialize())
            {
                return(null);
            }

            // PLAN: Process any UsdStage either constructing or updating GameObjects as needed.
            // This should include analysis of the time samples to see what attributes are
            // actually varying so they are updated minimally.
            UsdPrimVector prims = null;

            if (pathsToUpdate == null)
            {
                prims = scene.Stage.GetAllPrims();
            }
            else
            {
                prims = new UsdPrimVector();
                foreach (var path in pathsToUpdate)
                {
                    prims.Add(scene.Stage.GetPrimAtPath(new pxr.SdfPath(path)));
                }
            }

            for (int p = 0; p < prims.Count; p++)
            {
                // TODO: prims[p] generates garbage.
                UsdPrim     usdPrim = prims[p];
                UsdGeomMesh usdMesh = new UsdGeomMesh(usdPrim);

                if (!usdMesh)
                {
                    continue;
                }

                ExportUsd.BrushSample sample = new ExportUsd.BrushSample();

                if (mask == UpdateMask.All)
                {
                    scene.Read(usdPrim.GetPath(), sample);
                }
                else
                {
                    // TODO: Generalize this as a reusable mechanism for sparse reads.
                    if (mask == UpdateMask.Topology)
                    {
                        sample.brush = new Guid((string)usdPrim.GetCustomDataByKey(brushToken));
                        var fv = usdPrim.GetAttribute(faceVertexIndicesToken).Get(scene.Time);
                        sample.faceVertexIndices = USD.NET.IntrinsicTypeConverter.FromVtArray((VtIntArray)fv);
                    }
                    else
                    {
                        throw new NotImplementedException();
                    }
                }

                GameObject strokeObj;
                Mesh       unityMesh;

                //
                // Construct the GameObject if needed.
                //
                if (!objectMap.TryGetValue(usdPrim.GetPath(), out strokeObj))
                {
                    // On first import, we need to pull in all the data, regardless of what was requested.
                    mask = UpdateMask.All;

                    BrushDescriptor brush = BrushCatalog.m_Instance.GetBrush(sample.brush);
                    if (brush == null)
                    {
                        Debug.LogWarningFormat("Invalid brush GUID at path: <{0}> guid: {1}",
                                               usdPrim.GetPath(), sample.brush);
                        continue;
                    }
                    strokeObj = UnityEngine.Object.Instantiate(brush.m_BrushPrefab);

                    // Register the Prim/Object mapping.
                    objectMap.Add(usdPrim.GetPath(), strokeObj);

                    // Init the game object.
                    strokeObj.transform.parent = rootObj.transform;
                    strokeObj.GetComponent <MeshRenderer>().material = brush.Material;
                    strokeObj.GetComponent <MeshFilter>().sharedMesh = new Mesh();
                    strokeObj.AddComponent <BoxCollider>();
                    unityMesh = strokeObj.GetComponent <MeshFilter>().sharedMesh;
                }
                else
                {
                    unityMesh = strokeObj.GetComponent <MeshFilter>().sharedMesh;
                }

                //
                // Points
                // Note that points must come first, before all other mesh data.
                //
                if ((mask & UpdateMask.Points) == UpdateMask.Points)
                {
                    unityMesh.vertices = sample.points;
                }

                //
                // Bounds
                //
                if ((mask & UpdateMask.Bounds) == UpdateMask.Bounds)
                {
                    var bc = strokeObj.GetComponent <BoxCollider>();

                    bc.center = sample.extent.center;
                    bc.size   = sample.extent.size;

                    unityMesh.bounds = bc.bounds;
                }

                //
                // Topology
                //
                if ((mask & UpdateMask.Topology) == UpdateMask.Topology)
                {
                    unityMesh.triangles = sample.faceVertexIndices;
                }

                //
                // Normals
                //
                if ((mask & UpdateMask.Normals) == UpdateMask.Normals)
                {
                    unityMesh.normals = sample.normals;
                }

                //
                // Color & Opacity
                //
                if ((mask & UpdateMask.Colors) == UpdateMask.Colors && sample.colors != null)
                {
                    unityMesh.colors = sample.colors;
                }

                //
                // Tangents
                //
                if ((mask & UpdateMask.Tangents) == UpdateMask.Tangents && sample.tangents != null)
                {
                    unityMesh.tangents = sample.tangents;
                }

                //
                // UVs
                //
                if ((mask & UpdateMask.UVs) == UpdateMask.UVs)
                {
                    SetUv(unityMesh, 0, sample.uv);
                    SetUv(unityMesh, 1, sample.uv2);
                    SetUv(unityMesh, 2, sample.uv3);
                    SetUv(unityMesh, 3, sample.uv4);
                }
            } // For each prim

            return(rootObj);
        }
Example #17
0
 public static void YUpTest()
 {
     USD.NET.Scene scene = USD.NET.Scene.Create();
     AssertEqual(scene.UpAxis, USD.NET.Scene.UpAxes.Y);
     scene.Close();
 }
Example #18
0
        //
        // Start generates a USD scene procedurally, containing a single cube with a material, shader
        // and texture bound. It then inspects the cube to discover the material. A Unity material is
        // constructed and the parameters are copied in a generic way. Similarly, the texture is
        // discovered and loaded as a Unity Texture2D and bound to the material.
        //
        // Also See: https://docs.unity3d.com/Manual/MaterialsAccessingViaScript.html
        //
        void Start()
        {
            // Create a scene for this test, but could also be read from disk.
            USD.NET.Scene usdScene = CreateSceneWithShading();

            // Read the material and shader ID.
            var    usdMaterial = new MaterialSample();
            string shaderId;

            // ReadMaterial was designed for Unity and assumes there is one "surface" shader bound.
            if (!MaterialSample.ReadMaterial(usdScene, kCubePath, usdMaterial, out shaderId))
            {
                throw new System.Exception("Failed to read material");
            }

            // Map the shader ID to the corresponding Unity/USD shader pair.
            ShaderPair shader;

            if (shaderId == null || !m_shaderMap.TryGetValue(shaderId, out shader))
            {
                throw new System.Exception("Material had no surface bound");
            }

            //
            // Read and process the shader-specific parameters.
            //

            // UsdShade requires all connections target an attribute, but we actually want to deserialize
            // the entire prim, so we get just the prim path here.
            var shaderPath = new pxr.SdfPath(usdMaterial.surface.connectedPath).GetPrimPath();

            usdScene.Read(shaderPath, shader.usdShader);

            //
            // Construct material & process the inputs, textures, and keywords.
            //

            var mat = new UnityEngine.Material(shader.unityShader);

            // Apply material keywords.
            foreach (string keyword in usdMaterial.requiredKeywords ?? new string[0])
            {
                mat.EnableKeyword(keyword);
            }

            // Iterate over all input parameters and copy values and/or construct textures.
            foreach (var param in shader.usdShader.GetInputParameters())
            {
                if (!SetMaterialParameter(mat, param.unityName, param.value))
                {
                    throw new System.Exception("Incompatible shader data type: " + param.ToString());
                }
            }

            foreach (var param in shader.usdShader.GetInputTextures())
            {
                if (string.IsNullOrEmpty(param.connectedPath))
                {
                    // Not connected to a texture.
                    continue;
                }

                // Only 2D textures are supported in this example.
                var usdTexture = new Texture2DSample();

                // Again, we want the prim path, not the attribute path.
                var texturePath = new pxr.SdfPath(param.connectedPath).GetPrimPath();
                usdScene.Read(texturePath, usdTexture);

                // This example also only supports explicit sourceFiles, they cannot be connected.
                if (string.IsNullOrEmpty(usdTexture.sourceFile.defaultValue))
                {
                    continue;
                }

                // For details, see: https://docs.unity3d.com/Manual/MaterialsAccessingViaScript.html
                foreach (string keyword in param.requiredShaderKeywords)
                {
                    mat.EnableKeyword(keyword);
                }

                var data     = System.IO.File.ReadAllBytes(usdTexture.sourceFile.defaultValue);
                var unityTex = new Texture2D(2, 2);
                unityTex.LoadImage(data);
                mat.SetTexture(param.unityName, unityTex);
                Debug.Log("Set " + param.unityName + " to " + usdTexture.sourceFile.defaultValue);

                unityTex.Apply(updateMipmaps: true, makeNoLongerReadable: false);
            }

            //
            // Create and bind the geometry.
            //

            // Create a cube and set the material.
            // Note that geometry is handled minimally here and is incomplete.
            var cubeSample = new CubeSample();

            usdScene.Read(kCubePath, cubeSample);

            var go = GameObject.CreatePrimitive(PrimitiveType.Cube);

            go.transform.SetParent(transform, worldPositionStays: false);
            go.transform.localScale = Vector3.one * (float)cubeSample.size;
            m_cube = transform;

            go.GetComponent <MeshRenderer>().material = mat;
        }
Example #19
0
        /// Authors a USD Material, Shader, parameters and connections between the two.
        /// The USD shader structure consists of a Material, which is connected to a shader output. The
        /// Shader consists of input parameters which are either connected to other shaders or in the case
        /// of public parameters, back to the material which is the public interface for the shading
        /// network.
        ///
        /// This function creates a material, shader, inputs, outputs, zero or more textures, and for each
        /// texture, a single primvar reader node to read the UV data from the geometric primitive.
        static string CreateMaterialNetwork(USD.NET.Scene scene,
                                            IExportableMaterial material,
                                            string rootPath = null)
        {
            var matSample = new ExportMaterialSample();

            // Used scene object paths.
            string materialPath = GetMaterialPath(material, rootPath);
            string shaderPath   = GetShaderPath(material, materialPath);
            string displayColorPrimvarReaderPath   = GetPrimvarPath(material, "displayColor", shaderPath);
            string displayOpacityPrimvarReaderPath = GetPrimvarPath(material, "displayOpacity", shaderPath);

            // The material was already created.
            if (scene.GetPrimAtPath(materialPath) != null)
            {
                return(materialPath);
            }

            // Ensure the root material path is defined in the scene.
            scene.Stage.DefinePrim(new pxr.SdfPath(rootPath));

            // Connect the materail surface to the output of the shader.
            matSample.surface.SetConnectedPath(shaderPath, "outputs:result");
            scene.Write(materialPath, matSample);

            // Create the shader and conditionally connect the diffuse color to the MainTex output.
            var shaderSample = GetShaderSample(material);
            var texturePath  = CreateAlphaTexture(scene, shaderPath, material);

            if (texturePath != null)
            {
                // A texture was created, so connect the opacity input to the texture output.
                shaderSample.opacity.SetConnectedPath(texturePath, "outputs:a");
            }
            else
            {
                // TODO: currently primvars:displayOpacity is not multiplied when an alpha texture is
                //                present. However, this only affects the USD preview. The correct solution
                //                requires a multiply node in the shader graph, but this does not yet exist.
                scene.Write(displayOpacityPrimvarReaderPath, new PrimvarReader1fSample("displayOpacity"));
                shaderSample.opacity.SetConnectedPath(displayOpacityPrimvarReaderPath, "outputs:result");
            }

            // Create a primvar reader to read primvars:displayColor.
            scene.Write(displayColorPrimvarReaderPath, new PrimvarReader3fSample("displayColor"));

            // Connect the diffuse color to the primvar reader.
            shaderSample.diffuseColor.SetConnectedPath(displayColorPrimvarReaderPath, "outputs:result");

            scene.Write(shaderPath, shaderSample);

            //
            // Everything below is ad-hoc data, which is written using the low level USD API.
            // It consists of the Unity shader parameters and the non-exported texture URIs.
            // Also note that scene.GetPrimAtPath will return null when the prim is InValid,
            // so there is no need to call IsValid() on the resulting prim.
            //
            var shadeMaterial = new pxr.UsdShadeMaterial(scene.GetPrimAtPath(materialPath));
            var shadeShader   = new pxr.UsdShadeShader(scene.GetPrimAtPath(shaderPath));

            if (material.SupportsDetailedMaterialInfo)
            {
                CreateShaderInputs(shadeShader, shadeMaterial, material.FloatParams);
                CreateShaderInputs(shadeShader, shadeMaterial, material.ColorParams);
                CreateShaderInputs(shadeShader, shadeMaterial, material.VectorParams);
            }

            CreateTextureUris(shadeShader.GetPrim(), material);

            return(materialPath);
        }
Example #20
0
 /// Set the material:binding relationship on the mesh/curve to target the given materialPath.
 static void BindMaterial(USD.NET.Scene scene, string primPath, string materialPath)
 {
     scene.Write(primPath, new MaterialBindingSample(materialPath));
 }
Example #21
0
        // -------------------------------------------------------------------------------------------- //
        // Export Logic
        // -------------------------------------------------------------------------------------------- //

        /// Exports either all brush strokes or the given selection to the specified file.
        static public void ExportPayload(string outputFile)
        {
            // Would be nice to find a way to kick this off automatically.
            // Redundant calls are ignored.
            if (!InitUsd.Initialize())
            {
                return;
            }

            // Unity is left handed (DX), USD is right handed (GL)
            var payload      = ExportCollector.GetExportPayload(AxisConvention.kUsd);
            var brushCatalog = BrushCatalog.m_Instance;

            // The Scene object provids serialization methods arbitrary C# objects to USD.
            USD.NET.Scene scene = USD.NET.Scene.Create(outputFile);

            // The target time at which samples will be written.
            //
            // In this case, all data is being written to the "default" time, which means it can be
            // overridden by animated values later.
            scene.Time = null;

            // Bracketing times to specify the valid animation range.
            scene.StartTime = 1.0;
            scene.EndTime   = 1.0;

            const string kGeomName   = "/Geom";
            const string kCurvesName = "/Curves";

            string path = "";

            AddSketchRoot(scene, GetSketchPath()); // Create: </Sketch>

            CreateXform(scene, GetStrokesPath());  // Create: </Sketch/Strokes>
            CreateXform(scene, GetModelsPath());   // Create: </Sketch/Models>

            // Main export loop.
            try
            {
                foreach (ExportUtils.GroupPayload group in payload.groups)
                {
                    // Example: </Sketch/Strokes/Group_0>
                    path = GetGroupPath(group.id);
                    CreateXform(scene, path);

                    // Example: </Sketch/Strokes/Group_0/Geom>
                    CreateXform(scene, path + kGeomName);

                    // Example: </Sketch/Strokes/Group_0/Curves>
                    CreateXform(scene, path + kCurvesName);

                    int iBrushMeshPayload = -1;
                    foreach (var brushMeshPayload in group.brushMeshes)
                    {
                        ++iBrushMeshPayload;
                        // Conditionally moves Normal into Texcoord1 so that the normal semantic is respected.
                        // This only has an effect when layout.bFbxExportNormalAsTexcoord1 == true.
                        // Note that this modifies the GeometryPool in place.
                        FbxUtils.ApplyFbxTexcoordHack(brushMeshPayload.geometry);

                        // Brushes are expected to be batched by type/GUID.
                        Guid   brushGuid = brushMeshPayload.strokes[0].m_BrushGuid;
                        string brushName = "/" +
                                           SanitizeIdentifier(brushCatalog.GetBrush(brushGuid).DurableName) + "_";

                        // Example: </Sketch/Strokes/Group_0/Geom/Marker_0>
                        string meshPath = path + kGeomName + brushName;

                        // Example: </Sketch/Strokes/Group_0/Curves/Marker_0>
                        string curvePath = path + kCurvesName + brushName;

                        var geomPool       = brushMeshPayload.geometry;
                        var strokes        = brushMeshPayload.strokes;
                        var mat44          = Matrix4x4.identity;
                        var meshPrimPath   = new pxr.SdfPath(meshPath + iBrushMeshPayload.ToString());
                        var curvesPrimPath = new pxr.SdfPath(curvePath + iBrushMeshPayload.ToString());

                        //
                        // Geometry
                        //
                        BrushSample brushSample = GetBrushSample(geomPool, strokes, mat44);

                        // Write the BrushSample to the same point in the scenegraph at which it exists in Tilt
                        // Brush. Notice this method is Async, it is queued to a background thread to perform I/O
                        // which means it is not safe to read from the scene until WaitForWrites() is called.
                        scene.Write(meshPrimPath, brushSample);

                        //
                        // Stroke Curves
                        //
                        var curvesSample = GetCurvesSample(payload, strokes, Matrix4x4.identity);
                        scene.Write(curvesPrimPath, curvesSample);

                        //
                        // Materials
                        //
                        double?oldTime = scene.Time;
                        scene.Time = null;

                        string materialPath = CreateMaterialNetwork(
                            scene,
                            brushMeshPayload.exportableMaterial,
                            GetStrokesPath());

                        BindMaterial(scene, meshPrimPath.ToString(), materialPath);
                        BindMaterial(scene, curvesPrimPath.ToString(), materialPath);

                        scene.Time = oldTime;
                    }
                }

                //
                // Models
                //

                var knownModels = new Dictionary <Model, string>();

                int iModelMeshPayload = -1;
                foreach (var modelMeshPayload in payload.modelMeshes)
                {
                    ++iModelMeshPayload;
                    var modelId         = modelMeshPayload.modelId;
                    var modelNamePrefix = "/Model_"
                                          + SanitizeIdentifier(modelMeshPayload.model.GetExportName())
                                          + "_";
                    var modelName = modelNamePrefix + modelId;

                    var xf = modelMeshPayload.xform;
                    // Geometry pools may be repeated and should be turned into references.
                    var geomPool = modelMeshPayload.geometry;

                    var modelRootPath = new pxr.SdfPath(GetModelsPath() + modelName);

                    // Example: </Sketch/Models/Model_Andy_0>
                    CreateXform(scene, modelRootPath, xf);

                    // Example: </Sketch/Models/Model_Andy_0/Geom>
                    CreateXform(scene, modelRootPath + kGeomName);

                    string modelPathToReference;
                    if (knownModels.TryGetValue(modelMeshPayload.model, out modelPathToReference) &&
                        modelPathToReference != modelRootPath)
                    {
                        // Create an Xform, note that the world transform here will override the referenced model.
                        var meshXf = new MeshXformSample();
                        meshXf.transform = xf;
                        scene.Write(modelRootPath, meshXf);
                        // Add a USD reference to previously created model.
                        var prim = scene.Stage.GetPrimAtPath(modelRootPath);
                        prim.GetReferences().AddReference("", new pxr.SdfPath(modelPathToReference));
                        continue;
                    }

                    // Example: </Sketch/Models/Geom/Model_Andy_0/Mesh_0>
                    path = modelRootPath + kGeomName + "/Mesh_" + iModelMeshPayload.ToString();

                    var meshPrimPath = new pxr.SdfPath(path);
                    var meshSample   = new MeshSample();

                    GetMeshSample(geomPool, Matrix4x4.identity, meshSample);
                    scene.Write(path, meshSample);
                    scene.Stage.GetPrimAtPath(new pxr.SdfPath(path)).SetInstanceable(true);

                    //
                    // Materials
                    //

                    // Author at default time.
                    double?oldTime = scene.Time;
                    scene.Time = null;

                    // Model materials must live under the model root, since we will reference the model.
                    string materialPath = CreateMaterialNetwork(
                        scene,
                        modelMeshPayload.exportableMaterial,
                        modelRootPath);
                    BindMaterial(scene, meshPrimPath.ToString(), materialPath);

                    // Continue authoring at the desired time index.
                    scene.Time = oldTime;

                    //
                    // Setup to be referenced.
                    //
                    if (!knownModels.ContainsKey(modelMeshPayload.model))
                    {
                        knownModels.Add(modelMeshPayload.model, modelRootPath);
                    }
                }
            }
            catch
            {
                scene.Save();
                scene.Close();
                throw;
            }

            // Save will force a sync with all async reads and writes.
            scene.Save();
            scene.Close();
        }