// This exports the scene into glTF. Brush strokes are exported in the style of the FBX exporter // by building individual meshes for the brush strokes, merging them by brush type, and exporting // the merged meshes. The merged meshes are split at 64k vert boundaries as required by Unity. // Also, scene lights are exported. // Pass: // doExtras - true to add a bunch of poly-specific metadata to the scene // selfContained - true to force a more-compatible gltf that doesn't have http:// URIs // The drawback is that the result is messier and contains data that TBT does not need. public ExportResults ExportBrushStrokes( string outputFile, AxisConvention axes, bool binary, bool doExtras, bool includeLocalMediaContent, int gltfVersion, bool selfContained = false) { var payload = ExportCollector.GetExportPayload( axes, includeLocalMediaContent: includeLocalMediaContent, temporaryDirectory: Path.Combine(Application.temporaryCachePath, "exportgltf")); return(ExportHelper(payload, outputFile, binary, doExtras: doExtras, gltfVersion: gltfVersion, allowHttpUri: !selfContained)); }
public static void Export(string outputFile) { float scale = 100f; ExportUtils.SceneStatePayload payload = ExportCollector.GetExportPayload(AxisConvention.kStl); var buffer = new StringBuilder(); foreach (var group in payload.groups) { foreach (var brushMeshPayload in group.brushMeshes) { var pool = brushMeshPayload.geometry; var xf = brushMeshPayload.xform; var name = brushMeshPayload.legacyUniqueName; buffer.AppendFormat("solid {0}\n", name); for (int i = 0; i < pool.NumTriIndices; i += 3) { Vector3 normal = (pool.m_Normals[pool.m_Tris[i]] + pool.m_Normals[pool.m_Tris[i + 1]] + pool.m_Normals[pool.m_Tris[i + 2]]) / 3f; normal.Normalize(); normal = xf.MultiplyVector(normal); Vector3 v1 = xf * pool.m_Vertices[pool.m_Tris[i]] * scale; Vector3 v2 = xf * pool.m_Vertices[pool.m_Tris[i + 1]] * scale; Vector3 v3 = xf * pool.m_Vertices[pool.m_Tris[i + 2]] * scale; buffer.AppendFormat(" facet normal {0} {1} {2}\n", normal.x, normal.y, normal.z); buffer.Append(" outer loop\n"); buffer.AppendFormat(" vertex {0} {1} {2}\n", v1.x, v1.y, v1.z); buffer.AppendFormat(" vertex {0} {1} {2}\n", v2.x, v2.y, v2.z); buffer.AppendFormat(" vertex {0} {1} {2}\n", v3.x, v3.y, v3.z); buffer.Append(" endloop\n"); buffer.Append(" endfacet\n"); } buffer.AppendFormat("endsolid {0}\n", name); } } System.IO.File.WriteAllText(outputFile, buffer.ToString()); }
public static bool Export(string outputFile) { ExportUtils.SceneStatePayload payload = ExportCollector.GetExportPayload(AxisConvention.kVrml); var buffer = new StringBuilder("#VRML V2.0 utf8\n"); AppendSceneHeader(ref buffer); foreach (var group in payload.groups) { foreach (var brushMeshPayload in group.brushMeshes) { var pool = brushMeshPayload.geometry; Color32 lastColor = pool.m_Colors[0]; // Set to color of first vert initially int j = 0; // An index marking current position in pool.m_Tris int numVerts = 0; // Number of verts appended int firstVertInchunk = 0; // So chunks starts triangle-indexing verts at 0 in the wrl AppendchunkHeader(ref buffer); AppendchunkVerticesHeader(ref buffer); // Iterate through the verts-array and append them to the wrl. // Upon hitting a vertex with a different color (a new chunk), backtrack and iterate // through the tris-array, appending all triangles that are in the current chunk. // Continue again with the new chunk, etc., until all vertices are appended. for (int i = 0; i < pool.NumVerts; i++) { Color32 curColor = pool.m_Colors[i]; if (curColor.Equals(lastColor)) { // In the same chunk AppendVertex(ref buffer, pool, i); numVerts += 1; if (i == pool.NumVerts - 1) { // last vertex AppendchunkVerticesFooter(ref buffer); AppendchunkTrianglesHeader(ref buffer); while (j < pool.NumTriIndices) { // Append all remaining triangles AppendTriangle(ref buffer, pool, j, firstVertInchunk); j += 3; } AppendchunkTrianglesFooter(ref buffer); AppendchunkAppearanceHeader(ref buffer); AppendColorRGB(ref buffer, lastColor); AppendColorA(ref buffer, lastColor); AppendchunkAppearanceFooter(ref buffer); AppendchunkFooter(ref buffer); } } else { // New color, so new chunk AppendchunkVerticesFooter(ref buffer); AppendchunkTrianglesHeader(ref buffer); // Vertex "i" is part of a new chunk, therefore only append // triangles that contain vertices up to and not including "i" while (j < pool.NumTriIndices) { // Only check first index in triangle because triangles cannot span across chunks. // Either all vertices of the triangle are in chunk_n or all are in chunk_n+1. if (pool.m_Tris[j] < i) { AppendTriangle(ref buffer, pool, j, firstVertInchunk); j += 3; } else { break; } } AppendchunkTrianglesFooter(ref buffer); AppendchunkAppearanceHeader(ref buffer); AppendColorRGB(ref buffer, lastColor); AppendColorA(ref buffer, lastColor); AppendchunkAppearanceFooter(ref buffer); AppendchunkFooter(ref buffer); AppendchunkHeader(ref buffer); // Starting the next chunk AppendchunkVerticesHeader(ref buffer); AppendVertex(ref buffer, pool, i); firstVertInchunk = numVerts; numVerts += 1; lastColor = curColor; } } } } AppendSceneFooter(ref buffer); System.IO.File.WriteAllText(outputFile, buffer.ToString()); return true; }
// -------------------------------------------------------------------------------------------- // // 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(); }
// outputFile must be a full path. static void WriteObjectsAndConnections2(FbxExportGlobals G) { var payload = ExportCollector.GetExportPayload( AxisConvention.kFbxAccordingToUnity, includeLocalMediaContent: true); // Write out each brush entry's geometry. foreach (var brushMeshPayload in payload.groups.SelectMany(g => g.brushMeshes)) { // This code used to not set the transform; check that it didn't cause issues Debug.Assert(brushMeshPayload.xform.isIdentity); FbxNode parentNode = GetGroupNode(G.m_manager, G.m_scene, brushMeshPayload.group); ExportMeshPayload_Global(G, brushMeshPayload, parentNode); } // Models with exportable meshes. foreach (var sameInstance in payload.modelMeshes.GroupBy(m => (m.model, m.modelId))) { var modelMeshPayloads = sameInstance.ToList(); if (modelMeshPayloads.Count == 0) { continue; } // All of these pieces will come from the same Widget and therefore will have // the same group id, root transform, etc var first = modelMeshPayloads[0]; FbxNode parentParentNode = GetGroupNode(G.m_manager, G.m_scene, first.group); string rootNodeName = $"model_{first.model.GetExportName()}_{first.modelId}"; if (modelMeshPayloads.Count == 1 && first.localXform.isIdentity) { // Condense the two nodes into one; give the top-level node the same name // it would have had had it been multi-level. FbxNode newNode = ExportMeshPayload_Global(G, first, parentParentNode); newNode.SetName(rootNodeName); } else { FbxNode parentNode = FbxNode.Create(G.m_manager, rootNodeName); parentNode.SetLocalTransform(first.parentXform); parentParentNode.AddChild(parentNode); foreach (var modelMeshPayload in modelMeshPayloads) { ExportMeshPayload_Local(G, modelMeshPayload, parentNode); } } } foreach (ImageQuadPayload meshPayload in payload.imageQuads) { FbxNode groupNode = GetGroupNode(G.m_manager, G.m_scene, meshPayload.group); ExportMeshPayload_Global(G, meshPayload, groupNode); } // Things that can only be exported as transforms (videos, images, models, ...) foreach (var referenceThing in payload.referenceThings) { FbxNode node = FbxNode.Create(G.m_manager, "reference_" + referenceThing.name); node.SetLocalTransform(referenceThing.xform); GetGroupNode(G.m_manager, G.m_scene, referenceThing.group).AddChild(node); } }