diff --git a/Plugins/Nya/Mesh.cs b/Plugins/Nya/Mesh.cs index c89de00..eed4100 100644 --- a/Plugins/Nya/Mesh.cs +++ b/Plugins/Nya/Mesh.cs @@ -78,7 +78,7 @@ private static (FaceFlags, Polygon) ConvertFace( faceFlag.NoLight = face.NoLight | settings.ModelType == NyaArguments.ModelTypes.NoLight; // Read polygon - Polygon polygon = Mesh.ConvertPolygon(face, faceFlag, group, modelTextures, !settings.NoUV, ref vertices, ref uvTextures); + Polygon polygon = Mesh.ConvertPolygon(face, faceFlag, group, modelTextures, !settings.NoUV, settings.TextureMergeThreshold, ref vertices, ref uvTextures); return (faceFlag, polygon); } @@ -91,6 +91,8 @@ private static (FaceFlags, Polygon) ConvertFace( /// Model object group /// Textures from model file textures /// Unwrap model textures by UV + /// Texture similarity threshold percentage (0.0 to 100.0) + /// above which 2 textures will be considered identical /// Model vertices /// Embed model vertices private static Polygon ConvertPolygon( @@ -99,6 +101,7 @@ private static Polygon ConvertPolygon( Group group, List modelTextures, bool unwrapTextures, + double textureMergeThreshold, ref List vertices, ref List uvTextures) { @@ -143,6 +146,10 @@ private static Polygon ConvertPolygon( if (texture != null) { + List finalUvs = new List(face.Uv); + List finalNormals = new List(face.Normals); + List finalVertices = new List(face.Vertices); + // Canonicalize quad UV ordering so GetUnwrap sees // [TL, TR, BR, BL] every time. Mirrored faces arrive here // with the same 4 UV points but traced in the opposite @@ -152,63 +159,50 @@ private static Polygon ConvertPolygon( // rotated 90° relative to the other side. // Padded triangles keep uv[2]==uv[3] and must not be // reordered by this pass. - if (wasQuad) - { - // Find corner closest to UV top-left (minU, maxV in V-up space). - double minU = face.Uv.Select(i => group.Uv[i].X).Min(); - double maxV = face.Uv.Select(i => group.Uv[i].Y).Max(); + List rawUvs = face.Uv.Select(i => group.Uv[i]).ToList(); + var canonicalizationResult = CanonicalizeFace(rawUvs, wasQuad); - int topLeft = 0; - double bestDistSq = double.MaxValue; + // Reorder everything according to the canonical indices + finalUvs.Clear(); + finalNormals.Clear(); + finalVertices.Clear(); - for (int i = 0; i < 4; i++) - { - Vector3D c = group.Uv[face.Uv[i]]; - double du = c.X - minU; - double dv = maxV - c.Y; - double d = (du * du) + (dv * dv); - - if (d < bestDistSq) - { - bestDistSq = d; - topLeft = i; - } - } - - // Cyclic shift so topLeft lands at index 0. - List rotatedUvs = new List(4); - List rotatedNormals = new List(4); - List rotatedVertices = new List(4); + for (int i = 0; i < 4; i++) + { + int originalIndex = canonicalizationResult.NewToOldIndices[i]; + finalUvs.Add(face.Uv[originalIndex]); + finalNormals.Add(face.Normals[originalIndex]); + finalVertices.Add(face.Vertices[originalIndex]); + } - for (int i = 0; i < 4; i++) - { - rotatedUvs.Add(face.Uv[(i + topLeft) % 4]); - rotatedNormals.Add(face.Normals[(i + topLeft) % 4]); - rotatedVertices.Add(face.Vertices[(i + topLeft) % 4]); - } + // Generate texture + TextureResult result = Mesh.GetUvMappedTexture(texture, finalUvs, group.Uv, wasQuad, textureMergeThreshold, ref uvTextures); + faceFlag.TextureId = result.TextureId; - // Check UV winding. For a CW quad [TL, TR, BR, BL] in - // V-up UV space, (uv[1]-uv[0]) × (uv[3]-uv[0]) has - // negative Z. Positive Z means CCW — swap indices 1↔3 - // to convert [TL, BL, BR, TR] → [TL, TR, BR, BL]. - Vector3D e01 = group.Uv[rotatedUvs[1]] - group.Uv[rotatedUvs[0]]; - Vector3D e03 = group.Uv[rotatedUvs[3]] - group.Uv[rotatedUvs[0]]; - double signedArea = (e01.X * e03.Y) - (e01.Y * e03.X); + // Reorder vertices depending on how the texture matched + if (result.VertexPermutation != null) + { + List newUvs = new List(4); + List newNormals = new List(4); + List newVertices = new List(4); - if (signedArea > 0.0) + for (int i = 0; i < 4; i++) { - (rotatedUvs[1], rotatedUvs[3]) = (rotatedUvs[3], rotatedUvs[1]); - (rotatedNormals[1], rotatedNormals[3]) = (rotatedNormals[3], rotatedNormals[1]); - (rotatedVertices[1], rotatedVertices[3]) = (rotatedVertices[3], rotatedVertices[1]); + int srcIdx = result.VertexPermutation[i]; + newUvs.Add(finalUvs[srcIdx]); + newNormals.Add(finalNormals[srcIdx]); + newVertices.Add(finalVertices[srcIdx]); } - face.Uv = rotatedUvs; - face.Normals = rotatedNormals; - face.Vertices = rotatedVertices; + finalUvs = newUvs; + finalNormals = newNormals; + finalVertices = newVertices; } - // Generate texture - faceFlag.TextureId = Mesh.GetUvMappedTexture(texture, face.Uv, group.Uv, ref uvTextures); + // Reinjection + face.Uv = finalUvs; + face.Normals = finalNormals; + face.Vertices = finalVertices; } else { @@ -284,54 +278,316 @@ private static Polygon ConvertPolygon( return polygon; } + /// + /// Specifies transforms that can be applied to a texture. + /// + [Flags] + public enum UvTransform + { + /// + /// Original texture, no transform applied. + /// + None = 0, + + /// + /// Texture is mirrored horizontally. + /// + HorizontalFlip = 1, + + /// + /// Texture is mirrored vertically. + /// + VerticalFlip = 2, + + /// + /// Texture is mirrored horizontally and vertically. + /// + Both = HorizontalFlip | VerticalFlip + } + + /// + /// Represents the detailed result of a UV mapping operation. + /// + /// + /// This object is returned by . + /// + public class TextureResult + { + /// + /// Gets or sets the identifier of the texture (either already existing or newly created) + /// within the UV texture atlas. + /// + public int TextureId { get; set; } + + /// + /// Gets or sets the vertex permutation array used to map vertices of a polygon to their canonical order. + /// + public int[]? VertexPermutation { get; set; } = null; + } + /// /// Get UV mapped texture from base texture /// /// Base texture /// UV coord indicies for quad /// All UV coords + /// False if the polygon was a triangle before being converted to a quad. + /// True if the polygon always was a Quad + /// Texture similarity threshold percentage (0.0 to 100.0) + /// above which 2 textures will be considered identical /// UV texture atlas /// Number of already existing or new texture - private static int GetUvMappedTexture(Texture baseTexture, List uv, List uvCoords, ref List uvTextures) + private static TextureResult GetUvMappedTexture( + Texture baseTexture, + List uv, + List uvCoords, + bool wasQuad, + double textureMergeThreshold, + ref List uvTextures) + { + List currentFaceUvs = uv.Select(coord => uvCoords[coord]).ToList(); + + // In order to detect which textures are the same we first check whether their shapes are similar (including mirror versions). + // Similar is defined as a % of their length/width + // (fall back to half a pixel tolerance to insure pixel perfect behavior in case of high merge threshold) + double minU = currentFaceUvs.Min(p => p.X); + double maxU = currentFaceUvs.Max(p => p.X); + double minV = currentFaceUvs.Min(p => p.Y); + double maxV = currentFaceUvs.Max(p => p.Y); + double faceWidth = maxU - minU; + double faceHeight = maxV - minV; + double toleranceFactor = (100.0 - textureMergeThreshold) / 100.0; + double uEpsilon = Math.Max(faceWidth * toleranceFactor, 0.5 / baseTexture.Width); + double vEpsilon = Math.Max(faceHeight * toleranceFactor, 0.5 / baseTexture.Height); + + int bestTextureId = -1; + double bestSimilarityScore = -1.0; + int[]? bestPermutation = null; + for (int i = 0; i < uvTextures.Count; i++) + { + Texture existingTexture = uvTextures[i]; + + if (existingTexture.GetBaseName() != baseTexture.Name) + { + continue; + } + + List existingUvs = existingTexture.UV.Select(id => uvCoords[id]).ToList(); + if (Mesh.IsUvSameShape(existingUvs, currentFaceUvs, wasQuad, uEpsilon, vEpsilon, + out UvTransform detectedTransform, + out int[]? currentToCanonicalVertexOrder) && currentToCanonicalVertexOrder is not null) + { + //The shapes are similar, now we reorder to vertices so the content of the textures can be compared + List reorderedCurrentUvs = new List(4); + + for (int j = 0; j < 4; j++) + { + reorderedCurrentUvs.Add(currentFaceUvs[currentToCanonicalVertexOrder[j]]); + } + + Texture currentUnwrap = Texture.GetUnwrap(baseTexture, reorderedCurrentUvs); + + //And we compare the content of the textures + double currentScore = currentUnwrap.CalculateSimilarityTo(existingTexture); + + if (currentScore >= textureMergeThreshold && currentScore > bestSimilarityScore) + { + bestSimilarityScore = currentScore; + bestTextureId = i; + bestPermutation = currentToCanonicalVertexOrder; + + if (bestSimilarityScore >= 100.0) + { + break; + } + } + } + } + + if (bestTextureId >= 0) + { + return new TextureResult { TextureId = bestTextureId, VertexPermutation = bestPermutation }; + } + + // No match with existing texture, we extract a new one + Texture newUnwrap = Texture.GetUnwrap(baseTexture, currentFaceUvs); + newUnwrap.UV = uv.ToArray(); + + int newId = uvTextures.Count; + uvTextures.Add(newUnwrap); + + return new TextureResult { TextureId = newId }; + } + + /// + /// Determines whether two sets of UV coordinates share the same geometric shape within a specified tolerance, + /// checking across multiple orientation configurations (default orientation, horizontal flip, vertical flip and both). + /// The reference list of UV coordinates to compare against. + /// The list of UV coordinates being evaluated for a potential match. + /// False if the polygon was a triangle before being converted to a quad. + /// True if the polygon always was a Quad + /// The maximum allowed absolute difference along the U (X) axis. + /// The maximum allowed absolute difference along the V (Y) axis. + /// When this method returns, contains the applied to achieve the match; + /// otherwise, UvTransform.None. + /// When this method returns, contains an array mapping the current vertices + /// to their canonical sequence if a match is found; otherwise, null. + /// true if matches the shape of under any tested transformation; otherwise, false. + private static bool IsUvSameShape( + List existingUvs, + List testedUvs, + bool wasQuad, + double uEpsilon, + double vEpsilon, + out UvTransform transform, + out int[]? currentToCanonicalVertexOrder) { - // Check if texture mapped to this region exists already (with a tolerance to a half pixel difference) - double uEpsilon = 0.5 / baseTexture.Width; - double vEpsilon = 0.5 / baseTexture.Height; - var createdFromBase = uvTextures.Select((texture, index) => new KeyValuePair(index, texture)).Where(texture => texture.Value.GetBaseName() == baseTexture.Name).ToList(); - var existing = createdFromBase - .Where(texture => texture.Value.UV.Select((id, i) => + transform = UvTransform.None; + currentToCanonicalVertexOrder = null; + + if (existingUvs.Count != testedUvs.Count) + { + return false; + } + + Vector3D originExistingUvs = existingUvs[0]; + List centeredExisting = existingUvs.Select(p => + new Vector3D(p.X - originExistingUvs.X, p.Y - originExistingUvs.Y, p.Z)).ToList(); + + var configs = new[] + { + (Transform: UvTransform.None, MirrorFunc: (Func)(p => p)), + (Transform: UvTransform.HorizontalFlip, MirrorFunc: (p => new Vector3D(-p.X, p.Y, p.Z))), + (Transform: UvTransform.VerticalFlip, MirrorFunc: (p => new Vector3D(p.X, -p.Y, p.Z))), + (Transform: UvTransform.Both, MirrorFunc: (p => new Vector3D(-p.X, -p.Y, p.Z))) + }; + + foreach (var cfg in configs) + { + List mirroredTestedUvs = testedUvs.Select(cfg.MirrorFunc).ToList(); + + var canonicalizationResult = CanonicalizeFace(mirroredTestedUvs, wasQuad); + List canonicalizedTestedUvs = canonicalizationResult.OrderedCoords; + + Vector3D originCanonicalizedTestedUvs = canonicalizedTestedUvs[0]; + List centeredCanonicalizedTestedUvs = canonicalizedTestedUvs.Select(p => + new Vector3D(p.X - originCanonicalizedTestedUvs.X, p.Y - originCanonicalizedTestedUvs.Y, p.Z)).ToList(); + + bool match = true; + for (int i = 0; i < centeredExisting.Count; i++) { - Vector3D currentUv = uvCoords[id]; - Vector3D targetUv = uvCoords[uv[i]]; - bool matchHorizontal = Math.Abs(currentUv.X - targetUv.X) < uEpsilon; - bool matchVertical = Math.Abs(currentUv.Y - targetUv.Y) < vEpsilon; - - return matchHorizontal && matchVertical; - }).All(val => val)) - .DefaultIfEmpty(new KeyValuePair(-1, baseTexture)) - .First().Key; - - // If not, generate new texture - if (existing < 0) + if (Math.Abs(centeredExisting[i].X - centeredCanonicalizedTestedUvs[i].X) > uEpsilon || + Math.Abs(centeredExisting[i].Y - centeredCanonicalizedTestedUvs[i].Y) > vEpsilon) + { + match = false; + break; + } + } + + if (match) + { + transform = cfg.Transform; + currentToCanonicalVertexOrder = canonicalizationResult.NewToOldIndices; + return true; + } + } + + return false; + } + + /// + /// Contains the result of a polygon face canonicalization operation. + /// + /// The newly ordered and normalized list of 3D coordinates. + /// An array mapping each new position index back to its original index in the source list. + public record CanonicalizationResult(List OrderedCoords, int[] NewToOldIndices); + + /// + /// Canonicalizes a polygon face by enforcing a consistent vertex order. + /// + /// The initial list of 3D vector coordinates representing the face vertices. + /// False if the polygon was a triangle before being converted to a quad. + /// True if the polygon always was a Quad + /// + /// A tuple containing: + /// + /// orderedCoords: The newly ordered and normalized list of coordinates. + /// originalIndices: An array mapping each new position back to its original index in . + /// + /// + private static CanonicalizationResult CanonicalizeFace(List rawCoords, bool wasQuad) + { + if (rawCoords.Count != 4) { - List coords = uv.Select(coord => uvCoords[coord]).ToList(); - Texture unwrap = Texture.GetUnwrap(baseTexture, coords); - unwrap.UV = uv.ToArray(); - existing = uvTextures.Count; + int[] identity = Enumerable.Range(0, rawCoords.Count).ToArray(); + return new CanonicalizationResult(new List(rawCoords), identity); + } + + int vertexCount = wasQuad? 4 : 3; + + // Find corner closest to UV top-left (minU, maxV in V-up space). + double minU = rawCoords.Min(p => p.X); + double maxV = rawCoords.Max(p => p.Y); - var found = createdFromBase.FindIndex(pair => pair.Value.Hash == unwrap.Hash); + // Cyclic shift so topLeft lands at index 0. + int topLeft = 0; + double bestDistSq = double.MaxValue; - if (found < 0) + for (int vertexID = 0; vertexID < vertexCount; vertexID++) + { + double du = rawCoords[vertexID].X - minU; + double dv = maxV - rawCoords[vertexID].Y; + double d = du * du + dv * dv; + + if (d < bestDistSq) { - uvTextures.Add(unwrap); + bestDistSq = d; + topLeft = vertexID; } - else + } + + int[] indices = new int[4]; + + for (int vertexID = 0; vertexID < vertexCount; vertexID++) + { + indices[vertexID] = (topLeft + vertexID) % vertexCount; + } + + List ordered = new List(4); + + for (int vertexID = 0; vertexID < vertexCount; vertexID++) + { + ordered.Add(rawCoords[indices[vertexID]]); + } + + if(!wasQuad) + { + indices[3] = indices[2]; + ordered.Add(ordered.Last()); + } + + // Check UV winding. For a CW quad [TL, TR, BR, BL] in + // V-up UV space, (uv[1]-uv[0]) × (uv[3]-uv[0]) has + // negative Z. Positive Z means CCW — swap indices 1↔3 + // to convert [TL, BL, BR, TR] → [TL, TR, BR, BL]. + Vector3D e01 = ordered[1] - ordered[0]; + Vector3D e03 = ordered[3] - ordered[0]; + double signedArea = (e01.X * e03.Y) - (e01.Y * e03.X); + + if (signedArea > 0.0) // CCW → swap 1 et 3 + { + (indices[1], indices[3]) = (indices[3], indices[1]); + (ordered[1], ordered[3]) = (ordered[3], ordered[1]); + + //Update the duplicated last vertex when we are dealing with a triangle polygon + if(!wasQuad) { - return createdFromBase[found].Key; + indices[2] = indices[3]; + ordered[2] = ordered[3]; } } - return existing; + return new CanonicalizationResult(ordered, indices); } /// diff --git a/Plugins/Nya/NyaArguments.cs b/Plugins/Nya/NyaArguments.cs index fb3f89b..bba85c5 100644 --- a/Plugins/Nya/NyaArguments.cs +++ b/Plugins/Nya/NyaArguments.cs @@ -44,5 +44,14 @@ public enum ModelTypes [CmdHelp("Makes exporter NOT generate new textures based on the UV map.")] [CmdArgument("no-unwrap", "w")] public bool NoUV { get; set; } + + /// + /// Gets or sets the texture similarity threshold percentage (0.0-100.0) above which 2 textures will be considered identical + /// as to reuse one texture in place of the other and therefore save space in memory. + /// + [CmdHelp("Texture similarity threshold percentage (0.0 to 100.0) above which 2 textures will be considered identical as to reuse one in place of the other and therefore save space in memory.\nDefault value is 100 (pixel perfect match).")] + [CmdArgument("texture-merge-threshold", "m")] + public double TextureMergeThreshold { get; set; } = 100.0; + } } \ No newline at end of file diff --git a/Plugins/Nya/Texture.cs b/Plugins/Nya/Texture.cs index 2ad5bb4..3679098 100644 --- a/Plugins/Nya/Texture.cs +++ b/Plugins/Nya/Texture.cs @@ -111,30 +111,6 @@ public Texture(string name, SLIS.Image bitmap) : this( [FieldOrder(0)] public ushort Width { get; set; } - /// - /// Image hash - /// - private string hash = string.Empty; - - /// - /// Gets image hash - /// - public string Hash - { - get - { - if (string.IsNullOrWhiteSpace(this.hash)) - { - using (var sha1 = System.Security.Cryptography.SHA1.Create()) - { - this.hash = string.Concat(sha1.ComputeHash(this.Data.SelectMany(pair => new byte[] { (byte)((pair >> 8) & 0xf), (byte)(pair & 0xf) }).ToArray()).Select(x => x.ToString("X2"))); - } - } - - return this.hash; - } - } - /// /// Get UV unwrap texture /// @@ -220,5 +196,237 @@ public string GetBaseName() return this.Name; } + + /// + /// Compares this texture with another one. + /// The similarity score is based on color similarity and gradient (average detail/edge strength) + /// between the 2 images and between sub parts of both images. + /// + /// The texture to compare to + /// A similarity score between 0.0 (completely different) and 100.0 (exactly the same) + public double CalculateSimilarityTo(Texture other) + { + if (other == null) + { + return 0.0; + } + + if (ReferenceEquals(this, other)) + { + return 100.0; + } + + return CalculateRecursiveSimilarity(this, other, + 0, 0, this.Width, this.Height, + 0, 0, other.Width, other.Height); + } + + /// + /// Recursive similarity calculation using sliding windows. + /// The first image from which a region is being compared + /// The second image from which a region is being compared + /// x coordinate of the top left corner of the region of the first image being compared + /// y coordinate of the top left corner of the region of the first image being compared + /// Width of the region of the first image being compared + /// Height of the region of the first image being compared + /// x coordinate of the top left corner of the region of the second image being compared + /// y coordinate of the top left corner of the region of the second image being compared + /// Width of the region of the second image being compared + /// Height of the region of the second image being compared + /// A similarity score between 0.0 (completely different) and 100.0 (exactly the same) + /// + private static double CalculateRecursiveSimilarity( + Texture img1, Texture img2, + int x1, int y1, int w1, int h1, // région courante sur img1 + int x2, int y2, int w2, int h2) // région courante sur img2 + { + if (w1 * h1 < 4 || w2 * h2 < 4) + { + var avg1 = GetAverageColor(img1, x1, y1, w1, h1); + var avg2 = GetAverageColor(img2, x2, y2, w2, h2); + return ColorSimilarity(avg1, avg2); + } + + // Mean color similarity + var avgFullImg1 = GetAverageColor(img1, x1, y1, w1, h1); + var avgFullImg2 = GetAverageColor(img2, x2, y2, w2, h2); + double colorSimilarity = ColorSimilarity(avgFullImg1, avgFullImg2); + + // Structure similarity via mean gradient + double grad1 = GetMeanGradient(img1, x1, y1, w1, h1); + double grad2 = GetMeanGradient(img2, x2, y2, w2, h2); + double gradientSimilarity = GradientSimilarity(grad1, grad2); + + // The deeper we go, the less relevant is structure similarity + double depthWeight = (w1 / img1.Width)/2; + double fullImgSimilarity = colorSimilarity * (1-depthWeight) + gradientSimilarity * (depthWeight); + + int midW1 = (w1 + 1) / 2; + int midH1 = (h1 + 1) / 2; + int midW2 = (w2 + 1) / 2; + int midH2 = (h2 + 1) / 2; + + // We do the same for each quadrant of the img + double tl = CalculateRecursiveSimilarity(img1, img2, x1, y1, midW1, midH1, x2, y2, midW2, midH2); + double tr = CalculateRecursiveSimilarity(img1, img2, x1 + midW1, y1, w1 - midW1, midH1, x2 + midW2, y2, w2 - midW2, midH2); + double bl = CalculateRecursiveSimilarity(img1, img2, x1, y1 + midH1, midW1, h1 - midH1, x2, y2 + midH2, midW2, h2 - midH2); + double br = CalculateRecursiveSimilarity(img1, img2, x1 + midW1, y1 + midH1, w1 - midW1, h1 - midH1, x2 + midW2, y2 + midH2, w2 - midW2, h2 - midH2); + + double subImgSimilarity = (tl + tr + bl + br) / 4.0; + + // Weights can be adjusted as long as their sum is equal to 1.0 + return (fullImgSimilarity * 0.2 + subImgSimilarity * 0.8); + } + + /// + /// Computes average RGB color of a rectangular region in the texture. + /// The texture in which a region is being processed + /// x coordinate of the top left corner of the region of the image + /// y coordinate of the top left corner of the region of the image + /// width of the region of the image + /// height of the region of the image + /// The average color of the given region of the texture + /// + private static (byte R, byte G, byte B) GetAverageColor(Texture texture, int startX, int startY, int width, int height) + { + long sumR = 0, sumG = 0, sumB = 0; + int count = 0; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int px = startX + x; + int py = startY + y; + + if (px < texture.Width && py < texture.Height) + { + ushort pixel = texture.Data[py * texture.Width + px]; + byte r = (byte)((pixel & 0x1F) << 3); + byte g = (byte)(((pixel >> 5) & 0x1F) << 3); + byte b = (byte)(((pixel >> 10) & 0x1F) << 3); + + sumR += r; + sumG += g; + sumB += b; + count++; + } + } + } + + if (count == 0) + { + return (0, 0, 0); + } + + return ( + (byte)(sumR / count), + (byte)(sumG / count), + (byte)(sumB / count) + ); + } + + /// + /// Similarity between two RGB colors (0.0 to 100.0). + /// Uses a simple RGB difference between the colors. + /// The first color being compared + /// The second color being compared + /// A similarity score between 0.0 and 100.0 + /// + private static double ColorSimilarity((byte R, byte G, byte B) c1, (byte R, byte G, byte B) c2) + { + double simR = (255.0 - Math.Abs(c1.R - c2.R)) / 255.0; + double simG = (255.0 - Math.Abs(c1.G - c2.G)) / 255.0; + double simB = (255.0 - Math.Abs(c1.B - c2.B)) / 255.0; + + double avgSim = (simR + simG + simB) / 3.0 * 100.0; + const double maxDelta = 8; //Difference above which we consider 0% similarity + + return Math.Max(0, 100 * ((avgSim - (100 - maxDelta)) / maxDelta)); + } + + /// + /// Calculate a similarity score between 0.0 and 100.0 between two gradient values. + /// The first gradient being compared + /// The second gradient being compared + /// A similarity score between 0.0 (completely different) and 100.0 (identical) + /// + private static double GradientSimilarity(double g1, double g2) + { + if (g1 == 0 && g2 == 0) + { + return 100.0; + } + + if (g1 == 0 || g2 == 0) + { + return 0.0; + } + + double ratio = Math.Min(g1, g2) / Math.Max(g1, g2); + + return ratio * 100.0; + } + + /// + /// Calculates the mean gradient (average detail/edge strength) of a region. + /// Higher value = more details/texture variation. + /// The texture in which a region is being processed + /// x coordinate of the top left corner of the region of the image + /// y coordinate of the top left corner of the region of the image + /// width of the region of the image + /// height of the region of the image + /// The mean gradient of the given region of the texture + /// + private static double GetMeanGradient(Texture texture, int startX, int startY, int width, int height) + { + if (width < 2 || height < 2) + { + return 0.0; + } + + long totalGradient = 0; + int count = 0; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int px = startX + x; + int py = startY + y; + + if (px >= texture.Width - 1 || py >= texture.Height - 1) + { + continue; + } + + ushort p1 = texture.Data[py * texture.Width + px]; // current pixel + ushort p2 = texture.Data[py * texture.Width + (px + 1)]; // right + ushort p3 = texture.Data[(py + 1) * texture.Width + px]; // below + + byte r1 = (byte)((p1 & 0x1F) << 3); + byte g1 = (byte)(((p1 >> 5) & 0x1F) << 3); + byte b1 = (byte)(((p1 >> 10) & 0x1F) << 3); + + byte r2 = (byte)((p2 & 0x1F) << 3); + byte g2 = (byte)(((p2 >> 5) & 0x1F) << 3); + byte b2 = (byte)(((p2 >> 10) & 0x1F) << 3); + + byte r3 = (byte)((p3 & 0x1F) << 3); + byte g3 = (byte)(((p3 >> 5) & 0x1F) << 3); + byte b3 = (byte)(((p3 >> 10) & 0x1F) << 3); + + // Simple luminance approximation + int lum1 = (r1 * 299 + g1 * 587 + b1 * 114) / 1000; + int lum2 = (r2 * 299 + g2 * 587 + b2 * 114) / 1000; + int lum3 = (r3 * 299 + g3 * 587 + b3 * 114) / 1000; + + totalGradient += Math.Abs(lum1 - lum2) + Math.Abs(lum1 - lum3); + count++; + } + } + + return count == 0 ? 0.0 : (double) totalGradient / count; + } } } \ No newline at end of file diff --git a/README.MD b/README.MD index 551019c..fd46ede 100644 --- a/README.MD +++ b/README.MD @@ -88,6 +88,7 @@ Argument | Description -------------|------------------------------------------------------ w, no-unwrap | Disable UV mapping preprocessor t, type | Specify object type
``-t NoLight`` = Not shaded
``-t Flat`` = Flat shaded
``-t Smooth`` = Smooth shaded model +m, texture-merge-threshold | Texture similarity threshold percentage (0.0 to 100.0) above which 2 textures will be considered identical as to reuse one in place of the other and therefore save space in memory. Defaults to pixel perfect threshold (100.0). ### Custom plugins Custom plugins can be also written by referencing the ModelConverter.dll and implementing the interfaces within. Custom plugin dll with its dependencies can than be put inside ``/plugins/[plugin name]/`` folder.