private ExportResults ExportHelper( SceneStatePayload payload, string outputFile, bool binary, bool doExtras, int gltfVersion, bool allowHttpUri) { // TODO: Ownership of this temp directory is sloppy. // Payload and export share the same dir and we assume that the exporter: // 1. will not write files whose names conflict with payload's // 2. will clean up the entire directory when done // This works, as long as the payload isn't used for more than one export (it currently isn't) using (var exporter = new GlTF_ScriptableExporter(payload.temporaryDirectory, gltfVersion)) { exporter.AllowHttpUri = allowHttpUri; try { m_exporter = exporter; exporter.G.binary = binary; exporter.BeginExport(outputFile); exporter.SetMetadata(payload.generator, copyright: null); if (doExtras) { SetExtras(exporter, payload); } if (payload.env.skyCubemap != null) { // Add the skybox texture to the export. string texturePath = ExportUtils.GetTexturePath(payload.env.skyCubemap); string textureFilename = Path.GetFileName(texturePath); exporter.G.extras["TB_EnvironmentSkybox"] = ExportFileReference.CreateLocal(texturePath, textureFilename); } WriteObjectsAndConnections(exporter, payload); string[] exportedFiles = exporter.EndExport(); return(new ExportResults { success = true, exportedFiles = exportedFiles, numTris = exporter.NumTris }); } catch (InvalidOperationException e) { OutputWindowScript.Error("glTF export failed", e.Message); // TODO: anti-pattern. Let the exception bubble up so caller can log it properly // Actually, InvalidOperationException is now somewhat expected in experimental, since // the gltf exporter does not check IExportableMaterial.SupportsDetailedMaterialInfo. // But we still want the logging for standalone builds. Debug.LogException(e); return(new ExportResults { success = false }); } catch (IOException e) { OutputWindowScript.Error("glTF export failed", e.Message); return(new ExportResults { success = false }); } finally { payload.Destroy(); // The lifetime of ExportGlTF, GlTF_ScriptableExporter, and GlTF_Globals instances // is identical. This is solely to be pedantic. m_exporter = null; } } }
public static SceneStatePayload GetExportPayloadForGameObject(GameObject gameObject, AxisConvention axes, Environment env) { var payload = new SceneStatePayload(axes); payload.env.skyCubemap = env == null ? null : env.m_RenderSettings.m_SkyboxCubemap; payload.lights = new ExportUtils.LightsPayload(); // unused test code #if TEST_ENVIRONMENTS_WITH_SCENE_LIGHTS { var lights = payload.lights; lights.ambientColor = RenderSettings.ambientLight; #if true // Scene lights from Standard enviroment lights.elements.Add(new ExportUtils.Light { name = "SceneLight0", id = 0, type = LightType.Directional, lightColor = new Color32(198, 208, 253), xform = ExportUtils.ChangeBasisNonUniformScale( payload, Matrix4x4.TRS( new Vector3(0f, 0.21875f, -.545875f), Quaternion.Euler(60, 0, 26), Vector3.one)) }); lights.elements.Add(new ExportUtils.Light { name = "SceneLight1", id = 1, type = LightType.Directional, lightColor = new Color32(109, 107, 88), xform = ExportUtils.ChangeBasisNonUniformScale( payload, Matrix4x4.TRS( new Vector3(0f, 0.21875f, -.545875f), Quaternion.Euler(140, 0, 40), Vector3.one)), }); #else // Scene lights from Dress Form environment lights.elements.Add(new ExportUtils.Light { name = "SceneLight0", id = 0, type = LightType.Directional, lightColor = new Color32(255, 234, 198), xform = ExportUtils.ChangeBasisNonUniformScale( payload, Matrix4x4.TRS( new Vector3(0f, 0.7816665f, -.220875f), Quaternion.Euler(50, 42, 26), Vector3.one)) }); lights.elements.Add(new ExportUtils.Light { name = "SceneLight1", id = 1, type = LightType.Directional, lightColor = new Color32(91, 90, 74), xform = ExportUtils.ChangeBasisNonUniformScale( payload, Matrix4x4.TRS( new Vector3(0f, .636532f, -.6020539f), Quaternion.Euler(140, 47, 40), Vector3.one)), }); #endif } #endif // The determinant can be used to detect if the basis-change has a mirroring. // This matters because a mirroring turns the triangles inside-out, requiring // us to flip their winding to preserve the surface orientation. bool reverseTriangleWinding = (ExportUtils.GetBasisMatrix(payload).determinant < 0); BuildModelMeshesFromGameObject(gameObject, payload, reverseTriangleWinding); return(payload); }
// Get a material for the given mesh. If a _MainTex texture exists, reference to it from // UnityEditor.AssetBase. static IExportableMaterial GetMaterialForMeshData(MeshData data) { // Create parameter dictionaries. Dictionary <string, string> textureUris = new Dictionary <string, string>(); Dictionary <string, Vector2> textureSizes = new Dictionary <string, Vector2>(); Dictionary <string, Vector3> vectorParams = new Dictionary <string, Vector3>(); Dictionary <string, float> floatParams = new Dictionary <string, float>(); Dictionary <string, Color> colorParams = new Dictionary <string, Color>(); // Get material for the mesh. Material mat = data.renderer.sharedMaterial; Texture2D mainTex = (Texture2D)mat.GetTexture("_MainTex"); string uriBase = ""; if (mainTex) { string texturePath = ExportUtils.GetTexturePath(mainTex); textureUris.Add("BaseColorTex", ExportUtils.kTempPrefix + texturePath); Vector2 uvScale = mat.GetTextureScale("_MainTex"); Vector2 uvOffset = mat.GetTextureOffset("_MainTex"); colorParams.Add("UvAdjust", new Color(uvScale.x, uvScale.y, uvOffset.x, uvOffset.y)); } else { textureUris.Add("BaseColorTex", ExportUtils.kBuiltInPrefix + "whiteTextureMap.png"); colorParams.Add("UvAdjust", new Color(1.0f, 1.0f, 0.0f, 0.0f)); } // Check for secondary light map texture. bool hasLightMap = false; Texture2D lightMapTex = null; if (mat.HasProperty("_DetailAlbedoMap")) { lightMapTex = (Texture2D)mat.GetTexture("_DetailAlbedoMap"); } if (lightMapTex == null && mat.HasProperty("_LightMap")) { lightMapTex = (Texture2D)mat.GetTexture("_LightMap"); } if (lightMapTex) { hasLightMap = true; string texturePath = ExportUtils.GetTexturePath(lightMapTex); textureUris.Add("LightMapTex", ExportUtils.kTempPrefix + texturePath); } else { textureUris.Add("LightMapTex", ExportUtils.kBuiltInPrefix + "whiteTextureMap.png"); } // As much as possible, this should mimic the way that GltfMaterialConverter.ConvertMaterial() // converts materials from gltf2 to UnityMaterial methods. if (mat.HasProperty("_Color")) { colorParams.Add("BaseColorFactor", mat.color); } else { colorParams.Add("BaseColorFactor", Color.black); } // Create the exportable material. BrushDescriptor exportDescriptor = hasLightMap ? Resources.Load <BrushDescriptor>("Brushes/Poly/Environments/EnvironmentDiffuseLightMap") : Resources.Load <BrushDescriptor>("Brushes/Poly/Environments/EnvironmentDiffuse"); return(new DynamicExportableMaterial( // Things that come from the template blendMode: exportDescriptor.BlendMode, vertexLayout: exportDescriptor.VertexLayout, vertShaderUri: exportDescriptor.VertShaderUri, fragShaderUri: exportDescriptor.FragShaderUri, enableCull: exportDescriptor.EnableCull, // Things that vary durableName: data.renderer.gameObject.GetInstanceID().ToString(), emissiveFactor: 0.0f, // TODO: Should the export texture be the main pbr texture? hasExportTexture: false, exportTextureFilename: null, uriBase: uriBase, textureUris: textureUris, textureSizes: textureSizes, floatParams: floatParams, vectorParams: vectorParams, colorParams: colorParams)); }
// Helper for GetModelMeshPayloads() -- this is the level 4 iteration. // Pass: // mesh / meshMaterials - the Mesh to generate geometry for // payload - the payload being exported // prefab - the owner "prefab" // meshIndex - the index of this Mesh within its owner "prefab" static IEnumerable <PrefabGeometry> GetPrefabGameObjMeshes( Mesh mesh, Material[] meshMaterials, SceneStatePayload payload, Model prefab, int meshIndex) { if (meshMaterials.Length != mesh.subMeshCount) { throw new ArgumentException("meshMaterials length"); } string exportName = prefab.GetExportName(); IExportableMaterial[] exportableMaterials = meshMaterials.Select( mat => prefab.GetExportableMaterial(mat)).ToArray(); VertexLayout?[] layouts = exportableMaterials.Select(iem => iem?.VertexLayout).ToArray(); // TODO(b/142396408) // Is it better to write color/texcoord that we don't need, just to satisfy the layout? // Or should we remove color/texcoord from the layout if the Mesh doesn't have them? // Right now we do the former. I think the fix should probably be in the collector; // it shouldn't return an exportableMaterial that is a superset of what the mesh contains. GeometryPool[] subMeshPools = GeometryPool.FromMesh( mesh, layouts, fallbackColor: Color.white, useFallbackTexcoord: true); for (int subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++) { // See if this material is a Blocks material. Material mat = meshMaterials[subMeshIndex]; // At import time, ImportMaterialCollector now ensures that _every_ UnityEngine.Material // imported also comes with an IExportableMaterial of some kind (BrushDescriptor for // Blocks/TB, and DynamicExportableMaterial for everything else). This lookup shouldn't fail. IExportableMaterial exportableMaterial = exportableMaterials[subMeshIndex]; if (exportableMaterial == null) { Debug.LogWarning($"Model {prefab.HumanName} has a non-exportable material {mat.name}"); continue; } GeometryPool geo = subMeshPools[subMeshIndex]; // TODO(b/140634751): lingering sanity-checking; we can probably remove this for M24 if (geo.Layout.bUseColors) { Debug.Assert(geo.m_Colors.Count == geo.NumVerts); } // Important: This transform should only be applied once per prefab, since the pools are // shared among all the instances, and cannot include any mesh-local transformations. // If the pools share data (which is currently impossible), then additionally // the transform should only be applied once to each set of vertex data. ExportUtils.ConvertUnitsAndChangeBasis(geo, payload); if (payload.reverseWinding) { // There are many ways of reversing winding; choose the one that matches Unity's fbx flip ExportUtils.ReverseTriangleWinding(geo, 1, 2); } yield return(new PrefabGeometry { model = prefab, name = exportName + "_" + meshIndex + "_" + subMeshIndex, pool = geo, exportableMaterial = exportableMaterial, }); } }
private static void BuildModelsAsModelMeshes( SceneStatePayload payload, IEnumerable <ModelWidget> modelWidgets) { // This is a quadruple-nested loop that's triply flattened into a single IEnumerable. // The structure of the data is is: // 1. There are 1+ Models (which I'll call "prefab" because Model is a loaded term here) // 2. Each "prefab" is used by 1+ ModelWidgets ("instance") // 3. Each ModelWidget, and the "prefab", contains 1+ GameObjects (which has a single Mesh) // 4. Each Mesh has 1+ unity submeshes (almost always exactly 1) // These are converted to ModelMeshPayload // // The resulting ModelMeshPayloads are a mixture of data from all 4 levels, for example // - ModelMeshPayload.model comes from level 1 // - ModelMeshPayload.modelId comes from level 2 // - ModelMeshPayload.xform comes from level 3 // - ModelMeshPayload.pool comes from level 4 // // Orthogonal to this nesting, some data comes from the "prefab" and some from the "instance", // since we don't want to have to convert a mesh into GeometryPool once per instance. // So we iterate level 3 for the Model once, up front; then each ModelWidget level 3 is // a parallel iteration with that Model's pre-computed iteration. foreach (var group in modelWidgets.GroupBy(widget => widget.Model)) { Model prefab = group.Key; // Each element represents a GameObject in the "prefab" List <PrefabGeometry[]> prefabObjs = GetPrefabGameObjs(payload, prefab).ToList(); foreach ((ModelWidget widget, int widgetIndex) in group.WithIndex()) { Matrix4x4 widgetXform = ExportUtils.ChangeBasis(widget.transform, payload); // Acts like our AsScene[], AsCanvas[] accessors var AsWidget = new TransformExtensions.RelativeAccessor(widget.transform); // Verify that it's okay to parallel-iterate over the prefab and instance gameObjs. // It should be, unless one or the other has mucked with their m_MeshChildren. MeshFilter[] instanceObjs = widget.GetMeshes(); if (prefabObjs.Count != instanceObjs.Length) { Debug.LogError($"Bad Model instance: {prefabObjs.Count} {instanceObjs.Length}"); // TODO: check order as well, somehow continue; } // Parallel iterate, taking shared things (GeometryPool, material, name) from the // "prefab" and xforms, unique ids, etc from the "instance". for (int objIndex = 0; objIndex < prefabObjs.Count; ++objIndex) { PrefabGeometry[] prefabSubMeshes = prefabObjs[objIndex]; GameObject instanceObj = instanceObjs[objIndex].gameObject; Matrix4x4 localXform = ExportUtils.ChangeBasis(AsWidget[instanceObj.transform], payload); int objId = payload.idGenerator.GetIdFromInstanceId(instanceObj); Matrix4x4 xform = ExportUtils.ChangeBasis(instanceObj.transform, payload); foreach (PrefabGeometry prefabSubMesh in prefabSubMeshes) { payload.modelMeshes.Add(new ExportUtils.ModelMeshPayload( payload.groupIdMapping.GetId(widget.Group)) { // Copied from "prefab" model = prefabSubMesh.model, geometry = prefabSubMesh.pool, exportableMaterial = prefabSubMesh.exportableMaterial, geometryName = prefabSubMesh.name, // Unique to instance legacyUniqueName = $"{prefabSubMesh.name}_i{objId}", nodeName = $"{prefabSubMesh.name}_{widgetIndex}", parentXform = widgetXform, localXform = localXform, modelId = widgetIndex, xform = xform, }); } } } } }
internal static FbxSurfaceMaterial CreateFbxMaterial( FbxExportGlobals G, string meshNamespace, IExportableMaterial exportableMaterial, HashSet <string> createdMaterialNames) { string materialName; if (exportableMaterial is BrushDescriptor) { // Toolkit uses this guid (in "N" format) to look up a BrushDescriptor. // See Toolkit's ModelImportSettings.GetDescriptorForStroke materialName = $"{exportableMaterial.UniqueName:N}_{meshNamespace}_{exportableMaterial.DurableName}"; } else if (exportableMaterial is DynamicExportableMaterial dem) { // Comes from {fbx,obj,gltf,...} import from {Poly, Media Library} // This is a customized version of a BrushDescriptor -- almost certainly // Pbr{Blend,Opaque}{Double,Single}Sided with maybe an added texture and // some of its params customized. // TBT will merge the material created by Unity for this FbxMaterial with // the premade material it has for the parent guid. materialName = $"{dem.Parent.m_Guid:N}_{meshNamespace}_{dem.DurableName}"; } else { Debug.LogWarning($"Unknown class {exportableMaterial.GetType().Name}"); materialName = $"{meshNamespace}_{exportableMaterial.DurableName}"; } // If only ExportFbx were a non-static class we could merge it with FbxExportGlobals materialName = ExportUtils.CreateUniqueName(materialName, createdMaterialNames); FbxSurfaceLambert material = FbxSurfaceLambert.Create(G.m_scene, materialName); material.Ambient.Set(new FbxDouble3(0, 0, 0)); material.Diffuse.Set(new FbxDouble3(1.0, 1.0, 1.0)); if (exportableMaterial.EmissiveFactor > 0) { material.EmissiveFactor.Set(exportableMaterial.EmissiveFactor); material.Emissive.Set(new FbxDouble3(1.0, 1.0, 1.0)); } if (exportableMaterial.BlendMode != ExportableMaterialBlendMode.None) { var blendMode = FbxProperty.Create(material, Globals.FbxStringDT, "BlendMode"); switch (exportableMaterial.BlendMode) { case ExportableMaterialBlendMode.AlphaMask: blendMode.SetString(new FbxString("AlphaMask")); material.TransparencyFactor.Set(0.2); break; case ExportableMaterialBlendMode.AdditiveBlend: blendMode.SetString(new FbxString("AdditiveBlend")); break; } } // Export the texture if (exportableMaterial.HasExportTexture()) { // This is not perfectly unique, but it is good enough for fbx export // better would be to use <durable>_<guid> but that's ugly, and nobody uses // the textures anyway, so... let's leave well enough alone for now. string albedoTextureName = exportableMaterial.DurableName; var fullTextureDir = Path.Combine(G.m_outputDir, kRelativeTextureDir); if (!Directory.Exists(fullTextureDir)) { if (!FileUtils.InitializeDirectoryWithUserError(fullTextureDir)) { throw new IOException("Cannot write textures"); } } string src = exportableMaterial.GetExportTextureFilename(); var textureFileName = albedoTextureName + ".png"; var textureFilePath = Path.Combine(fullTextureDir, textureFileName); FileInfo srcInfo = new FileInfo(src); if (srcInfo.Exists && !new FileInfo(textureFilePath).Exists) { srcInfo.CopyTo(textureFilePath); } FbxFileTexture texture = FbxFileTexture.Create(G.m_scene, albedoTextureName + "_texture"); texture.SetFileName(textureFilePath); texture.SetTextureUse(FbxTexture.ETextureUse.eStandard); texture.SetMappingType(FbxTexture.EMappingType.eUV); texture.SetMaterialUse(FbxFileTexture.EMaterialUse.eModelMaterial); texture.UVSet.Set(new FbxString("uv0")); material.Diffuse.ConnectSrcObject(texture); material.TransparentColor.ConnectSrcObject(texture); } else { foreach (var kvp in exportableMaterial.TextureUris) { string parameterName = kvp.Key; string textureUri = kvp.Value; if (ExportFileReference.IsHttp(textureUri)) { // fbx can't deal with http references to textures continue; } ExportFileReference fileRef = ExportFileReference.GetOrCreateSafeLocal( G.m_disambiguationContext, textureUri, exportableMaterial.UriBase, $"{meshNamespace}_{Path.GetFileName(textureUri)}"); AddTextureToMaterial(G, fileRef, material, parameterName); } } return(material); }