using System; using System.Collections.Generic; using System.Linq; using System.Text; using NUnit.Framework; namespace SharpGLTF { /// /// Centralices the access to test files. /// public static class TestFiles { #region lifecycle private static void _EnsureInitialized() { if (_TestFilesDir != null) return; var wdir = TestContext.CurrentContext.WorkDirectory; var examplesFound = false; while (wdir.Length > 3) { _TestFilesDir = System.IO.Path.Combine(wdir, "TestFiles"); if (wdir.ToLowerInvariant().EndsWith("tests") && System.IO.Directory.Exists(_TestFilesDir)) { examplesFound = true; break; } wdir = System.IO.Path.GetDirectoryName(wdir); } Assert.That(examplesFound, "TestFiles directory not found; please, run '1_DownloadTestFiles.cmd' before running the tests."); _AssetFilesDir = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(_TestFilesDir), "Assets"); } private static string _UsingExternalFiles(params string[] subPath) { _EnsureInitialized(); return System.IO.Path.Combine(new string[] { _TestFilesDir }.Concat(subPath).ToArray()); } private static string _UsingInternalFiles(params string[] subPath) { _EnsureInitialized(); return System.IO.Path.Combine(new string[] { _AssetFilesDir }.Concat(subPath).ToArray()); } #endregion #region data /// /// Path to Tests/Assets/ /// private static string _AssetFilesDir; /// /// Path to Tests/TestFiles/ /// private static string _TestFilesDir; private static readonly string _SchemaDir = _UsingExternalFiles("glTF-Schema"); private static readonly string _ValidationDir = _UsingExternalFiles("glTF-Validator"); internal static readonly string _SampleModelsDir = _UsingExternalFiles("glTF-Sample-Models"); private static readonly string _BabylonJsMeshesDir = _UsingExternalFiles("BabylonJS-Assets"); private static readonly string _GeneratedModelsDir = _UsingExternalFiles("GeneratedReferenceModels", "v_0_6_1"); #endregion #region properties public static string KhronosSampleModelsDirectory => _SampleModelsDir; #endregion #region API public static IReadOnlyList GetSchemaExtensionsModelsPaths() { return GetModelPathsInDirectory(_SchemaDir, "extensions", "2.0"); } public static IEnumerable GetReferenceModelPaths(bool useNegative = false) { var dirPath = _GeneratedModelsDir; if (dirPath.EndsWith(".zip")) dirPath = dirPath.Substring(0, dirPath.Length - 4); var manifestsPath = System.IO.Path.Combine(dirPath, useNegative? "Negative" : "Positive"); var manifests = System.IO.Directory.GetFiles(manifestsPath, "Manifest.json", System.IO.SearchOption.AllDirectories) .Skip(1) .ToArray(); foreach (var m in manifests) { var d = System.IO.Path.GetDirectoryName(m); var content = System.IO.File.ReadAllText(m); var doc = Newtonsoft.Json.Linq.JObject.Parse(content); var models = doc.SelectToken("models"); foreach(var model in models) { var mdlPath = (String)model.SelectToken("fileName"); var loadable = !useNegative; if (loadable) loadable = (Boolean)model.SelectToken("loadable"); mdlPath = System.IO.Path.Combine(d, mdlPath); yield return mdlPath; } } yield break; } public static IReadOnlyList GetSampleModelsPaths() { var entries = KhronosSampleModel.Load(); var files = entries .SelectMany(item => item.GetPaths(_SampleModelsDir, "2.0")) .ToList(); return files; } public static IReadOnlyList GetKhronosValidationPaths() { var skip = new string[] { "empty_object.gltf", // need to look further "custom_property.gltf", "integer_written_as_float.gltf", "unknown_type.gltf", "valid.gltf", // valid just because objects are unused "get_elements_sparse.gltf", // valid just because objects are unused "invalid_elements_float.gltf", // sure, it has invalid floats, but then the accessor is not used. "not_found.gltf", // it fails at a tricky time "non_relative_uri.gltf", // absolute path pointing to a http which is not supported. "unrecognized_format.gltf", // might require to dig into the image "multiple_extensions.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty... "invalid_tangent.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty... "primitive_incompatible_mode.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty... "primitive_no_position.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty... "index_buffer_degenerate_triangle.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty... "node_skinned_mesh_without_skin.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty... "duplicate_extension_entry.gltf", "named_objects.gltf", // gltf-validator says valid, but Buffer should not be. "unused_objects.gltf", "ignored_animated_transform.gltf", // an channel animated a node with a skin has no effect (warning) since nodes with skin have no transform "ignored_local_transform.gltf", // a transform in a node with a skin has no effect (warning) since nodes with skin have no transform "ignored_parent_transform.gltf", // a transform in a node with a skin has no effect (warning) since nodes with skin have no transform "misplaced_bin_chunk.glb", "valid_placeholder.glb", "undeclared_extension.gltf", "unexpected_extension.gltf", "unresolved_source.gltf", "unresolved_light_empty_root_ext.gltf", "unresolved_light_no_root_ext.gltf", "invalid_image_mime_type.gltf", // actual images cannot be validated }; var files = GetModelPathsInDirectory(_ValidationDir, "test") .Where(item => skip.All(f=>!item.EndsWith(f))); return files .OrderBy(item => item) .ToList(); } public static IReadOnlyList GetBabylonJSModelsPaths() { var skipAlways = new string[] { "\\Elf\\Elf.gltf", // validator reports invalid inverse bind matrices. "\\meshes\\Tests\\AssetGenerator", // already covered separately. "\\meshes\\KHR_materials_volume_testing.glb", // draco compression- "\\meshes\\Yeti\\MayaExport\\", // validator reports out of bounds accesor "\\meshes\\Demos\\retargeting\\riggedMesh.glb", // validator reports errors "\\meshes\\Buildings\\road gap.glb", // uses KHR_Draco compression "\\meshes\\Buildings\\Road corner.glb", // uses KHR_Draco compression "\\meshes\\Tests\\BadDraco\\Box-draco.glb", // uses KHR_Draco compression }; var files = GetModelPathsInDirectory(_BabylonJsMeshesDir); return files .Where(item => !item.ToUpperInvariant().Contains("GLTF-DRACO")) .Where(item => !item.ToUpperInvariant().Contains("GLTF-MESHOPT")) // not supported yet .Where(item => skipAlways.All(f => !item.Contains(f))) .OrderBy(item => item) .ToList(); } public static string GetPollyFileModelPath() { return _UsingExternalFiles("glTF-Blender-Exporter", "polly", "project_polly.glb"); } public static string GetUniVRMModelPath() { return _UsingExternalFiles("UniVRM", "AliciaSolid_vrm-0.51.vrm"); } public static IEnumerable GetMeshIntancingModelPaths() { var fromBabylon = GetBabylonJSModelsPaths() .Where(item => item.ToLowerInvariant().Contains("teapot")); var meshInstPath = _UsingInternalFiles("gltf-GpuMeshInstancing"); var fromLocal = System.IO.Directory.GetFiles(meshInstPath, "*.glb", System.IO.SearchOption.AllDirectories); return fromBabylon.Concat(fromLocal); } private static IReadOnlyList GetModelPathsInDirectory(params string[] paths) { var dirPath = System.IO.Path.Combine(paths); if (dirPath.EndsWith(".zip")) dirPath = dirPath.Substring(0, dirPath.Length-4); // if (!System.IO.Path.IsPathFullyQualified(dirPath)) throw new ArgumentException(nameof(dirPath)); if (!System.IO.Path.IsPathRooted(dirPath)) throw new ArgumentException(nameof(dirPath)); var gltf = System.IO.Directory.GetFiles(dirPath, "*.gltf", System.IO.SearchOption.AllDirectories); var glbb = System.IO.Directory.GetFiles(dirPath, "*.glb", System.IO.SearchOption.AllDirectories); return gltf.Concat(glbb).ToList(); } #endregion } [System.Diagnostics.DebuggerDisplay("{Name}")] class KhronosSampleModel { #region loaders public static KhronosSampleModel[] Load() { var path = System.IO.Path.Combine(TestFiles._SampleModelsDir, "2.0", "model-index.json"); var text = System.IO.File.ReadAllText(path); return Read(text); } public static KhronosSampleModel[] Read(string json) { var opts = new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; return System.Text.Json.JsonSerializer.Deserialize(json, opts); } #endregion #region data public string Name { get; set; } public string Screenshot { get; set; } public Dictionary Variants { get; set; } = new Dictionary(); #endregion #region API public IEnumerable GetPaths(params string[] basePath) { var rootPath = System.IO.Path.Combine(basePath); foreach(var variant in Variants) { if (variant.Key == "glTF-Draco") continue; // draco is not supported by SharpGLTF yield return System.IO.Path.Combine(rootPath, Name, variant.Key, variant.Value); } } #endregion } }