// ------------------------------------------------------------------------------------------ // // Create a USD scene for testing // ------------------------------------------------------------------------------------------ // static private Scene CreateSceneWithShading() { var scene = Scene.Create(); var cubePath = kCubePath; var materialPath = "/Model/Materials/SimpleMat"; var shaderPath = "/Model/Materials/SimpleMat/StandardShader"; var albedoMapPath = "/Model/Materials/SimpleMat/AlbedoMap"; var normalMapPath = "/Model/Materials/SimpleMat/NormalMap"; var emissionMapPath = "/Model/Materials/SimpleMat/EmissionMap"; var metallicMapPath = "/Model/Materials/SimpleMat/MetallicMap"; var occlusionMapPath = "/Model/Materials/SimpleMat/OcclusionMap"; var parallaxMapPath = "/Model/Materials/SimpleMat/ParallaxMap"; var detailNormalMapPath = "/Model/Materials/SimpleMat/DetailNormalMap"; var detailMaskPath = "/Model/Materials/SimpleMat/DetailMask"; var textureFilePath = @"Assets/Samples/USD/1.0.3-preview.1/ImportMaterials/Textures/"; var cube = new CubeSample(); cube.size = 7; cube.transform = Matrix4x4.identity; // // Setup Material. // var material = new MaterialSample(); material.surface.SetConnectedPath(shaderPath, "outputs:out"); // Various shader keywords are required to enable the standard shader to work as intended, // while these can be encoded as part of the schema, often they require some logic (e.g. is // emission color != black?). material.requiredKeywords = new string[] { "_EMISSION" }; // // Setup Shader. // var shader = new StandardShaderSample(); shader.id = new pxr.TfToken("Unity.Standard"); // Note that USD requires all connections target attributes, hence "outputs:out" appended to // the paths below. Think of this as a shader network graph, where a texture object could // be executed per pixel with a sampler, "outputs:out" would be the sampled color. shader.smoothnessScale.defaultValue = 0.99f; shader.enableSpecularHighlights.defaultValue = true; shader.enableGlossyReflections.defaultValue = true; shader.albedo.defaultValue = Color.white; shader.albedoMap.SetConnectedPath(albedoMapPath, "outputs:out"); shader.normalMapScale.defaultValue = 0.76f; shader.normalMap.SetConnectedPath(normalMapPath, "outputs:out"); shader.emission.defaultValue = Color.white * 0.3f; shader.emissionMap.SetConnectedPath(emissionMapPath, "outputs:out"); shader.metallicMap.SetConnectedPath(metallicMapPath, "outputs:out"); shader.occlusionMapScale.defaultValue = 0.65f; shader.occlusionMap.SetConnectedPath(occlusionMapPath, "outputs:out"); shader.parallaxMapScale.defaultValue = 0.1f; shader.parallaxMap.SetConnectedPath(parallaxMapPath, "outputs:out"); shader.detailMask.SetConnectedPath(detailMaskPath); shader.detailNormalMapScale.defaultValue = .05f; shader.detailNormalMap.SetConnectedPath(detailNormalMapPath, "outputs:out"); // // Setup Textures. // var albedoTexture = new Texture2DSample(); albedoTexture.sourceFile.defaultValue = textureFilePath + "albedoMap.png"; albedoTexture.sRgb = true; var normalTexture = new Texture2DSample(); normalTexture.sourceFile.defaultValue = textureFilePath + "normalMap.png"; normalTexture.sRgb = true; var emissionTexture = new Texture2DSample(); emissionTexture.sourceFile.defaultValue = textureFilePath + "emissionMap.png"; emissionTexture.sRgb = true; var metallicTexture = new Texture2DSample(); metallicTexture.sourceFile.defaultValue = textureFilePath + "metallicMap.png"; metallicTexture.sRgb = true; var occlusionTexture = new Texture2DSample(); occlusionTexture.sourceFile.defaultValue = textureFilePath + "occlusionMap.png"; occlusionTexture.sRgb = true; var parallaxTexture = new Texture2DSample(); parallaxTexture.sourceFile.defaultValue = textureFilePath + "parallaxMap.png"; parallaxTexture.sRgb = true; var detailNormalTexture = new Texture2DSample(); detailNormalTexture.sourceFile.defaultValue = textureFilePath + "detailMap.png"; detailNormalTexture.sRgb = true; var detailMaskTexture = new Texture2DSample(); detailMaskTexture.sourceFile.defaultValue = textureFilePath + "metallicMap.png"; detailMaskTexture.sRgb = true; // // Write Everything. // scene.Write(cubePath, cube); scene.Write(materialPath, material); scene.Write(shaderPath, shader); scene.Write(albedoMapPath, albedoTexture); scene.Write(normalMapPath, normalTexture); scene.Write(emissionMapPath, emissionTexture); scene.Write(metallicMapPath, metallicTexture); scene.Write(occlusionMapPath, occlusionTexture); scene.Write(parallaxMapPath, parallaxTexture); scene.Write(detailNormalMapPath, detailNormalTexture); scene.Write(detailMaskPath, detailMaskTexture); // // Bind Material. // MaterialSample.Bind(scene, cubePath, materialPath); // // Write out the scene as a string, for debugging. // string s; scene.Stage.ExportToString(out s); Debug.Log("Loading:\n" + s); return(scene); }
// // 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() { // Get the current local path m_localPath = PackageUtils.GetCallerRelativeToProjectFolderPath(); // Create a scene for this test, but could also be read from disk. 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; }
public static void ReadWriteTest() { // Game plan: // 1. Create a cube // 2. Create a material // 3. Create a shader // 4. Create a texture // 5. Connect the material to the shader's output // 6. Connect the shader's albedo parameter to the texture's output // 7. Connect the texture to the source file on disk // 8. Write all values // 9. Bind the cube to the material var scene = Scene.Create(); var cubePath = "/Model/Geom/Cube"; var materialPath = "/Model/Materials/SimpleMat"; var shaderPath = "/Model/Materials/SimpleMat/PreviewMaterial"; var texturePath = "/Model/Materials/SimpleMat/TextureReader"; var primvarReaderPath = "/Model/Materials/SimpleMat/UvReader"; var cube = new CubeSample(); cube.size = 1; var material = new MaterialSample(); material.surface.SetConnectedPath(shaderPath, "outputs:result"); var shader = new PreviewSurfaceSample(); shader.diffuseColor.defaultValue = Vector3.one; shader.diffuseColor.SetConnectedPath(texturePath, "outputs:rgb"); var texture = new TextureReaderSample(); texture.file.defaultValue = new SdfAssetPath(@"C:\A\Bogus\Texture\Path.png"); var primvarReader = new PrimvarReaderSample <Vector2>(); primvarReader.varname.defaultValue = new TfToken("st"); scene.Write("/Model", new XformSample()); scene.Write("/Model/Geom", new XformSample()); scene.Write("/Model/Materials", new XformSample()); scene.Write(cubePath, cube); scene.Write(materialPath, material); scene.Write(shaderPath, shader); scene.Write(texturePath, texture); scene.Write(primvarReaderPath, primvarReader); MaterialSample.Bind(scene, cubePath, materialPath); var material2 = new MaterialSample(); var shader2 = new PreviewSurfaceSample(); var texture2 = new TextureReaderSample(); var primvarReader2 = new PrimvarReaderSample <Vector2>(); scene.Read(materialPath, material2); scene.Read(shaderPath, shader2); scene.Read(texturePath, texture2); scene.Read(primvarReaderPath, primvarReader2); var param = shader2.GetInputParameters().First(); AssertEqual(shader.diffuseColor.connectedPath, param.connectedPath); AssertEqual("diffuseColor", param.usdName); AssertEqual(shader.diffuseColor.defaultValue, param.value); AssertEqual("_DiffuseColor", param.unityName); AssertEqual(material.surface.defaultValue, material2.surface.defaultValue); AssertEqual(material.surface.connectedPath, material2.surface.connectedPath); AssertEqual(shader.diffuseColor.defaultValue, shader2.diffuseColor.defaultValue); AssertEqual(shader.diffuseColor.connectedPath, shader2.diffuseColor.connectedPath); AssertEqual(shader.id, shader2.id); AssertEqual(texture.file.defaultValue, texture2.file.defaultValue); AssertEqual(primvarReader.varname.defaultValue, primvarReader.varname.defaultValue); PrintScene(scene); }
/// <summary> /// Builds a Unity Material from the given USD material sample. /// </summary> public static Material BuildMaterial(Scene scene, string materialPath, MaterialSample sample, SceneImportOptions options) { if (string.IsNullOrEmpty(sample.surface.connectedPath)) { Debug.LogWarning("Material has no connected surface: <" + materialPath + ">"); return(null); } var previewSurf = new UnityPreviewSurfaceSample(); scene.Read(new pxr.SdfPath(sample.surface.connectedPath).GetPrimPath(), previewSurf); // Currently, only UsdPreviewSurface is supported. if (previewSurf.id == null || previewSurf.id != "UsdPreviewSurface") { Debug.LogWarning("Unknown surface type: <" + sample.surface.connectedPath + ">" + "Surface ID: " + previewSurf.id); return(null); } Material mat = null; if (options.materialMap.useOriginalShaderIfAvailable && !string.IsNullOrEmpty(previewSurf.unity.shaderName)) { // We may or may not have the original shader. var shader = Shader.Find(previewSurf.unity.shaderName); if (shader) { mat = new Material(shader); mat.shaderKeywords = previewSurf.unity.shaderKeywords; } else { Debug.LogWarning("Original shader not found: " + previewSurf.unity.shaderName); } } if (mat == null) { if (previewSurf.useSpecularWorkflow.defaultValue == 1) { // Metallic workflow. mat = Material.Instantiate(options.materialMap.SpecularWorkflowMaterial); } else { // Metallic workflow. mat = Material.Instantiate(options.materialMap.MetallicWorkflowMaterial); } } foreach (var kvp in previewSurf.unity.colorArgs) { mat.SetColor(kvp.Key, kvp.Value); } foreach (var kvp in previewSurf.unity.floatArgs) { mat.SetFloat(kvp.Key, kvp.Value); } foreach (var kvp in previewSurf.unity.vectorArgs) { mat.SetVector(kvp.Key, kvp.Value); } var pipeline = UnityEngine.Rendering.GraphicsSettings.renderPipelineAsset; if (!pipeline) { var matAdapter = new StandardShaderImporter(mat); matAdapter.ImportParametersFromUsd(scene, materialPath, sample, previewSurf, options); matAdapter.ImportFromUsd(); } else if (pipeline.GetType().Name == "HDRenderPipelineAsset") { // Robustness: Comparing a strng ^ here is not great, but there is no other option. var matAdapter = new HdrpShaderImporter(mat); matAdapter.ImportParametersFromUsd(scene, materialPath, sample, previewSurf, options); matAdapter.ImportFromUsd(); } else { // Fallback to the Standard importer, which may pickup some attributes by luck. var matAdapter = new StandardShaderImporter(mat); matAdapter.ImportParametersFromUsd(scene, materialPath, sample, previewSurf, options); matAdapter.ImportFromUsd(); } return(mat); }
static void ExportMesh(ObjectContext objContext, ExportContext exportContext, Mesh mesh, Material sharedMaterial, Material[] sharedMaterials, bool exportMeshPose = true) { if (mesh.isReadable == false) { Debug.LogWarning("Mesh not readable: " + objContext.path); return; } string path = objContext.path; if (mesh == null) { Debug.LogWarning("Null mesh for: " + path); return; } var scene = exportContext.scene; bool unvarying = scene.Time == null; bool slowAndSafeConversion = exportContext.basisTransform == BasisTransformation.SlowAndSafe; var sample = (MeshSample)objContext.sample; var go = objContext.gameObject; if (mesh.bounds.center == Vector3.zero && mesh.bounds.extents == Vector3.zero) { mesh.RecalculateBounds(); } sample.extent = mesh.bounds; if (slowAndSafeConversion) { // Unity uses a forward vector that matches DirectX, but USD matches OpenGL, so a change of // basis is required. There are shortcuts, but this is fully general. sample.ConvertTransform(); sample.extent.center = UnityTypeConverter.ChangeBasis(sample.extent.center); } // Only export the mesh topology on the first frame. if (unvarying) { // TODO: Technically a mesh could be the root transform, which is not handled correctly here. // It should ahve the same logic for root prims as in ExportXform. sample.transform = XformExporter.GetLocalTransformMatrix( go.transform, scene.UpAxis == Scene.UpAxes.Z, new pxr.SdfPath(path).IsRootPrimPath(), exportContext.basisTransform); sample.normals = mesh.normals; sample.points = mesh.vertices; sample.tangents = mesh.tangents; sample.colors = mesh.colors; if (sample.colors != null && sample.colors.Length == 0) { sample.colors = null; } if ((sample.colors == null || sample.colors.Length == 0) && (sharedMaterial != null && sharedMaterial.HasProperty("_Color"))) { sample.colors = new Color[1]; sample.colors[0] = sharedMaterial.color.linear; } // Gah. There is no way to inspect a meshes UVs. sample.st = mesh.uv; // Set face vertex counts and indices. var tris = mesh.triangles; if (slowAndSafeConversion) { // Unity uses a forward vector that matches DirectX, but USD matches OpenGL, so a change // of basis is required. There are shortcuts, but this is fully general. for (int i = 0; i < sample.points.Length; i++) { sample.points[i] = UnityTypeConverter.ChangeBasis(sample.points[i]); if (sample.normals != null && sample.normals.Length == sample.points.Length) { sample.normals[i] = UnityTypeConverter.ChangeBasis(sample.normals[i]); } } for (int i = 0; i < tris.Length; i += 3) { var t = tris[i]; tris[i] = tris[i + 1]; tris[i + 1] = t; } } sample.SetTriangles(tris); UnityEngine.Profiling.Profiler.BeginSample("USD: Mesh Write"); scene.Write(path, sample); UnityEngine.Profiling.Profiler.EndSample(); // TODO: this is a bit of a half-measure, we need real support for primvar interpolation. // Set interpolation based on color count. if (sample.colors != null && sample.colors.Length == 1) { pxr.UsdPrim usdPrim = scene.GetPrimAtPath(path); var colorPrimvar = new pxr.UsdGeomPrimvar(usdPrim.GetAttribute(pxr.UsdGeomTokens.primvarsDisplayColor)); colorPrimvar.SetInterpolation(pxr.UsdGeomTokens.constant); var opacityPrimvar = new pxr.UsdGeomPrimvar(usdPrim.GetAttribute(pxr.UsdGeomTokens.primvarsDisplayOpacity)); opacityPrimvar.SetInterpolation(pxr.UsdGeomTokens.constant); } string usdMaterialPath; if (exportContext.exportMaterials && sharedMaterial != null) { if (!exportContext.matMap.TryGetValue(sharedMaterial, out usdMaterialPath)) { Debug.LogError("Invalid material bound for: " + path); } else { MaterialSample.Bind(scene, path, usdMaterialPath); } } // In USD subMeshes are represented as UsdGeomSubsets. // When there are multiple subMeshes, convert them into UsdGeomSubsets. if (mesh.subMeshCount > 1) { // Build a table of face indices, used to convert the subMesh triangles to face indices. var faceTable = new Dictionary <Vector3, int>(); for (int i = 0; i < tris.Length; i += 3) { if (!slowAndSafeConversion) { faceTable.Add(new Vector3(tris[i], tris[i + 1], tris[i + 2]), i / 3); } else { // Under slow and safe export, index 0 and 1 are swapped. // This swap will not be present in the subMesh indices, so must be undone here. faceTable.Add(new Vector3(tris[i + 1], tris[i], tris[i + 2]), i / 3); } } var usdPrim = scene.GetPrimAtPath(path); var usdGeomMesh = new pxr.UsdGeomMesh(usdPrim); // Process each subMesh and create a UsdGeomSubset of faces this subMesh targets. for (int si = 0; si < mesh.subMeshCount; si++) { int[] indices = mesh.GetTriangles(si); int[] faceIndices = new int[indices.Length / 3]; for (int i = 0; i < indices.Length; i += 3) { faceIndices[i / 3] = faceTable[new Vector3(indices[i], indices[i + 1], indices[i + 2])]; } var materialBindToken = new pxr.TfToken("materialBind"); var vtIndices = UnityTypeConverter.ToVtArray(faceIndices); var subset = pxr.UsdGeomSubset.CreateUniqueGeomSubset( usdGeomMesh, // The object of which this subset belongs. "subMeshes", // An arbitrary name for the subset. pxr.UsdGeomTokens.face, // Indicator that these represent face indices vtIndices, // The actual face indices. materialBindToken // familyName = "materialBind" ); if (exportContext.exportMaterials) { if (si >= sharedMaterials.Length || !exportContext.matMap.TryGetValue(sharedMaterials[si], out usdMaterialPath)) { Debug.LogError("Invalid material bound for: " + path); } else { MaterialSample.Bind(scene, subset.GetPath(), usdMaterialPath); } } } } } else { // Only write the transform when animating. var meshSample = new MeshSampleBase(); meshSample.extent = sample.extent; meshSample.transform = XformExporter.GetLocalTransformMatrix( go.transform, scene.UpAxis == Scene.UpAxes.Z, new pxr.SdfPath(path).IsRootPrimPath(), exportContext.basisTransform); if (exportMeshPose) { meshSample.points = mesh.vertices; // Set face vertex counts and indices. var tris = mesh.triangles; if (slowAndSafeConversion) { // Unity uses a forward vector that matches DirectX, but USD matches OpenGL, so a change // of basis is required. There are shortcuts, but this is fully general. for (int i = 0; i < meshSample.points.Length; i++) { meshSample.points[i] = UnityTypeConverter.ChangeBasis(meshSample.points[i]); } for (int i = 0; i < tris.Length; i += 3) { var t = tris[i]; tris[i] = tris[i + 1]; tris[i + 1] = t; } } sample.SetTriangles(tris); } UnityEngine.Profiling.Profiler.BeginSample("USD: Mesh Write"); scene.Write(path, meshSample); UnityEngine.Profiling.Profiler.EndSample(); } }