diff --git a/src/ExternalTextureHelper.cs b/src/ExternalTextureHelper.cs new file mode 100644 index 0000000..423b29f --- /dev/null +++ b/src/ExternalTextureHelper.cs @@ -0,0 +1,141 @@ +using SharpGLTF.Schema2; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace i3dm.export; + +internal static class ExternalTextureHelper +{ + public static WriteSettings ConfigureExternalTextureUris(ModelRoot model, Dictionary externalTextures, string outputDirectory, bool suppressSatelliteWrite = false) + { + var relativeUrisUsed = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var image in model.LogicalImages) + { + var relativeUri = ResolveRelativeUriForImage(image, externalTextures); + if (string.IsNullOrWhiteSpace(relativeUri)) continue; + + image.AlternateWriteFileName = relativeUri; + relativeUrisUsed.Add(relativeUri); + } + + EnsureOutputDirectories(outputDirectory, relativeUrisUsed); + return CreateSatelliteWriteSettings(suppressSatelliteWrite); + } + + public static MemoryStream WriteGlbToStream(ModelRoot model, WriteSettings writeSettings) + { + var stream = new MemoryStream(); + model.WriteGLB(stream, writeSettings); + stream.Position = 0; + return stream; + } + + public static void CollectExternalTextures(Dictionary externalTextures, string modelPath, ModelRoot modelRoot) + { + if (externalTextures == null) return; + + foreach (var image in modelRoot.LogicalImages) + { + if (!TryGetExternalTextureReference(image, modelPath, out var absoluteSourcePath, out var relativeTextureUri)) continue; + externalTextures[absoluteSourcePath] = relativeTextureUri; + } + } + + public static bool TryGetExternalTextureReference(Image image, string modelPath, out string absoluteSourcePath, out string relativeTextureUri) + { + absoluteSourcePath = null; + relativeTextureUri = null; + + if (image.Content.IsEmpty) return false; + var sourcePath = image.Content.SourcePath; + if (string.IsNullOrWhiteSpace(sourcePath)) return false; + + var modelDirectory = Path.GetDirectoryName(modelPath) ?? string.Empty; + var modelName = Path.GetFileNameWithoutExtension(modelPath); + absoluteSourcePath = GetAbsoluteTexturePath(sourcePath, modelDirectory); + relativeTextureUri = $"textures/{modelName}/{Path.GetFileName(absoluteSourcePath)}"; + return true; + } + + public static string ResolveRelativeUriForImage(Image image, Dictionary externalTextures) + { + if (image.Content.IsEmpty) return null; + var sourcePath = image.Content.SourcePath; + if (string.IsNullOrWhiteSpace(sourcePath)) return null; + + var fileName = Path.GetFileName(sourcePath); + var matches = externalTextures + .Where(kvp => Path.GetFileName(kvp.Key).Equals(fileName, StringComparison.OrdinalIgnoreCase)) + .Select(kvp => kvp.Value) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return matches.Count == 1 ? matches[0] : $"textures/_shared/{fileName}"; + } + + public static void EnsureOutputDirectories(string outputDirectory, IEnumerable relativeUris) + { + if (string.IsNullOrWhiteSpace(outputDirectory)) return; + + foreach (var rel in relativeUris) + { + var fsRel = rel.Replace('/', Path.DirectorySeparatorChar); + var dir = Path.GetDirectoryName(Path.Combine(outputDirectory, fsRel)); + if (!string.IsNullOrWhiteSpace(dir)) + { + Directory.CreateDirectory(dir); + } + } + } + + public static void CopyTextureIfMissing(string outputDirectory, string absoluteSourcePath, string relativeTextureUri) + { + var destination = Path.Combine(outputDirectory, relativeTextureUri.Replace('/', Path.DirectorySeparatorChar)); + var destinationDirectory = Path.GetDirectoryName(destination); + if (!string.IsNullOrWhiteSpace(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + if (!File.Exists(destination)) + { + File.Copy(absoluteSourcePath, destination); + } + } + + public static void CopyExternalTextures(string outputDirectory, IReadOnlyDictionary externalTextures, ISet copiedDestinations = null) + { + foreach (var texture in externalTextures) + { + var destination = Path.Combine(outputDirectory, texture.Value.Replace('/', Path.DirectorySeparatorChar)); + if (copiedDestinations != null && !copiedDestinations.Add(destination)) continue; + CopyTextureIfMissing(outputDirectory, texture.Key, texture.Value); + } + } + + private static WriteSettings CreateSatelliteWriteSettings(bool suppressSatelliteWrite) + { + var settings = new WriteSettings + { + ImageWriting = ResourceWriteMode.SatelliteFile + }; + + if (suppressSatelliteWrite) + { + settings.ImageWriteCallback = (ctx, assetName, image) => assetName; + } + + return settings; + } + + private static string GetAbsoluteTexturePath(string sourcePath, string modelDirectory) + { + if (string.IsNullOrWhiteSpace(sourcePath)) return sourcePath; + if (Path.IsPathRooted(sourcePath)) return Path.GetFullPath(sourcePath); + if (string.IsNullOrEmpty(modelDirectory)) return Path.GetFullPath(sourcePath); + return Path.GetFullPath(Path.Combine(modelDirectory, sourcePath)); + } +} diff --git a/src/GPUTileHandler.cs b/src/GPUTileHandler.cs index 56947b8..96a5554 100644 --- a/src/GPUTileHandler.cs +++ b/src/GPUTileHandler.cs @@ -30,38 +30,8 @@ public static void SaveGPUTile(string filePath, List instances, bool U return; } - var relativeUrisUsed = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var image in model.LogicalImages) - { - if (image.Content.IsEmpty) continue; - - var sourcePath = image.Content.SourcePath; - if (string.IsNullOrWhiteSpace(sourcePath)) continue; // embedded images stay embedded - - var fileName = Path.GetFileName(sourcePath); - - var matches = externalTextures - .Where(kvp => Path.GetFileName(kvp.Key).Equals(fileName, StringComparison.OrdinalIgnoreCase)) - .Select(kvp => kvp.Value) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - var relativeUri = matches.Count == 1 ? matches[0] : $"textures/_shared/{fileName}"; - - image.AlternateWriteFileName = relativeUri; - relativeUrisUsed.Add(relativeUri); - } - var outputDirectory = Path.GetDirectoryName(filePath) ?? string.Empty; - foreach (var rel in relativeUrisUsed) - { - var fsRel = rel.Replace('/', Path.DirectorySeparatorChar); - var dir = Path.GetDirectoryName(Path.Combine(outputDirectory, fsRel)); - if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - } - - var writeSettings = new WriteSettings { ImageWriting = ResourceWriteMode.SatelliteFile }; + var writeSettings = ExternalTextureHelper.ConfigureExternalTextureUris(model, externalTextures, outputDirectory); model.SaveGLB(filePath, writeSettings); } @@ -149,7 +119,7 @@ private static SceneBuilder AddModels(IEnumerable instances, Point tra var modelPath = (string)model; var modelRoot = ModelRoot.Load(modelPath); - CollectExternalTextures(externalTextures, modelPath, modelRoot); + ExternalTextureHelper.CollectExternalTextures(externalTextures, modelPath, modelRoot); var meshNodeCount = AddModelInstancesToScene(sceneBuilder, instances, UseScaleNonUniform, translation, modelPath, modelRoot); if (meshNodeCountsByModel != null) meshNodeCountsByModel[modelPath] = meshNodeCount; @@ -212,38 +182,6 @@ private static SceneBuilder GetSceneBuilder(IMeshBuilder meshBu return sceneBuilder; } - private static void CollectExternalTextures(Dictionary externalTextures, string modelPath, ModelRoot modelRoot) - { - if (externalTextures == null) return; - - var modelName = Path.GetFileNameWithoutExtension(modelPath); - var modelDirectory = Path.GetDirectoryName(modelPath) ?? string.Empty; - - foreach (var image in modelRoot.LogicalImages) - { - if (image.Content.IsEmpty) continue; - var sourcePath = image.Content.SourcePath; - if (string.IsNullOrWhiteSpace(sourcePath)) continue; - - var absoluteSourcePath = GetAbsoluteTexturePath(sourcePath, modelDirectory); - var fileName = Path.GetFileName(absoluteSourcePath); - - externalTextures[absoluteSourcePath] = $"textures/{modelName}/{fileName}"; - } - } - - - private static string GetAbsoluteTexturePath(string sourcePath, string modelDirectory) - { - if (string.IsNullOrWhiteSpace(sourcePath)) return sourcePath; - - if (Path.IsPathRooted(sourcePath)) return Path.GetFullPath(sourcePath); - - if (string.IsNullOrEmpty(modelDirectory)) return Path.GetFullPath(sourcePath); - - return Path.GetFullPath(Path.Combine(modelDirectory, sourcePath)); - } - private static AffineTransform GetInstanceTransform(Instance instance, bool UseScaleNonUniform, Point translation) { var point = (Point)instance.Position; diff --git a/src/ImplicitTiling.cs b/src/ImplicitTiling.cs index d21443e..d0789bb 100644 --- a/src/ImplicitTiling.cs +++ b/src/ImplicitTiling.cs @@ -60,8 +60,8 @@ public static List GenerateTiles(Options o, NpgsqlConnection conn, Boundin } else { - var bytes = CreateTile(o, instances, useGpuInstancing, useI3dm); - SaveTile(contentDirectory, tile, bytes, useGpuInstancing, useI3dm, instances, (bool)o.UseExternalModel); + var bytes = CreateTile(o, instances, useGpuInstancing, useI3dm, contentDirectory); + SaveTile(contentDirectory, tile, bytes, useGpuInstancing, useI3dm); } } else @@ -100,8 +100,8 @@ public static List GenerateTiles(Options o, NpgsqlConnection conn, Boundin else { var instances = InstancesRepository.GetInstances(conn, o.Table, o.GeometryColumn, bbox, source_epsg, where, (bool)o.UseScaleNonUniform, useGpuInstancing, keepProjection); - var bytes = CreateTile(o, instances, useGpuInstancing, useI3dm); - SaveTile(contentDirectory, tile, bytes, useGpuInstancing, useI3dm, instances, (bool)o.UseExternalModel); + var bytes = CreateTile(o, instances, useGpuInstancing, useI3dm, contentDirectory); + SaveTile(contentDirectory, tile, bytes, useGpuInstancing, useI3dm); } var t1 = new Tile(tile.Z, tile.X, tile.Y); @@ -119,7 +119,7 @@ private static void SaveGpuTile(string contentDirectory, Tile tile, List instances = null, bool useExternalModel = false) + private static void SaveTile(string contentDirectory, Tile tile, byte[] bytes, bool useGpuInstancing, bool useI3dm) { var extension = useGpuInstancing ? "glb" : "cmpt"; if (useI3dm) @@ -130,14 +130,9 @@ private static void SaveTile(string contentDirectory, Tile tile, byte[] bytes, b Console.Write($"\rCreating tile: {file} "); File.WriteAllBytes(file, bytes); - - if (!useGpuInstancing && !useExternalModel && instances != null && instances.Count > 0) - { - TileHandler.CopyExternalTexturesForEmbeddedModels(contentDirectory, instances); - } } - private static byte[] CreateTile(Options o, List instances, bool useGpuInstancing, bool useI3dm) + private static byte[] CreateTile(Options o, List instances, bool useGpuInstancing, bool useI3dm, string contentDirectory = null) { byte[] tile; @@ -148,12 +143,12 @@ private static byte[] CreateTile(Options o, List instances, bool useGp else if(!useI3dm) { // create cmpt - tile = TileHandler.GetCmptTile(instances, (bool)o.UseExternalModel, (bool)o.UseScaleNonUniform); + tile = TileHandler.GetCmptTile(instances, (bool)o.UseExternalModel, (bool)o.UseScaleNonUniform, contentDirectory); } else { // take the first model for i3dm - tile = TileHandler.GetI3dmTile(instances, (bool)o.UseExternalModel, (bool)o.UseScaleNonUniform, instances.First().Model); + tile = TileHandler.GetI3dmTile(instances, (bool)o.UseExternalModel, (bool)o.UseScaleNonUniform, instances.First().Model, contentDirectory); } return tile; diff --git a/src/TileHandler.cs b/src/TileHandler.cs index 3d5318e..ee9c8e5 100644 --- a/src/TileHandler.cs +++ b/src/TileHandler.cs @@ -13,7 +13,7 @@ namespace i3dm.export; public static class TileHandler { - public static byte[] GetCmptTile(List instances, bool UseExternalModel = false, bool UseScaleNonUniform = false) + public static byte[] GetCmptTile(List instances, bool UseExternalModel = false, bool UseScaleNonUniform = false, string outputDirectory = null) { var uniqueModels = instances.Select(s => s.Model).Distinct(); @@ -21,7 +21,7 @@ public static byte[] GetCmptTile(List instances, bool UseExternalModel foreach (var model in uniqueModels) { - var bytesI3dm = GetI3dmTile(instances, UseExternalModel, UseScaleNonUniform, model); + var bytesI3dm = GetI3dmTile(instances, UseExternalModel, UseScaleNonUniform, model, outputDirectory); tiles.Add(bytesI3dm); } @@ -29,7 +29,7 @@ public static byte[] GetCmptTile(List instances, bool UseExternalModel return bytes; } - public static byte[] GetI3dmTile(List instances, bool UseExternalModel, bool UseScaleNonUniform, object model) + public static byte[] GetI3dmTile(List instances, bool UseExternalModel, bool UseScaleNonUniform, object model, string outputDirectory = null) { var positions = new List(); var scales = new List(); @@ -43,7 +43,7 @@ public static byte[] GetI3dmTile(List instances, bool UseExternalModel CalculateArrays(modelInstances, UseScaleNonUniform, positions, scales, scalesNonUniform, normalUps, normalRights, tags); - var i3dm = GetI3dm(model, positions, firstPosition, scales, scalesNonUniform, normalUps, normalRights, tags, UseExternalModel, UseScaleNonUniform); + var i3dm = GetI3dm(model, positions, firstPosition, scales, scalesNonUniform, normalUps, normalRights, tags, UseExternalModel, UseScaleNonUniform, outputDirectory); var bytesI3dm = I3dmWriter.Write(i3dm); return bytesI3dm; } @@ -79,7 +79,7 @@ internal static void CalculateArrays(List instances, bool UseScaleNonU } } - internal static I3dm.Tile.I3dm GetI3dm(object model, List positions, Point rtcCenter, List scales, List scalesNonUniform, List normalUps, List normalRights, List tags, bool UseExternalModel = false, bool UseScaleNonUniform = false) + internal static I3dm.Tile.I3dm GetI3dm(object model, List positions, Point rtcCenter, List scales, List scalesNonUniform, List normalUps, List normalRights, List tags, bool UseExternalModel = false, bool UseScaleNonUniform = false, string outputDirectory = null) { I3dm.Tile.I3dm i3dm = null; @@ -87,7 +87,27 @@ internal static I3dm.Tile.I3dm GetI3dm(object model, List positions, Po { if (!UseExternalModel) { - var glbBytes = GetEmbeddedGlbBytesWithRewrittenExternalImageUris((string)model); + var modelPath = (string)model; + var modelRoot = ModelRoot.Load(modelPath); + var externalTextures = new Dictionary(StringComparer.OrdinalIgnoreCase); + ExternalTextureHelper.CollectExternalTextures(externalTextures, modelPath, modelRoot); + + byte[] glbBytes; + if (externalTextures.Count == 0) + { + glbBytes = File.ReadAllBytes(modelPath); + } + else + { + if (!string.IsNullOrWhiteSpace(outputDirectory)) + { + ExternalTextureHelper.CopyExternalTextures(outputDirectory, externalTextures); + } + + var writeSettings = ExternalTextureHelper.ConfigureExternalTextureUris(modelRoot, externalTextures, outputDirectory, suppressSatelliteWrite: true); + using var stream = ExternalTextureHelper.WriteGlbToStream(modelRoot, writeSettings); + glbBytes = stream.ToArray(); + } i3dm = new I3dm.Tile.I3dm(positions, glbBytes); } else @@ -137,90 +157,9 @@ public static void CopyExternalTexturesForEmbeddedModels(string outputDirectory, foreach (var modelPath in modelPaths) { var modelRoot = ModelRoot.Load(modelPath); - var modelName = Path.GetFileNameWithoutExtension(modelPath); - var modelDirectory = Path.GetDirectoryName(modelPath) ?? string.Empty; - - foreach (var image in modelRoot.LogicalImages) - { - if (image.Content.IsEmpty) continue; - var sourcePath = image.Content.SourcePath; - if (string.IsNullOrWhiteSpace(sourcePath)) continue; - - var absoluteSourcePath = GetAbsoluteTexturePath(sourcePath, modelDirectory); - var destination = Path.Combine(outputDirectory, "textures", modelName, Path.GetFileName(absoluteSourcePath)); - - if (!copiedDestinations.Add(destination)) continue; - - var destinationDirectory = Path.GetDirectoryName(destination); - if (!string.IsNullOrWhiteSpace(destinationDirectory)) - { - Directory.CreateDirectory(destinationDirectory); - } - - if (!File.Exists(destination)) - { - File.Copy(absoluteSourcePath, destination); - } - } - } - } - - private static string GetAbsoluteTexturePath(string sourcePath, string modelDirectory) - { - if (string.IsNullOrWhiteSpace(sourcePath)) return sourcePath; - if (Path.IsPathRooted(sourcePath)) return Path.GetFullPath(sourcePath); - if (string.IsNullOrEmpty(modelDirectory)) return Path.GetFullPath(sourcePath); - return Path.GetFullPath(Path.Combine(modelDirectory, sourcePath)); - } - - private static byte[] GetEmbeddedGlbBytesWithRewrittenExternalImageUris(string modelPath) - { - var modelRoot = ModelRoot.Load(modelPath); - var modelName = Path.GetFileNameWithoutExtension(modelPath); - var modelDirectory = Path.GetDirectoryName(modelPath) ?? string.Empty; - var hasExternalImages = false; - - foreach (var image in modelRoot.LogicalImages) - { - if (image.Content.IsEmpty) continue; - var sourcePath = image.Content.SourcePath; - if (string.IsNullOrWhiteSpace(sourcePath)) continue; - - var absoluteSourcePath = GetAbsoluteTexturePath(sourcePath, modelDirectory); - image.AlternateWriteFileName = $"textures/{modelName}/{Path.GetFileName(absoluteSourcePath)}"; - hasExternalImages = true; - } - - if (!hasExternalImages) - { - return File.ReadAllBytes(modelPath); - } - - var temporaryDirectory = Path.Combine(Path.GetTempPath(), $"i3dm_export_{Guid.NewGuid():N}"); - var temporaryGlbPath = Path.Combine(temporaryDirectory, "model.glb"); - Directory.CreateDirectory(temporaryDirectory); - foreach (var image in modelRoot.LogicalImages) - { - if (string.IsNullOrWhiteSpace(image.AlternateWriteFileName)) continue; - var relativePath = image.AlternateWriteFileName.Replace('/', Path.DirectorySeparatorChar); - var imageDirectory = Path.GetDirectoryName(Path.Combine(temporaryDirectory, relativePath)); - if (!string.IsNullOrWhiteSpace(imageDirectory)) - { - Directory.CreateDirectory(imageDirectory); - } - } - try - { - var writeSettings = new WriteSettings { ImageWriting = ResourceWriteMode.SatelliteFile }; - modelRoot.SaveGLB(temporaryGlbPath, writeSettings); - return File.ReadAllBytes(temporaryGlbPath); - } - finally - { - if (Directory.Exists(temporaryDirectory)) - { - Directory.Delete(temporaryDirectory, true); - } + var externalTextures = new Dictionary(StringComparer.OrdinalIgnoreCase); + ExternalTextureHelper.CollectExternalTextures(externalTextures, modelPath, modelRoot); + ExternalTextureHelper.CopyExternalTextures(outputDirectory, externalTextures, copiedDestinations); } } diff --git a/tests/TileHandlerTests.cs b/tests/TileHandlerTests.cs index dbd57ce..3da72ba 100644 --- a/tests/TileHandlerTests.cs +++ b/tests/TileHandlerTests.cs @@ -401,7 +401,10 @@ public void NonGpuTileWithExternalTextures_RewritesImageUriToTexturesFolder() instance.Model = "./testfixtures/external_textures/Lov_asp_1_cr.glb"; instances.Add(instance); - var tile = TileHandler.GetCmptTile(instances, UseExternalModel: false); + var contentDir = Path.Combine(TestContext.CurrentContext.WorkDirectory, "content_external_textures_non_gpu_uri"); + Directory.CreateDirectory(contentDir); + + var tile = TileHandler.GetCmptTile(instances, UseExternalModel: false, outputDirectory: contentDir); var cmpt = CmptReader.Read(new MemoryStream(tile)); var i3dm = I3dmReader.Read(new MemoryStream(cmpt.Tiles.First()));