From aeed8503c2d314810abb4ec88d5b422f837a8e30 Mon Sep 17 00:00:00 2001 From: azyges Date: Fri, 2 Jan 2026 09:54:34 +0900 Subject: [PATCH] highly experimental runtime model decimation + file cache adjustment to clean up processed file copies --- LightlessSync/FileCache/CacheMonitor.cs | 96 +- LightlessSync/FileCache/FileCacheManager.cs | 42 +- .../Configurations/PlayerPerformanceConfig.cs | 8 + .../Configurations/XivDataStorageConfig.cs | 1 + .../Factories/FileDownloadManagerFactory.cs | 5 + .../Pairs/IPairPerformanceSubject.cs | 1 + LightlessSync/PlayerData/Pairs/Pair.cs | 1 + .../PlayerData/Pairs/PairHandlerAdapter.cs | 65 +- .../Pairs/PairHandlerAdapterFactory.cs | 5 + .../PlayerData/Pairs/PairHandlerRegistry.cs | 2 +- LightlessSync/PlayerData/Pairs/PairLedger.cs | 3 +- .../Pairs/PairPerformanceMetricsCache.cs | 3 +- LightlessSync/Plugin.cs | 2 + .../Services/ModelDecimation/MdlDecimator.cs | 1439 +++++++++++++++ .../ModelDecimation/ModelDecimationService.cs | 275 +++ .../Services/PlayerPerformanceService.cs | 33 +- .../TextureDownscaleService.cs | 2 +- LightlessSync/Services/XivDataAnalyzer.cs | 39 +- .../Algorithms/DecimationAlgorithm.cs | 169 ++ .../FastQuadricMeshSimplification.cs | 1549 +++++++++++++++++ .../ThirdParty/MeshDecimator/BoneWeight.cs | 249 +++ .../Collections/ResizableArray.cs | 179 ++ .../MeshDecimator/Collections/UVChannels.cs | 79 + .../ThirdParty/MeshDecimator/LICENSE.md | 21 + .../MeshDecimator/Math/MathHelper.cs | 286 +++ .../MeshDecimator/Math/SymmetricMatrix.cs | 303 ++++ .../ThirdParty/MeshDecimator/Math/Vector2.cs | 425 +++++ .../ThirdParty/MeshDecimator/Math/Vector2d.cs | 425 +++++ .../ThirdParty/MeshDecimator/Math/Vector2i.cs | 348 ++++ .../ThirdParty/MeshDecimator/Math/Vector3.cs | 494 ++++++ .../ThirdParty/MeshDecimator/Math/Vector3d.cs | 481 +++++ .../ThirdParty/MeshDecimator/Math/Vector3i.cs | 368 ++++ .../ThirdParty/MeshDecimator/Math/Vector4.cs | 467 +++++ .../ThirdParty/MeshDecimator/Math/Vector4d.cs | 467 +++++ .../ThirdParty/MeshDecimator/Math/Vector4i.cs | 388 +++++ .../ThirdParty/MeshDecimator/Mesh.cs | 955 ++++++++++ .../MeshDecimator/MeshDecimation.cs | 180 ++ LightlessSync/UI/CompactUI.cs | 1 + LightlessSync/UI/Components/DrawFolderTag.cs | 1 + LightlessSync/UI/Components/DrawUserPair.cs | 16 +- LightlessSync/UI/DrawEntityFactory.cs | 1 + LightlessSync/UI/Handlers/IdDisplayHandler.cs | 4 +- LightlessSync/UI/Models/PairUiEntry.cs | 1 + .../UI/Models/VisiblePairSortMode.cs | 1 + LightlessSync/UI/SettingsUi.cs | 185 ++ .../WebAPI/Files/FileDownloadManager.cs | 9 + 46 files changed, 9990 insertions(+), 84 deletions(-) create mode 100644 LightlessSync/Services/ModelDecimation/MdlDecimator.cs create mode 100644 LightlessSync/Services/ModelDecimation/ModelDecimationService.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/LICENSE.md create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/Mesh.cs create mode 100644 LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index fde9b6d..5143f26 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -481,53 +481,13 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } } + long totalSizeExtras = 0; FileCacheSize = totalSize; - if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled")) - { - var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList(); + totalSizeExtras += GetFolderSize(Path.Combine(_configService.Current.CacheFolder, "downscaled"), token, isWine); + totalSizeExtras += GetFolderSize(Path.Combine(_configService.Current.CacheFolder, "decimated"), token, isWine); - long totalSizeDownscaled = 0; - - foreach (var f in filesDownscaled) - { - token.ThrowIfCancellationRequested(); - - try - { - long size = 0; - - if (!isWine) - { - try - { - size = _fileCompactor.GetFileSizeOnDisk(f); - } - catch (Exception ex) - { - Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName); - size = f.Length; - } - } - else - { - size = f.Length; - } - - totalSizeDownscaled += size; - } - catch (Exception ex) - { - Logger.LogTrace(ex, "Error getting size for {file}", f.FullName); - } - } - - FileCacheSize = (totalSize + totalSizeDownscaled); - } - else - { - FileCacheSize = totalSize; - } + FileCacheSize = totalSize + totalSizeExtras; var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); if (FileCacheSize < maxCacheInBytes) @@ -559,6 +519,54 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase HaltScanLocks.Clear(); } + private long GetFolderSize(string directory, CancellationToken token, bool isWine) + { + if (!Directory.Exists(directory)) + { + return 0; + } + + var files = Directory.EnumerateFiles(directory) + .Select(f => new FileInfo(f)) + .OrderBy(f => f.LastAccessTime) + .ToList(); + + long totalSize = 0; + foreach (var file in files) + { + token.ThrowIfCancellationRequested(); + + try + { + long size; + if (!isWine) + { + try + { + size = _fileCompactor.GetFileSizeOnDisk(file); + } + catch (Exception ex) + { + Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", file.FullName); + size = file.Length; + } + } + else + { + size = file.Length; + } + + totalSize += size; + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Error getting size for {file}", file.FullName); + } + } + + return totalSize; + } + public void ResumeScan(string source) { if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0; diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 854cdd9..e27c23c 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -582,9 +582,10 @@ public sealed class FileCacheManager : IHostedService } } - public void RemoveHashedFile(string hash, string prefixedFilePath) + public void RemoveHashedFile(string hash, string prefixedFilePath, bool removeDerivedFiles = true) { var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath); + var removedHash = false; if (_fileCaches.TryGetValue(hash, out var caches)) { @@ -597,11 +598,16 @@ public sealed class FileCacheManager : IHostedService if (caches.IsEmpty) { - _fileCaches.TryRemove(hash, out _); + removedHash = _fileCaches.TryRemove(hash, out _); } } _fileCachesByPrefixedPath.TryRemove(normalizedPath, out _); + + if (removeDerivedFiles && removedHash) + { + RemoveDerivedCacheFiles(hash); + } } public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true) @@ -617,7 +623,8 @@ public sealed class FileCacheManager : IHostedService fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1); fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); } - RemoveHashedFile(oldHash, prefixedPath); + var removeDerivedFiles = !string.Equals(oldHash, fileCache.Hash, StringComparison.OrdinalIgnoreCase); + RemoveHashedFile(oldHash, prefixedPath, removeDerivedFiles); AddHashedFile(fileCache); } @@ -767,7 +774,7 @@ public sealed class FileCacheManager : IHostedService { try { - RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath, removeDerivedFiles: false); var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext; File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true); var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)); @@ -784,6 +791,33 @@ public sealed class FileCacheManager : IHostedService } } + private void RemoveDerivedCacheFiles(string hash) + { + var cacheFolder = _configService.Current.CacheFolder; + if (string.IsNullOrWhiteSpace(cacheFolder)) + { + return; + } + + TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "downscaled", $"{hash}.tex")); + TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "decimated", $"{hash}.mdl")); + } + + private void TryDeleteDerivedCacheFile(string path) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch (Exception ex) + { + _logger.LogTrace(ex, "Failed to delete derived cache file {path}", path); + } + } + private void AddHashedFile(FileCacheEntity fileCache) { var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath); diff --git a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs index 7da9ac2..ce380f4 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -22,4 +22,12 @@ public class PlayerPerformanceConfig : ILightlessConfiguration public int TextureDownscaleMaxDimension { get; set; } = 2048; public bool OnlyDownscaleUncompressedTextures { get; set; } = true; public bool KeepOriginalTextureFiles { get; set; } = false; + public bool EnableModelDecimation { get; set; } = false; + public int ModelDecimationTriangleThreshold { get; set; } = 50_000; + public double ModelDecimationTargetRatio { get; set; } = 0.8; + public bool ModelDecimationAllowBody { get; set; } = false; + public bool ModelDecimationAllowFaceHead { get; set; } = false; + public bool ModelDecimationAllowTail { get; set; } = false; + public bool ModelDecimationAllowClothing { get; set; } = true; + public bool ModelDecimationAllowAccessories { get; set; } = true; } \ No newline at end of file diff --git a/LightlessSync/LightlessConfiguration/Configurations/XivDataStorageConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/XivDataStorageConfig.cs index 8444ae8..ce7990a 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/XivDataStorageConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/XivDataStorageConfig.cs @@ -5,6 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations; public class XivDataStorageConfig : ILightlessConfiguration { public ConcurrentDictionary TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public ConcurrentDictionary EffectiveTriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); public ConcurrentDictionary>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); public int Version { get; set; } = 0; } \ No newline at end of file diff --git a/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs b/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs index e3697cf..211a6fc 100644 --- a/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs +++ b/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs @@ -1,6 +1,7 @@ using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ModelDecimation; using LightlessSync.Services.TextureCompression; using LightlessSync.WebAPI.Files; using Microsoft.Extensions.Logging; @@ -16,6 +17,7 @@ public class FileDownloadManagerFactory private readonly FileCompactor _fileCompactor; private readonly LightlessConfigService _configService; private readonly TextureDownscaleService _textureDownscaleService; + private readonly ModelDecimationService _modelDecimationService; private readonly TextureMetadataHelper _textureMetadataHelper; public FileDownloadManagerFactory( @@ -26,6 +28,7 @@ public class FileDownloadManagerFactory FileCompactor fileCompactor, LightlessConfigService configService, TextureDownscaleService textureDownscaleService, + ModelDecimationService modelDecimationService, TextureMetadataHelper textureMetadataHelper) { _loggerFactory = loggerFactory; @@ -35,6 +38,7 @@ public class FileDownloadManagerFactory _fileCompactor = fileCompactor; _configService = configService; _textureDownscaleService = textureDownscaleService; + _modelDecimationService = modelDecimationService; _textureMetadataHelper = textureMetadataHelper; } @@ -48,6 +52,7 @@ public class FileDownloadManagerFactory _fileCompactor, _configService, _textureDownscaleService, + _modelDecimationService, _textureMetadataHelper); } } diff --git a/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs b/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs index cd62f98..b6355a8 100644 --- a/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs +++ b/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs @@ -16,4 +16,5 @@ public interface IPairPerformanceSubject long LastAppliedApproximateVRAMBytes { get; set; } long LastAppliedApproximateEffectiveVRAMBytes { get; set; } long LastAppliedDataTris { get; set; } + long LastAppliedApproximateEffectiveTris { get; set; } } diff --git a/LightlessSync/PlayerData/Pairs/Pair.cs b/LightlessSync/PlayerData/Pairs/Pair.cs index 2a85cd3..e95b7fe 100644 --- a/LightlessSync/PlayerData/Pairs/Pair.cs +++ b/LightlessSync/PlayerData/Pairs/Pair.cs @@ -69,6 +69,7 @@ public class Pair public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID; public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1; public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1; + public long LastAppliedApproximateEffectiveTris => TryGetHandler()?.LastAppliedApproximateEffectiveTris ?? -1; public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1; public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1; public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty; diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index b753657..03c4f94 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -12,6 +12,7 @@ using LightlessSync.Services; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ModelDecimation; using LightlessSync.Services.PairProcessing; using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.TextureCompression; @@ -45,6 +46,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private readonly ServerConfigurationManager _serverConfigManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; private readonly TextureDownscaleService _textureDownscaleService; + private readonly ModelDecimationService _modelDecimationService; private readonly PairStateCache _pairStateCache; private readonly PairPerformanceMetricsCache _performanceMetricsCache; private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor; @@ -162,6 +164,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa public long LastAppliedDataBytes { get; private set; } public long LastAppliedDataTris { get; set; } = -1; + public long LastAppliedApproximateEffectiveTris { get; set; } = -1; public long LastAppliedApproximateVRAMBytes { get; set; } = -1; public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1; public CharacterData? LastReceivedCharacterData { get; private set; } @@ -198,6 +201,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa PairProcessingLimiter pairProcessingLimiter, ServerConfigurationManager serverConfigManager, TextureDownscaleService textureDownscaleService, + ModelDecimationService modelDecimationService, PairStateCache pairStateCache, PairPerformanceMetricsCache performanceMetricsCache, PenumbraTempCollectionJanitor tempCollectionJanitor) : base(logger, mediator) @@ -217,6 +221,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _pairProcessingLimiter = pairProcessingLimiter; _serverConfigManager = serverConfigManager; _textureDownscaleService = textureDownscaleService; + _modelDecimationService = modelDecimationService; _pairStateCache = pairStateCache; _performanceMetricsCache = performanceMetricsCache; _tempCollectionJanitor = tempCollectionJanitor; @@ -242,7 +247,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return; } - if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0 + if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0 || LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { _forceApplyMods = true; @@ -579,6 +584,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _forceApplyMods = true; LastAppliedDataBytes = -1; LastAppliedDataTris = -1; + LastAppliedApproximateEffectiveTris = -1; LastAppliedApproximateVRAMBytes = -1; LastAppliedApproximateEffectiveVRAMBytes = -1; } @@ -626,6 +632,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _forceFullReapply = true; LastAppliedDataBytes = -1; LastAppliedDataTris = -1; + LastAppliedApproximateEffectiveTris = -1; LastAppliedApproximateVRAMBytes = -1; LastAppliedApproximateEffectiveVRAMBytes = -1; } @@ -725,6 +732,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private void ApplyCachedMetrics(PairPerformanceMetrics metrics) { LastAppliedDataTris = metrics.TriangleCount; + LastAppliedApproximateEffectiveTris = metrics.ApproximateEffectiveTris; LastAppliedApproximateVRAMBytes = metrics.ApproximateVramBytes; LastAppliedApproximateEffectiveVRAMBytes = metrics.ApproximateEffectiveVramBytes; } @@ -732,6 +740,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private void StorePerformanceMetrics(CharacterData charaData) { if (LastAppliedDataTris < 0 + || LastAppliedApproximateEffectiveTris < 0 || LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { @@ -747,7 +756,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _performanceMetricsCache.StoreMetrics( Ident, dataHash, - new PairPerformanceMetrics(LastAppliedDataTris, LastAppliedApproximateVRAMBytes, LastAppliedApproximateEffectiveVRAMBytes)); + new PairPerformanceMetrics(LastAppliedDataTris, LastAppliedApproximateVRAMBytes, LastAppliedApproximateEffectiveVRAMBytes, LastAppliedApproximateEffectiveTris)); } private bool HasMissingCachedFiles(CharacterData characterData) @@ -1079,7 +1088,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe()); var forceFullReapply = _forceFullReapply - || LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0; + || LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0; DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forceFullReapply); } @@ -1895,6 +1904,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false); } + + var downloadedModelHashes = toDownloadReplacements + .Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))) + .Select(static replacement => replacement.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (downloadedModelHashes.Count > 0) + { + await _modelDecimationService.WaitForPendingJobsAsync(downloadedModelHashes, downloadToken).ConfigureAwait(false); + } } toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); @@ -2117,18 +2137,19 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _needsCollectionRebuild = false; if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { - _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List()); - } - if (LastAppliedDataTris < 0) - { - await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false); - } + _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List()); + } - StorePerformanceMetrics(charaData); - _lastSuccessfulDataHash = GetDataHashSafe(charaData); - _lastSuccessfulApplyAt = DateTime.UtcNow; - ClearFailureState(); - Logger.LogDebug("[{applicationId}] Application finished", _applicationId); + if (LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0) + { + await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false); + } + + StorePerformanceMetrics(charaData); + _lastSuccessfulDataHash = GetDataHashSafe(charaData); + _lastSuccessfulApplyAt = DateTime.UtcNow; + ClearFailureState(); + Logger.LogDebug("[{applicationId}] Application finished", _applicationId); } catch (OperationCanceledException) { @@ -2397,9 +2418,18 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa foreach (var gamePath in item.GamePaths) { - var preferredPath = skipDownscaleForPair - ? fileCache.ResolvedFilepath - : _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); + var preferredPath = fileCache.ResolvedFilepath; + if (!skipDownscaleForPair) + { + if (gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) + { + preferredPath = _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); + } + else if (gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) + { + preferredPath = _modelDecimationService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); + } + } outputDict[(gamePath, item.Hash)] = preferredPath; } } @@ -2541,6 +2571,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _cachedData = null; LastAppliedDataBytes = -1; LastAppliedDataTris = -1; + LastAppliedApproximateEffectiveTris = -1; LastAppliedApproximateVRAMBytes = -1; LastAppliedApproximateEffectiveVRAMBytes = -1; } diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs index fee3b9a..088b115 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs @@ -4,6 +4,7 @@ using LightlessSync.PlayerData.Factories; using LightlessSync.Services; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ModelDecimation; using LightlessSync.Services.PairProcessing; using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.TextureCompression; @@ -30,6 +31,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ServerConfigurationManager _serverConfigManager; private readonly TextureDownscaleService _textureDownscaleService; + private readonly ModelDecimationService _modelDecimationService; private readonly PairStateCache _pairStateCache; private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor; @@ -51,6 +53,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory PairProcessingLimiter pairProcessingLimiter, ServerConfigurationManager serverConfigManager, TextureDownscaleService textureDownscaleService, + ModelDecimationService modelDecimationService, PairStateCache pairStateCache, PairPerformanceMetricsCache pairPerformanceMetricsCache, PenumbraTempCollectionJanitor tempCollectionJanitor) @@ -70,6 +73,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory _pairProcessingLimiter = pairProcessingLimiter; _serverConfigManager = serverConfigManager; _textureDownscaleService = textureDownscaleService; + _modelDecimationService = modelDecimationService; _pairStateCache = pairStateCache; _pairPerformanceMetricsCache = pairPerformanceMetricsCache; _tempCollectionJanitor = tempCollectionJanitor; @@ -98,6 +102,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory _pairProcessingLimiter, _serverConfigManager, _textureDownscaleService, + _modelDecimationService, _pairStateCache, _pairPerformanceMetricsCache, _tempCollectionJanitor); diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs index ec05ee7..881c35c 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs @@ -89,7 +89,7 @@ public sealed class PairHandlerRegistry : IDisposable } if (handler.LastReceivedCharacterData is not null && - (handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0)) + (handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0 || handler.LastAppliedApproximateEffectiveTris < 0)) { handler.ApplyLastReceivedData(forced: true); } diff --git a/LightlessSync/PlayerData/Pairs/PairLedger.cs b/LightlessSync/PlayerData/Pairs/PairLedger.cs index b151e1f..fdb226e 100644 --- a/LightlessSync/PlayerData/Pairs/PairLedger.cs +++ b/LightlessSync/PlayerData/Pairs/PairLedger.cs @@ -258,7 +258,8 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase if (handler.LastAppliedApproximateVRAMBytes >= 0 && handler.LastAppliedDataTris >= 0 - && handler.LastAppliedApproximateEffectiveVRAMBytes >= 0) + && handler.LastAppliedApproximateEffectiveVRAMBytes >= 0 + && handler.LastAppliedApproximateEffectiveTris >= 0) { continue; } diff --git a/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs b/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs index 110d845..5d83cee 100644 --- a/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs +++ b/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs @@ -5,7 +5,8 @@ namespace LightlessSync.PlayerData.Pairs; public readonly record struct PairPerformanceMetrics( long TriangleCount, long ApproximateVramBytes, - long ApproximateEffectiveVramBytes); + long ApproximateEffectiveVramBytes, + long ApproximateEffectiveTris); /// /// caches performance metrics keyed by pair ident diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 09c4cd8..fafd1c7 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -40,6 +40,7 @@ using System.Reflection; using OtterTex; using LightlessSync.Services.LightFinder; using LightlessSync.Services.PairProcessing; +using LightlessSync.Services.ModelDecimation; using LightlessSync.UI.Models; namespace LightlessSync; @@ -126,6 +127,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs new file mode 100644 index 0000000..36feb4a --- /dev/null +++ b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs @@ -0,0 +1,1439 @@ +using Lumina.Data.Parsing; +using Lumina.Extensions; +using MeshDecimator; +using MeshDecimator.Math; +using Microsoft.Extensions.Logging; +using Penumbra.GameData.Files.ModelStructs; +using System.Buffers.Binary; +using MdlFile = Penumbra.GameData.Files.MdlFile; +using MsLogger = Microsoft.Extensions.Logging.ILogger; + +namespace LightlessSync.Services.ModelDecimation; + +internal static class MdlDecimator +{ + private const int MaxStreams = 3; + private const int ReadRetryCount = 8; + private const int ReadRetryDelayMs = 250; + + private static readonly HashSet SupportedUsages = + [ + MdlFile.VertexUsage.Position, + MdlFile.VertexUsage.Normal, + MdlFile.VertexUsage.Tangent1, + MdlFile.VertexUsage.UV, + MdlFile.VertexUsage.Color, + MdlFile.VertexUsage.BlendWeights, + MdlFile.VertexUsage.BlendIndices, + ]; + + private static readonly HashSet SupportedTypes = + [ + MdlFile.VertexType.Single2, + MdlFile.VertexType.Single3, + MdlFile.VertexType.Single4, + MdlFile.VertexType.Half2, + MdlFile.VertexType.Half4, + MdlFile.VertexType.UByte4, + MdlFile.VertexType.NByte4, + ]; + + public static bool TryDecimate(string sourcePath, string destinationPath, int triangleThreshold, double targetRatio, MsLogger logger) + { + try + { + if (!TryReadModelBytes(sourcePath, logger, out var data)) + { + logger.LogInformation("Skipping model decimation; source file locked or unreadable: {Path}", sourcePath); + return false; + } + var mdl = new MdlFile(data); + if (!mdl.Valid) + { + logger.LogInformation("Skipping model decimation; invalid mdl: {Path}", sourcePath); + return false; + } + + if (mdl.LodCount != 1) + { + logger.LogInformation("Skipping model decimation; unsupported LOD count for {Path}", sourcePath); + return false; + } + + if (HasShapeData(mdl)) + { + logger.LogInformation("Skipping model decimation; shape/morph data present for {Path}", sourcePath); + return false; + } + + const int lodIndex = 0; + var lod = mdl.Lods[lodIndex]; + var meshes = mdl.Meshes.ToArray(); + if (meshes.Length == 0) + { + logger.LogInformation("Skipping model decimation; no meshes for {Path}", sourcePath); + return false; + } + + if (lod.MeshCount == 0) + { + logger.LogInformation("Skipping model decimation; no meshes for {Path}", sourcePath); + return false; + } + + var lodMeshStart = (int)lod.MeshIndex; + var lodMeshEnd = lodMeshStart + lod.MeshCount; + if (lodMeshStart < 0 || lodMeshEnd > meshes.Length) + { + logger.LogInformation("Skipping model decimation; invalid LOD mesh range for {Path}", sourcePath); + return false; + } + + var anyDecimated = false; + var newSubMeshes = new List(mdl.SubMeshes.Length); + var newVertexBuffer = new List(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0); + var newIndexBuffer = new List(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0); + var subMeshCursor = 0; + + for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++) + { + var mesh = meshes[meshIndex]; + var meshSubMeshes = mdl.SubMeshes + .Skip(mesh.SubMeshIndex) + .Take(mesh.SubMeshCount) + .ToArray(); + + var meshIndexBase = newIndexBuffer.Count; + var vertexBufferBase = newVertexBuffer.Count; + + MeshStruct updatedMesh; + MdlStructs.SubmeshStruct[] updatedSubMeshes; + byte[][] vertexStreams; + int[] indices; + bool decimated; + + if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd + && TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, triangleThreshold, targetRatio, + out updatedMesh, + out updatedSubMeshes, + out vertexStreams, + out indices, + out decimated, + logger)) + { + updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase); + } + else + { + if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd) + { + logger.LogDebug("Skipping decimation for mesh {MeshIndex} in {Path}", meshIndex, sourcePath); + } + + updatedMesh = mesh; + updatedSubMeshes = CopySubMeshes(meshSubMeshes, meshIndexBase, mesh.StartIndex); + vertexStreams = CopyVertexStreams(mdl, lodIndex, mesh); + indices = ReadIndices(mdl, lodIndex, mesh); + decimated = false; + } + + anyDecimated |= decimated; + + var vertexCount = updatedMesh.VertexCount; + var streamSizes = new int[MaxStreams]; + for (var stream = 0; stream < MaxStreams; stream++) + { + var stride = updatedMesh.VertexBufferStride(stream); + if (stride > 0 && vertexCount > 0) + { + streamSizes[stream] = stride * vertexCount; + } + } + + updatedMesh.VertexBufferOffset1 = (uint)vertexBufferBase; + updatedMesh.VertexBufferOffset2 = (uint)(vertexBufferBase + streamSizes[0]); + updatedMesh.VertexBufferOffset3 = (uint)(vertexBufferBase + streamSizes[0] + streamSizes[1]); + + newVertexBuffer.AddRange(vertexStreams[0]); + newVertexBuffer.AddRange(vertexStreams[1]); + newVertexBuffer.AddRange(vertexStreams[2]); + + updatedMesh.StartIndex = (uint)meshIndexBase; + updatedMesh.SubMeshIndex = (ushort)subMeshCursor; + updatedMesh.SubMeshCount = (ushort)updatedSubMeshes.Length; + updatedMesh.IndexCount = (uint)indices.Length; + + meshes[meshIndex] = updatedMesh; + newSubMeshes.AddRange(updatedSubMeshes); + subMeshCursor += updatedSubMeshes.Length; + newIndexBuffer.AddRange(indices.Select(static i => (ushort)i)); + } + + if (!anyDecimated) + { + logger.LogInformation("Skipping model decimation; no eligible meshes for {Path}", sourcePath); + return false; + } + + var indexBytes = BuildIndexBytes(newIndexBuffer); + + mdl.Meshes = meshes; + mdl.SubMeshes = [.. newSubMeshes]; + mdl.VertexOffset[lodIndex] = 0; + mdl.IndexOffset[lodIndex] = (uint)newVertexBuffer.Count; + mdl.VertexBufferSize[lodIndex] = (uint)newVertexBuffer.Count; + mdl.IndexBufferSize[lodIndex] = (uint)indexBytes.Length; + + mdl.Lods[lodIndex] = mdl.Lods[lodIndex] with + { + VertexDataOffset = 0, + VertexBufferSize = (uint)newVertexBuffer.Count, + IndexDataOffset = (uint)newVertexBuffer.Count, + IndexBufferSize = (uint)indexBytes.Length, + }; + + for (var clearIndex = 1; clearIndex < mdl.VertexOffset.Length; clearIndex++) + { + mdl.VertexOffset[clearIndex] = 0; + mdl.IndexOffset[clearIndex] = 0; + mdl.VertexBufferSize[clearIndex] = 0; + mdl.IndexBufferSize[clearIndex] = 0; + + if (clearIndex < mdl.Lods.Length) + { + mdl.Lods[clearIndex] = mdl.Lods[clearIndex] with + { + VertexDataOffset = 0, + VertexBufferSize = 0, + IndexDataOffset = 0, + IndexBufferSize = 0, + }; + } + } + + mdl.RemainingData = [.. newVertexBuffer, .. indexBytes]; + + var outputData = mdl.Write(); + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + var tempPath = destinationPath + ".tmp"; + File.WriteAllBytes(tempPath, outputData); + File.Move(tempPath, destinationPath, overwrite: true); + return true; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to decimate model {Path}", sourcePath); + return false; + } + } + + private static bool TryReadModelBytes(string sourcePath, MsLogger logger, out byte[] data) + { + Exception? lastError = null; + for (var attempt = 0; attempt < ReadRetryCount; attempt++) + { + try + { + data = ReadAllBytesShared(sourcePath); + return true; + } + catch (IOException ex) + { + lastError = ex; + } + catch (UnauthorizedAccessException ex) + { + lastError = ex; + } + + if (attempt < ReadRetryCount - 1) + { + Thread.Sleep(ReadRetryDelayMs); + } + } + + if (lastError != null) + { + logger.LogDebug(lastError, "Failed to read model for decimation after {Attempts} attempts: {Path}", ReadRetryCount, sourcePath); + } + + data = []; + return false; + } + + private static byte[] ReadAllBytesShared(string sourcePath) + { + using var stream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var length = stream.Length; + if (length <= 0) + { + throw new IOException("Model file length is zero."); + } + + if (length > int.MaxValue) + { + throw new IOException("Model file too large."); + } + + var buffer = new byte[(int)length]; + var totalRead = 0; + while (totalRead < buffer.Length) + { + var read = stream.Read(buffer, totalRead, buffer.Length - totalRead); + if (read == 0) + { + break; + } + + totalRead += read; + } + + if (totalRead != buffer.Length || stream.Length != length) + { + throw new IOException("Model file length changed during read."); + } + + return buffer; + } + + private static bool TryProcessMesh( + MdlFile mdl, + int lodIndex, + int meshIndex, + MeshStruct mesh, + MdlStructs.SubmeshStruct[] meshSubMeshes, + int triangleThreshold, + double targetRatio, + out MeshStruct updatedMesh, + out MdlStructs.SubmeshStruct[] updatedSubMeshes, + out byte[][] vertexStreams, + out int[] indices, + out bool decimated, + MsLogger logger) + { + updatedMesh = mesh; + updatedSubMeshes = []; + vertexStreams = [[], [], []]; + indices = []; + decimated = false; + + if (mesh.VertexCount == 0 || mesh.IndexCount == 0) + { + return false; + } + + if (meshSubMeshes.Length == 0) + { + return false; + } + + var triangleCount = (int)(mesh.IndexCount / 3); + if (triangleCount < triangleThreshold) + { + return false; + } + + if (!TryBuildVertexFormat(mdl.VertexDeclarations[meshIndex], out var format, out var reason)) + { + logger.LogDebug("Mesh {MeshIndex} vertex format unsupported: {Reason}", meshIndex, reason); + return false; + } + + if (!TryDecodeMeshData(mdl, lodIndex, mesh, format, meshSubMeshes, out var decoded, out var subMeshIndices, out var decodeReason)) + { + logger.LogDebug("Mesh {MeshIndex} decode failed: {Reason}", meshIndex, decodeReason); + return false; + } + + var targetTriangles = (int)Math.Floor(triangleCount * targetRatio); + if (targetTriangles < 1 || targetTriangles >= triangleCount) + { + return false; + } + + var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices); + var algorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default); + algorithm.Logger = logger; + algorithm.Initialize(meshDecimatorMesh); + algorithm.DecimateMesh(targetTriangles); + var decimatedMesh = algorithm.ToMesh(); + + if (decimatedMesh.SubMeshCount != meshSubMeshes.Length) + { + logger.LogDebug("Mesh {MeshIndex} submesh count changed after decimation", meshIndex); + return false; + } + + if (!TryEncodeMeshData(decimatedMesh, format, mesh, meshSubMeshes, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason)) + { + logger.LogDebug("Mesh {MeshIndex} encode failed: {Reason}", meshIndex, encodeReason); + return false; + } + + decimated = true; + return true; + } + + private static Mesh BuildMesh(DecodedMeshData decoded, int[][] subMeshIndices) + { + var mesh = new Mesh(decoded.Positions, subMeshIndices); + if (decoded.Normals != null) + { + mesh.Normals = decoded.Normals; + } + + if (decoded.Tangents != null) + { + mesh.Tangents = decoded.Tangents; + } + + if (decoded.Colors != null) + { + mesh.Colors = decoded.Colors; + } + + if (decoded.BoneWeights != null) + { + mesh.BoneWeights = decoded.BoneWeights; + } + + if (decoded.UvChannels != null) + { + for (var channel = 0; channel < decoded.UvChannels.Length; channel++) + { + mesh.SetUVs(channel, decoded.UvChannels[channel]); + } + } + + return mesh; + } + + private static bool TryDecodeMeshData( + MdlFile mdl, + int lodIndex, + MeshStruct mesh, + VertexFormat format, + MdlStructs.SubmeshStruct[] meshSubMeshes, + out DecodedMeshData decoded, + out int[][] subMeshIndices, + out string? reason) + { + decoded = default!; + subMeshIndices = []; + reason = null; + + if (!TryBuildSubMeshIndices(mdl, lodIndex, mesh, meshSubMeshes, out subMeshIndices, out reason)) + { + return false; + } + + var vertexCount = mesh.VertexCount; + var positions = new Vector3d[vertexCount]; + Vector3[]? normals = format.HasNormals ? new Vector3[vertexCount] : null; + Vector4[]? tangents = format.HasTangents ? new Vector4[vertexCount] : null; + Vector4[]? colors = format.HasColors ? new Vector4[vertexCount] : null; + BoneWeight[]? boneWeights = format.HasSkinning ? new BoneWeight[vertexCount] : null; + + Vector2[][]? uvChannels = null; + if (format.UvChannelCount > 0) + { + uvChannels = new Vector2[format.UvChannelCount][]; + for (var channel = 0; channel < format.UvChannelCount; channel++) + { + uvChannels[channel] = new Vector2[vertexCount]; + } + } + + var streams = new BinaryReader[MaxStreams]; + for (var streamIndex = 0; streamIndex < MaxStreams; streamIndex++) + { + streams[streamIndex] = new BinaryReader(new MemoryStream(mdl.RemainingData)); + streams[streamIndex].BaseStream.Position = mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(streamIndex); + } + + var uvLookup = format.UvElements.ToDictionary(static element => ElementKey.From(element.Element), static element => element); + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + byte[]? indices = null; + float[]? weights = null; + + foreach (var element in format.SortedElements) + { + var usage = (MdlFile.VertexUsage)element.Usage; + var type = (MdlFile.VertexType)element.Type; + var stream = streams[element.Stream]; + + switch (usage) + { + case MdlFile.VertexUsage.Position: + positions[vertexIndex] = ReadPosition(type, stream); + break; + case MdlFile.VertexUsage.Normal when normals != null: + normals[vertexIndex] = ReadNormal(type, stream); + break; + case MdlFile.VertexUsage.Tangent1 when tangents != null: + tangents[vertexIndex] = ReadTangent(type, stream); + break; + case MdlFile.VertexUsage.Color when colors != null: + colors[vertexIndex] = ReadColor(type, stream); + break; + case MdlFile.VertexUsage.BlendIndices: + indices = ReadIndices(type, stream); + break; + case MdlFile.VertexUsage.BlendWeights: + weights = ReadWeights(type, stream); + break; + case MdlFile.VertexUsage.UV when uvChannels != null: + if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement)) + { + reason = "UV mapping missing."; + return false; + } + ReadUv(type, stream, uvElement, uvChannels, vertexIndex); + break; + default: + if (usage == MdlFile.VertexUsage.Normal || usage == MdlFile.VertexUsage.Tangent1 + || usage == MdlFile.VertexUsage.Color) + { + _ = ReadAndDiscard(type, stream); + } + break; + } + } + + if (boneWeights != null) + { + if (indices == null || weights == null || indices.Length != 4 || weights.Length != 4) + { + reason = "Missing or invalid skinning data."; + return false; + } + + NormalizeWeights(weights); + boneWeights[vertexIndex] = new BoneWeight(indices[0], indices[1], indices[2], indices[3], weights[0], weights[1], weights[2], weights[3]); + } + } + + decoded = new DecodedMeshData(positions, normals, tangents, colors, boneWeights, uvChannels); + return true; + } + + private static bool TryEncodeMeshData( + Mesh decimatedMesh, + VertexFormat format, + MeshStruct originalMesh, + MdlStructs.SubmeshStruct[] originalSubMeshes, + out MeshStruct updatedMesh, + out MdlStructs.SubmeshStruct[] updatedSubMeshes, + out byte[][] vertexStreams, + out int[] indices, + out string? reason) + { + updatedMesh = originalMesh; + updatedSubMeshes = []; + vertexStreams = [[], [], []]; + indices = []; + reason = null; + + var vertexCount = decimatedMesh.Vertices.Length; + if (vertexCount > ushort.MaxValue) + { + reason = "Vertex count exceeds ushort range."; + return false; + } + + var normals = decimatedMesh.Normals; + var tangents = decimatedMesh.Tangents; + var colors = decimatedMesh.Colors; + var boneWeights = decimatedMesh.BoneWeights; + + if (format.HasNormals && normals == null) + { + reason = "Missing normals after decimation."; + return false; + } + + if (format.HasTangents && tangents == null) + { + reason = "Missing tangents after decimation."; + return false; + } + + if (format.HasColors && colors == null) + { + reason = "Missing colors after decimation."; + return false; + } + + if (format.HasSkinning && boneWeights == null) + { + reason = "Missing bone weights after decimation."; + return false; + } + + var uvChannels = Array.Empty(); + if (format.UvChannelCount > 0) + { + uvChannels = new Vector2[format.UvChannelCount][]; + for (var channel = 0; channel < format.UvChannelCount; channel++) + { + if (decimatedMesh.GetUVDimension(channel) != 2) + { + reason = "Unsupported UV dimension after decimation."; + return false; + } + uvChannels[channel] = decimatedMesh.GetUVs2D(channel); + } + } + + var streamBuffers = new byte[MaxStreams][]; + for (var stream = 0; stream < MaxStreams; stream++) + { + var stride = originalMesh.VertexBufferStride(stream); + if (stride == 0 || vertexCount == 0) + { + streamBuffers[stream] = []; + continue; + } + + streamBuffers[stream] = new byte[stride * vertexCount]; + } + + var uvLookup = format.UvElements.ToDictionary(static element => ElementKey.From(element.Element), static element => element); + + foreach (var element in format.SortedElements) + { + var stride = originalMesh.VertexBufferStride(element.Stream); + if (stride == 0) + { + continue; + } + + var elementSize = GetElementSize((MdlFile.VertexType)element.Type); + if (element.Offset + elementSize > stride) + { + reason = "Vertex element stride overflow."; + return false; + } + } + + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + foreach (var element in format.SortedElements) + { + var usage = (MdlFile.VertexUsage)element.Usage; + var type = (MdlFile.VertexType)element.Type; + var stream = element.Stream; + var stride = originalMesh.VertexBufferStride(stream); + if (stride == 0) + { + continue; + } + + var baseOffset = vertexIndex * stride + element.Offset; + var target = streamBuffers[stream].AsSpan(baseOffset, GetElementSize(type)); + + switch (usage) + { + case MdlFile.VertexUsage.Position: + WritePosition(type, decimatedMesh.Vertices[vertexIndex], target); + break; + case MdlFile.VertexUsage.Normal when normals != null: + WriteNormal(type, normals[vertexIndex], target); + break; + case MdlFile.VertexUsage.Tangent1 when tangents != null: + WriteTangent(type, tangents[vertexIndex], target); + break; + case MdlFile.VertexUsage.Color when colors != null: + WriteColor(type, colors[vertexIndex], target); + break; + case MdlFile.VertexUsage.BlendIndices when boneWeights != null: + WriteBlendIndices(type, boneWeights[vertexIndex], target); + break; + case MdlFile.VertexUsage.BlendWeights when boneWeights != null: + WriteBlendWeights(type, boneWeights[vertexIndex], target); + break; + case MdlFile.VertexUsage.UV when format.UvChannelCount > 0: + if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement)) + { + reason = "UV mapping missing."; + return false; + } + WriteUv(type, uvElement, uvChannels, vertexIndex, target); + break; + } + } + } + + updatedMesh.VertexCount = (ushort)vertexCount; + + var newSubMeshes = new List(originalSubMeshes.Length); + var indexList = new List(); + + for (var subMeshIndex = 0; subMeshIndex < originalSubMeshes.Length; subMeshIndex++) + { + var subMeshIndices = decimatedMesh.GetIndices(subMeshIndex); + if (subMeshIndices.Any(index => index < 0 || index >= vertexCount)) + { + reason = "Decimated indices out of range."; + return false; + } + + var offset = indexList.Count; + indexList.AddRange(subMeshIndices); + + var updatedSubMesh = originalSubMeshes[subMeshIndex] with + { + IndexOffset = (uint)offset, + IndexCount = (uint)subMeshIndices.Length, + }; + newSubMeshes.Add(updatedSubMesh); + } + + updatedSubMeshes = newSubMeshes.ToArray(); + indices = indexList.ToArray(); + vertexStreams = streamBuffers; + return true; + } + + private static bool TryBuildSubMeshIndices( + MdlFile mdl, + int lodIndex, + MeshStruct mesh, + MdlStructs.SubmeshStruct[] meshSubMeshes, + out int[][] subMeshIndices, + out string? reason) + { + reason = null; + subMeshIndices = new int[meshSubMeshes.Length][]; + var meshIndices = ReadIndices(mdl, lodIndex, mesh); + + for (var subMeshIndex = 0; subMeshIndex < meshSubMeshes.Length; subMeshIndex++) + { + var subMesh = meshSubMeshes[subMeshIndex]; + if (subMesh.IndexCount == 0) + { + subMeshIndices[subMeshIndex] = []; + continue; + } + + var relativeOffset = (int)(subMesh.IndexOffset - mesh.StartIndex); + if (relativeOffset < 0 || relativeOffset + subMesh.IndexCount > meshIndices.Length) + { + reason = "Submesh index range out of bounds."; + return false; + } + + var slice = meshIndices.Skip(relativeOffset).Take((int)subMesh.IndexCount).Select(static i => (int)i).ToArray(); + subMeshIndices[subMeshIndex] = slice; + } + + return true; + } + + private static byte[] BuildIndexBytes(List indices) + { + var indexBytes = new byte[indices.Count * sizeof(ushort)]; + for (var i = 0; i < indices.Count; i++) + { + BinaryPrimitives.WriteUInt16LittleEndian(indexBytes.AsSpan(i * 2, 2), indices[i]); + } + + return indexBytes; + } + + private static int[] ReadIndices(MdlFile mdl, int lodIndex, MeshStruct mesh) + { + using var reader = new BinaryReader(new MemoryStream(mdl.RemainingData)); + reader.BaseStream.Position = mdl.IndexOffset[lodIndex] + mesh.StartIndex * sizeof(ushort); + var values = reader.ReadStructuresAsArray((int)mesh.IndexCount); + return values.Select(static i => (int)i).ToArray(); + } + + private static byte[][] CopyVertexStreams(MdlFile mdl, int lodIndex, MeshStruct mesh) + { + var streams = new byte[MaxStreams][]; + for (var stream = 0; stream < MaxStreams; stream++) + { + var stride = mesh.VertexBufferStride(stream); + if (stride == 0 || mesh.VertexCount == 0) + { + streams[stream] = []; + continue; + } + + var size = stride * mesh.VertexCount; + var offset = mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(stream); + streams[stream] = mdl.RemainingData.AsSpan((int)offset, size).ToArray(); + } + + return streams; + } + + private static MdlStructs.SubmeshStruct[] CopySubMeshes(MdlStructs.SubmeshStruct[] source, int newMeshIndexBase, uint meshStartIndex) + { + var result = new MdlStructs.SubmeshStruct[source.Length]; + for (var i = 0; i < source.Length; i++) + { + var relativeOffset = (int)(source[i].IndexOffset - meshStartIndex); + result[i] = source[i] with + { + IndexOffset = (uint)(newMeshIndexBase + relativeOffset), + }; + } + + return result; + } + + private static MdlStructs.SubmeshStruct[] OffsetSubMeshes(MdlStructs.SubmeshStruct[] source, int meshIndexBase) + { + var result = new MdlStructs.SubmeshStruct[source.Length]; + for (var i = 0; i < source.Length; i++) + { + result[i] = source[i] with + { + IndexOffset = (uint)(meshIndexBase + source[i].IndexOffset), + }; + } + + return result; + } + + private static bool TryBuildVertexFormat(MdlStructs.VertexDeclarationStruct declaration, out VertexFormat format, out string? reason) + { + reason = null; + format = default!; + + var elements = declaration.VertexElements; + foreach (var element in elements) + { + if (element.Stream >= MaxStreams) + { + reason = "Vertex stream index out of range."; + return false; + } + + var usage = (MdlFile.VertexUsage)element.Usage; + var type = (MdlFile.VertexType)element.Type; + + if (!SupportedUsages.Contains(usage)) + { + reason = $"Unsupported usage {usage}."; + return false; + } + + if (!SupportedTypes.Contains(type)) + { + reason = $"Unsupported vertex type {type}."; + return false; + } + } + + var positionElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Position).ToArray(); + if (positionElements.Length != 1) + { + reason = "Expected single position element."; + return false; + } + + var positionType = (MdlFile.VertexType)positionElements[0].Type; + if (positionType != MdlFile.VertexType.Single3 && positionType != MdlFile.VertexType.Single4) + { + reason = "Unsupported position element type."; + return false; + } + + var normalElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Normal).ToArray(); + if (normalElements.Length > 1) + { + reason = "Multiple normal elements unsupported."; + return false; + } + + if (normalElements.Length == 1) + { + var normalType = (MdlFile.VertexType)normalElements[0].Type; + if (normalType != MdlFile.VertexType.Single3 && normalType != MdlFile.VertexType.Single4 && normalType != MdlFile.VertexType.NByte4) + { + reason = "Unsupported normal element type."; + return false; + } + } + + var tangentElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Tangent1).ToArray(); + if (tangentElements.Length > 1) + { + reason = "Multiple tangent elements unsupported."; + return false; + } + + if (tangentElements.Length == 1) + { + var tangentType = (MdlFile.VertexType)tangentElements[0].Type; + if (tangentType != MdlFile.VertexType.Single4 && tangentType != MdlFile.VertexType.NByte4) + { + reason = "Unsupported tangent element type."; + return false; + } + } + + var colorElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Color).ToArray(); + if (colorElements.Length > 1) + { + reason = "Multiple color elements unsupported."; + return false; + } + + MdlStructs.VertexElement? colorElement = null; + if (colorElements.Length == 1) + { + var colorType = (MdlFile.VertexType)colorElements[0].Type; + if (colorType != MdlFile.VertexType.UByte4 && colorType != MdlFile.VertexType.NByte4 && colorType != MdlFile.VertexType.Single4) + { + reason = "Unsupported color element type."; + return false; + } + + colorElement = colorElements[0]; + } + + var blendIndicesElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.BlendIndices).ToArray(); + var blendWeightsElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.BlendWeights).ToArray(); + if (blendIndicesElements.Length != blendWeightsElements.Length) + { + reason = "Blend indices/weights mismatch."; + return false; + } + + if (blendIndicesElements.Length > 1 || blendWeightsElements.Length > 1) + { + reason = "Multiple blend elements unsupported."; + return false; + } + + if (blendIndicesElements.Length == 1) + { + var indexType = (MdlFile.VertexType)blendIndicesElements[0].Type; + if (indexType != MdlFile.VertexType.UByte4) + { + reason = "Unsupported blend index type."; + return false; + } + + var weightType = (MdlFile.VertexType)blendWeightsElements[0].Type; + if (weightType != MdlFile.VertexType.UByte4 && weightType != MdlFile.VertexType.NByte4 && weightType != MdlFile.VertexType.Single4) + { + reason = "Unsupported blend weight type."; + return false; + } + } + + if (!TryBuildUvElements(elements, out var uvElements, out var uvChannelCount, out reason)) + { + return false; + } + + var sortedElements = elements.OrderBy(static element => element.Offset).ToList(); + format = new VertexFormat( + sortedElements, + normalElements.Length == 1 ? normalElements[0] : (MdlStructs.VertexElement?)null, + tangentElements.Length == 1 ? tangentElements[0] : (MdlStructs.VertexElement?)null, + colorElement, + blendIndicesElements.Length == 1 ? blendIndicesElements[0] : (MdlStructs.VertexElement?)null, + blendWeightsElements.Length == 1 ? blendWeightsElements[0] : (MdlStructs.VertexElement?)null, + uvElements, + uvChannelCount); + return true; + } + + private static bool TryBuildUvElements( + IReadOnlyList elements, + out List uvElements, + out int uvChannelCount, + out string? reason) + { + uvElements = []; + uvChannelCount = 0; + reason = null; + + var uvList = elements + .Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.UV) + .OrderBy(static e => e.UsageIndex) + .ToList(); + + foreach (var element in uvList) + { + var type = (MdlFile.VertexType)element.Type; + if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2) + { + if (uvChannelCount + 1 > Mesh.UVChannelCount) + { + reason = "Too many UV channels."; + return false; + } + + uvElements.Add(new UvElementPacking(element, uvChannelCount, null)); + uvChannelCount += 1; + } + else if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4) + { + if (uvChannelCount + 2 > Mesh.UVChannelCount) + { + reason = "Too many UV channels."; + return false; + } + + uvElements.Add(new UvElementPacking(element, uvChannelCount, uvChannelCount + 1)); + uvChannelCount += 2; + } + else + { + reason = "Unsupported UV type."; + return false; + } + } + + return true; + } + + private static bool HasShapeData(MdlFile mdl) + => mdl.Shapes.Length > 0 + || mdl.ShapeMeshes.Length > 0 + || mdl.ShapeValues.Length > 0 + || mdl.NeckMorphs.Length > 0; + + private static Vector3d ReadPosition(MdlFile.VertexType type, BinaryReader reader) + { + switch (type) + { + case MdlFile.VertexType.Single3: + return new Vector3d(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + case MdlFile.VertexType.Single4: + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var z = reader.ReadSingle(); + _ = reader.ReadSingle(); + return new Vector3d(x, y, z); + default: + throw new InvalidOperationException($"Unsupported position type {type}"); + } + } + + private static Vector3 ReadNormal(MdlFile.VertexType type, BinaryReader reader) + { + switch (type) + { + case MdlFile.VertexType.Single3: + return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + case MdlFile.VertexType.Single4: + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var z = reader.ReadSingle(); + _ = reader.ReadSingle(); + return new Vector3(x, y, z); + case MdlFile.VertexType.NByte4: + return ReadNByte4(reader).ToVector3(); + default: + throw new InvalidOperationException($"Unsupported normal type {type}"); + } + } + + private static Vector4 ReadTangent(MdlFile.VertexType type, BinaryReader reader) + { + return type switch + { + MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + MdlFile.VertexType.NByte4 => ReadNByte4(reader), + _ => throw new InvalidOperationException($"Unsupported tangent type {type}"), + }; + } + + private static Vector4 ReadColor(MdlFile.VertexType type, BinaryReader reader) + { + return type switch + { + MdlFile.VertexType.UByte4 => ReadUByte4(reader), + MdlFile.VertexType.NByte4 => ReadUByte4(reader), + MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + _ => throw new InvalidOperationException($"Unsupported color type {type}"), + }; + } + + private static void ReadUv(MdlFile.VertexType type, BinaryReader reader, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex) + { + if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2) + { + var uv = type == MdlFile.VertexType.Half2 + ? new Vector2(ReadHalf(reader), ReadHalf(reader)) + : new Vector2(reader.ReadSingle(), reader.ReadSingle()); + + uvChannels[mapping.FirstChannel][vertexIndex] = uv; + return; + } + + if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4) + { + var uv = type == MdlFile.VertexType.Half4 + ? new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader)) + : new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + uvChannels[mapping.FirstChannel][vertexIndex] = new Vector2(uv.x, uv.y); + if (mapping.SecondChannel.HasValue) + { + uvChannels[mapping.SecondChannel.Value][vertexIndex] = new Vector2(uv.z, uv.w); + } + } + } + + private static byte[] ReadIndices(MdlFile.VertexType type, BinaryReader reader) + { + return type switch + { + MdlFile.VertexType.UByte4 => new[] { reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte() }, + _ => throw new InvalidOperationException($"Unsupported indices type {type}"), + }; + } + + private static float[] ReadWeights(MdlFile.VertexType type, BinaryReader reader) + { + return type switch + { + MdlFile.VertexType.UByte4 => ReadUByte4(reader).ToFloatArray(), + MdlFile.VertexType.NByte4 => ReadUByte4(reader).ToFloatArray(), + MdlFile.VertexType.Single4 => new[] { reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() }, + _ => throw new InvalidOperationException($"Unsupported weights type {type}"), + }; + } + + private static Vector4 ReadUByte4(BinaryReader reader) + { + return new Vector4( + reader.ReadByte() / 255f, + reader.ReadByte() / 255f, + reader.ReadByte() / 255f, + reader.ReadByte() / 255f); + } + + private static Vector4 ReadNByte4(BinaryReader reader) + { + var value = ReadUByte4(reader); + return (value * 2f) - new Vector4(1f, 1f, 1f, 1f); + } + + private static Vector4 ReadAndDiscard(MdlFile.VertexType type, BinaryReader reader) + { + return type switch + { + MdlFile.VertexType.Single2 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), 0, 0), + MdlFile.VertexType.Single3 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0), + MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + MdlFile.VertexType.Half2 => new Vector4(ReadHalf(reader), ReadHalf(reader), 0, 0), + MdlFile.VertexType.Half4 => new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader)), + MdlFile.VertexType.UByte4 => ReadUByte4(reader), + MdlFile.VertexType.NByte4 => ReadUByte4(reader), + _ => Vector4.zero, + }; + } + + private static void WritePosition(MdlFile.VertexType type, Vector3d value, Span target) + { + WriteVector3(type, new Vector3((float)value.x, (float)value.y, (float)value.z), target); + } + + private static void WriteNormal(MdlFile.VertexType type, Vector3 value, Span target) + { + WriteVector3(type, value, target, normalized: type == MdlFile.VertexType.NByte4); + } + + private static void WriteTangent(MdlFile.VertexType type, Vector4 value, Span target) + { + if (type == MdlFile.VertexType.NByte4) + { + WriteNByte4(value, target); + return; + } + + WriteVector4(type, value, target); + } + + private static void WriteColor(MdlFile.VertexType type, Vector4 value, Span target) + { + if (type == MdlFile.VertexType.Single4) + { + WriteVector4(type, value, target); + return; + } + + WriteUByte4(value, target); + } + + private static void WriteBlendIndices(MdlFile.VertexType type, BoneWeight weights, Span target) + { + if (type != MdlFile.VertexType.UByte4) + { + return; + } + + target[0] = (byte)Math.Clamp(weights.boneIndex0, 0, 255); + target[1] = (byte)Math.Clamp(weights.boneIndex1, 0, 255); + target[2] = (byte)Math.Clamp(weights.boneIndex2, 0, 255); + target[3] = (byte)Math.Clamp(weights.boneIndex3, 0, 255); + } + + private static void WriteBlendWeights(MdlFile.VertexType type, BoneWeight weights, Span target) + { + if (type != MdlFile.VertexType.UByte4 && type != MdlFile.VertexType.NByte4) + { + if (type == MdlFile.VertexType.Single4) + { + BinaryPrimitives.WriteSingleLittleEndian(target[..4], weights.boneWeight0); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), weights.boneWeight1); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), weights.boneWeight2); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), weights.boneWeight3); + } + return; + } + + var w0 = Clamp01(weights.boneWeight0); + var w1 = Clamp01(weights.boneWeight1); + var w2 = Clamp01(weights.boneWeight2); + var w3 = Clamp01(weights.boneWeight3); + NormalizeWeights(ref w0, ref w1, ref w2, ref w3); + + target[0] = ToByte(w0); + target[1] = ToByte(w1); + target[2] = ToByte(w2); + target[3] = ToByte(w3); + } + + private static void WriteUv(MdlFile.VertexType type, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex, Span target) + { + if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2) + { + var uv = uvChannels[mapping.FirstChannel][vertexIndex]; + WriteVector2(type, uv, target); + return; + } + + if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4) + { + var uv0 = uvChannels[mapping.FirstChannel][vertexIndex]; + var uv1 = mapping.SecondChannel.HasValue + ? uvChannels[mapping.SecondChannel.Value][vertexIndex] + : Vector2.zero; + WriteVector4(type, new Vector4(uv0.x, uv0.y, uv1.x, uv1.y), target); + } + } + + private static void WriteVector2(MdlFile.VertexType type, Vector2 value, Span target) + { + if (type == MdlFile.VertexType.Single2) + { + BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y); + return; + } + + if (type == MdlFile.VertexType.Half2) + { + WriteHalf(target[..2], value.x); + WriteHalf(target.Slice(2, 2), value.y); + } + } + + private static void WriteVector3(MdlFile.VertexType type, Vector3 value, Span target, bool normalized = false) + { + if (type == MdlFile.VertexType.Single3) + { + BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z); + return; + } + + if (type == MdlFile.VertexType.Single4) + { + BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), 1f); + return; + } + + if (type == MdlFile.VertexType.NByte4 && normalized) + { + WriteNByte4(new Vector4(value.x, value.y, value.z, 0f), target); + } + } + + private static void WriteVector4(MdlFile.VertexType type, Vector4 value, Span target) + { + if (type == MdlFile.VertexType.Single4) + { + BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), value.w); + return; + } + + if (type == MdlFile.VertexType.Half4) + { + WriteHalf(target[..2], value.x); + WriteHalf(target.Slice(2, 2), value.y); + WriteHalf(target.Slice(4, 2), value.z); + WriteHalf(target.Slice(6, 2), value.w); + return; + } + } + + private static void WriteUByte4(Vector4 value, Span target) + { + target[0] = ToByte(Clamp01(value.x)); + target[1] = ToByte(Clamp01(value.y)); + target[2] = ToByte(Clamp01(value.z)); + target[3] = ToByte(Clamp01(value.w)); + } + + private static void WriteNByte4(Vector4 value, Span target) + { + var normalized = (value * 0.5f) + new Vector4(0.5f); + WriteUByte4(normalized, target); + } + + private static void WriteHalf(Span target, float value) + { + var half = (Half)value; + BinaryPrimitives.WriteUInt16LittleEndian(target, BitConverter.HalfToUInt16Bits(half)); + } + + private static float ReadHalf(BinaryReader reader) + => (float)BitConverter.UInt16BitsToHalf(reader.ReadUInt16()); + + private static float Clamp01(float value) + => Math.Clamp(value, 0f, 1f); + + private static byte ToByte(float value) + => (byte)Math.Clamp((int)Math.Round(value * 255f), 0, 255); + + private static void NormalizeWeights(float[] weights) + { + var sum = weights.Sum(); + if (sum <= float.Epsilon) + { + return; + } + + for (var i = 0; i < weights.Length; i++) + { + weights[i] /= sum; + } + } + + private static void NormalizeWeights(ref float w0, ref float w1, ref float w2, ref float w3) + { + var sum = w0 + w1 + w2 + w3; + if (sum <= float.Epsilon) + { + return; + } + + w0 /= sum; + w1 /= sum; + w2 /= sum; + w3 /= sum; + } + + private static int GetElementSize(MdlFile.VertexType type) + => type switch + { + MdlFile.VertexType.Single2 => 8, + MdlFile.VertexType.Single3 => 12, + MdlFile.VertexType.Single4 => 16, + MdlFile.VertexType.Half2 => 4, + MdlFile.VertexType.Half4 => 8, + MdlFile.VertexType.UByte4 => 4, + MdlFile.VertexType.NByte4 => 4, + _ => throw new InvalidOperationException($"Unsupported vertex type {type}"), + }; + + private readonly record struct ElementKey(byte Stream, byte Offset, byte Type, byte Usage, byte UsageIndex) + { + public static ElementKey From(MdlStructs.VertexElement element) + => new(element.Stream, element.Offset, element.Type, element.Usage, element.UsageIndex); + } + + private sealed class VertexFormat + { + public VertexFormat( + List sortedElements, + MdlStructs.VertexElement? normalElement, + MdlStructs.VertexElement? tangentElement, + MdlStructs.VertexElement? colorElement, + MdlStructs.VertexElement? blendIndicesElement, + MdlStructs.VertexElement? blendWeightsElement, + List uvElements, + int uvChannelCount) + { + SortedElements = sortedElements; + NormalElement = normalElement; + TangentElement = tangentElement; + ColorElement = colorElement; + BlendIndicesElement = blendIndicesElement; + BlendWeightsElement = blendWeightsElement; + UvElements = uvElements; + UvChannelCount = uvChannelCount; + } + + public List SortedElements { get; } + public MdlStructs.VertexElement? NormalElement { get; } + public MdlStructs.VertexElement? TangentElement { get; } + public MdlStructs.VertexElement? ColorElement { get; } + public MdlStructs.VertexElement? BlendIndicesElement { get; } + public MdlStructs.VertexElement? BlendWeightsElement { get; } + public List UvElements { get; } + public int UvChannelCount { get; } + + public bool HasNormals => NormalElement.HasValue; + public bool HasTangents => TangentElement.HasValue; + public bool HasColors => ColorElement.HasValue; + public bool HasSkinning => BlendIndicesElement.HasValue && BlendWeightsElement.HasValue; + } + + private readonly record struct UvElementPacking(MdlStructs.VertexElement Element, int FirstChannel, int? SecondChannel); + + private sealed class DecodedMeshData + { + public DecodedMeshData( + Vector3d[] positions, + Vector3[]? normals, + Vector4[]? tangents, + Vector4[]? colors, + BoneWeight[]? boneWeights, + Vector2[][]? uvChannels) + { + Positions = positions; + Normals = normals; + Tangents = tangents; + Colors = colors; + BoneWeights = boneWeights; + UvChannels = uvChannels; + } + + public Vector3d[] Positions { get; } + public Vector3[]? Normals { get; } + public Vector4[]? Tangents { get; } + public Vector4[]? Colors { get; } + public BoneWeight[]? BoneWeights { get; } + public Vector2[][]? UvChannels { get; } + } +} + +internal static class MeshDecimatorVectorExtensions +{ + public static Vector3 ToVector3(this Vector4 value) + => new(value.x, value.y, value.z); + + public static float[] ToFloatArray(this Vector4 value) + => [value.x, value.y, value.z, value.w]; +} diff --git a/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs b/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs new file mode 100644 index 0000000..8dc2571 --- /dev/null +++ b/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs @@ -0,0 +1,275 @@ +using LightlessSync.LightlessConfiguration; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace LightlessSync.Services.ModelDecimation; + +public sealed class ModelDecimationService +{ + private const int MaxConcurrentJobs = 1; + private const double MinTargetRatio = 0.01; + private const double MaxTargetRatio = 0.99; + + private readonly ILogger _logger; + private readonly LightlessConfigService _configService; + private readonly PlayerPerformanceConfigService _performanceConfigService; + private readonly XivDataStorageService _xivDataStorageService; + private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs); + + private readonly ConcurrentDictionary _activeJobs = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _decimatedPaths = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _failedHashes = new(StringComparer.OrdinalIgnoreCase); + + public ModelDecimationService( + ILogger logger, + LightlessConfigService configService, + PlayerPerformanceConfigService performanceConfigService, + XivDataStorageService xivDataStorageService) + { + _logger = logger; + _configService = configService; + _performanceConfigService = performanceConfigService; + _xivDataStorageService = xivDataStorageService; + } + + public void ScheduleDecimation(string hash, string filePath, string? gamePath = null) + { + if (!ShouldScheduleDecimation(hash, filePath, gamePath)) + { + return; + } + + if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash)) + { + return; + } + + _logger.LogInformation("Queued model decimation for {Hash}", hash); + + _activeJobs[hash] = Task.Run(async () => + { + await _decimationSemaphore.WaitAsync().ConfigureAwait(false); + try + { + await DecimateInternalAsync(hash, filePath).ConfigureAwait(false); + } + catch (Exception ex) + { + _failedHashes[hash] = 1; + _logger.LogWarning(ex, "Model decimation failed for {Hash}", hash); + } + finally + { + _decimationSemaphore.Release(); + _activeJobs.TryRemove(hash, out _); + } + }, CancellationToken.None); + } + + public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null) + => IsDecimationEnabled() + && filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase) + && IsDecimationAllowed(gamePath) + && !ShouldSkipByTriangleCache(hash); + + public string GetPreferredPath(string hash, string originalPath) + { + if (!IsDecimationEnabled()) + { + return originalPath; + } + + if (_decimatedPaths.TryGetValue(hash, out var existing) && File.Exists(existing)) + { + return existing; + } + + var resolved = GetExistingDecimatedPath(hash); + if (!string.IsNullOrEmpty(resolved)) + { + _decimatedPaths[hash] = resolved; + return resolved; + } + + return originalPath; + } + + public Task WaitForPendingJobsAsync(IEnumerable? hashes, CancellationToken token) + { + if (hashes is null) + { + return Task.CompletedTask; + } + + var pending = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var hash in hashes) + { + if (string.IsNullOrEmpty(hash) || !seen.Add(hash)) + { + continue; + } + + if (_activeJobs.TryGetValue(hash, out var job)) + { + pending.Add(job); + } + } + + if (pending.Count == 0) + { + return Task.CompletedTask; + } + + return Task.WhenAll(pending).WaitAsync(token); + } + + private Task DecimateInternalAsync(string hash, string sourcePath) + { + if (!File.Exists(sourcePath)) + { + _failedHashes[hash] = 1; + _logger.LogWarning("Cannot decimate model {Hash}; source path missing: {Path}", hash, sourcePath); + return Task.CompletedTask; + } + + if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio)) + { + _logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash); + return Task.CompletedTask; + } + + _logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio); + + var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl"); + if (File.Exists(destination)) + { + _decimatedPaths[hash] = destination; + return Task.CompletedTask; + } + + if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, _logger)) + { + _failedHashes[hash] = 1; + _logger.LogInformation("Model decimation skipped for {Hash}", hash); + return Task.CompletedTask; + } + + _decimatedPaths[hash] = destination; + _logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination); + return Task.CompletedTask; + } + + private bool IsDecimationEnabled() + => _performanceConfigService.Current.EnableModelDecimation; + + private bool ShouldSkipByTriangleCache(string hash) + { + if (string.IsNullOrEmpty(hash)) + { + return false; + } + + if (!_xivDataStorageService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) || cachedTris <= 0) + { + return false; + } + + var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold); + return threshold > 0 && cachedTris < threshold; + } + + private bool IsDecimationAllowed(string? gamePath) + { + if (string.IsNullOrWhiteSpace(gamePath)) + { + return true; + } + + var normalized = NormalizeGamePath(gamePath); + if (normalized.Contains("/hair/", StringComparison.Ordinal)) + { + return false; + } + + if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal)) + { + return _performanceConfigService.Current.ModelDecimationAllowClothing; + } + + if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal)) + { + return _performanceConfigService.Current.ModelDecimationAllowAccessories; + } + + if (normalized.Contains("/chara/human/", StringComparison.Ordinal)) + { + if (normalized.Contains("/body/", StringComparison.Ordinal)) + { + return _performanceConfigService.Current.ModelDecimationAllowBody; + } + + if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal)) + { + return _performanceConfigService.Current.ModelDecimationAllowFaceHead; + } + + if (normalized.Contains("/tail/", StringComparison.Ordinal)) + { + return _performanceConfigService.Current.ModelDecimationAllowTail; + } + } + + return true; + } + + private static string NormalizeGamePath(string path) + => path.Replace('\\', '/').ToLowerInvariant(); + + private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio) + { + triangleThreshold = 15_000; + targetRatio = 0.8; + + var config = _performanceConfigService.Current; + if (!config.EnableModelDecimation) + { + return false; + } + + triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold); + targetRatio = config.ModelDecimationTargetRatio; + if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio)) + { + return false; + } + + targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio); + return true; + } + + private string? GetExistingDecimatedPath(string hash) + { + var candidate = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl"); + return File.Exists(candidate) ? candidate : null; + } + + private string GetDecimatedDirectory() + { + var directory = Path.Combine(_configService.Current.CacheFolder, "decimated"); + if (!Directory.Exists(directory)) + { + try + { + Directory.CreateDirectory(directory); + } + catch (Exception ex) + { + _logger.LogTrace(ex, "Failed to create decimated directory {Directory}", directory); + } + } + + return directory; + } +} diff --git a/LightlessSync/Services/PlayerPerformanceService.cs b/LightlessSync/Services/PlayerPerformanceService.cs index e77ccd7..553e87b 100644 --- a/LightlessSync/Services/PlayerPerformanceService.cs +++ b/LightlessSync/Services/PlayerPerformanceService.cs @@ -4,6 +4,7 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ModelDecimation; using LightlessSync.Services.TextureCompression; using LightlessSync.UI; using LightlessSync.WebAPI.Files.Models; @@ -18,12 +19,14 @@ public class PlayerPerformanceService private readonly ILogger _logger; private readonly LightlessMediator _mediator; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly ModelDecimationService _modelDecimationService; private readonly TextureDownscaleService _textureDownscaleService; private readonly Dictionary _warnedForPlayers = new(StringComparer.Ordinal); public PlayerPerformanceService(ILogger logger, LightlessMediator mediator, PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager, - XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService) + XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService, + ModelDecimationService modelDecimationService) { _logger = logger; _mediator = mediator; @@ -31,6 +34,7 @@ public class PlayerPerformanceService _fileCacheManager = fileCacheManager; _xivDataAnalyzer = xivDataAnalyzer; _textureDownscaleService = textureDownscaleService; + _modelDecimationService = modelDecimationService; } public async Task CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData) @@ -111,10 +115,12 @@ public class PlayerPerformanceService var config = _playerPerformanceConfigService.Current; long triUsage = 0; + long effectiveTriUsage = 0; if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List? playerReplacements)) { pairHandler.LastAppliedDataTris = 0; + pairHandler.LastAppliedApproximateEffectiveTris = 0; return true; } @@ -125,12 +131,31 @@ public class PlayerPerformanceService foreach (var hash in moddedModelHashes) { - triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); + var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); + triUsage += tris; + + long effectiveTris = tris; + var fileEntry = _fileCacheManager.GetFileCacheByHash(hash); + if (fileEntry != null) + { + var preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath); + if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase)) + { + var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false); + if (decimatedTris > 0) + { + effectiveTris = decimatedTris; + } + } + } + + effectiveTriUsage += effectiveTris; } pairHandler.LastAppliedDataTris = triUsage; + pairHandler.LastAppliedApproximateEffectiveTris = effectiveTriUsage; - _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); + _logger.LogDebug("Calculated triangle usage for {p}", pairHandler); // no warning of any kind on ignored pairs if (config.UIDsToIgnore @@ -274,4 +299,4 @@ public class PlayerPerformanceService private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) => thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm); -} \ No newline at end of file +} diff --git a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs index 5113e20..6fa6f92 100644 --- a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs +++ b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs @@ -678,7 +678,7 @@ public sealed class TextureDownscaleService if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase)) { - _fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath); + _fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false); } _fileCacheManager.UpdateHashedFile(replacement, computeProperties: false); diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index 9d32883..33a1ecd 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -8,6 +8,7 @@ using LightlessSync.Interop.GameModel; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Handlers; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; using System.Runtime.InteropServices; namespace LightlessSync.Services; @@ -18,6 +19,7 @@ public sealed class XivDataAnalyzer private readonly FileCacheManager _fileCacheManager; private readonly XivDataStorageService _configService; private readonly List _failedCalculatedTris = []; + private readonly List _failedCalculatedEffectiveTris = []; public XivDataAnalyzer(ILogger logger, FileCacheManager fileCacheManager, XivDataStorageService configService) @@ -162,16 +164,41 @@ public sealed class XivDataAnalyzer if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) return 0; - var filePath = path.ResolvedFilepath; + return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris); + } + public async Task GetEffectiveTrianglesByHash(string hash, string filePath) + { + if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0) + return cachedTris; + + if (_failedCalculatedEffectiveTris.Contains(hash, StringComparer.Ordinal)) + return 0; + + if (string.IsNullOrEmpty(filePath) + || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase) + || !File.Exists(filePath)) + { + return 0; + } + + return CalculateTrianglesFromPath(hash, filePath, _configService.Current.EffectiveTriangleDictionary, _failedCalculatedEffectiveTris); + } + + private long CalculateTrianglesFromPath( + string hash, + string filePath, + ConcurrentDictionary cache, + List failedList) + { try { _logger.LogDebug("Detected Model File {path}, calculating Tris", filePath); var file = new MdlFile(filePath); if (file.LodCount <= 0) { - _failedCalculatedTris.Add(hash); - _configService.Current.TriangleDictionary[hash] = 0; + failedList.Add(hash); + cache[hash] = 0; _configService.Save(); return 0; } @@ -195,7 +222,7 @@ public sealed class XivDataAnalyzer if (tris > 0) { _logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris); - _configService.Current.TriangleDictionary[hash] = tris; + cache[hash] = tris; _configService.Save(); break; } @@ -205,8 +232,8 @@ public sealed class XivDataAnalyzer } catch (Exception e) { - _failedCalculatedTris.Add(hash); - _configService.Current.TriangleDictionary[hash] = 0; + failedList.Add(hash); + cache[hash] = 0; _configService.Save(); _logger.LogWarning(e, "Could not parse file {file}", filePath); return 0; diff --git a/LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs b/LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs new file mode 100644 index 0000000..723eef6 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs @@ -0,0 +1,169 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using Microsoft.Extensions.Logging; + +namespace MeshDecimator.Algorithms +{ + /// + /// A decimation algorithm. + /// + public abstract class DecimationAlgorithm + { + #region Delegates + /// + /// A callback for decimation status reports. + /// + /// The current iteration, starting at zero. + /// The original count of triangles. + /// The current count of triangles. + /// The target count of triangles. + public delegate void StatusReportCallback(int iteration, int originalTris, int currentTris, int targetTris); + #endregion + + #region Fields + private bool preserveBorders = false; + private int maxVertexCount = 0; + private bool verbose = false; + + private StatusReportCallback statusReportInvoker = null; + #endregion + + #region Properties + /// + /// Gets or sets if borders should be kept. + /// Default value: false + /// + [Obsolete("Use the 'DecimationAlgorithm.PreserveBorders' property instead.", false)] + public bool KeepBorders + { + get { return preserveBorders; } + set { preserveBorders = value; } + } + + /// + /// Gets or sets if borders should be preserved. + /// Default value: false + /// + public bool PreserveBorders + { + get { return preserveBorders; } + set { preserveBorders = value; } + } + + /// + /// Gets or sets if linked vertices should be kept. + /// Default value: false + /// + [Obsolete("This feature has been removed, for more details why please read the readme.", true)] + public bool KeepLinkedVertices + { + get { return false; } + set { } + } + + /// + /// Gets or sets the maximum vertex count. Set to zero for no limitation. + /// Default value: 0 (no limitation) + /// + public int MaxVertexCount + { + get { return maxVertexCount; } + set { maxVertexCount = Math.MathHelper.Max(value, 0); } + } + + /// + /// Gets or sets if verbose information should be printed in the console. + /// Default value: false + /// + public bool Verbose + { + get { return verbose; } + set { verbose = value; } + } + + /// + /// Gets or sets the logger used for diagnostics. + /// + public ILogger? Logger { get; set; } + #endregion + + #region Events + /// + /// An event for status reports for this algorithm. + /// + public event StatusReportCallback StatusReport + { + add { statusReportInvoker += value; } + remove { statusReportInvoker -= value; } + } + #endregion + + #region Protected Methods + /// + /// Reports the current status of the decimation. + /// + /// The current iteration, starting at zero. + /// The original count of triangles. + /// The current count of triangles. + /// The target count of triangles. + protected void ReportStatus(int iteration, int originalTris, int currentTris, int targetTris) + { + var statusReportInvoker = this.statusReportInvoker; + if (statusReportInvoker != null) + { + statusReportInvoker.Invoke(iteration, originalTris, currentTris, targetTris); + } + } + #endregion + + #region Public Methods + /// + /// Initializes the algorithm with the original mesh. + /// + /// The mesh. + public abstract void Initialize(Mesh mesh); + + /// + /// Decimates the mesh. + /// + /// The target triangle count. + public abstract void DecimateMesh(int targetTrisCount); + + /// + /// Decimates the mesh without losing any quality. + /// + public abstract void DecimateMeshLossless(); + + /// + /// Returns the resulting mesh. + /// + /// The resulting mesh. + public abstract Mesh ToMesh(); + #endregion + } +} diff --git a/LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs b/LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs new file mode 100644 index 0000000..fe22c85 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs @@ -0,0 +1,1549 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +#region Original License +///////////////////////////////////////////// +// +// Mesh Simplification Tutorial +// +// (C) by Sven Forstmann in 2014 +// +// License : MIT +// http://opensource.org/licenses/MIT +// +//https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification +#endregion + +using System; +using System.Collections.Generic; +using MeshDecimator.Collections; +using MeshDecimator.Math; +using Microsoft.Extensions.Logging; + +namespace MeshDecimator.Algorithms +{ + /// + /// The fast quadric mesh simplification algorithm. + /// + public sealed class FastQuadricMeshSimplification : DecimationAlgorithm + { + #region Consts + private const double DoubleEpsilon = 1.0E-3; + #endregion + + #region Classes + #region Triangle + private struct Triangle + { + #region Fields + public int v0; + public int v1; + public int v2; + public int subMeshIndex; + + public int va0; + public int va1; + public int va2; + + public double err0; + public double err1; + public double err2; + public double err3; + + public bool deleted; + public bool dirty; + public Vector3d n; + #endregion + + #region Properties + public int this[int index] + { + get + { + return (index == 0 ? v0 : (index == 1 ? v1 : v2)); + } + set + { + switch (index) + { + case 0: + v0 = value; + break; + case 1: + v1 = value; + break; + case 2: + v2 = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + } + #endregion + + #region Constructor + public Triangle(int v0, int v1, int v2, int subMeshIndex) + { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + this.subMeshIndex = subMeshIndex; + + this.va0 = v0; + this.va1 = v1; + this.va2 = v2; + + err0 = err1 = err2 = err3 = 0; + deleted = dirty = false; + n = new Vector3d(); + } + #endregion + + #region Public Methods + public void GetAttributeIndices(int[] attributeIndices) + { + attributeIndices[0] = va0; + attributeIndices[1] = va1; + attributeIndices[2] = va2; + } + + public void SetAttributeIndex(int index, int value) + { + switch (index) + { + case 0: + va0 = value; + break; + case 1: + va1 = value; + break; + case 2: + va2 = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + + public void GetErrors(double[] err) + { + err[0] = err0; + err[1] = err1; + err[2] = err2; + } + #endregion + } + #endregion + + #region Vertex + private struct Vertex + { + public Vector3d p; + public int tstart; + public int tcount; + public SymmetricMatrix q; + public bool border; + public bool seam; + public bool foldover; + + public Vertex(Vector3d p) + { + this.p = p; + this.tstart = 0; + this.tcount = 0; + this.q = new SymmetricMatrix(); + this.border = true; + this.seam = false; + this.foldover = false; + } + } + #endregion + + #region Ref + private struct Ref + { + public int tid; + public int tvertex; + + public void Set(int tid, int tvertex) + { + this.tid = tid; + this.tvertex = tvertex; + } + } + #endregion + + #region Border Vertex + private struct BorderVertex + { + public int index; + public int hash; + + public BorderVertex(int index, int hash) + { + this.index = index; + this.hash = hash; + } + } + #endregion + + #region Border Vertex Comparer + private class BorderVertexComparer : IComparer + { + public static readonly BorderVertexComparer instance = new BorderVertexComparer(); + + public int Compare(BorderVertex x, BorderVertex y) + { + return x.hash.CompareTo(y.hash); + } + } + #endregion + #endregion + + #region Fields + private bool preserveSeams = false; + private bool preserveFoldovers = false; + private bool enableSmartLink = true; + private int maxIterationCount = 100; + private double agressiveness = 7.0; + private double vertexLinkDistanceSqr = double.Epsilon; + + private int subMeshCount = 0; + private ResizableArray triangles = null; + private ResizableArray vertices = null; + private ResizableArray refs = null; + + private ResizableArray vertNormals = null; + private ResizableArray vertTangents = null; + private UVChannels vertUV2D = null; + private UVChannels vertUV3D = null; + private UVChannels vertUV4D = null; + private ResizableArray vertColors = null; + private ResizableArray vertBoneWeights = null; + + private int remainingVertices = 0; + + // Pre-allocated buffers + private double[] errArr = new double[3]; + private int[] attributeIndexArr = new int[3]; + #endregion + + #region Properties + /// + /// Gets or sets if seams should be preserved. + /// Default value: false + /// + public bool PreserveSeams + { + get { return preserveSeams; } + set { preserveSeams = value; } + } + + /// + /// Gets or sets if foldovers should be preserved. + /// Default value: false + /// + public bool PreserveFoldovers + { + get { return preserveFoldovers; } + set { preserveFoldovers = value; } + } + + /// + /// Gets or sets if a feature for smarter vertex linking should be enabled, reducing artifacts in the + /// decimated result at the cost of a slightly more expensive initialization by treating vertices at + /// the same position as the same vertex while separating the attributes. + /// Default value: true + /// + public bool EnableSmartLink + { + get { return enableSmartLink; } + set { enableSmartLink = value; } + } + + /// + /// Gets or sets the maximum iteration count. Higher number is more expensive but can bring you closer to your target quality. + /// Sometimes a lower maximum count might be desired in order to lower the performance cost. + /// Default value: 100 + /// + public int MaxIterationCount + { + get { return maxIterationCount; } + set { maxIterationCount = value; } + } + + /// + /// Gets or sets the agressiveness of this algorithm. Higher number equals higher quality, but more expensive to run. + /// Default value: 7.0 + /// + public double Agressiveness + { + get { return agressiveness; } + set { agressiveness = value; } + } + + /// + /// Gets or sets the maximum squared distance between two vertices in order to link them. + /// Note that this value is only used if EnableSmartLink is true. + /// Default value: double.Epsilon + /// + public double VertexLinkDistanceSqr + { + get { return vertexLinkDistanceSqr; } + set { vertexLinkDistanceSqr = value; } + } + #endregion + + #region Constructor + /// + /// Creates a new fast quadric mesh simplification algorithm. + /// + public FastQuadricMeshSimplification() + { + triangles = new ResizableArray(0); + vertices = new ResizableArray(0); + refs = new ResizableArray(0); + } + #endregion + + #region Private Methods + #region Initialize Vertex Attribute + private ResizableArray InitializeVertexAttribute(T[] attributeValues, string attributeName) + { + if (attributeValues != null && attributeValues.Length == vertices.Length) + { + var newArray = new ResizableArray(attributeValues.Length, attributeValues.Length); + var newArrayData = newArray.Data; + Array.Copy(attributeValues, 0, newArrayData, 0, attributeValues.Length); + return newArray; + } + else if (attributeValues != null && attributeValues.Length > 0) + { + Logger?.LogError( + "Failed to set vertex attribute '{Attribute}' with {ActualLength} length of array, when {ExpectedLength} was needed.", + attributeName, + attributeValues.Length, + vertices.Length); + } + return null; + } + #endregion + + #region Calculate Error + private double VertexError(ref SymmetricMatrix q, double x, double y, double z) + { + return q.m0*x*x + 2*q.m1*x*y + 2*q.m2*x*z + 2*q.m3*x + q.m4*y*y + + 2*q.m5*y*z + 2*q.m6*y + q.m7*z*z + 2*q.m8*z + q.m9; + } + + private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d result, out int resultIndex) + { + // compute interpolated vertex + SymmetricMatrix q = (vert0.q + vert1.q); + bool border = (vert0.border & vert1.border); + double error = 0.0; + double det = q.Determinant1(); + if (det != 0.0 && !border) + { + // q_delta is invertible + result = new Vector3d( + -1.0 / det * q.Determinant2(), // vx = A41/det(q_delta) + 1.0 / det * q.Determinant3(), // vy = A42/det(q_delta) + -1.0 / det * q.Determinant4()); // vz = A43/det(q_delta) + error = VertexError(ref q, result.x, result.y, result.z); + resultIndex = 2; + } + else + { + // det = 0 -> try to find best result + Vector3d p1 = vert0.p; + Vector3d p2 = vert1.p; + Vector3d p3 = (p1 + p2) * 0.5f; + double error1 = VertexError(ref q, p1.x, p1.y, p1.z); + double error2 = VertexError(ref q, p2.x, p2.y, p2.z); + double error3 = VertexError(ref q, p3.x, p3.y, p3.z); + error = MathHelper.Min(error1, error2, error3); + if (error == error3) + { + result = p3; + resultIndex = 2; + } + else if (error == error2) + { + result = p2; + resultIndex = 1; + } + else if (error == error1) + { + result = p1; + resultIndex = 0; + } + else + { + result = p3; + resultIndex = 2; + } + } + return error; + } + #endregion + + #region Flipped + /// + /// Check if a triangle flips when this edge is removed + /// + private bool Flipped(ref Vector3d p, int i0, int i1, ref Vertex v0, bool[] deleted) + { + int tcount = v0.tcount; + var refs = this.refs.Data; + var triangles = this.triangles.Data; + var vertices = this.vertices.Data; + for (int k = 0; k < tcount; k++) + { + Ref r = refs[v0.tstart + k]; + if (triangles[r.tid].deleted) + continue; + + int s = r.tvertex; + int id1 = triangles[r.tid][(s + 1) % 3]; + int id2 = triangles[r.tid][(s + 2) % 3]; + if (id1 == i1 || id2 == i1) + { + deleted[k] = true; + continue; + } + + Vector3d d1 = vertices[id1].p - p; + d1.Normalize(); + Vector3d d2 = vertices[id2].p - p; + d2.Normalize(); + double dot = Vector3d.Dot(ref d1, ref d2); + if (System.Math.Abs(dot) > 0.999) + return true; + + Vector3d n; + Vector3d.Cross(ref d1, ref d2, out n); + n.Normalize(); + deleted[k] = false; + dot = Vector3d.Dot(ref n, ref triangles[r.tid].n); + if (dot < 0.2) + return true; + } + + return false; + } + #endregion + + #region Update Triangles + /// + /// Update triangle connections and edge error after a edge is collapsed. + /// + private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray deleted, ref int deletedTriangles) + { + Vector3d p; + int pIndex; + int tcount = v.tcount; + var triangles = this.triangles.Data; + var vertices = this.vertices.Data; + for (int k = 0; k < tcount; k++) + { + Ref r = refs[v.tstart + k]; + int tid = r.tid; + Triangle t = triangles[tid]; + if (t.deleted) + continue; + + if (deleted[k]) + { + triangles[tid].deleted = true; + ++deletedTriangles; + continue; + } + + t[r.tvertex] = i0; + if (ia0 != -1) + { + t.SetAttributeIndex(r.tvertex, ia0); + } + + t.dirty = true; + t.err0 = CalculateError(ref vertices[t.v0], ref vertices[t.v1], out p, out pIndex); + t.err1 = CalculateError(ref vertices[t.v1], ref vertices[t.v2], out p, out pIndex); + t.err2 = CalculateError(ref vertices[t.v2], ref vertices[t.v0], out p, out pIndex); + t.err3 = MathHelper.Min(t.err0, t.err1, t.err2); + triangles[tid] = t; + refs.Add(r); + } + } + #endregion + + #region Move/Merge Vertex Attributes + private void MoveVertexAttributes(int i0, int i1) + { + if (vertNormals != null) + { + vertNormals[i0] = vertNormals[i1]; + } + if (vertTangents != null) + { + vertTangents[i0] = vertTangents[i1]; + } + if (vertUV2D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + var vertUV = vertUV2D[i]; + if (vertUV != null) + { + vertUV[i0] = vertUV[i1]; + } + } + } + if (vertUV3D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + var vertUV = vertUV3D[i]; + if (vertUV != null) + { + vertUV[i0] = vertUV[i1]; + } + } + } + if (vertUV4D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + var vertUV = vertUV4D[i]; + if (vertUV != null) + { + vertUV[i0] = vertUV[i1]; + } + } + } + if (vertColors != null) + { + vertColors[i0] = vertColors[i1]; + } + if (vertBoneWeights != null) + { + vertBoneWeights[i0] = vertBoneWeights[i1]; + } + } + + private void MergeVertexAttributes(int i0, int i1) + { + if (vertNormals != null) + { + vertNormals[i0] = (vertNormals[i0] + vertNormals[i1]) * 0.5f; + } + if (vertTangents != null) + { + vertTangents[i0] = (vertTangents[i0] + vertTangents[i1]) * 0.5f; + } + if (vertUV2D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + var vertUV = vertUV2D[i]; + if (vertUV != null) + { + vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; + } + } + } + if (vertUV3D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + var vertUV = vertUV3D[i]; + if (vertUV != null) + { + vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; + } + } + } + if (vertUV4D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + var vertUV = vertUV4D[i]; + if (vertUV != null) + { + vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; + } + } + } + if (vertColors != null) + { + vertColors[i0] = (vertColors[i0] + vertColors[i1]) * 0.5f; + } + + // TODO: Do we have to blend bone weights at all or can we just keep them as it is in this scenario? + } + #endregion + + #region Are UVs The Same + private bool AreUVsTheSame(int channel, int indexA, int indexB) + { + if (vertUV2D != null) + { + var vertUV = vertUV2D[channel]; + if (vertUV != null) + { + var uvA = vertUV[indexA]; + var uvB = vertUV[indexB]; + return uvA == uvB; + } + } + + if (vertUV3D != null) + { + var vertUV = vertUV3D[channel]; + if (vertUV != null) + { + var uvA = vertUV[indexA]; + var uvB = vertUV[indexB]; + return uvA == uvB; + } + } + + if (vertUV4D != null) + { + var vertUV = vertUV4D[channel]; + if (vertUV != null) + { + var uvA = vertUV[indexA]; + var uvB = vertUV[indexB]; + return uvA == uvB; + } + } + + return false; + } + #endregion + + #region Remove Vertex Pass + /// + /// Remove vertices and mark deleted triangles + /// + private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double threshold, ResizableArray deleted0, ResizableArray deleted1, ref int deletedTris) + { + var triangles = this.triangles.Data; + int triangleCount = this.triangles.Length; + var vertices = this.vertices.Data; + + bool preserveBorders = base.PreserveBorders; + int maxVertexCount = base.MaxVertexCount; + if (maxVertexCount <= 0) + maxVertexCount = int.MaxValue; + + Vector3d p; + int pIndex; + for (int tid = 0; tid < triangleCount; tid++) + { + if (triangles[tid].dirty || triangles[tid].deleted || triangles[tid].err3 > threshold) + continue; + + triangles[tid].GetErrors(errArr); + triangles[tid].GetAttributeIndices(attributeIndexArr); + for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++) + { + if (errArr[edgeIndex] > threshold) + continue; + + int nextEdgeIndex = ((edgeIndex + 1) % 3); + int i0 = triangles[tid][edgeIndex]; + int i1 = triangles[tid][nextEdgeIndex]; + + // Border check + if (vertices[i0].border != vertices[i1].border) + continue; + // Seam check + else if (vertices[i0].seam != vertices[i1].seam) + continue; + // Foldover check + else if (vertices[i0].foldover != vertices[i1].foldover) + continue; + // If borders should be preserved + else if (preserveBorders && vertices[i0].border) + continue; + // If seams should be preserved + else if (preserveSeams && vertices[i0].seam) + continue; + // If foldovers should be preserved + else if (preserveFoldovers && vertices[i0].foldover) + continue; + + // Compute vertex to collapse to + CalculateError(ref vertices[i0], ref vertices[i1], out p, out pIndex); + deleted0.Resize(vertices[i0].tcount); // normals temporarily + deleted1.Resize(vertices[i1].tcount); // normals temporarily + + // Don't remove if flipped + if (Flipped(ref p, i0, i1, ref vertices[i0], deleted0.Data)) + continue; + if (Flipped(ref p, i1, i0, ref vertices[i1], deleted1.Data)) + continue; + + int ia0 = attributeIndexArr[edgeIndex]; + + // Not flipped, so remove edge + vertices[i0].p = p; + vertices[i0].q += vertices[i1].q; + + if (pIndex == 1) + { + // Move vertex attributes from ia1 to ia0 + int ia1 = attributeIndexArr[nextEdgeIndex]; + MoveVertexAttributes(ia0, ia1); + } + else if (pIndex == 2) + { + // Merge vertex attributes ia0 and ia1 into ia0 + int ia1 = attributeIndexArr[nextEdgeIndex]; + MergeVertexAttributes(ia0, ia1); + } + + if (vertices[i0].seam) + { + ia0 = -1; + } + + int tstart = refs.Length; + UpdateTriangles(i0, ia0, ref vertices[i0], deleted0, ref deletedTris); + UpdateTriangles(i0, ia0, ref vertices[i1], deleted1, ref deletedTris); + + int tcount = refs.Length - tstart; + if (tcount <= vertices[i0].tcount) + { + // save ram + if (tcount > 0) + { + var refsArr = refs.Data; + Array.Copy(refsArr, tstart, refsArr, vertices[i0].tstart, tcount); + } + } + else + { + // append + vertices[i0].tstart = tstart; + } + + vertices[i0].tcount = tcount; + --remainingVertices; + break; + } + + // Check if we are already done + if ((startTrisCount - deletedTris) <= targetTrisCount && remainingVertices < maxVertexCount) + break; + } + } + #endregion + + #region Update Mesh + /// + /// Compact triangles, compute edge error and build reference list. + /// + /// The iteration index. + private void UpdateMesh(int iteration) + { + var triangles = this.triangles.Data; + var vertices = this.vertices.Data; + + int triangleCount = this.triangles.Length; + int vertexCount = this.vertices.Length; + if (iteration > 0) // compact triangles + { + int dst = 0; + for (int i = 0; i < triangleCount; i++) + { + if (!triangles[i].deleted) + { + if (dst != i) + { + triangles[dst] = triangles[i]; + } + dst++; + } + } + this.triangles.Resize(dst); + triangles = this.triangles.Data; + triangleCount = dst; + } + + UpdateReferences(); + + // Identify boundary : vertices[].border=0,1 + if (iteration == 0) + { + var refs = this.refs.Data; + + var vcount = new List(8); + var vids = new List(8); + int vsize = 0; + for (int i = 0; i < vertexCount; i++) + { + vertices[i].border = false; + vertices[i].seam = false; + vertices[i].foldover = false; + } + + int ofs; + int id; + int borderVertexCount = 0; + double borderMinX = double.MaxValue; + double borderMaxX = double.MinValue; + for (int i = 0; i < vertexCount; i++) + { + int tstart = vertices[i].tstart; + int tcount = vertices[i].tcount; + vcount.Clear(); + vids.Clear(); + vsize = 0; + + for (int j = 0; j < tcount; j++) + { + int tid = refs[tstart + j].tid; + for (int k = 0; k < 3; k++) + { + ofs = 0; + id = triangles[tid][k]; + while (ofs < vsize) + { + if (vids[ofs] == id) + break; + + ++ofs; + } + + if (ofs == vsize) + { + vcount.Add(1); + vids.Add(id); + ++vsize; + } + else + { + ++vcount[ofs]; + } + } + } + + for (int j = 0; j < vsize; j++) + { + if (vcount[j] == 1) + { + id = vids[j]; + vertices[id].border = true; + ++borderVertexCount; + + if (enableSmartLink) + { + if (vertices[id].p.x < borderMinX) + { + borderMinX = vertices[id].p.x; + } + if (vertices[id].p.x > borderMaxX) + { + borderMaxX = vertices[id].p.x; + } + } + } + } + } + + if (enableSmartLink) + { + // First find all border vertices + var borderVertices = new BorderVertex[borderVertexCount]; + int borderIndexCount = 0; + double borderAreaWidth = borderMaxX - borderMinX; + for (int i = 0; i < vertexCount; i++) + { + if (vertices[i].border) + { + int vertexHash = (int)(((((vertices[i].p.x - borderMinX) / borderAreaWidth) * 2.0) - 1.0) * int.MaxValue); + borderVertices[borderIndexCount] = new BorderVertex(i, vertexHash); + ++borderIndexCount; + } + } + + // Sort the border vertices by hash + Array.Sort(borderVertices, 0, borderIndexCount, BorderVertexComparer.instance); + + // Calculate the maximum hash distance based on the maximum vertex link distance + double vertexLinkDistance = System.Math.Sqrt(vertexLinkDistanceSqr); + int hashMaxDistance = System.Math.Max((int)((vertexLinkDistance / borderAreaWidth) * int.MaxValue), 1); + + // Then find identical border vertices and bind them together as one + for (int i = 0; i < borderIndexCount; i++) + { + int myIndex = borderVertices[i].index; + if (myIndex == -1) + continue; + + var myPoint = vertices[myIndex].p; + for (int j = i + 1; j < borderIndexCount; j++) + { + int otherIndex = borderVertices[j].index; + if (otherIndex == -1) + continue; + else if ((borderVertices[j].hash - borderVertices[i].hash) > hashMaxDistance) // There is no point to continue beyond this point + break; + + var otherPoint = vertices[otherIndex].p; + var sqrX = ((myPoint.x - otherPoint.x) * (myPoint.x - otherPoint.x)); + var sqrY = ((myPoint.y - otherPoint.y) * (myPoint.y - otherPoint.y)); + var sqrZ = ((myPoint.z - otherPoint.z) * (myPoint.z - otherPoint.z)); + var sqrMagnitude = sqrX + sqrY + sqrZ; + + if (sqrMagnitude <= vertexLinkDistanceSqr) + { + borderVertices[j].index = -1; // NOTE: This makes sure that the "other" vertex is not processed again + vertices[myIndex].border = false; + vertices[otherIndex].border = false; + + if (AreUVsTheSame(0, myIndex, otherIndex)) + { + vertices[myIndex].foldover = true; + vertices[otherIndex].foldover = true; + } + else + { + vertices[myIndex].seam = true; + vertices[otherIndex].seam = true; + } + + int otherTriangleCount = vertices[otherIndex].tcount; + int otherTriangleStart = vertices[otherIndex].tstart; + for (int k = 0; k < otherTriangleCount; k++) + { + var r = refs[otherTriangleStart + k]; + triangles[r.tid][r.tvertex] = myIndex; + } + } + } + } + + // Update the references again + UpdateReferences(); + } + + // Init Quadrics by Plane & Edge Errors + // + // required at the beginning ( iteration == 0 ) + // recomputing during the simplification is not required, + // but mostly improves the result for closed meshes + for (int i = 0; i < vertexCount; i++) + { + vertices[i].q = new SymmetricMatrix(); + } + + int v0, v1, v2; + Vector3d n, p0, p1, p2, p10, p20, dummy; + int dummy2; + SymmetricMatrix sm; + for (int i = 0; i < triangleCount; i++) + { + v0 = triangles[i].v0; + v1 = triangles[i].v1; + v2 = triangles[i].v2; + + p0 = vertices[v0].p; + p1 = vertices[v1].p; + p2 = vertices[v2].p; + p10 = p1 - p0; + p20 = p2 - p0; + Vector3d.Cross(ref p10, ref p20, out n); + n.Normalize(); + triangles[i].n = n; + + sm = new SymmetricMatrix(n.x, n.y, n.z, -Vector3d.Dot(ref n, ref p0)); + vertices[v0].q += sm; + vertices[v1].q += sm; + vertices[v2].q += sm; + } + + for (int i = 0; i < triangleCount; i++) + { + // Calc Edge Error + var triangle = triangles[i]; + triangles[i].err0 = CalculateError(ref vertices[triangle.v0], ref vertices[triangle.v1], out dummy, out dummy2); + triangles[i].err1 = CalculateError(ref vertices[triangle.v1], ref vertices[triangle.v2], out dummy, out dummy2); + triangles[i].err2 = CalculateError(ref vertices[triangle.v2], ref vertices[triangle.v0], out dummy, out dummy2); + triangles[i].err3 = MathHelper.Min(triangles[i].err0, triangles[i].err1, triangles[i].err2); + } + } + } + #endregion + + #region Update References + private void UpdateReferences() + { + int triangleCount = this.triangles.Length; + int vertexCount = this.vertices.Length; + var triangles = this.triangles.Data; + var vertices = this.vertices.Data; + + // Init Reference ID list + for (int i = 0; i < vertexCount; i++) + { + vertices[i].tstart = 0; + vertices[i].tcount = 0; + } + + for (int i = 0; i < triangleCount; i++) + { + ++vertices[triangles[i].v0].tcount; + ++vertices[triangles[i].v1].tcount; + ++vertices[triangles[i].v2].tcount; + } + + int tstart = 0; + remainingVertices = 0; + for (int i = 0; i < vertexCount; i++) + { + vertices[i].tstart = tstart; + if (vertices[i].tcount > 0) + { + tstart += vertices[i].tcount; + vertices[i].tcount = 0; + ++remainingVertices; + } + } + + // Write References + this.refs.Resize(tstart); + var refs = this.refs.Data; + for (int i = 0; i < triangleCount; i++) + { + int v0 = triangles[i].v0; + int v1 = triangles[i].v1; + int v2 = triangles[i].v2; + int start0 = vertices[v0].tstart; + int count0 = vertices[v0].tcount; + int start1 = vertices[v1].tstart; + int count1 = vertices[v1].tcount; + int start2 = vertices[v2].tstart; + int count2 = vertices[v2].tcount; + + refs[start0 + count0].Set(i, 0); + refs[start1 + count1].Set(i, 1); + refs[start2 + count2].Set(i, 2); + + ++vertices[v0].tcount; + ++vertices[v1].tcount; + ++vertices[v2].tcount; + } + } + #endregion + + #region Compact Mesh + /// + /// Finally compact mesh before exiting. + /// + private void CompactMesh() + { + int dst = 0; + var vertices = this.vertices.Data; + int vertexCount = this.vertices.Length; + for (int i = 0; i < vertexCount; i++) + { + vertices[i].tcount = 0; + } + + var vertNormals = (this.vertNormals != null ? this.vertNormals.Data : null); + var vertTangents = (this.vertTangents != null ? this.vertTangents.Data : null); + var vertUV2D = (this.vertUV2D != null ? this.vertUV2D.Data : null); + var vertUV3D = (this.vertUV3D != null ? this.vertUV3D.Data : null); + var vertUV4D = (this.vertUV4D != null ? this.vertUV4D.Data : null); + var vertColors = (this.vertColors != null ? this.vertColors.Data : null); + var vertBoneWeights = (this.vertBoneWeights != null ? this.vertBoneWeights.Data : null); + + var triangles = this.triangles.Data; + int triangleCount = this.triangles.Length; + for (int i = 0; i < triangleCount; i++) + { + var triangle = triangles[i]; + if (!triangle.deleted) + { + if (triangle.va0 != triangle.v0) + { + int iDest = triangle.va0; + int iSrc = triangle.v0; + vertices[iDest].p = vertices[iSrc].p; + if (vertBoneWeights != null) + { + vertBoneWeights[iDest] = vertBoneWeights[iSrc]; + } + triangle.v0 = triangle.va0; + } + if (triangle.va1 != triangle.v1) + { + int iDest = triangle.va1; + int iSrc = triangle.v1; + vertices[iDest].p = vertices[iSrc].p; + if (vertBoneWeights != null) + { + vertBoneWeights[iDest] = vertBoneWeights[iSrc]; + } + triangle.v1 = triangle.va1; + } + if (triangle.va2 != triangle.v2) + { + int iDest = triangle.va2; + int iSrc = triangle.v2; + vertices[iDest].p = vertices[iSrc].p; + if (vertBoneWeights != null) + { + vertBoneWeights[iDest] = vertBoneWeights[iSrc]; + } + triangle.v2 = triangle.va2; + } + + triangles[dst++] = triangle; + + vertices[triangle.v0].tcount = 1; + vertices[triangle.v1].tcount = 1; + vertices[triangle.v2].tcount = 1; + } + } + + triangleCount = dst; + this.triangles.Resize(triangleCount); + triangles = this.triangles.Data; + + dst = 0; + for (int i = 0; i < vertexCount; i++) + { + var vert = vertices[i]; + if (vert.tcount > 0) + { + vert.tstart = dst; + vertices[i] = vert; + + if (dst != i) + { + vertices[dst].p = vert.p; + if (vertNormals != null) vertNormals[dst] = vertNormals[i]; + if (vertTangents != null) vertTangents[dst] = vertTangents[i]; + if (vertUV2D != null) + { + for (int j = 0; j < Mesh.UVChannelCount; j++) + { + var vertUV = vertUV2D[j]; + if (vertUV != null) + { + vertUV[dst] = vertUV[i]; + } + } + } + if (vertUV3D != null) + { + for (int j = 0; j < Mesh.UVChannelCount; j++) + { + var vertUV = vertUV3D[j]; + if (vertUV != null) + { + vertUV[dst] = vertUV[i]; + } + } + } + if (vertUV4D != null) + { + for (int j = 0; j < Mesh.UVChannelCount; j++) + { + var vertUV = vertUV4D[j]; + if (vertUV != null) + { + vertUV[dst] = vertUV[i]; + } + } + } + if (vertColors != null) vertColors[dst] = vertColors[i]; + if (vertBoneWeights != null) vertBoneWeights[dst] = vertBoneWeights[i]; + } + ++dst; + } + } + + for (int i = 0; i < triangleCount; i++) + { + var triangle = triangles[i]; + triangle.v0 = vertices[triangle.v0].tstart; + triangle.v1 = vertices[triangle.v1].tstart; + triangle.v2 = vertices[triangle.v2].tstart; + triangles[i] = triangle; + } + + vertexCount = dst; + this.vertices.Resize(vertexCount); + if (vertNormals != null) this.vertNormals.Resize(vertexCount, true); + if (vertTangents != null) this.vertTangents.Resize(vertexCount, true); + if (vertUV2D != null) this.vertUV2D.Resize(vertexCount, true); + if (vertUV3D != null) this.vertUV3D.Resize(vertexCount, true); + if (vertUV4D != null) this.vertUV4D.Resize(vertexCount, true); + if (vertColors != null) this.vertColors.Resize(vertexCount, true); + if (vertBoneWeights != null) this.vertBoneWeights.Resize(vertexCount, true); + } + #endregion + #endregion + + #region Public Methods + #region Initialize + /// + /// Initializes the algorithm with the original mesh. + /// + /// The mesh. + public override void Initialize(Mesh mesh) + { + if (mesh == null) + throw new ArgumentNullException("mesh"); + + int meshSubMeshCount = mesh.SubMeshCount; + int meshTriangleCount = mesh.TriangleCount; + var meshVertices = mesh.Vertices; + var meshNormals = mesh.Normals; + var meshTangents = mesh.Tangents; + var meshColors = mesh.Colors; + var meshBoneWeights = mesh.BoneWeights; + subMeshCount = meshSubMeshCount; + + vertices.Resize(meshVertices.Length); + var vertArr = vertices.Data; + for (int i = 0; i < meshVertices.Length; i++) + { + vertArr[i] = new Vertex(meshVertices[i]); + } + + triangles.Resize(meshTriangleCount); + var trisArr = triangles.Data; + int triangleIndex = 0; + for (int subMeshIndex = 0; subMeshIndex < meshSubMeshCount; subMeshIndex++) + { + int[] subMeshIndices = mesh.GetIndices(subMeshIndex); + int subMeshTriangleCount = subMeshIndices.Length / 3; + for (int i = 0; i < subMeshTriangleCount; i++) + { + int offset = i * 3; + int v0 = subMeshIndices[offset]; + int v1 = subMeshIndices[offset + 1]; + int v2 = subMeshIndices[offset + 2]; + trisArr[triangleIndex++] = new Triangle(v0, v1, v2, subMeshIndex); + } + } + + vertNormals = InitializeVertexAttribute(meshNormals, "normals"); + vertTangents = InitializeVertexAttribute(meshTangents, "tangents"); + vertColors = InitializeVertexAttribute(meshColors, "colors"); + vertBoneWeights = InitializeVertexAttribute(meshBoneWeights, "boneWeights"); + + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + int uvDim = mesh.GetUVDimension(i); + string uvAttributeName = string.Format("uv{0}", i); + if (uvDim == 2) + { + if (vertUV2D == null) + vertUV2D = new UVChannels(); + + var uvs = mesh.GetUVs2D(i); + vertUV2D[i] = InitializeVertexAttribute(uvs, uvAttributeName); + } + else if (uvDim == 3) + { + if (vertUV3D == null) + vertUV3D = new UVChannels(); + + var uvs = mesh.GetUVs3D(i); + vertUV3D[i] = InitializeVertexAttribute(uvs, uvAttributeName); + } + else if (uvDim == 4) + { + if (vertUV4D == null) + vertUV4D = new UVChannels(); + + var uvs = mesh.GetUVs4D(i); + vertUV4D[i] = InitializeVertexAttribute(uvs, uvAttributeName); + } + } + } + #endregion + + #region Decimate Mesh + /// + /// Decimates the mesh. + /// + /// The target triangle count. + public override void DecimateMesh(int targetTrisCount) + { + if (targetTrisCount < 0) + throw new ArgumentOutOfRangeException("targetTrisCount"); + + int deletedTris = 0; + ResizableArray deleted0 = new ResizableArray(20); + ResizableArray deleted1 = new ResizableArray(20); + var triangles = this.triangles.Data; + int triangleCount = this.triangles.Length; + int startTrisCount = triangleCount; + var vertices = this.vertices.Data; + + int maxVertexCount = base.MaxVertexCount; + if (maxVertexCount <= 0) + maxVertexCount = int.MaxValue; + + for (int iteration = 0; iteration < maxIterationCount; iteration++) + { + ReportStatus(iteration, startTrisCount, (startTrisCount - deletedTris), targetTrisCount); + if ((startTrisCount - deletedTris) <= targetTrisCount && remainingVertices < maxVertexCount) + break; + + // Update mesh once in a while + if ((iteration % 5) == 0) + { + UpdateMesh(iteration); + triangles = this.triangles.Data; + triangleCount = this.triangles.Length; + vertices = this.vertices.Data; + } + + // Clear dirty flag + for (int i = 0; i < triangleCount; i++) + { + triangles[i].dirty = false; + } + + // All triangles with edges below the threshold will be removed + // + // The following numbers works well for most models. + // If it does not, try to adjust the 3 parameters + double threshold = 0.000000001 * System.Math.Pow(iteration + 3, agressiveness); + + if (Verbose && (iteration % 5) == 0) + { + Logger?.LogTrace( + "Iteration {Iteration} - triangles {Triangles} threshold {Threshold}", + iteration, + (startTrisCount - deletedTris), + threshold); + } + + // Remove vertices & mark deleted triangles + RemoveVertexPass(startTrisCount, targetTrisCount, threshold, deleted0, deleted1, ref deletedTris); + } + + CompactMesh(); + } + #endregion + + #region Decimate Mesh Lossless + /// + /// Decimates the mesh without losing any quality. + /// + public override void DecimateMeshLossless() + { + int deletedTris = 0; + ResizableArray deleted0 = new ResizableArray(0); + ResizableArray deleted1 = new ResizableArray(0); + var triangles = this.triangles.Data; + int triangleCount = this.triangles.Length; + int startTrisCount = triangleCount; + var vertices = this.vertices.Data; + + ReportStatus(0, startTrisCount, startTrisCount, -1); + for (int iteration = 0; iteration < 9999; iteration++) + { + // Update mesh constantly + UpdateMesh(iteration); + triangles = this.triangles.Data; + triangleCount = this.triangles.Length; + vertices = this.vertices.Data; + + ReportStatus(iteration, startTrisCount, triangleCount, -1); + + // Clear dirty flag + for (int i = 0; i < triangleCount; i++) + { + triangles[i].dirty = false; + } + + // All triangles with edges below the threshold will be removed + // + // The following numbers works well for most models. + // If it does not, try to adjust the 3 parameters + double threshold = DoubleEpsilon; + + if (Verbose) + { + Logger?.LogTrace("Lossless iteration {Iteration}", iteration); + } + + // Remove vertices & mark deleted triangles + RemoveVertexPass(startTrisCount, 0, threshold, deleted0, deleted1, ref deletedTris); + + if (deletedTris <= 0) + break; + + deletedTris = 0; + } + + CompactMesh(); + } + #endregion + + #region To Mesh + /// + /// Returns the resulting mesh. + /// + /// The resulting mesh. + public override Mesh ToMesh() + { + int vertexCount = this.vertices.Length; + int triangleCount = this.triangles.Length; + var vertices = new Vector3d[vertexCount]; + var indices = new int[subMeshCount][]; + + var vertArr = this.vertices.Data; + for (int i = 0; i < vertexCount; i++) + { + vertices[i] = vertArr[i].p; + } + + // First get the sub-mesh offsets + var triArr = this.triangles.Data; + int[] subMeshOffsets = new int[subMeshCount]; + int lastSubMeshOffset = -1; + for (int i = 0; i < triangleCount; i++) + { + var triangle = triArr[i]; + if (triangle.subMeshIndex != lastSubMeshOffset) + { + for (int j = lastSubMeshOffset + 1; j < triangle.subMeshIndex; j++) + { + subMeshOffsets[j] = i; + } + subMeshOffsets[triangle.subMeshIndex] = i; + lastSubMeshOffset = triangle.subMeshIndex; + } + } + for (int i = lastSubMeshOffset + 1; i < subMeshCount; i++) + { + subMeshOffsets[i] = triangleCount; + } + + // Then setup the sub-meshes + for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) + { + int startOffset = subMeshOffsets[subMeshIndex]; + if (startOffset < triangleCount) + { + int endOffset = ((subMeshIndex + 1) < subMeshCount ? subMeshOffsets[subMeshIndex + 1] : triangleCount); + int subMeshTriangleCount = endOffset - startOffset; + if (subMeshTriangleCount < 0) subMeshTriangleCount = 0; + int[] subMeshIndices = new int[subMeshTriangleCount * 3]; + + for (int triangleIndex = startOffset; triangleIndex < endOffset; triangleIndex++) + { + var triangle = triArr[triangleIndex]; + int offset = (triangleIndex - startOffset) * 3; + subMeshIndices[offset] = triangle.v0; + subMeshIndices[offset + 1] = triangle.v1; + subMeshIndices[offset + 2] = triangle.v2; + } + + indices[subMeshIndex] = subMeshIndices; + } + else + { + // This mesh doesn't have any triangles left + indices[subMeshIndex] = new int[0]; + } + } + + Mesh newMesh = new Mesh(vertices, indices); + + if (vertNormals != null) + { + newMesh.Normals = vertNormals.Data; + } + if (vertTangents != null) + { + newMesh.Tangents = vertTangents.Data; + } + if (vertColors != null) + { + newMesh.Colors = vertColors.Data; + } + if (vertBoneWeights != null) + { + newMesh.BoneWeights = vertBoneWeights.Data; + } + + if (vertUV2D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + if (vertUV2D[i] != null) + { + var uvSet = vertUV2D[i].Data; + newMesh.SetUVs(i, uvSet); + } + } + } + + if (vertUV3D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + if (vertUV3D[i] != null) + { + var uvSet = vertUV3D[i].Data; + newMesh.SetUVs(i, uvSet); + } + } + } + + if (vertUV4D != null) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + if (vertUV4D[i] != null) + { + var uvSet = vertUV4D[i].Data; + newMesh.SetUVs(i, uvSet); + } + } + } + + return newMesh; + } + #endregion + #endregion + } +} diff --git a/LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs b/LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs new file mode 100644 index 0000000..6501468 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs @@ -0,0 +1,249 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using MeshDecimator.Math; + +namespace MeshDecimator +{ + /// + /// A bone weight. + /// + public struct BoneWeight : IEquatable + { + #region Fields + /// + /// The first bone index. + /// + public int boneIndex0; + /// + /// The second bone index. + /// + public int boneIndex1; + /// + /// The third bone index. + /// + public int boneIndex2; + /// + /// The fourth bone index. + /// + public int boneIndex3; + + /// + /// The first bone weight. + /// + public float boneWeight0; + /// + /// The second bone weight. + /// + public float boneWeight1; + /// + /// The third bone weight. + /// + public float boneWeight2; + /// + /// The fourth bone weight. + /// + public float boneWeight3; + #endregion + + #region Constructor + /// + /// Creates a new bone weight. + /// + /// The first bone index. + /// The second bone index. + /// The third bone index. + /// The fourth bone index. + /// The first bone weight. + /// The second bone weight. + /// The third bone weight. + /// The fourth bone weight. + public BoneWeight(int boneIndex0, int boneIndex1, int boneIndex2, int boneIndex3, float boneWeight0, float boneWeight1, float boneWeight2, float boneWeight3) + { + this.boneIndex0 = boneIndex0; + this.boneIndex1 = boneIndex1; + this.boneIndex2 = boneIndex2; + this.boneIndex3 = boneIndex3; + + this.boneWeight0 = boneWeight0; + this.boneWeight1 = boneWeight1; + this.boneWeight2 = boneWeight2; + this.boneWeight3 = boneWeight3; + } + #endregion + + #region Operators + /// + /// Returns if two bone weights equals eachother. + /// + /// The left hand side bone weight. + /// The right hand side bone weight. + /// If equals. + public static bool operator ==(BoneWeight lhs, BoneWeight rhs) + { + return (lhs.boneIndex0 == rhs.boneIndex0 && lhs.boneIndex1 == rhs.boneIndex1 && lhs.boneIndex2 == rhs.boneIndex2 && lhs.boneIndex3 == rhs.boneIndex3 && + new Vector4(lhs.boneWeight0, lhs.boneWeight1, lhs.boneWeight2, lhs.boneWeight3) == new Vector4(rhs.boneWeight0, rhs.boneWeight1, rhs.boneWeight2, rhs.boneWeight3)); + } + + /// + /// Returns if two bone weights don't equal eachother. + /// + /// The left hand side bone weight. + /// The right hand side bone weight. + /// If not equals. + public static bool operator !=(BoneWeight lhs, BoneWeight rhs) + { + return !(lhs == rhs); + } + #endregion + + #region Private Methods + private void MergeBoneWeight(int boneIndex, float weight) + { + if (boneIndex == boneIndex0) + { + boneWeight0 = (boneWeight0 + weight) * 0.5f; + } + else if (boneIndex == boneIndex1) + { + boneWeight1 = (boneWeight1 + weight) * 0.5f; + } + else if (boneIndex == boneIndex2) + { + boneWeight2 = (boneWeight2 + weight) * 0.5f; + } + else if (boneIndex == boneIndex3) + { + boneWeight3 = (boneWeight3 + weight) * 0.5f; + } + else if(boneWeight0 == 0f) + { + boneIndex0 = boneIndex; + boneWeight0 = weight; + } + else if (boneWeight1 == 0f) + { + boneIndex1 = boneIndex; + boneWeight1 = weight; + } + else if (boneWeight2 == 0f) + { + boneIndex2 = boneIndex; + boneWeight2 = weight; + } + else if (boneWeight3 == 0f) + { + boneIndex3 = boneIndex; + boneWeight3 = weight; + } + Normalize(); + } + + private void Normalize() + { + float mag = (float)System.Math.Sqrt(boneWeight0 * boneWeight0 + boneWeight1 * boneWeight1 + boneWeight2 * boneWeight2 + boneWeight3 * boneWeight3); + if (mag > float.Epsilon) + { + boneWeight0 /= mag; + boneWeight1 /= mag; + boneWeight2 /= mag; + boneWeight3 /= mag; + } + else + { + boneWeight0 = boneWeight1 = boneWeight2 = boneWeight3 = 0f; + } + } + #endregion + + #region Public Methods + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return boneIndex0.GetHashCode() ^ boneIndex1.GetHashCode() << 2 ^ boneIndex2.GetHashCode() >> 2 ^ boneIndex3.GetHashCode() >> + 1 ^ boneWeight0.GetHashCode() << 5 ^ boneWeight1.GetHashCode() << 4 ^ boneWeight2.GetHashCode() >> 4 ^ boneWeight3.GetHashCode() >> 3; + } + + /// + /// Returns if this bone weight is equal to another object. + /// + /// The other object to compare to. + /// If equals. + public override bool Equals(object obj) + { + if (!(obj is BoneWeight)) + { + return false; + } + BoneWeight other = (BoneWeight)obj; + return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 && + boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3); + } + + /// + /// Returns if this bone weight is equal to another one. + /// + /// The other bone weight to compare to. + /// If equals. + public bool Equals(BoneWeight other) + { + return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 && + boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3); + } + + /// + /// Returns a nicely formatted string for this bone weight. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}:{4:F1}, {1}:{5:F1}, {2}:{6:F1}, {3}:{7:F1})", + boneIndex0, boneIndex1, boneIndex2, boneIndex3, boneWeight0, boneWeight1, boneWeight2, boneWeight3); + } + #endregion + + #region Static + /// + /// Merges two bone weights and stores the merged result in the first parameter. + /// + /// The first bone weight, also stores result. + /// The second bone weight. + public static void Merge(ref BoneWeight a, ref BoneWeight b) + { + if (b.boneWeight0 > 0f) a.MergeBoneWeight(b.boneIndex0, b.boneWeight0); + if (b.boneWeight1 > 0f) a.MergeBoneWeight(b.boneIndex1, b.boneWeight1); + if (b.boneWeight2 > 0f) a.MergeBoneWeight(b.boneIndex2, b.boneWeight2); + if (b.boneWeight3 > 0f) a.MergeBoneWeight(b.boneIndex3, b.boneWeight3); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs b/LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs new file mode 100644 index 0000000..2c69814 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs @@ -0,0 +1,179 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; + +namespace MeshDecimator.Collections +{ + /// + /// A resizable array. + /// + /// The item type. + internal sealed class ResizableArray + { + #region Fields + private T[] items = null; + private int length = 0; + + private static T[] emptyArr = new T[0]; + #endregion + + #region Properties + /// + /// Gets the length of this array. + /// + public int Length + { + get { return length; } + } + + /// + /// Gets the internal data buffer for this array. + /// + public T[] Data + { + get { return items; } + } + + /// + /// Gets or sets the element value at a specific index. + /// + /// The element index. + /// The element value. + public T this[int index] + { + get { return items[index]; } + set { items[index] = value; } + } + #endregion + + #region Constructor + /// + /// Creates a new resizable array. + /// + /// The initial array capacity. + public ResizableArray(int capacity) + : this(capacity, 0) + { + + } + + /// + /// Creates a new resizable array. + /// + /// The initial array capacity. + /// The initial length of the array. + public ResizableArray(int capacity, int length) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException("capacity"); + else if (length < 0 || length > capacity) + throw new ArgumentOutOfRangeException("length"); + + if (capacity > 0) + items = new T[capacity]; + else + items = emptyArr; + + this.length = length; + } + #endregion + + #region Private Methods + private void IncreaseCapacity(int capacity) + { + T[] newItems = new T[capacity]; + Array.Copy(items, 0, newItems, 0, System.Math.Min(length, capacity)); + items = newItems; + } + #endregion + + #region Public Methods + /// + /// Clears this array. + /// + public void Clear() + { + Array.Clear(items, 0, length); + length = 0; + } + + /// + /// Resizes this array. + /// + /// The new length. + /// If exess memory should be trimmed. + public void Resize(int length, bool trimExess = false) + { + if (length < 0) + throw new ArgumentOutOfRangeException("capacity"); + + if (length > items.Length) + { + IncreaseCapacity(length); + } + else if (length < this.length) + { + //Array.Clear(items, capacity, length - capacity); + } + + this.length = length; + + if (trimExess) + { + TrimExcess(); + } + } + + /// + /// Trims any excess memory for this array. + /// + public void TrimExcess() + { + if (items.Length == length) // Nothing to do + return; + + T[] newItems = new T[length]; + Array.Copy(items, 0, newItems, 0, length); + items = newItems; + } + + /// + /// Adds a new item to the end of this array. + /// + /// The new item. + public void Add(T item) + { + if (length >= items.Length) + { + IncreaseCapacity(items.Length << 1); + } + + items[length++] = item; + } + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs b/LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs new file mode 100644 index 0000000..073728a --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs @@ -0,0 +1,79 @@ +using System; + +namespace MeshDecimator.Collections +{ + /// + /// A collection of UV channels. + /// + /// The UV vector type. + internal sealed class UVChannels + { + #region Fields + private ResizableArray[] channels = null; + private TVec[][] channelsData = null; + #endregion + + #region Properties + /// + /// Gets the channel collection data. + /// + public TVec[][] Data + { + get + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + if (channels[i] != null) + { + channelsData[i] = channels[i].Data; + } + else + { + channelsData[i] = null; + } + } + return channelsData; + } + } + + /// + /// Gets or sets a specific channel by index. + /// + /// The channel index. + public ResizableArray this[int index] + { + get { return channels[index]; } + set { channels[index] = value; } + } + #endregion + + #region Constructor + /// + /// Creates a new collection of UV channels. + /// + public UVChannels() + { + channels = new ResizableArray[Mesh.UVChannelCount]; + channelsData = new TVec[Mesh.UVChannelCount][]; + } + #endregion + + #region Public Methods + /// + /// Resizes all channels at once. + /// + /// The new capacity. + /// If exess memory should be trimmed. + public void Resize(int capacity, bool trimExess = false) + { + for (int i = 0; i < Mesh.UVChannelCount; i++) + { + if (channels[i] != null) + { + channels[i].Resize(capacity, trimExess); + } + } + } + #endregion + } +} diff --git a/LightlessSync/ThirdParty/MeshDecimator/LICENSE.md b/LightlessSync/ThirdParty/MeshDecimator/LICENSE.md new file mode 100644 index 0000000..1f1f192 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs new file mode 100644 index 0000000..b530d3d --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs @@ -0,0 +1,286 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; + +namespace MeshDecimator.Math +{ + /// + /// Math helpers. + /// + public static class MathHelper + { + #region Consts + /// + /// The Pi constant. + /// + public const float PI = 3.14159274f; + + /// + /// The Pi constant. + /// + public const double PId = 3.1415926535897932384626433832795; + + /// + /// Degrees to radian constant. + /// + public const float Deg2Rad = PI / 180f; + + /// + /// Degrees to radian constant. + /// + public const double Deg2Radd = PId / 180.0; + + /// + /// Radians to degrees constant. + /// + public const float Rad2Deg = 180f / PI; + + /// + /// Radians to degrees constant. + /// + public const double Rad2Degd = 180.0 / PId; + #endregion + + #region Min + /// + /// Returns the minimum of two values. + /// + /// The first value. + /// The second value. + /// The minimum value. + public static int Min(int val1, int val2) + { + return (val1 < val2 ? val1 : val2); + } + + /// + /// Returns the minimum of three values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The minimum value. + public static int Min(int val1, int val2, int val3) + { + return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); + } + + /// + /// Returns the minimum of two values. + /// + /// The first value. + /// The second value. + /// The minimum value. + public static float Min(float val1, float val2) + { + return (val1 < val2 ? val1 : val2); + } + + /// + /// Returns the minimum of three values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The minimum value. + public static float Min(float val1, float val2, float val3) + { + return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); + } + + /// + /// Returns the minimum of two values. + /// + /// The first value. + /// The second value. + /// The minimum value. + public static double Min(double val1, double val2) + { + return (val1 < val2 ? val1 : val2); + } + + /// + /// Returns the minimum of three values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The minimum value. + public static double Min(double val1, double val2, double val3) + { + return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); + } + #endregion + + #region Max + /// + /// Returns the maximum of two values. + /// + /// The first value. + /// The second value. + /// The maximum value. + public static int Max(int val1, int val2) + { + return (val1 > val2 ? val1 : val2); + } + + /// + /// Returns the maximum of three values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The maximum value. + public static int Max(int val1, int val2, int val3) + { + return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3)); + } + + /// + /// Returns the maximum of two values. + /// + /// The first value. + /// The second value. + /// The maximum value. + public static float Max(float val1, float val2) + { + return (val1 > val2 ? val1 : val2); + } + + /// + /// Returns the maximum of three values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The maximum value. + public static float Max(float val1, float val2, float val3) + { + return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3)); + } + + /// + /// Returns the maximum of two values. + /// + /// The first value. + /// The second value. + /// The maximum value. + public static double Max(double val1, double val2) + { + return (val1 > val2 ? val1 : val2); + } + + /// + /// Returns the maximum of three values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The maximum value. + public static double Max(double val1, double val2, double val3) + { + return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3)); + } + #endregion + + #region Clamping + /// + /// Clamps a value between a minimum and a maximum value. + /// + /// The value to clamp. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static float Clamp(float value, float min, float max) + { + return (value >= min ? (value <= max ? value : max) : min); + } + + /// + /// Clamps a value between a minimum and a maximum value. + /// + /// The value to clamp. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static double Clamp(double value, double min, double max) + { + return (value >= min ? (value <= max ? value : max) : min); + } + + /// + /// Clamps the value between 0 and 1. + /// + /// The value to clamp. + /// The clamped value. + public static float Clamp01(float value) + { + return (value > 0f ? (value < 1f ? value : 1f) : 0f); + } + + /// + /// Clamps the value between 0 and 1. + /// + /// The value to clamp. + /// The clamped value. + public static double Clamp01(double value) + { + return (value > 0.0 ? (value < 1.0 ? value : 1.0) : 0.0); + } + #endregion + + #region Triangle Area + /// + /// Calculates the area of a triangle. + /// + /// The first point. + /// The second point. + /// The third point. + /// The triangle area. + public static float TriangleArea(ref Vector3 p0, ref Vector3 p1, ref Vector3 p2) + { + var dx = p1 - p0; + var dy = p2 - p0; + return dx.Magnitude * ((float)System.Math.Sin(Vector3.Angle(ref dx, ref dy) * Deg2Rad) * dy.Magnitude) * 0.5f; + } + + /// + /// Calculates the area of a triangle. + /// + /// The first point. + /// The second point. + /// The third point. + /// The triangle area. + public static double TriangleArea(ref Vector3d p0, ref Vector3d p1, ref Vector3d p2) + { + var dx = p1 - p0; + var dy = p2 - p0; + return dx.Magnitude * (System.Math.Sin(Vector3d.Angle(ref dx, ref dy) * Deg2Radd) * dy.Magnitude) * 0.5f; + } + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs new file mode 100644 index 0000000..3daa4e7 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs @@ -0,0 +1,303 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; + +namespace MeshDecimator.Math +{ + /// + /// A symmetric matrix. + /// + public struct SymmetricMatrix + { + #region Fields + /// + /// The m11 component. + /// + public double m0; + /// + /// The m12 component. + /// + public double m1; + /// + /// The m13 component. + /// + public double m2; + /// + /// The m14 component. + /// + public double m3; + /// + /// The m22 component. + /// + public double m4; + /// + /// The m23 component. + /// + public double m5; + /// + /// The m24 component. + /// + public double m6; + /// + /// The m33 component. + /// + public double m7; + /// + /// The m34 component. + /// + public double m8; + /// + /// The m44 component. + /// + public double m9; + #endregion + + #region Properties + /// + /// Gets the component value with a specific index. + /// + /// The component index. + /// The value. + public double this[int index] + { + get + { + switch (index) + { + case 0: + return m0; + case 1: + return m1; + case 2: + return m2; + case 3: + return m3; + case 4: + return m4; + case 5: + return m5; + case 6: + return m6; + case 7: + return m7; + case 8: + return m8; + case 9: + return m9; + default: + throw new IndexOutOfRangeException(); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a symmetric matrix with a value in each component. + /// + /// The component value. + public SymmetricMatrix(double c) + { + this.m0 = c; + this.m1 = c; + this.m2 = c; + this.m3 = c; + this.m4 = c; + this.m5 = c; + this.m6 = c; + this.m7 = c; + this.m8 = c; + this.m9 = c; + } + + /// + /// Creates a symmetric matrix. + /// + /// The m11 component. + /// The m12 component. + /// The m13 component. + /// The m14 component. + /// The m22 component. + /// The m23 component. + /// The m24 component. + /// The m33 component. + /// The m34 component. + /// The m44 component. + public SymmetricMatrix(double m0, double m1, double m2, double m3, + double m4, double m5, double m6, double m7, double m8, double m9) + { + this.m0 = m0; + this.m1 = m1; + this.m2 = m2; + this.m3 = m3; + this.m4 = m4; + this.m5 = m5; + this.m6 = m6; + this.m7 = m7; + this.m8 = m8; + this.m9 = m9; + } + + /// + /// Creates a symmetric matrix from a plane. + /// + /// The plane x-component. + /// The plane y-component + /// The plane z-component + /// The plane w-component + public SymmetricMatrix(double a, double b, double c, double d) + { + this.m0 = a * a; + this.m1 = a * b; + this.m2 = a * c; + this.m3 = a * d; + + this.m4 = b * b; + this.m5 = b * c; + this.m6 = b * d; + + this.m7 = c * c; + this.m8 = c * d; + + this.m9 = d * d; + } + #endregion + + #region Operators + /// + /// Adds two matrixes together. + /// + /// The left hand side. + /// The right hand side. + /// The resulting matrix. + public static SymmetricMatrix operator +(SymmetricMatrix a, SymmetricMatrix b) + { + return new SymmetricMatrix( + a.m0 + b.m0, a.m1 + b.m1, a.m2 + b.m2, a.m3 + b.m3, + a.m4 + b.m4, a.m5 + b.m5, a.m6 + b.m6, + a.m7 + b.m7, a.m8 + b.m8, + a.m9 + b.m9 + ); + } + #endregion + + #region Internal Methods + /// + /// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7) + /// + /// + internal double Determinant1() + { + double det = + m0 * m4 * m7 + + m2 * m1 * m5 + + m1 * m5 * m2 - + m2 * m4 * m2 - + m0 * m5 * m5 - + m1 * m1 * m7; + return det; + } + + /// + /// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8) + /// + /// + internal double Determinant2() + { + double det = + m1 * m5 * m8 + + m3 * m4 * m7 + + m2 * m6 * m5 - + m3 * m5 * m5 - + m1 * m6 * m7 - + m2 * m4 * m8; + return det; + } + + /// + /// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8) + /// + /// + internal double Determinant3() + { + double det = + m0 * m5 * m8 + + m3 * m1 * m7 + + m2 * m6 * m2 - + m3 * m5 * m2 - + m0 * m6 * m7 - + m2 * m1 * m8; + return det; + } + + /// + /// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8) + /// + /// + internal double Determinant4() + { + double det = + m0 * m4 * m8 + + m3 * m1 * m5 + + m1 * m6 * m2 - + m3 * m4 * m2 - + m0 * m6 * m5 - + m1 * m1 * m8; + return det; + } + #endregion + + #region Public Methods + /// + /// Computes the determinant of this matrix. + /// + /// The a11 index. + /// The a12 index. + /// The a13 index. + /// The a21 index. + /// The a22 index. + /// The a23 index. + /// The a31 index. + /// The a32 index. + /// The a33 index. + /// The determinant value. + public double Determinant(int a11, int a12, int a13, + int a21, int a22, int a23, + int a31, int a32, int a33) + { + double det = + this[a11] * this[a22] * this[a33] + + this[a13] * this[a21] * this[a32] + + this[a12] * this[a23] * this[a31] - + this[a13] * this[a22] * this[a31] - + this[a11] * this[a23] * this[a32] - + this[a12] * this[a21] * this[a33]; + return det; + } + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs new file mode 100644 index 0000000..68f06f4 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs @@ -0,0 +1,425 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A single precision 2D vector. + /// + public struct Vector2 : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector2 zero = new Vector2(0, 0); + #endregion + + #region Consts + /// + /// The vector epsilon. + /// + public const float Epsilon = 9.99999944E-11f; + #endregion + + #region Fields + /// + /// The x component. + /// + public float x; + /// + /// The y component. + /// + public float y; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public float Magnitude + { + get { return (float)System.Math.Sqrt(x * x + y * y); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public float MagnitudeSqr + { + get { return (x * x + y * y); } + } + + /// + /// Gets a normalized vector from this vector. + /// + public Vector2 Normalized + { + get + { + Vector2 result; + Normalize(ref this, out result); + return result; + } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public float this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + default: + throw new IndexOutOfRangeException("Invalid Vector2 index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector2 index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector2(float value) + { + this.x = value; + this.y = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + public Vector2(float x, float y) + { + this.x = x; + this.y = y; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector2 operator +(Vector2 a, Vector2 b) + { + return new Vector2(a.x + b.x, a.y + b.y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector2 operator -(Vector2 a, Vector2 b) + { + return new Vector2(a.x - b.x, a.y - b.y); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector2 operator *(Vector2 a, float d) + { + return new Vector2(a.x * d, a.y * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector2 operator *(float d, Vector2 a) + { + return new Vector2(a.x * d, a.y * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector2 operator /(Vector2 a, float d) + { + return new Vector2(a.x / d, a.y / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector2 operator -(Vector2 a) + { + return new Vector2(-a.x, -a.y); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector2 lhs, Vector2 rhs) + { + return (lhs - rhs).MagnitudeSqr < Epsilon; + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector2 lhs, Vector2 rhs) + { + return (lhs - rhs).MagnitudeSqr >= Epsilon; + } + + /// + /// Explicitly converts from a double-precision vector into a single-precision vector. + /// + /// The double-precision vector. + public static explicit operator Vector2(Vector2d v) + { + return new Vector2((float)v.x, (float)v.y); + } + + /// + /// Implicitly converts from an integer vector into a single-precision vector. + /// + /// The integer vector. + public static implicit operator Vector2(Vector2i v) + { + return new Vector2(v.x, v.y); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x and y components of an existing vector. + /// + /// The x value. + /// The y value. + public void Set(float x, float y) + { + this.x = x; + this.y = y; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector2 scale) + { + x *= scale.x; + y *= scale.y; + } + + /// + /// Normalizes this vector. + /// + public void Normalize() + { + float mag = this.Magnitude; + if (mag > Epsilon) + { + x /= mag; + y /= mag; + } + else + { + x = y = 0; + } + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(float min, float max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector2)) + { + return false; + } + Vector2 vector = (Vector2)other; + return (x == vector.x && y == vector.y); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector2 other) + { + return (x == other.x && y == other.y); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1})", + x.ToString("F1", CultureInfo.InvariantCulture), + y.ToString("F1", CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The float format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Dot Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + public static float Dot(ref Vector2 lhs, ref Vector2 rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y; + } + + /// + /// Performs a linear interpolation between two vectors. + /// + /// The vector to interpolate from. + /// The vector to interpolate to. + /// The time fraction. + /// The resulting vector. + public static void Lerp(ref Vector2 a, ref Vector2 b, float t, out Vector2 result) + { + result = new Vector2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + } + + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector2 a, ref Vector2 b, out Vector2 result) + { + result = new Vector2(a.x * b.x, a.y * b.y); + } + + /// + /// Normalizes a vector. + /// + /// The vector to normalize. + /// The resulting normalized vector. + public static void Normalize(ref Vector2 value, out Vector2 result) + { + float mag = value.Magnitude; + if (mag > Epsilon) + { + result = new Vector2(value.x / mag, value.y / mag); + } + else + { + result = Vector2.zero; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs new file mode 100644 index 0000000..72f62aa --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs @@ -0,0 +1,425 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A double precision 2D vector. + /// + public struct Vector2d : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector2d zero = new Vector2d(0, 0); + #endregion + + #region Consts + /// + /// The vector epsilon. + /// + public const double Epsilon = double.Epsilon; + #endregion + + #region Fields + /// + /// The x component. + /// + public double x; + /// + /// The y component. + /// + public double y; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public double Magnitude + { + get { return System.Math.Sqrt(x * x + y * y); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public double MagnitudeSqr + { + get { return (x * x + y * y); } + } + + /// + /// Gets a normalized vector from this vector. + /// + public Vector2d Normalized + { + get + { + Vector2d result; + Normalize(ref this, out result); + return result; + } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public double this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + default: + throw new IndexOutOfRangeException("Invalid Vector2d index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector2d index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector2d(double value) + { + this.x = value; + this.y = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + public Vector2d(double x, double y) + { + this.x = x; + this.y = y; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector2d operator +(Vector2d a, Vector2d b) + { + return new Vector2d(a.x + b.x, a.y + b.y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector2d operator -(Vector2d a, Vector2d b) + { + return new Vector2d(a.x - b.x, a.y - b.y); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector2d operator *(Vector2d a, double d) + { + return new Vector2d(a.x * d, a.y * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector2d operator *(double d, Vector2d a) + { + return new Vector2d(a.x * d, a.y * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector2d operator /(Vector2d a, double d) + { + return new Vector2d(a.x / d, a.y / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector2d operator -(Vector2d a) + { + return new Vector2d(-a.x, -a.y); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector2d lhs, Vector2d rhs) + { + return (lhs - rhs).MagnitudeSqr < Epsilon; + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector2d lhs, Vector2d rhs) + { + return (lhs - rhs).MagnitudeSqr >= Epsilon; + } + + /// + /// Implicitly converts from a single-precision vector into a double-precision vector. + /// + /// The single-precision vector. + public static implicit operator Vector2d(Vector2 v) + { + return new Vector2d(v.x, v.y); + } + + /// + /// Implicitly converts from an integer vector into a double-precision vector. + /// + /// The integer vector. + public static implicit operator Vector2d(Vector2i v) + { + return new Vector2d(v.x, v.y); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x and y components of an existing vector. + /// + /// The x value. + /// The y value. + public void Set(double x, double y) + { + this.x = x; + this.y = y; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector2d scale) + { + x *= scale.x; + y *= scale.y; + } + + /// + /// Normalizes this vector. + /// + public void Normalize() + { + double mag = this.Magnitude; + if (mag > Epsilon) + { + x /= mag; + y /= mag; + } + else + { + x = y = 0; + } + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(double min, double max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector2d)) + { + return false; + } + Vector2d vector = (Vector2d)other; + return (x == vector.x && y == vector.y); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector2d other) + { + return (x == other.x && y == other.y); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1})", + x.ToString("F1", CultureInfo.InvariantCulture), + y.ToString("F1", CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The float format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Dot Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + public static double Dot(ref Vector2d lhs, ref Vector2d rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y; + } + + /// + /// Performs a linear interpolation between two vectors. + /// + /// The vector to interpolate from. + /// The vector to interpolate to. + /// The time fraction. + /// The resulting vector. + public static void Lerp(ref Vector2d a, ref Vector2d b, double t, out Vector2d result) + { + result = new Vector2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + } + + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector2d a, ref Vector2d b, out Vector2d result) + { + result = new Vector2d(a.x * b.x, a.y * b.y); + } + + /// + /// Normalizes a vector. + /// + /// The vector to normalize. + /// The resulting normalized vector. + public static void Normalize(ref Vector2d value, out Vector2d result) + { + double mag = value.Magnitude; + if (mag > Epsilon) + { + result = new Vector2d(value.x / mag, value.y / mag); + } + else + { + result = Vector2d.zero; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs new file mode 100644 index 0000000..20b808b --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs @@ -0,0 +1,348 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A 2D integer vector. + /// + public struct Vector2i : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector2i zero = new Vector2i(0, 0); + #endregion + + #region Fields + /// + /// The x component. + /// + public int x; + /// + /// The y component. + /// + public int y; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public int Magnitude + { + get { return (int)System.Math.Sqrt(x * x + y * y); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public int MagnitudeSqr + { + get { return (x * x + y * y); } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public int this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + default: + throw new IndexOutOfRangeException("Invalid Vector2i index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector2i index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector2i(int value) + { + this.x = value; + this.y = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + public Vector2i(int x, int y) + { + this.x = x; + this.y = y; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector2i operator +(Vector2i a, Vector2i b) + { + return new Vector2i(a.x + b.x, a.y + b.y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector2i operator -(Vector2i a, Vector2i b) + { + return new Vector2i(a.x - b.x, a.y - b.y); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector2i operator *(Vector2i a, int d) + { + return new Vector2i(a.x * d, a.y * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector2i operator *(int d, Vector2i a) + { + return new Vector2i(a.x * d, a.y * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector2i operator /(Vector2i a, int d) + { + return new Vector2i(a.x / d, a.y / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector2i operator -(Vector2i a) + { + return new Vector2i(-a.x, -a.y); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector2i lhs, Vector2i rhs) + { + return (lhs.x == rhs.x && lhs.y == rhs.y); + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector2i lhs, Vector2i rhs) + { + return (lhs.x != rhs.x || lhs.y != rhs.y); + } + + /// + /// Explicitly converts from a single-precision vector into an integer vector. + /// + /// The single-precision vector. + public static explicit operator Vector2i(Vector2 v) + { + return new Vector2i((int)v.x, (int)v.y); + } + + /// + /// Explicitly converts from a double-precision vector into an integer vector. + /// + /// The double-precision vector. + public static explicit operator Vector2i(Vector2d v) + { + return new Vector2i((int)v.x, (int)v.y); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x and y components of an existing vector. + /// + /// The x value. + /// The y value. + public void Set(int x, int y) + { + this.x = x; + this.y = y; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector2i scale) + { + x *= scale.x; + y *= scale.y; + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(int min, int max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector2i)) + { + return false; + } + Vector2i vector = (Vector2i)other; + return (x == vector.x && y == vector.y); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector2i other) + { + return (x == other.x && y == other.y); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1})", + x.ToString(CultureInfo.InvariantCulture), + y.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The integer format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector2i a, ref Vector2i b, out Vector2i result) + { + result = new Vector2i(a.x * b.x, a.y * b.y); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs new file mode 100644 index 0000000..4c91aa5 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs @@ -0,0 +1,494 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A single precision 3D vector. + /// + public struct Vector3 : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector3 zero = new Vector3(0, 0, 0); + #endregion + + #region Consts + /// + /// The vector epsilon. + /// + public const float Epsilon = 9.99999944E-11f; + #endregion + + #region Fields + /// + /// The x component. + /// + public float x; + /// + /// The y component. + /// + public float y; + /// + /// The z component. + /// + public float z; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public float Magnitude + { + get { return (float)System.Math.Sqrt(x * x + y * y + z * z); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public float MagnitudeSqr + { + get { return (x * x + y * y + z * z); } + } + + /// + /// Gets a normalized vector from this vector. + /// + public Vector3 Normalized + { + get + { + Vector3 result; + Normalize(ref this, out result); + return result; + } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public float this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + default: + throw new IndexOutOfRangeException("Invalid Vector3 index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector3 index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector3(float value) + { + this.x = value; + this.y = value; + this.z = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + /// The z value. + public Vector3(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Creates a new vector from a double precision vector. + /// + /// The double precision vector. + public Vector3(Vector3d vector) + { + this.x = (float)vector.x; + this.y = (float)vector.y; + this.z = (float)vector.z; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector3 operator +(Vector3 a, Vector3 b) + { + return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector3 operator -(Vector3 a, Vector3 b) + { + return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector3 operator *(Vector3 a, float d) + { + return new Vector3(a.x * d, a.y * d, a.z * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector3 operator *(float d, Vector3 a) + { + return new Vector3(a.x * d, a.y * d, a.z * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector3 operator /(Vector3 a, float d) + { + return new Vector3(a.x / d, a.y / d, a.z / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector3 operator -(Vector3 a) + { + return new Vector3(-a.x, -a.y, -a.z); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector3 lhs, Vector3 rhs) + { + return (lhs - rhs).MagnitudeSqr < Epsilon; + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector3 lhs, Vector3 rhs) + { + return (lhs - rhs).MagnitudeSqr >= Epsilon; + } + + /// + /// Explicitly converts from a double-precision vector into a single-precision vector. + /// + /// The double-precision vector. + public static explicit operator Vector3(Vector3d v) + { + return new Vector3((float)v.x, (float)v.y, (float)v.z); + } + + /// + /// Implicitly converts from an integer vector into a single-precision vector. + /// + /// The integer vector. + public static implicit operator Vector3(Vector3i v) + { + return new Vector3(v.x, v.y, v.z); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x, y and z components of an existing vector. + /// + /// The x value. + /// The y value. + /// The z value. + public void Set(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector3 scale) + { + x *= scale.x; + y *= scale.y; + z *= scale.z; + } + + /// + /// Normalizes this vector. + /// + public void Normalize() + { + float mag = this.Magnitude; + if (mag > Epsilon) + { + x /= mag; + y /= mag; + z /= mag; + } + else + { + x = y = z = 0; + } + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(float min, float max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + + if (z < min) z = min; + else if (z > max) z = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector3)) + { + return false; + } + Vector3 vector = (Vector3)other; + return (x == vector.x && y == vector.y && z == vector.z); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector3 other) + { + return (x == other.x && y == other.y && z == other.z); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1}, {2})", + x.ToString("F1", CultureInfo.InvariantCulture), + y.ToString("F1", CultureInfo.InvariantCulture), + z.ToString("F1", CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The float format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1}, {2})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture), + z.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Dot Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + public static float Dot(ref Vector3 lhs, ref Vector3 rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; + } + + /// + /// Cross Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + /// The resulting vector. + public static void Cross(ref Vector3 lhs, ref Vector3 rhs, out Vector3 result) + { + result = new Vector3(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x); + } + + /// + /// Calculates the angle between two vectors. + /// + /// The from vector. + /// The to vector. + /// The angle. + public static float Angle(ref Vector3 from, ref Vector3 to) + { + Vector3 fromNormalized = from.Normalized; + Vector3 toNormalized = to.Normalized; + return (float)System.Math.Acos(MathHelper.Clamp(Vector3.Dot(ref fromNormalized, ref toNormalized), -1f, 1f)) * MathHelper.Rad2Deg; + } + + /// + /// Performs a linear interpolation between two vectors. + /// + /// The vector to interpolate from. + /// The vector to interpolate to. + /// The time fraction. + /// The resulting vector. + public static void Lerp(ref Vector3 a, ref Vector3 b, float t, out Vector3 result) + { + result = new Vector3(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t); + } + + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector3 a, ref Vector3 b, out Vector3 result) + { + result = new Vector3(a.x * b.x, a.y * b.y, a.z * b.z); + } + + /// + /// Normalizes a vector. + /// + /// The vector to normalize. + /// The resulting normalized vector. + public static void Normalize(ref Vector3 value, out Vector3 result) + { + float mag = value.Magnitude; + if (mag > Epsilon) + { + result = new Vector3(value.x / mag, value.y / mag, value.z / mag); + } + else + { + result = Vector3.zero; + } + } + + /// + /// Normalizes both vectors and makes them orthogonal to each other. + /// + /// The normal vector. + /// The tangent. + public static void OrthoNormalize(ref Vector3 normal, ref Vector3 tangent) + { + normal.Normalize(); + Vector3 proj = normal * Vector3.Dot(ref tangent, ref normal); + tangent -= proj; + tangent.Normalize(); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs new file mode 100644 index 0000000..11ebed1 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs @@ -0,0 +1,481 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A double precision 3D vector. + /// + public struct Vector3d : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector3d zero = new Vector3d(0, 0, 0); + #endregion + + #region Consts + /// + /// The vector epsilon. + /// + public const double Epsilon = double.Epsilon; + #endregion + + #region Fields + /// + /// The x component. + /// + public double x; + /// + /// The y component. + /// + public double y; + /// + /// The z component. + /// + public double z; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public double Magnitude + { + get { return System.Math.Sqrt(x * x + y * y + z * z); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public double MagnitudeSqr + { + get { return (x * x + y * y + z * z); } + } + + /// + /// Gets a normalized vector from this vector. + /// + public Vector3d Normalized + { + get + { + Vector3d result; + Normalize(ref this, out result); + return result; + } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public double this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + default: + throw new IndexOutOfRangeException("Invalid Vector3d index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector3d index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector3d(double value) + { + this.x = value; + this.y = value; + this.z = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + /// The z value. + public Vector3d(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Creates a new vector from a single precision vector. + /// + /// The single precision vector. + public Vector3d(Vector3 vector) + { + this.x = vector.x; + this.y = vector.y; + this.z = vector.z; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector3d operator +(Vector3d a, Vector3d b) + { + return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector3d operator -(Vector3d a, Vector3d b) + { + return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector3d operator *(Vector3d a, double d) + { + return new Vector3d(a.x * d, a.y * d, a.z * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector3d operator *(double d, Vector3d a) + { + return new Vector3d(a.x * d, a.y * d, a.z * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector3d operator /(Vector3d a, double d) + { + return new Vector3d(a.x / d, a.y / d, a.z / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector3d operator -(Vector3d a) + { + return new Vector3d(-a.x, -a.y, -a.z); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector3d lhs, Vector3d rhs) + { + return (lhs - rhs).MagnitudeSqr < Epsilon; + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector3d lhs, Vector3d rhs) + { + return (lhs - rhs).MagnitudeSqr >= Epsilon; + } + + /// + /// Implicitly converts from a single-precision vector into a double-precision vector. + /// + /// The single-precision vector. + public static implicit operator Vector3d(Vector3 v) + { + return new Vector3d(v.x, v.y, v.z); + } + + /// + /// Implicitly converts from an integer vector into a double-precision vector. + /// + /// The integer vector. + public static implicit operator Vector3d(Vector3i v) + { + return new Vector3d(v.x, v.y, v.z); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x, y and z components of an existing vector. + /// + /// The x value. + /// The y value. + /// The z value. + public void Set(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector3d scale) + { + x *= scale.x; + y *= scale.y; + z *= scale.z; + } + + /// + /// Normalizes this vector. + /// + public void Normalize() + { + double mag = this.Magnitude; + if (mag > Epsilon) + { + x /= mag; + y /= mag; + z /= mag; + } + else + { + x = y = z = 0; + } + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(double min, double max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + + if (z < min) z = min; + else if (z > max) z = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector3d)) + { + return false; + } + Vector3d vector = (Vector3d)other; + return (x == vector.x && y == vector.y && z == vector.z); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector3d other) + { + return (x == other.x && y == other.y && z == other.z); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1}, {2})", + x.ToString("F1", CultureInfo.InvariantCulture), + y.ToString("F1", CultureInfo.InvariantCulture), + z.ToString("F1", CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The float format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1}, {2})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture), + z.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Dot Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + public static double Dot(ref Vector3d lhs, ref Vector3d rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; + } + + /// + /// Cross Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + /// The resulting vector. + public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result) + { + result = new Vector3d(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x); + } + + /// + /// Calculates the angle between two vectors. + /// + /// The from vector. + /// The to vector. + /// The angle. + public static double Angle(ref Vector3d from, ref Vector3d to) + { + Vector3d fromNormalized = from.Normalized; + Vector3d toNormalized = to.Normalized; + return System.Math.Acos(MathHelper.Clamp(Vector3d.Dot(ref fromNormalized, ref toNormalized), -1.0, 1.0)) * MathHelper.Rad2Degd; + } + + /// + /// Performs a linear interpolation between two vectors. + /// + /// The vector to interpolate from. + /// The vector to interpolate to. + /// The time fraction. + /// The resulting vector. + public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d result) + { + result = new Vector3d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t); + } + + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result) + { + result = new Vector3d(a.x * b.x, a.y * b.y, a.z * b.z); + } + + /// + /// Normalizes a vector. + /// + /// The vector to normalize. + /// The resulting normalized vector. + public static void Normalize(ref Vector3d value, out Vector3d result) + { + double mag = value.Magnitude; + if (mag > Epsilon) + { + result = new Vector3d(value.x / mag, value.y / mag, value.z / mag); + } + else + { + result = Vector3d.zero; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs new file mode 100644 index 0000000..d36d6d1 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs @@ -0,0 +1,368 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A 3D integer vector. + /// + public struct Vector3i : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector3i zero = new Vector3i(0, 0, 0); + #endregion + + #region Fields + /// + /// The x component. + /// + public int x; + /// + /// The y component. + /// + public int y; + /// + /// The z component. + /// + public int z; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public int Magnitude + { + get { return (int)System.Math.Sqrt(x * x + y * y + z * z); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public int MagnitudeSqr + { + get { return (x * x + y * y + z * z); } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public int this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + default: + throw new IndexOutOfRangeException("Invalid Vector3i index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector3i index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector3i(int value) + { + this.x = value; + this.y = value; + this.z = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + /// The z value. + public Vector3i(int x, int y, int z) + { + this.x = x; + this.y = y; + this.z = z; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector3i operator +(Vector3i a, Vector3i b) + { + return new Vector3i(a.x + b.x, a.y + b.y, a.z + b.z); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector3i operator -(Vector3i a, Vector3i b) + { + return new Vector3i(a.x - b.x, a.y - b.y, a.z - b.z); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector3i operator *(Vector3i a, int d) + { + return new Vector3i(a.x * d, a.y * d, a.z * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector3i operator *(int d, Vector3i a) + { + return new Vector3i(a.x * d, a.y * d, a.z * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector3i operator /(Vector3i a, int d) + { + return new Vector3i(a.x / d, a.y / d, a.z / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector3i operator -(Vector3i a) + { + return new Vector3i(-a.x, -a.y, -a.z); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector3i lhs, Vector3i rhs) + { + return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z); + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector3i lhs, Vector3i rhs) + { + return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z); + } + + /// + /// Explicitly converts from a single-precision vector into an integer vector. + /// + /// The single-precision vector. + public static implicit operator Vector3i(Vector3 v) + { + return new Vector3i((int)v.x, (int)v.y, (int)v.z); + } + + /// + /// Explicitly converts from a double-precision vector into an integer vector. + /// + /// The double-precision vector. + public static explicit operator Vector3i(Vector3d v) + { + return new Vector3i((int)v.x, (int)v.y, (int)v.z); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x, y and z components of an existing vector. + /// + /// The x value. + /// The y value. + /// The z value. + public void Set(int x, int y, int z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector3i scale) + { + x *= scale.x; + y *= scale.y; + z *= scale.z; + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(int min, int max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + + if (z < min) z = min; + else if (z > max) z = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector3i)) + { + return false; + } + Vector3i vector = (Vector3i)other; + return (x == vector.x && y == vector.y && z == vector.z); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector3i other) + { + return (x == other.x && y == other.y && z == other.z); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1}, {2})", + x.ToString(CultureInfo.InvariantCulture), + y.ToString(CultureInfo.InvariantCulture), + z.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The integer format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1}, {2})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture), + z.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector3i a, ref Vector3i b, out Vector3i result) + { + result = new Vector3i(a.x * b.x, a.y * b.y, a.z * b.z); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs new file mode 100644 index 0000000..bf1d655 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs @@ -0,0 +1,467 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A single precision 4D vector. + /// + public struct Vector4 : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector4 zero = new Vector4(0, 0, 0, 0); + #endregion + + #region Consts + /// + /// The vector epsilon. + /// + public const float Epsilon = 9.99999944E-11f; + #endregion + + #region Fields + /// + /// The x component. + /// + public float x; + /// + /// The y component. + /// + public float y; + /// + /// The z component. + /// + public float z; + /// + /// The w component. + /// + public float w; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public float Magnitude + { + get { return (float)System.Math.Sqrt(x * x + y * y + z * z + w * w); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public float MagnitudeSqr + { + get { return (x * x + y * y + z * z + w * w); } + } + + /// + /// Gets a normalized vector from this vector. + /// + public Vector4 Normalized + { + get + { + Vector4 result; + Normalize(ref this, out result); + return result; + } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public float this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + case 3: + return w; + default: + throw new IndexOutOfRangeException("Invalid Vector4 index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector4 index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector4(float value) + { + this.x = value; + this.y = value; + this.z = value; + this.w = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public Vector4(float x, float y, float z, float w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector4 operator +(Vector4 a, Vector4 b) + { + return new Vector4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector4 operator -(Vector4 a, Vector4 b) + { + return new Vector4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector4 operator *(Vector4 a, float d) + { + return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector4 operator *(float d, Vector4 a) + { + return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector4 operator /(Vector4 a, float d) + { + return new Vector4(a.x / d, a.y / d, a.z / d, a.w / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector4 operator -(Vector4 a) + { + return new Vector4(-a.x, -a.y, -a.z, -a.w); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector4 lhs, Vector4 rhs) + { + return (lhs - rhs).MagnitudeSqr < Epsilon; + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector4 lhs, Vector4 rhs) + { + return (lhs - rhs).MagnitudeSqr >= Epsilon; + } + + /// + /// Explicitly converts from a double-precision vector into a single-precision vector. + /// + /// The double-precision vector. + public static explicit operator Vector4(Vector4d v) + { + return new Vector4((float)v.x, (float)v.y, (float)v.z, (float)v.w); + } + + /// + /// Implicitly converts from an integer vector into a single-precision vector. + /// + /// The integer vector. + public static implicit operator Vector4(Vector4i v) + { + return new Vector4(v.x, v.y, v.z, v.w); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x, y and z components of an existing vector. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public void Set(float x, float y, float z, float w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector4 scale) + { + x *= scale.x; + y *= scale.y; + z *= scale.z; + w *= scale.w; + } + + /// + /// Normalizes this vector. + /// + public void Normalize() + { + float mag = this.Magnitude; + if (mag > Epsilon) + { + x /= mag; + y /= mag; + z /= mag; + w /= mag; + } + else + { + x = y = z = w = 0; + } + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(float min, float max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + + if (z < min) z = min; + else if (z > max) z = max; + + if (w < min) w = min; + else if (w > max) w = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector4)) + { + return false; + } + Vector4 vector = (Vector4)other; + return (x == vector.x && y == vector.y && z == vector.z && w == vector.w); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector4 other) + { + return (x == other.x && y == other.y && z == other.z && w == other.w); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1}, {2}, {3})", + x.ToString("F1", CultureInfo.InvariantCulture), + y.ToString("F1", CultureInfo.InvariantCulture), + z.ToString("F1", CultureInfo.InvariantCulture), + w.ToString("F1", CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The float format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1}, {2}, {3})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture), + z.ToString(format, CultureInfo.InvariantCulture), + w.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Dot Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + public static float Dot(ref Vector4 lhs, ref Vector4 rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w; + } + + /// + /// Performs a linear interpolation between two vectors. + /// + /// The vector to interpolate from. + /// The vector to interpolate to. + /// The time fraction. + /// The resulting vector. + public static void Lerp(ref Vector4 a, ref Vector4 b, float t, out Vector4 result) + { + result = new Vector4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); + } + + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector4 a, ref Vector4 b, out Vector4 result) + { + result = new Vector4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); + } + + /// + /// Normalizes a vector. + /// + /// The vector to normalize. + /// The resulting normalized vector. + public static void Normalize(ref Vector4 value, out Vector4 result) + { + float mag = value.Magnitude; + if (mag > Epsilon) + { + result = new Vector4(value.x / mag, value.y / mag, value.z / mag, value.w / mag); + } + else + { + result = Vector4.zero; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs new file mode 100644 index 0000000..c984c08 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs @@ -0,0 +1,467 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A double precision 4D vector. + /// + public struct Vector4d : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector4d zero = new Vector4d(0, 0, 0, 0); + #endregion + + #region Consts + /// + /// The vector epsilon. + /// + public const double Epsilon = double.Epsilon; + #endregion + + #region Fields + /// + /// The x component. + /// + public double x; + /// + /// The y component. + /// + public double y; + /// + /// The z component. + /// + public double z; + /// + /// The w component. + /// + public double w; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public double Magnitude + { + get { return System.Math.Sqrt(x * x + y * y + z * z + w * w); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public double MagnitudeSqr + { + get { return (x * x + y * y + z * z + w * w); } + } + + /// + /// Gets a normalized vector from this vector. + /// + public Vector4d Normalized + { + get + { + Vector4d result; + Normalize(ref this, out result); + return result; + } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public double this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + case 3: + return w; + default: + throw new IndexOutOfRangeException("Invalid Vector4d index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector4d index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector4d(double value) + { + this.x = value; + this.y = value; + this.z = value; + this.w = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public Vector4d(double x, double y, double z, double w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector4d operator +(Vector4d a, Vector4d b) + { + return new Vector4d(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector4d operator -(Vector4d a, Vector4d b) + { + return new Vector4d(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector4d operator *(Vector4d a, double d) + { + return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector4d operator *(double d, Vector4d a) + { + return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector4d operator /(Vector4d a, double d) + { + return new Vector4d(a.x / d, a.y / d, a.z / d, a.w / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector4d operator -(Vector4d a) + { + return new Vector4d(-a.x, -a.y, -a.z, -a.w); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector4d lhs, Vector4d rhs) + { + return (lhs - rhs).MagnitudeSqr < Epsilon; + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector4d lhs, Vector4d rhs) + { + return (lhs - rhs).MagnitudeSqr >= Epsilon; + } + + /// + /// Implicitly converts from a single-precision vector into a double-precision vector. + /// + /// The single-precision vector. + public static implicit operator Vector4d(Vector4 v) + { + return new Vector4d(v.x, v.y, v.z, v.w); + } + + /// + /// Implicitly converts from an integer vector into a double-precision vector. + /// + /// The integer vector. + public static implicit operator Vector4d(Vector4i v) + { + return new Vector4d(v.x, v.y, v.z, v.w); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x, y and z components of an existing vector. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public void Set(double x, double y, double z, double w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector4d scale) + { + x *= scale.x; + y *= scale.y; + z *= scale.z; + w *= scale.w; + } + + /// + /// Normalizes this vector. + /// + public void Normalize() + { + double mag = this.Magnitude; + if (mag > Epsilon) + { + x /= mag; + y /= mag; + z /= mag; + w /= mag; + } + else + { + x = y = z = w = 0; + } + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(double min, double max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + + if (z < min) z = min; + else if (z > max) z = max; + + if (w < min) w = min; + else if (w > max) w = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector4d)) + { + return false; + } + Vector4d vector = (Vector4d)other; + return (x == vector.x && y == vector.y && z == vector.z && w == vector.w); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector4d other) + { + return (x == other.x && y == other.y && z == other.z && w == other.w); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1}, {2}, {3})", + x.ToString("F1", CultureInfo.InvariantCulture), + y.ToString("F1", CultureInfo.InvariantCulture), + z.ToString("F1", CultureInfo.InvariantCulture), + w.ToString("F1", CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The float format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1}, {2}, {3})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture), + z.ToString(format, CultureInfo.InvariantCulture), + w.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Dot Product of two vectors. + /// + /// The left hand side vector. + /// The right hand side vector. + public static double Dot(ref Vector4d lhs, ref Vector4d rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w; + } + + /// + /// Performs a linear interpolation between two vectors. + /// + /// The vector to interpolate from. + /// The vector to interpolate to. + /// The time fraction. + /// The resulting vector. + public static void Lerp(ref Vector4d a, ref Vector4d b, double t, out Vector4d result) + { + result = new Vector4d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); + } + + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector4d a, ref Vector4d b, out Vector4d result) + { + result = new Vector4d(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); + } + + /// + /// Normalizes a vector. + /// + /// The vector to normalize. + /// The resulting normalized vector. + public static void Normalize(ref Vector4d value, out Vector4d result) + { + double mag = value.Magnitude; + if (mag > Epsilon) + { + result = new Vector4d(value.x / mag, value.y / mag, value.z / mag, value.w / mag); + } + else + { + result = Vector4d.zero; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs new file mode 100644 index 0000000..cc52459 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs @@ -0,0 +1,388 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Globalization; + +namespace MeshDecimator.Math +{ + /// + /// A 4D integer vector. + /// + public struct Vector4i : IEquatable + { + #region Static Read-Only + /// + /// The zero vector. + /// + public static readonly Vector4i zero = new Vector4i(0, 0, 0, 0); + #endregion + + #region Fields + /// + /// The x component. + /// + public int x; + /// + /// The y component. + /// + public int y; + /// + /// The z component. + /// + public int z; + /// + /// The w component. + /// + public int w; + #endregion + + #region Properties + /// + /// Gets the magnitude of this vector. + /// + public int Magnitude + { + get { return (int)System.Math.Sqrt(x * x + y * y + z * z + w * w); } + } + + /// + /// Gets the squared magnitude of this vector. + /// + public int MagnitudeSqr + { + get { return (x * x + y * y + z * z + w * w); } + } + + /// + /// Gets or sets a specific component by index in this vector. + /// + /// The component index. + public int this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + case 3: + return w; + default: + throw new IndexOutOfRangeException("Invalid Vector4i index!"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector4i index!"); + } + } + } + #endregion + + #region Constructor + /// + /// Creates a new vector with one value for all components. + /// + /// The value. + public Vector4i(int value) + { + this.x = value; + this.y = value; + this.z = value; + this.w = value; + } + + /// + /// Creates a new vector. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public Vector4i(int x, int y, int z, int w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + #endregion + + #region Operators + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector4i operator +(Vector4i a, Vector4i b) + { + return new Vector4i(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static Vector4i operator -(Vector4i a, Vector4i b) + { + return new Vector4i(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + } + + /// + /// Scales the vector uniformly. + /// + /// The vector. + /// The scaling value. + /// The resulting vector. + public static Vector4i operator *(Vector4i a, int d) + { + return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d); + } + + /// + /// Scales the vector uniformly. + /// + /// The scaling value. + /// The vector. + /// The resulting vector. + public static Vector4i operator *(int d, Vector4i a) + { + return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d); + } + + /// + /// Divides the vector with a float. + /// + /// The vector. + /// The dividing float value. + /// The resulting vector. + public static Vector4i operator /(Vector4i a, int d) + { + return new Vector4i(a.x / d, a.y / d, a.z / d, a.w / d); + } + + /// + /// Subtracts the vector from a zero vector. + /// + /// The vector. + /// The resulting vector. + public static Vector4i operator -(Vector4i a) + { + return new Vector4i(-a.x, -a.y, -a.z, -a.w); + } + + /// + /// Returns if two vectors equals eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If equals. + public static bool operator ==(Vector4i lhs, Vector4i rhs) + { + return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w); + } + + /// + /// Returns if two vectors don't equal eachother. + /// + /// The left hand side vector. + /// The right hand side vector. + /// If not equals. + public static bool operator !=(Vector4i lhs, Vector4i rhs) + { + return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w); + } + + /// + /// Explicitly converts from a single-precision vector into an integer vector. + /// + /// The single-precision vector. + public static explicit operator Vector4i(Vector4 v) + { + return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w); + } + + /// + /// Explicitly converts from a double-precision vector into an integer vector. + /// + /// The double-precision vector. + public static explicit operator Vector4i(Vector4d v) + { + return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w); + } + #endregion + + #region Public Methods + #region Instance + /// + /// Set x, y and z components of an existing vector. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public void Set(int x, int y, int z, int w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /// + /// Multiplies with another vector component-wise. + /// + /// The vector to multiply with. + public void Scale(ref Vector4i scale) + { + x *= scale.x; + y *= scale.y; + z *= scale.z; + w *= scale.w; + } + + /// + /// Clamps this vector between a specific range. + /// + /// The minimum component value. + /// The maximum component value. + public void Clamp(int min, int max) + { + if (x < min) x = min; + else if (x > max) x = max; + + if (y < min) y = min; + else if (y > max) y = max; + + if (z < min) z = min; + else if (z > max) z = max; + + if (w < min) w = min; + else if (w > max) w = max; + } + #endregion + + #region Object + /// + /// Returns a hash code for this vector. + /// + /// The hash code. + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public override bool Equals(object other) + { + if (!(other is Vector4i)) + { + return false; + } + Vector4i vector = (Vector4i)other; + return (x == vector.x && y == vector.y && z == vector.z && w == vector.w); + } + + /// + /// Returns if this vector is equal to another one. + /// + /// The other vector to compare to. + /// If equals. + public bool Equals(Vector4i other) + { + return (x == other.x && y == other.y && z == other.z && w == other.w); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The string. + public override string ToString() + { + return string.Format("({0}, {1}, {2}, {3})", + x.ToString(CultureInfo.InvariantCulture), + y.ToString(CultureInfo.InvariantCulture), + z.ToString(CultureInfo.InvariantCulture), + w.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Returns a nicely formatted string for this vector. + /// + /// The integer format. + /// The string. + public string ToString(string format) + { + return string.Format("({0}, {1}, {2}, {3})", + x.ToString(format, CultureInfo.InvariantCulture), + y.ToString(format, CultureInfo.InvariantCulture), + z.ToString(format, CultureInfo.InvariantCulture), + w.ToString(format, CultureInfo.InvariantCulture)); + } + #endregion + + #region Static + /// + /// Multiplies two vectors component-wise. + /// + /// The first vector. + /// The second vector. + /// The resulting vector. + public static void Scale(ref Vector4i a, ref Vector4i b, out Vector4i result) + { + result = new Vector4i(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Mesh.cs b/LightlessSync/ThirdParty/MeshDecimator/Mesh.cs new file mode 100644 index 0000000..2e38821 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/Mesh.cs @@ -0,0 +1,955 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using System.Collections.Generic; +using MeshDecimator.Math; + +namespace MeshDecimator +{ + /// + /// A mesh. + /// + public sealed class Mesh + { + #region Consts + /// + /// The count of supported UV channels. + /// + public const int UVChannelCount = 4; + #endregion + + #region Fields + private Vector3d[] vertices = null; + private int[][] indices = null; + private Vector3[] normals = null; + private Vector4[] tangents = null; + private Vector2[][] uvs2D = null; + private Vector3[][] uvs3D = null; + private Vector4[][] uvs4D = null; + private Vector4[] colors = null; + private BoneWeight[] boneWeights = null; + + private static readonly int[] emptyIndices = new int[0]; + #endregion + + #region Properties + /// + /// Gets the count of vertices of this mesh. + /// + public int VertexCount + { + get { return vertices.Length; } + } + + /// + /// Gets or sets the count of submeshes in this mesh. + /// + public int SubMeshCount + { + get { return indices.Length; } + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException("value"); + + int[][] newIndices = new int[value][]; + Array.Copy(indices, 0, newIndices, 0, MathHelper.Min(indices.Length, newIndices.Length)); + indices = newIndices; + } + } + + /// + /// Gets the total count of triangles in this mesh. + /// + public int TriangleCount + { + get + { + int triangleCount = 0; + for (int i = 0; i < indices.Length; i++) + { + if (indices[i] != null) + { + triangleCount += indices[i].Length / 3; + } + } + return triangleCount; + } + } + + /// + /// Gets or sets the vertices for this mesh. Note that this resets all other vertex attributes. + /// + public Vector3d[] Vertices + { + get { return vertices; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + vertices = value; + ClearVertexAttributes(); + } + } + + /// + /// Gets or sets the combined indices for this mesh. Once set, the sub-mesh count gets set to 1. + /// + public int[] Indices + { + get + { + if (indices.Length == 1) + { + return indices[0] ?? emptyIndices; + } + else + { + List indexList = new List(TriangleCount * 3); + for (int i = 0; i < indices.Length; i++) + { + if (indices[i] != null) + { + indexList.AddRange(indices[i]); + } + } + return indexList.ToArray(); + } + } + set + { + if (value == null) + throw new ArgumentNullException("value"); + else if ((value.Length % 3) != 0) + throw new ArgumentException("The index count must be multiple by 3.", "value"); + + SubMeshCount = 1; + SetIndices(0, value); + } + } + + /// + /// Gets or sets the normals for this mesh. + /// + public Vector3[] Normals + { + get { return normals; } + set + { + if (value != null && value.Length != vertices.Length) + throw new ArgumentException(string.Format("The vertex normals must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); + + normals = value; + } + } + + /// + /// Gets or sets the tangents for this mesh. + /// + public Vector4[] Tangents + { + get { return tangents; } + set + { + if (value != null && value.Length != vertices.Length) + throw new ArgumentException(string.Format("The vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); + + tangents = value; + } + } + + /// + /// Gets or sets the first UV set for this mesh. + /// + public Vector2[] UV1 + { + get { return GetUVs2D(0); } + set { SetUVs(0, value); } + } + + /// + /// Gets or sets the second UV set for this mesh. + /// + public Vector2[] UV2 + { + get { return GetUVs2D(1); } + set { SetUVs(1, value); } + } + + /// + /// Gets or sets the third UV set for this mesh. + /// + public Vector2[] UV3 + { + get { return GetUVs2D(2); } + set { SetUVs(2, value); } + } + + /// + /// Gets or sets the fourth UV set for this mesh. + /// + public Vector2[] UV4 + { + get { return GetUVs2D(3); } + set { SetUVs(3, value); } + } + + /// + /// Gets or sets the vertex colors for this mesh. + /// + public Vector4[] Colors + { + get { return colors; } + set + { + if (value != null && value.Length != vertices.Length) + throw new ArgumentException(string.Format("The vertex colors must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); + + colors = value; + } + } + + /// + /// Gets or sets the vertex bone weights for this mesh. + /// + public BoneWeight[] BoneWeights + { + get { return boneWeights; } + set + { + if (value != null && value.Length != vertices.Length) + throw new ArgumentException(string.Format("The vertex bone weights must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); + + boneWeights = value; + } + } + #endregion + + #region Constructor + /// + /// Creates a new mesh. + /// + /// The mesh vertices. + /// The mesh indices. + public Mesh(Vector3d[] vertices, int[] indices) + { + if (vertices == null) + throw new ArgumentNullException("vertices"); + else if (indices == null) + throw new ArgumentNullException("indices"); + else if ((indices.Length % 3) != 0) + throw new ArgumentException("The index count must be multiple by 3.", "indices"); + + this.vertices = vertices; + this.indices = new int[1][]; + this.indices[0] = indices; + } + + /// + /// Creates a new mesh. + /// + /// The mesh vertices. + /// The mesh indices. + public Mesh(Vector3d[] vertices, int[][] indices) + { + if (vertices == null) + throw new ArgumentNullException("vertices"); + else if (indices == null) + throw new ArgumentNullException("indices"); + + for (int i = 0; i < indices.Length; i++) + { + if (indices[i] != null && (indices[i].Length % 3) != 0) + throw new ArgumentException(string.Format("The index count must be multiple by 3 at sub-mesh index {0}.", i), "indices"); + } + + this.vertices = vertices; + this.indices = indices; + } + #endregion + + #region Private Methods + private void ClearVertexAttributes() + { + normals = null; + tangents = null; + uvs2D = null; + uvs3D = null; + uvs4D = null; + colors = null; + boneWeights = null; + } + #endregion + + #region Public Methods + #region Recalculate Normals + /// + /// Recalculates the normals for this mesh smoothly. + /// + public void RecalculateNormals() + { + int vertexCount = vertices.Length; + Vector3[] normals = new Vector3[vertexCount]; + + int subMeshCount = this.indices.Length; + for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) + { + int[] indices = this.indices[subMeshIndex]; + if (indices == null) + continue; + + int indexCount = indices.Length; + for (int i = 0; i < indexCount; i += 3) + { + int i0 = indices[i]; + int i1 = indices[i + 1]; + int i2 = indices[i + 2]; + + var v0 = (Vector3)vertices[i0]; + var v1 = (Vector3)vertices[i1]; + var v2 = (Vector3)vertices[i2]; + + var nx = v1 - v0; + var ny = v2 - v0; + Vector3 normal; + Vector3.Cross(ref nx, ref ny, out normal); + normal.Normalize(); + + normals[i0] += normal; + normals[i1] += normal; + normals[i2] += normal; + } + } + + for (int i = 0; i < vertexCount; i++) + { + normals[i].Normalize(); + } + + this.normals = normals; + } + #endregion + + #region Recalculate Tangents + /// + /// Recalculates the tangents for this mesh. + /// + public void RecalculateTangents() + { + // Make sure we have the normals first + if (normals == null) + return; + + // Also make sure that we have the first UV set + bool uvIs2D = (uvs2D != null && uvs2D[0] != null); + bool uvIs3D = (uvs3D != null && uvs3D[0] != null); + bool uvIs4D = (uvs4D != null && uvs4D[0] != null); + if (!uvIs2D && !uvIs3D && !uvIs4D) + return; + + int vertexCount = vertices.Length; + + var tangents = new Vector4[vertexCount]; + var tan1 = new Vector3[vertexCount]; + var tan2 = new Vector3[vertexCount]; + + Vector2[] uv2D = (uvIs2D ? uvs2D[0] : null); + Vector3[] uv3D = (uvIs3D ? uvs3D[0] : null); + Vector4[] uv4D = (uvIs4D ? uvs4D[0] : null); + + int subMeshCount = this.indices.Length; + for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) + { + int[] indices = this.indices[subMeshIndex]; + if (indices == null) + continue; + + int indexCount = indices.Length; + for (int i = 0; i < indexCount; i += 3) + { + int i0 = indices[i]; + int i1 = indices[i + 1]; + int i2 = indices[i + 2]; + + var v0 = vertices[i0]; + var v1 = vertices[i1]; + var v2 = vertices[i2]; + + float s1, s2, t1, t2; + if (uvIs2D) + { + var w0 = uv2D[i0]; + var w1 = uv2D[i1]; + var w2 = uv2D[i2]; + s1 = w1.x - w0.x; + s2 = w2.x - w0.x; + t1 = w1.y - w0.y; + t2 = w2.y - w0.y; + } + else if (uvIs3D) + { + var w0 = uv3D[i0]; + var w1 = uv3D[i1]; + var w2 = uv3D[i2]; + s1 = w1.x - w0.x; + s2 = w2.x - w0.x; + t1 = w1.y - w0.y; + t2 = w2.y - w0.y; + } + else + { + var w0 = uv4D[i0]; + var w1 = uv4D[i1]; + var w2 = uv4D[i2]; + s1 = w1.x - w0.x; + s2 = w2.x - w0.x; + t1 = w1.y - w0.y; + t2 = w2.y - w0.y; + } + + + float x1 = (float)(v1.x - v0.x); + float x2 = (float)(v2.x - v0.x); + float y1 = (float)(v1.y - v0.y); + float y2 = (float)(v2.y - v0.y); + float z1 = (float)(v1.z - v0.z); + float z2 = (float)(v2.z - v0.z); + float r = 1f / (s1 * t2 - s2 * t1); + + var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); + var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); + + tan1[i0] += sdir; + tan1[i1] += sdir; + tan1[i2] += sdir; + tan2[i0] += tdir; + tan2[i1] += tdir; + tan2[i2] += tdir; + } + } + + for (int i = 0; i < vertexCount; i++) + { + var n = normals[i]; + var t = tan1[i]; + + var tmp = (t - n * Vector3.Dot(ref n, ref t)); + tmp.Normalize(); + + Vector3 c; + Vector3.Cross(ref n, ref t, out c); + float dot = Vector3.Dot(ref c, ref tan2[i]); + float w = (dot < 0f ? -1f : 1f); + tangents[i] = new Vector4(tmp.x, tmp.y, tmp.z, w); + } + + this.tangents = tangents; + } + #endregion + + #region Triangles + /// + /// Returns the count of triangles for a specific sub-mesh in this mesh. + /// + /// The sub-mesh index. + /// The triangle count. + public int GetTriangleCount(int subMeshIndex) + { + if (subMeshIndex < 0 || subMeshIndex >= indices.Length) + throw new IndexOutOfRangeException(); + + return indices[subMeshIndex].Length / 3; + } + + /// + /// Returns the triangle indices of a specific sub-mesh in this mesh. + /// + /// The sub-mesh index. + /// The triangle indices. + public int[] GetIndices(int subMeshIndex) + { + if (subMeshIndex < 0 || subMeshIndex >= indices.Length) + throw new IndexOutOfRangeException(); + + return indices[subMeshIndex] ?? emptyIndices; + } + + /// + /// Returns the triangle indices for all sub-meshes in this mesh. + /// + /// The sub-mesh triangle indices. + public int[][] GetSubMeshIndices() + { + var subMeshIndices = new int[indices.Length][]; + for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++) + { + subMeshIndices[subMeshIndex] = indices[subMeshIndex] ?? emptyIndices; + } + return subMeshIndices; + } + + /// + /// Sets the triangle indices of a specific sub-mesh in this mesh. + /// + /// The sub-mesh index. + /// The triangle indices. + public void SetIndices(int subMeshIndex, int[] indices) + { + if (subMeshIndex < 0 || subMeshIndex >= this.indices.Length) + throw new IndexOutOfRangeException(); + else if (indices == null) + throw new ArgumentNullException("indices"); + else if ((indices.Length % 3) != 0) + throw new ArgumentException("The index count must be multiple by 3.", "indices"); + + this.indices[subMeshIndex] = indices; + } + #endregion + + #region UV Sets + #region Getting + /// + /// Returns the UV dimension for a specific channel. + /// + /// + /// The UV dimension count. + public int GetUVDimension(int channel) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs2D != null && uvs2D[channel] != null) + { + return 2; + } + else if (uvs3D != null && uvs3D[channel] != null) + { + return 3; + } + else if (uvs4D != null && uvs4D[channel] != null) + { + return 4; + } + else + { + return 0; + } + } + + /// + /// Returns the UVs (2D) from a specific channel. + /// + /// The channel index. + /// The UVs. + public Vector2[] GetUVs2D(int channel) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs2D != null && uvs2D[channel] != null) + { + return uvs2D[channel]; + } + else + { + return null; + } + } + + /// + /// Returns the UVs (3D) from a specific channel. + /// + /// The channel index. + /// The UVs. + public Vector3[] GetUVs3D(int channel) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs3D != null && uvs3D[channel] != null) + { + return uvs3D[channel]; + } + else + { + return null; + } + } + + /// + /// Returns the UVs (4D) from a specific channel. + /// + /// The channel index. + /// The UVs. + public Vector4[] GetUVs4D(int channel) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs4D != null && uvs4D[channel] != null) + { + return uvs4D[channel]; + } + else + { + return null; + } + } + + /// + /// Returns the UVs (2D) from a specific channel. + /// + /// The channel index. + /// The UVs. + public void GetUVs(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + else if (uvs == null) + throw new ArgumentNullException("uvs"); + + uvs.Clear(); + if (uvs2D != null && uvs2D[channel] != null) + { + var uvData = uvs2D[channel]; + if (uvData != null) + { + uvs.AddRange(uvData); + } + } + } + + /// + /// Returns the UVs (3D) from a specific channel. + /// + /// The channel index. + /// The UVs. + public void GetUVs(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + else if (uvs == null) + throw new ArgumentNullException("uvs"); + + uvs.Clear(); + if (uvs3D != null && uvs3D[channel] != null) + { + var uvData = uvs3D[channel]; + if (uvData != null) + { + uvs.AddRange(uvData); + } + } + } + + /// + /// Returns the UVs (4D) from a specific channel. + /// + /// The channel index. + /// The UVs. + public void GetUVs(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + else if (uvs == null) + throw new ArgumentNullException("uvs"); + + uvs.Clear(); + if (uvs4D != null && uvs4D[channel] != null) + { + var uvData = uvs4D[channel]; + if (uvData != null) + { + uvs.AddRange(uvData); + } + } + } + #endregion + + #region Setting + /// + /// Sets the UVs (2D) for a specific channel. + /// + /// The channel index. + /// The UVs. + public void SetUVs(int channel, Vector2[] uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs != null && uvs.Length > 0) + { + if (uvs.Length != vertices.Length) + throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvs.Length, vertices.Length)); + + if (uvs2D == null) + uvs2D = new Vector2[UVChannelCount][]; + + int uvCount = uvs.Length; + var uvSet = new Vector2[uvCount]; + uvs2D[channel] = uvSet; + uvs.CopyTo(uvSet, 0); + } + else + { + if (uvs2D != null) + { + uvs2D[channel] = null; + } + } + + if (uvs3D != null) + { + uvs3D[channel] = null; + } + if (uvs4D != null) + { + uvs4D[channel] = null; + } + } + + /// + /// Sets the UVs (3D) for a specific channel. + /// + /// The channel index. + /// The UVs. + public void SetUVs(int channel, Vector3[] uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs != null && uvs.Length > 0) + { + int uvCount = uvs.Length; + if (uvCount != vertices.Length) + throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); + + if (uvs3D == null) + uvs3D = new Vector3[UVChannelCount][]; + + var uvSet = new Vector3[uvCount]; + uvs3D[channel] = uvSet; + uvs.CopyTo(uvSet, 0); + } + else + { + if (uvs3D != null) + { + uvs3D[channel] = null; + } + } + + if (uvs2D != null) + { + uvs2D[channel] = null; + } + if (uvs4D != null) + { + uvs4D[channel] = null; + } + } + + /// + /// Sets the UVs (4D) for a specific channel. + /// + /// The channel index. + /// The UVs. + public void SetUVs(int channel, Vector4[] uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs != null && uvs.Length > 0) + { + int uvCount = uvs.Length; + if (uvCount != vertices.Length) + throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); + + if (uvs4D == null) + uvs4D = new Vector4[UVChannelCount][]; + + var uvSet = new Vector4[uvCount]; + uvs4D[channel] = uvSet; + uvs.CopyTo(uvSet, 0); + } + else + { + if (uvs4D != null) + { + uvs4D[channel] = null; + } + } + + if (uvs2D != null) + { + uvs2D[channel] = null; + } + if (uvs3D != null) + { + uvs3D[channel] = null; + } + } + + /// + /// Sets the UVs (2D) for a specific channel. + /// + /// The channel index. + /// The UVs. + public void SetUVs(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs != null && uvs.Count > 0) + { + int uvCount = uvs.Count; + if (uvCount != vertices.Length) + throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); + + if (uvs2D == null) + uvs2D = new Vector2[UVChannelCount][]; + + var uvSet = new Vector2[uvCount]; + uvs2D[channel] = uvSet; + uvs.CopyTo(uvSet, 0); + } + else + { + if (uvs2D != null) + { + uvs2D[channel] = null; + } + } + + if (uvs3D != null) + { + uvs3D[channel] = null; + } + if (uvs4D != null) + { + uvs4D[channel] = null; + } + } + + /// + /// Sets the UVs (3D) for a specific channel. + /// + /// The channel index. + /// The UVs. + public void SetUVs(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs != null && uvs.Count > 0) + { + int uvCount = uvs.Count; + if (uvCount != vertices.Length) + throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); + + if (uvs3D == null) + uvs3D = new Vector3[UVChannelCount][]; + + var uvSet = new Vector3[uvCount]; + uvs3D[channel] = uvSet; + uvs.CopyTo(uvSet, 0); + } + else + { + if (uvs3D != null) + { + uvs3D[channel] = null; + } + } + + if (uvs2D != null) + { + uvs2D[channel] = null; + } + if (uvs4D != null) + { + uvs4D[channel] = null; + } + } + + /// + /// Sets the UVs (4D) for a specific channel. + /// + /// The channel index. + /// The UVs. + public void SetUVs(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException("channel"); + + if (uvs != null && uvs.Count > 0) + { + int uvCount = uvs.Count; + if (uvCount != vertices.Length) + throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); + + if (uvs4D == null) + uvs4D = new Vector4[UVChannelCount][]; + + var uvSet = new Vector4[uvCount]; + uvs4D[channel] = uvSet; + uvs.CopyTo(uvSet, 0); + } + else + { + if (uvs4D != null) + { + uvs4D[channel] = null; + } + } + + if (uvs2D != null) + { + uvs2D[channel] = null; + } + if (uvs3D != null) + { + uvs3D[channel] = null; + } + } + #endregion + #endregion + + #region To String + /// + /// Returns the text-representation of this mesh. + /// + /// The text-representation. + public override string ToString() + { + return string.Format("Vertices: {0}", vertices.Length); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs b/LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs new file mode 100644 index 0000000..cb13fe8 --- /dev/null +++ b/LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs @@ -0,0 +1,180 @@ +#region License +/* +MIT License + +Copyright(c) 2017-2018 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using MeshDecimator.Algorithms; + +namespace MeshDecimator +{ + #region Algorithm + /// + /// The decimation algorithms. + /// + public enum Algorithm + { + /// + /// The default algorithm. + /// + Default, + /// + /// The fast quadric mesh simplification algorithm. + /// + FastQuadricMesh + } + #endregion + + /// + /// The mesh decimation API. + /// + public static class MeshDecimation + { + #region Public Methods + #region Create Algorithm + /// + /// Creates a specific decimation algorithm. + /// + /// The desired algorithm. + /// The decimation algorithm. + public static DecimationAlgorithm CreateAlgorithm(Algorithm algorithm) + { + DecimationAlgorithm alg = null; + + switch (algorithm) + { + case Algorithm.Default: + case Algorithm.FastQuadricMesh: + alg = new FastQuadricMeshSimplification(); + break; + default: + throw new ArgumentException("The specified algorithm is not supported.", "algorithm"); + } + + return alg; + } + #endregion + + #region Decimate Mesh + /// + /// Decimates a mesh. + /// + /// The mesh to decimate. + /// The target triangle count. + /// The decimated mesh. + public static Mesh DecimateMesh(Mesh mesh, int targetTriangleCount) + { + return DecimateMesh(Algorithm.Default, mesh, targetTriangleCount); + } + + /// + /// Decimates a mesh. + /// + /// The desired algorithm. + /// The mesh to decimate. + /// The target triangle count. + /// The decimated mesh. + public static Mesh DecimateMesh(Algorithm algorithm, Mesh mesh, int targetTriangleCount) + { + if (mesh == null) + throw new ArgumentNullException("mesh"); + + var decimationAlgorithm = CreateAlgorithm(algorithm); + return DecimateMesh(decimationAlgorithm, mesh, targetTriangleCount); + } + + /// + /// Decimates a mesh. + /// + /// The decimation algorithm. + /// The mesh to decimate. + /// The target triangle count. + /// The decimated mesh. + public static Mesh DecimateMesh(DecimationAlgorithm algorithm, Mesh mesh, int targetTriangleCount) + { + if (algorithm == null) + throw new ArgumentNullException("algorithm"); + else if (mesh == null) + throw new ArgumentNullException("mesh"); + + int currentTriangleCount = mesh.TriangleCount; + if (targetTriangleCount > currentTriangleCount) + targetTriangleCount = currentTriangleCount; + else if (targetTriangleCount < 0) + targetTriangleCount = 0; + + algorithm.Initialize(mesh); + algorithm.DecimateMesh(targetTriangleCount); + return algorithm.ToMesh(); + } + #endregion + + #region Decimate Mesh Lossless + /// + /// Decimates a mesh without losing any quality. + /// + /// The mesh to decimate. + /// The decimated mesh. + public static Mesh DecimateMeshLossless(Mesh mesh) + { + return DecimateMeshLossless(Algorithm.Default, mesh); + } + + /// + /// Decimates a mesh without losing any quality. + /// + /// The desired algorithm. + /// The mesh to decimate. + /// The decimated mesh. + public static Mesh DecimateMeshLossless(Algorithm algorithm, Mesh mesh) + { + if (mesh == null) + throw new ArgumentNullException("mesh"); + + var decimationAlgorithm = CreateAlgorithm(algorithm); + return DecimateMeshLossless(decimationAlgorithm, mesh); + } + + /// + /// Decimates a mesh without losing any quality. + /// + /// The decimation algorithm. + /// The mesh to decimate. + /// The decimated mesh. + public static Mesh DecimateMeshLossless(DecimationAlgorithm algorithm, Mesh mesh) + { + if (algorithm == null) + throw new ArgumentNullException("algorithm"); + else if (mesh == null) + throw new ArgumentNullException("mesh"); + + int currentTriangleCount = mesh.TriangleCount; + algorithm.Initialize(mesh); + algorithm.DecimateMeshLossless(); + return algorithm.ToMesh(); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index b1195b4..089f104 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -944,6 +944,7 @@ public class CompactUi : WindowMediatorSubscriberBase VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes), VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes), VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris), + VisiblePairSortMode.EffectiveTriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveTris), VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)], VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList), _ => SortEntries(entryList), diff --git a/LightlessSync/UI/Components/DrawFolderTag.cs b/LightlessSync/UI/Components/DrawFolderTag.cs index b91617a..0870a0d 100644 --- a/LightlessSync/UI/Components/DrawFolderTag.cs +++ b/LightlessSync/UI/Components/DrawFolderTag.cs @@ -326,6 +326,7 @@ public class DrawFolderTag : DrawFolderBase VisiblePairSortMode.VramUsage => "VRAM usage (descending)", VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)", VisiblePairSortMode.TriangleCount => "Triangle count (descending)", + VisiblePairSortMode.EffectiveTriangleCount => "Effective triangle count (descending)", VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs", _ => "Default", }; diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index c8725e2..5524226 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -429,6 +429,7 @@ public class DrawUserPair _pair.LastAppliedApproximateVRAMBytes, _pair.LastAppliedApproximateEffectiveVRAMBytes, _pair.LastAppliedDataTris, + _pair.LastAppliedApproximateEffectiveTris, _pair.IsPaired, groupDisplays is null ? ImmutableArray.Empty : ImmutableArray.CreateRange(groupDisplays)); @@ -444,6 +445,8 @@ public class DrawUserPair private static string BuildTooltip(in TooltipSnapshot snapshot) { var builder = new StringBuilder(256); + static string FormatTriangles(long count) => + count > 1000 ? (count / 1000d).ToString("0.0'k'") : count.ToString(); if (snapshot.IsPaused) { @@ -510,9 +513,13 @@ public class DrawUserPair { builder.Append(Environment.NewLine); builder.Append("Approx. Triangle Count (excl. Vanilla): "); - builder.Append(snapshot.LastAppliedDataTris > 1000 - ? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'") - : snapshot.LastAppliedDataTris); + builder.Append(FormatTriangles(snapshot.LastAppliedDataTris)); + if (snapshot.LastAppliedApproximateEffectiveTris >= 0) + { + builder.Append(" (Effective: "); + builder.Append(FormatTriangles(snapshot.LastAppliedApproximateEffectiveTris)); + builder.Append(')'); + } } } @@ -544,11 +551,12 @@ public class DrawUserPair long LastAppliedApproximateVRAMBytes, long LastAppliedApproximateEffectiveVRAMBytes, long LastAppliedDataTris, + long LastAppliedApproximateEffectiveTris, bool IsPaired, ImmutableArray GroupDisplays) { public static TooltipSnapshot Empty { get; } = - new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray.Empty); + new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, -1, false, ImmutableArray.Empty); } private void DrawPairedClientMenu() diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index e7bcc87..08f81b6 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -217,6 +217,7 @@ public class DrawEntityFactory entry.PairStatus, handler?.LastAppliedDataBytes ?? -1, handler?.LastAppliedDataTris ?? -1, + handler?.LastAppliedApproximateEffectiveTris ?? -1, handler?.LastAppliedApproximateVRAMBytes ?? -1, handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1, handler); diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 74a6571..46ebe7d 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -415,7 +415,9 @@ public class IdDisplayHandler var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0 ? pair.LastAppliedApproximateEffectiveVRAMBytes : pair.LastAppliedApproximateVRAMBytes; - var triangleCount = pair.LastAppliedDataTris; + var triangleCount = pair.LastAppliedApproximateEffectiveTris >= 0 + ? pair.LastAppliedApproximateEffectiveTris + : pair.LastAppliedDataTris; if (vramBytes < 0 && triangleCount < 0) { return null; diff --git a/LightlessSync/UI/Models/PairUiEntry.cs b/LightlessSync/UI/Models/PairUiEntry.cs index c25b6fd..fcda8ec 100644 --- a/LightlessSync/UI/Models/PairUiEntry.cs +++ b/LightlessSync/UI/Models/PairUiEntry.cs @@ -21,6 +21,7 @@ public sealed record PairUiEntry( IndividualPairStatus? PairStatus, long LastAppliedDataBytes, long LastAppliedDataTris, + long LastAppliedApproximateEffectiveTris, long LastAppliedApproximateVramBytes, long LastAppliedApproximateEffectiveVramBytes, IPairHandlerAdapter? Handler) diff --git a/LightlessSync/UI/Models/VisiblePairSortMode.cs b/LightlessSync/UI/Models/VisiblePairSortMode.cs index ec133b9..615ac9f 100644 --- a/LightlessSync/UI/Models/VisiblePairSortMode.cs +++ b/LightlessSync/UI/Models/VisiblePairSortMode.cs @@ -7,4 +7,5 @@ public enum VisiblePairSortMode EffectiveVramUsage = 2, TriangleCount = 3, PreferredDirectPairs = 4, + EffectiveTriangleCount = 5, } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 730a788..9084436 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -574,6 +574,94 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private void DrawTriangleDecimationCounters() + { + HashSet trackedPairs = new(); + + var snapshot = _pairUiService.GetSnapshot(); + + foreach (var pair in snapshot.DirectPairs) + { + trackedPairs.Add(pair); + } + + foreach (var group in snapshot.GroupPairs.Values) + { + foreach (var pair in group) + { + trackedPairs.Add(pair); + } + } + + long totalOriginalTris = 0; + long totalEffectiveTris = 0; + var hasData = false; + + foreach (var pair in trackedPairs) + { + if (!pair.IsVisible) + continue; + + var original = pair.LastAppliedDataTris; + var effective = pair.LastAppliedApproximateEffectiveTris; + + if (original >= 0) + { + hasData = true; + totalOriginalTris += original; + } + + if (effective >= 0) + { + hasData = true; + totalEffectiveTris += effective; + } + } + + if (!hasData) + { + ImGui.TextDisabled("Triangle usage has not been calculated yet."); + return; + } + + var savedTris = Math.Max(0L, totalOriginalTris - totalEffectiveTris); + var originalText = FormatTriangleCount(totalOriginalTris); + var effectiveText = FormatTriangleCount(totalEffectiveTris); + var savedText = FormatTriangleCount(savedTris); + + ImGui.TextUnformatted($"Total triangle usage (original): {originalText}"); + ImGui.TextUnformatted($"Total triangle usage (effective): {effectiveText}"); + + if (savedTris > 0) + { + UiSharedService.ColorText($"Triangles saved by decimation: {savedText}", UIColors.Get("LightlessGreen")); + } + else + { + ImGui.TextUnformatted($"Triangles saved by decimation: {savedText}"); + } + + static string FormatTriangleCount(long triangleCount) + { + if (triangleCount < 0) + { + return "n/a"; + } + + if (triangleCount >= 1_000_000) + { + return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m tris"); + } + + if (triangleCount >= 1_000) + { + return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris"); + } + + return $"{triangleCount} tris"; + } + } + private void DrawThemeVectorRow(MainStyle.StyleVector2Option option) { ImGui.TableNextRow(); @@ -1495,6 +1583,7 @@ public class SettingsUi : WindowMediatorSubscriberBase DrawPairPropertyRow("Approx. VRAM", FormatBytes(pair.LastAppliedApproximateVRAMBytes)); DrawPairPropertyRow("Effective VRAM", FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes)); DrawPairPropertyRow("Last Triangles", pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture)); + DrawPairPropertyRow("Effective Triangles", pair.LastAppliedApproximateEffectiveTris < 0 ? "n/a" : pair.LastAppliedApproximateEffectiveTris.ToString(CultureInfo.InvariantCulture)); ImGui.EndTable(); } @@ -3528,6 +3617,102 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TreePop(); } + ImGui.Separator(); + + if (_uiShared.MediumTreeNode("Model Optimization", UIColors.Get("DimRed"))) + { + _uiShared.MediumText("Warning", UIColors.Get("DimRed")); + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Model decimation is a "), + new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances.")); + + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("this shit is placeholder still owo")); + + var performanceConfig = _playerPerformanceConfigService.Current; + var enableDecimation = performanceConfig.EnableModelDecimation; + if (ImGui.Checkbox("Enable model decimation", ref enableDecimation)) + { + performanceConfig.EnableModelDecimation = enableDecimation; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("When enabled, Lightless generates a decimated copy of given model after download."); + + var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 10_000, 100_000)) + { + performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 10_000, 100_000); + _playerPerformanceConfigService.Save(); + } + ImGui.SameLine(); + ImGui.Text("triangles"); + _uiShared.DrawHelpText($"Models below this triangle count are left untouched.{UiSharedService.TooltipSeparator}Default: 50,000"); + + var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0); + var clampedPercent = Math.Clamp(targetPercent, 70f, 99f); + if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon) + { + performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0; + _playerPerformanceConfigService.Save(); + targetPercent = clampedPercent; + } + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + if (ImGui.SliderFloat("Target triangle ratio", ref targetPercent, 70f, 99f, "%.0f%%")) + { + performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.7f, 0.99f); + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText($"Target ratio relative to original triangle count (70% keeps 70% of triangles).{UiSharedService.TooltipSeparator}Default: 70%"); + + ImGui.Dummy(new Vector2(5)); + ImGui.TextUnformatted("Decimation targets"); + _uiShared.DrawHelpText("Hair mods are always excluded from decimation."); + + var allowBody = performanceConfig.ModelDecimationAllowBody; + if (ImGui.Checkbox("Body", ref allowBody)) + { + performanceConfig.ModelDecimationAllowBody = allowBody; + _playerPerformanceConfigService.Save(); + } + + var allowFaceHead = performanceConfig.ModelDecimationAllowFaceHead; + if (ImGui.Checkbox("Face/head", ref allowFaceHead)) + { + performanceConfig.ModelDecimationAllowFaceHead = allowFaceHead; + _playerPerformanceConfigService.Save(); + } + + var allowTail = performanceConfig.ModelDecimationAllowTail; + if (ImGui.Checkbox("Tails/Ears", ref allowTail)) + { + performanceConfig.ModelDecimationAllowTail = allowTail; + _playerPerformanceConfigService.Save(); + } + + var allowClothing = performanceConfig.ModelDecimationAllowClothing; + if (ImGui.Checkbox("Clothing (body/legs/shoes/gloves/hats)", ref allowClothing)) + { + performanceConfig.ModelDecimationAllowClothing = allowClothing; + _playerPerformanceConfigService.Save(); + } + + var allowAccessories = performanceConfig.ModelDecimationAllowAccessories; + if (ImGui.Checkbox("Accessories (earring/rings/bracelet/necklace)", ref allowAccessories)) + { + performanceConfig.ModelDecimationAllowAccessories = allowAccessories; + _playerPerformanceConfigService.Save(); + } + + ImGui.Dummy(new Vector2(5)); + DrawTriangleDecimationCounters(); + ImGui.Dummy(new Vector2(5)); + + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); + ImGui.TreePop(); + } + ImGui.Separator(); ImGui.Dummy(new Vector2(10)); diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 552bcb3..c0f4fc1 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -6,6 +6,7 @@ using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ModelDecimation; using LightlessSync.Services.TextureCompression; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; @@ -25,6 +26,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase private readonly FileTransferOrchestrator _orchestrator; private readonly LightlessConfigService _configService; private readonly TextureDownscaleService _textureDownscaleService; + private readonly ModelDecimationService _modelDecimationService; private readonly TextureMetadataHelper _textureMetadataHelper; private readonly ConcurrentDictionary _activeDownloadStreams; @@ -41,6 +43,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase FileCompactor fileCompactor, LightlessConfigService configService, TextureDownscaleService textureDownscaleService, + ModelDecimationService modelDecimationService, TextureMetadataHelper textureMetadataHelper) : base(logger, mediator) { _downloadStatus = new Dictionary(StringComparer.Ordinal); @@ -49,6 +52,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase _fileCompactor = fileCompactor; _configService = configService; _textureDownscaleService = textureDownscaleService; + _modelDecimationService = modelDecimationService; _textureMetadataHelper = textureMetadataHelper; _activeDownloadStreams = new(); _lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads; @@ -1026,6 +1030,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase () => _textureMetadataHelper.DetermineMapKind(gamePath, filePath)); } + if (!skipDownscale && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath)) + { + _modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath); + } + if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase)) { Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file",