reworked mesh decimation yes

This commit is contained in:
2026-01-19 09:50:54 +09:00
parent b57d54d69c
commit 54d6a0a1a4
74 changed files with 15788 additions and 8308 deletions

View File

@@ -1,5 +1,6 @@
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
@@ -71,11 +72,50 @@ public sealed class ModelDecimationService
});
}
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
{
if (!ShouldScheduleBatchDecimation(hash, filePath, settings))
{
return;
}
if (_decimationDeduplicator.TryGetExisting(hash, out _))
{
return;
}
_failedHashes.TryRemove(hash, out _);
_decimatedPaths.TryRemove(hash, out _);
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
_decimationDeduplicator.GetOrStart(hash, async () =>
{
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
try
{
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
}
catch (Exception ex)
{
_failedHashes[hash] = 1;
_logger.LogWarning(ex, "Batch model decimation failed for {Hash}", hash);
}
finally
{
_decimationSemaphore.Release();
}
});
}
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
=> IsDecimationEnabled()
{
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
return IsDecimationEnabled()
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
&& IsDecimationAllowed(gamePath)
&& !ShouldSkipByTriangleCache(hash);
&& !ShouldSkipByTriangleCache(hash, threshold);
}
public string GetPreferredPath(string hash, string originalPath)
{
@@ -131,6 +171,23 @@ public sealed class ModelDecimationService
}
private Task DecimateInternalAsync(string hash, string sourcePath)
{
if (!TryGetDecimationSettings(out var settings))
{
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
return Task.CompletedTask;
}
return DecimateInternalAsync(hash, sourcePath, settings, allowExisting: true);
}
private Task DecimateInternalAsync(
string hash,
string sourcePath,
ModelDecimationSettings settings,
bool allowExisting,
string? destinationOverride = null,
bool registerDecimatedPath = true)
{
if (!File.Exists(sourcePath))
{
@@ -139,34 +196,47 @@ public sealed class ModelDecimationService
return Task.CompletedTask;
}
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio, out var normalizeTangents))
if (!TryNormalizeSettings(settings, out var normalized))
{
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
_logger.LogInformation("Model decimation skipped for {Hash}; invalid settings.", hash);
return Task.CompletedTask;
}
_logger.LogInformation(
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents})",
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents}, avoid body intersection {AvoidBodyIntersection})",
hash,
triangleThreshold,
targetRatio,
normalizeTangents);
normalized.TriangleThreshold,
normalized.TargetRatio,
normalized.NormalizeTangents,
normalized.AvoidBodyIntersection);
var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
if (File.Exists(destination))
var destination = destinationOverride ?? Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
var inPlace = string.Equals(destination, sourcePath, StringComparison.OrdinalIgnoreCase);
if (!inPlace && File.Exists(destination))
{
RegisterDecimatedModel(hash, sourcePath, destination);
return Task.CompletedTask;
if (allowExisting)
{
if (registerDecimatedPath)
{
RegisterDecimatedModel(hash, sourcePath, destination);
}
return Task.CompletedTask;
}
TryDelete(destination);
}
if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, normalizeTangents, _logger))
if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger))
{
_failedHashes[hash] = 1;
_logger.LogInformation("Model decimation skipped for {Hash}", hash);
return Task.CompletedTask;
}
RegisterDecimatedModel(hash, sourcePath, destination);
if (registerDecimatedPath)
{
RegisterDecimatedModel(hash, sourcePath, destination);
}
_logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
return Task.CompletedTask;
}
@@ -255,7 +325,7 @@ public sealed class ModelDecimationService
private bool IsDecimationEnabled()
=> _performanceConfigService.Current.EnableModelDecimation;
private bool ShouldSkipByTriangleCache(string hash)
private bool ShouldSkipByTriangleCache(string hash, int triangleThreshold)
{
if (string.IsNullOrEmpty(hash))
{
@@ -267,7 +337,7 @@ public sealed class ModelDecimationService
return false;
}
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
var threshold = Math.Max(0, triangleThreshold);
return threshold > 0 && cachedTris < threshold;
}
@@ -318,11 +388,14 @@ public sealed class ModelDecimationService
private static string NormalizeGamePath(string path)
=> path.Replace('\\', '/').ToLowerInvariant();
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio, out bool normalizeTangents)
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
{
triangleThreshold = 15_000;
targetRatio = 0.8;
normalizeTangents = true;
settings = new ModelDecimationSettings(
ModelDecimationDefaults.TriangleThreshold,
ModelDecimationDefaults.TargetRatio,
ModelDecimationDefaults.NormalizeTangents,
ModelDecimationDefaults.AvoidBodyIntersection,
new ModelDecimationAdvancedSettings());
var config = _performanceConfigService.Current;
if (!config.EnableModelDecimation)
@@ -330,15 +403,86 @@ public sealed class ModelDecimationService
return false;
}
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
targetRatio = config.ModelDecimationTargetRatio;
normalizeTangents = config.ModelDecimationNormalizeTangents;
if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
var advanced = NormalizeAdvancedSettings(config.ModelDecimationAdvanced);
settings = new ModelDecimationSettings(
Math.Max(0, config.ModelDecimationTriangleThreshold),
config.ModelDecimationTargetRatio,
config.ModelDecimationNormalizeTangents,
config.ModelDecimationAvoidBodyIntersection,
advanced);
return TryNormalizeSettings(settings, out settings);
}
private static bool TryNormalizeSettings(ModelDecimationSettings settings, out ModelDecimationSettings normalized)
{
var ratio = settings.TargetRatio;
if (double.IsNaN(ratio) || double.IsInfinity(ratio))
{
normalized = default;
return false;
}
ratio = Math.Clamp(ratio, MinTargetRatio, MaxTargetRatio);
var advanced = NormalizeAdvancedSettings(settings.Advanced);
normalized = new ModelDecimationSettings(
Math.Max(0, settings.TriangleThreshold),
ratio,
settings.NormalizeTangents,
settings.AvoidBodyIntersection,
advanced);
return true;
}
private static ModelDecimationAdvancedSettings NormalizeAdvancedSettings(ModelDecimationAdvancedSettings? settings)
{
var source = settings ?? new ModelDecimationAdvancedSettings();
return new ModelDecimationAdvancedSettings
{
MinComponentTriangles = Math.Clamp(source.MinComponentTriangles, 0, 1000),
MaxCollapseEdgeLengthFactor = ClampFloat(source.MaxCollapseEdgeLengthFactor, 0.1f, 10f, ModelDecimationAdvancedSettings.DefaultMaxCollapseEdgeLengthFactor),
NormalSimilarityThresholdDegrees = ClampFloat(source.NormalSimilarityThresholdDegrees, 0f, 180f, ModelDecimationAdvancedSettings.DefaultNormalSimilarityThresholdDegrees),
BoneWeightSimilarityThreshold = ClampFloat(source.BoneWeightSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBoneWeightSimilarityThreshold),
UvSimilarityThreshold = ClampFloat(source.UvSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultUvSimilarityThreshold),
UvSeamAngleCos = ClampFloat(source.UvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultUvSeamAngleCos),
BlockUvSeamVertices = source.BlockUvSeamVertices,
AllowBoundaryCollapses = source.AllowBoundaryCollapses,
BodyCollisionDistanceFactor = ClampFloat(source.BodyCollisionDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionDistanceFactor),
BodyCollisionNoOpDistanceFactor = ClampFloat(source.BodyCollisionNoOpDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpDistanceFactor),
BodyCollisionAdaptiveRelaxFactor = ClampFloat(source.BodyCollisionAdaptiveRelaxFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveRelaxFactor),
BodyCollisionAdaptiveNearRatio = ClampFloat(source.BodyCollisionAdaptiveNearRatio, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveNearRatio),
BodyCollisionAdaptiveUvThreshold = ClampFloat(source.BodyCollisionAdaptiveUvThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveUvThreshold),
BodyCollisionNoOpUvSeamAngleCos = ClampFloat(source.BodyCollisionNoOpUvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpUvSeamAngleCos),
BodyCollisionProtectionFactor = ClampFloat(source.BodyCollisionProtectionFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProtectionFactor),
BodyProxyTargetRatioMin = ClampFloat(source.BodyProxyTargetRatioMin, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyProxyTargetRatioMin),
BodyCollisionProxyInflate = ClampFloat(source.BodyCollisionProxyInflate, 0f, 0.1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProxyInflate),
BodyCollisionPenetrationFactor = ClampFloat(source.BodyCollisionPenetrationFactor, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionPenetrationFactor),
MinBodyCollisionDistance = ClampFloat(source.MinBodyCollisionDistance, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionDistance),
MinBodyCollisionCellSize = ClampFloat(source.MinBodyCollisionCellSize, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionCellSize),
};
}
private static float ClampFloat(float value, float min, float max, float fallback)
{
if (float.IsNaN(value) || float.IsInfinity(value))
{
return fallback;
}
return Math.Clamp(value, min, max);
}
private bool ShouldScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
{
if (string.IsNullOrWhiteSpace(filePath) || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
{
return false;
}
targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
if (!TryNormalizeSettings(settings, out _))
{
return false;
}
return true;
}