private void IdentifyCandidate(DumpFormat dumpFormat, ref MatchCandidate mc, Texture2D mcTex) { if (dumpFormat.usePeixotoCandidateIdentification) { IdentifyCandidatePeixoto(dumpFormat, ref mc, mcTex); return; } if (mcTex.GetPixel(0, 0).a < 0.01f || mcTex.GetPixel(mcTex.width - 1, 0).a < 0.01f) { if (dumpFormat.bgParts.Length == 0) { mc.bgPartIndex = 1; } else { mc.bgPartIndex = dumpFormat.bgParts.Length; } mc.isMask = true; } else { //TODO - What if there is no part? (Full bg only) //TODO - What if different bg parts have the same size? For now, too bad... :) if (dumpFormat.bgParts.Length == 0) { //Well we already know it's not a mask so we might be able to assume that it's a BG texture, right? ^^' mc.bgPartIndex = -1; } else { for (var j = 0; j < dumpFormat.bgParts.Length; j++) { if (mcTex.width != dumpFormat.bgParts[j].size.x || mcTex.height != dumpFormat.bgParts[j].size.y) { continue; } mc.bgPartIndex = j; break; } } } }
private void IdentifyCandidatePeixoto(DumpFormat dumpFormat, ref MatchCandidate mc, Texture2D mcTex) { //As all the textures are squared and the BG ones are 2 256x256 textures, // I can quickly check that as an early identification of mask textures. if (mcTex.width != 256) { mc.bgPartIndex = 2; mc.isMask = true; return; } var bottomLeftColors = mcTex.GetPixels(0, 0, 16, 16); var avgColor = bottomLeftColors.GetAverage(); var isBgPart = avgColor.Compare(peixotoBgBandColor) >= 0.999f; if (isBgPart) { var topRightColors = mcTex.GetPixels(mcTex.width - 32, mcTex.height - 32, 32, 32); avgColor = topRightColors.GetAverage(); var isRightBgPart = avgColor.Compare(peixotoRightBgColor) >= 0.999f; mc.isMask = false; mc.bgPartIndex = isRightBgPart ? 1 : 0; } else { mc.bgPartIndex = 2; mc.isMask = true; } //sRGB or Linear doesn't seem to make any difference for these old assets. // Let's keep the unity default, sRGB. // var name = Path.GetFileNameWithoutExtension(fm.fileInfos[mc.fileInfoIndices[0]].Name); // var testSrgb = new Texture2D(16, 16, TextureFormat.RGBA32, false, false); // testSrgb.SetPixels(bottomLeftColors); // fm.SaveTextureToPng(testSrgb, "./", $"{name}_srgb"); // Object.Destroy(testSrgb); // var testLinear = new Texture2D(16, 16, TextureFormat.RGBA32, false, true); // testLinear.SetPixels(bottomLeftColors); // fm.SaveTextureToPng(testSrgb, "./", $"{name}_linear"); // Object.Destroy(testLinear); }
public IEnumerator RecreateTextures(string processedPath, string bgInfoPath, string alphaChannelPath, string resultsPath, DumpFormat dumpFormat, System.Action <ProgressInfo> progressCb, System.Action doneCb) { reportSb.Clear(); reportSb.AppendLine("== Texture recreation report =="); reportSb.AppendLine("== Textures recreation Started! =="); reportSb.AppendLine("Selected Format: " + dumpFormat.name); progressCb(new ProgressInfo("Loading Bg Infos", 0, 0, 0f)); yield return(new WaitForEndOfFrame()); yield return(new WaitForEndOfFrame()); taskTime = Time.unscaledTime; processedPath = Path.Combine(processedPath, game.ToString()); alphaChannelPath = Path.Combine(alphaChannelPath, game.ToString()); bgInfoPath = Path.Combine(bgInfoPath, game.ToString()); resultsPath = Path.Combine(resultsPath, dumpFormat.name); fm.CreateDirectory(resultsPath); var bgInfosCount = fm.LoadFiles(bgInfoPath, "json"); if (bgInfosCount <= 0) { reportSb.AppendLine("== Texture recreation aborted - No Bg Info =="); doneCb(); yield break; } var bgInfos = fm.GetObjectsFromFiles <BgInfo>(); var processedTexCount = fm.LoadFiles(processedPath, "png", SearchOption.AllDirectories); if (processedTexCount <= 0) { reportSb.AppendLine("== Texture recreation aborted - No Processed Texture =="); doneCb(); yield break; } progressCb(new ProgressInfo(string.Concat("Recreating Textures - ", bgInfos[0].namePrefix), 1, bgInfosCount, 0 / (float)(bgInfosCount - 1))); yield return(new WaitForEndOfFrame()); yield return(new WaitForEndOfFrame()); for (var i = 0; i < bgInfosCount; i++) { var bgInfo = bgInfos[i]; //Get the right processed background var expectedBgName = string.Concat(bgInfo.namePrefix, ".png"); var bgTexFileInfo = fm.fileInfos.FirstOrDefault(x => x.Name == expectedBgName); if (bgTexFileInfo == null) { reportSb.AppendLine(string.Concat("Missing upscaled BG texture: ", bgInfo.namePrefix)); continue; } progressCb(new ProgressInfo(string.Concat("Recreating Textures - ", bgInfo.namePrefix), i + 1, bgInfosCount, i / (float)(bgInfosCount - 1))); yield return(new WaitForEndOfFrame()); Texture2D processedTexAms = null; var processedTex = fm.GetTextureFromFileInfo(bgTexFileInfo); //float texRatioFloat = processedTex.width / (float)bgInfo.bgTexSize.x; var bgRatioFloat = processedTex.width / (float)bgInfo.bgTexSize.x; var maskRatioFloat = processedTex.width / (float)baseDumpFormat.maskUsageSize.x; if (bgRatioFloat - Mathf.Floor(bgRatioFloat) != 0f || maskRatioFloat - Mathf.Floor(maskRatioFloat) != 0f) { reportSb.AppendLine(string.Concat( "Error: This tool is not compatible with non integer scaling. Please fix this processed texture:", processedTex.name)); Object.Destroy(processedTex); continue; } var bgRatio = Mathf.RoundToInt(bgRatioFloat); var maskRatio = Mathf.RoundToInt(maskRatioFloat); CompensatePixelShift(dumpFormat.texPixelShift, processedTex, maskRatio); //Dynamic bg size only works for full background with no parts then... it's not good. //I really hope Resident evil 3 or something doesn't have backgrounds texture with different size AND splitted in some bullshit way. //If so I will need to analyze the background files during the matching phase and save all that data in the BG info instead. BgTexturePart[] bgParts; if (dumpFormat.bgParts == null || dumpFormat.bgParts.Length == 0) { var fullBgPart = new BgTexturePart(); fullBgPart.size = new Vector2Int(bgInfo.bgTexSize.x, bgInfo.bgTexSize.y); fullBgPart.patches = new[] { new Patch(0, 0, 0, 0, bgInfo.bgTexSize.x, bgInfo.bgTexSize.y) }; bgParts = new[] { fullBgPart }; } else { bgParts = dumpFormat.bgParts; } DumpMatch dumpMatch; if (bgInfo.texDumpMatches.Count(x => x.formatName == dumpFormat.name) <= 0) { dumpMatch = bgInfo.texDumpMatches.FirstOrDefault( x => x.formatName == dumpFormat.alternateFormatName); } else { dumpMatch = bgInfo.texDumpMatches.FirstOrDefault(x => x.formatName == dumpFormat.name); } if (string.IsNullOrEmpty(dumpMatch.formatName)) { Object.Destroy(processedTex); continue; } var matchGroupTexCount = bgParts.Length + (bgInfo.hasMask ? 1 : 0); //int matchGroupCount = dumpMatch.texNames.Length / matchGroupTexCount; //progressCb(new ProgressInfo(string.Concat(bgInfo.namePrefix, " - Recreating BG textures"), i + 1, bgInfosCount, i / (float)(bgInfosCount - 1))); //yield return new WaitForEndOfFrame(); //Generate the Bg textures for (var j = 0; j < bgParts.Length; j++) { var bgPartTex = new Texture2D( Mathf.RoundToInt(bgParts[j].size.x * bgRatio), Mathf.RoundToInt(bgParts[j].size.y * bgRatio), TextureFormat.RGBA32, false); bgPartTex.Fill(new Color32(0, 0, 0, 255)); for (var k = 0; k < bgParts[j].patches.Length; k++) { var p = bgParts[j].patches[k]; p.Scale(bgRatio); var pColors = processedTex.GetPixels( p.dstPos.x, p.dstPos.y, p.size.x, p.size.y); if (bgParts[j].needGapCompensation && k == 1) { bgPartTex.SetPixels( p.srcPos.x, p.srcPos.y + 1, p.size.x, p.size.y, pColors); } else { bgPartTex.SetPixels( p.srcPos.x, p.srcPos.y, p.size.x, p.size.y, pColors); } } for (var k = 0; k < dumpMatch.partIndices.Length; k++) { if (dumpMatch.partIndices[k] != j) { continue; } switch (dumpFormat.BgFormat) { case ImageFormat.Png: fm.SaveTextureToPng(bgPartTex, resultsPath, dumpMatch.texNames[k]); break; case ImageFormat.Jpg: fm.SaveTextureToJPG(bgPartTex, resultsPath, dumpMatch.texNames[k], dumpFormat.jpgQuality); break; case ImageFormat.DdsBc7: fm.SaveTextureToDds(bgPartTex, resultsPath, dumpMatch.texNames[k]); break; case ImageFormat.DdsBc3: fm.SaveTextureToDds(bgPartTex, resultsPath, dumpMatch.texNames[k]); break; } } Object.Destroy(bgPartTex); } //2. Generate the mask texture if (bgInfo.hasMask) { //progressCb(new ProgressInfo(string.Concat(bgInfo.namePrefix, " - Recreating mask"), i+1, bgInfosCount, i / (float)(bgInfosCount-1))); //yield return new WaitForEndOfFrame(); //One per group of masks var alphaChannels = new Texture2D[bgInfo.groupsCount]; var hasMissingAlphaChannels = false; for (var g = 0; g < bgInfo.groupsCount; g++) { alphaChannels[g] = fm.GetTextureFromPath(Path.Combine(alphaChannelPath, string.Concat(bgInfo.namePrefix, "_", g))); if (alphaChannels[g] != null) { continue; } reportSb.AppendLine(string.Concat(bgInfo.namePrefix, " is missing the alpha channel texture ", g, ".")); hasMissingAlphaChannels = true; } if (hasMissingAlphaChannels) { Object.Destroy(processedTex); reportSb.AppendLine(string.Concat("Missing alpha channel textures: ", bgInfo.namePrefix)); continue; } if (bgInfo.useProcessedMaskTex) { //Object.Destroy(processedTex); processedTexAms = fm.GetTextureFromPath(Path.Combine(processedPath, string.Concat(bgInfo.namePrefix, altMaskSourceSuffix))); if (processedTexAms == null) { processedTexAms = fm.GetTextureFromPath(Path.Combine(processedPath, "AMS", string.Concat(bgInfo.namePrefix, altMaskSourceSuffix))); if (processedTexAms == null) { reportSb.AppendLine(string.Concat("Missing special mask source textures: ", bgInfo.namePrefix)); continue; } } CompensatePixelShift(dumpFormat.texPixelShift, processedTexAms, maskRatio); } //Reconstruct the mask texture itself based on processed BG and the smoothed alpha texture var texWidth = Mathf.RoundToInt((dumpFormat.maskForcedSize.x != 0 ? dumpFormat.maskForcedSize.x : bgInfo.maskTexSize.x) * maskRatio); var texHeight = Mathf.RoundToInt((dumpFormat.maskForcedSize.x != 0 ? dumpFormat.maskForcedSize.y : bgInfo.maskTexSize.y) * maskRatio); var maskTex = new Texture2D(texWidth, texHeight, TextureFormat.RGBA32, false); maskTex.Fill(new Color32()); Texture2D alternateMaskTex = null; if (dumpFormat.isMonochromaticMask && bgInfo.useProcessedMaskTex) { alternateMaskTex = new Texture2D(texWidth, texHeight, TextureFormat.RGBA32, false); alternateMaskTex.Fill(new Color32()); } for (var j = 0; j < bgInfo.masks.Length; j++) { var mask = bgInfo.masks[j]; mask.patch.Scale(maskRatio); Color[] pColors; var isAltMaskPatch = false; if (bgInfo.useProcessedMaskTex == false || mask.ignoreAltMaskSource) { if (dumpFormat.isMonochromaticMask) { pColors = new Color[mask.patch.size.x * mask.patch.size.y]; for (var k = 0; k < pColors.Length; k++) { pColors[k] = Color.white; } } else { pColors = processedTex.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y, mask.patch.size.x, mask.patch.size.y); } } else { if (!dumpFormat.isMonochromaticMask) { pColors = processedTexAms.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y, mask.patch.size.x, mask.patch.size.y); } else { var amsHistogram = new Histogram(3, 16, 1f); var origHistogram = new Histogram(3, 16, 1f); pColors = processedTex.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y, mask.patch.size.x, mask.patch.size.y); origHistogram.AddValues(pColors); pColors = processedTexAms.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y, mask.patch.size.x, mask.patch.size.y); amsHistogram.AddValues(pColors); var histogramMatchValue = amsHistogram.Compare(origHistogram); isAltMaskPatch = histogramMatchValue < dumpFormat.monoMaskAmsHistogramMinMatchValue - Mathf.Epsilon; Debug.Log($"{bgInfo.namePrefix}_mask_{j}_{histogramMatchValue:0.00000}"); if (!isAltMaskPatch) { for (var k = 0; k < pColors.Length; k++) { pColors[k] = Color.white; } } else { var amsTestTex = new Texture2D(mask.patch.size.x, mask.patch.size.y, TextureFormat.RGBA32, false); amsTestTex.SetPixels(pColors); fm.SaveTextureToPng(amsTestTex, resultsPath, $"{bgInfo.namePrefix}_mask_{j}_{histogramMatchValue}"); } } } var aColors = alphaChannels[mask.groupIndex].GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y, mask.patch.size.x, mask.patch.size.y); for (var k = 0; k < pColors.Length; k++) { pColors[k].a = aColors[k].a; } var srcPosY = mask.patch.srcPos.y + (maskTex.height - bgInfo.maskTexSize.y * maskRatio); if (isAltMaskPatch) { alternateMaskTex.SetPixels(mask.patch.srcPos.x, srcPosY, mask.patch.size.x, mask.patch.size.y, pColors); } else { maskTex.SetPixels(mask.patch.srcPos.x, srcPosY, mask.patch.size.x, mask.patch.size.y, pColors); } } for (var j = 0; j < dumpMatch.partIndices.Length; j++) { if (dumpMatch.partIndices[j] == matchGroupTexCount - 1) { if (alternateMaskTex != null) { if (dumpFormat.MaskFormat == ImageFormat.Png) { fm.SaveTextureToPng(alternateMaskTex, resultsPath, $"{dumpMatch.texNames[j]}_alt"); } else { fm.SaveTextureToDds(alternateMaskTex, resultsPath, $"{dumpMatch.texNames[j]}_alt"); } } if (dumpFormat.MaskFormat == ImageFormat.Png) { fm.SaveTextureToPng(maskTex, resultsPath, dumpMatch.texNames[j]); } else { fm.SaveTextureToDds(maskTex, resultsPath, dumpMatch.texNames[j]); } } } //Clean up the mess :) //(and yes don't reuse the texture object, since other games might need dynamic texture size //AND texture.resize IS a hidden constructor too) Object.Destroy(maskTex); for (var g = 0; g < bgInfo.groupsCount; g++) { Object.Destroy(alphaChannels[g]); } } Object.Destroy(processedTex); Object.Destroy(processedTexAms); } yield return(new WaitForEndOfFrame()); reportSb.AppendLine(string.Format("== Textures recreation Done! ({0} seconds) ==", (Time.unscaledTime - taskTime).ToString("#.0"))); fm.OpenFolder(resultsPath); doneCb(); }
public IEnumerator MatchTextures(string bgInfoPath, string dumpTexPath, DumpFormat dumpFormat, TextureMatchingConfig config, System.Action <ProgressInfo> progressCb, System.Action doneCb) { if (dumpFormat.Equals(baseDumpFormat)) { Debug.LogWarning(string.Format( "The current base texture format ({0}) is the same as the selected texture format for matching ({1}). This is useless.", baseDumpFormat.name, dumpFormat.name)); doneCb(); yield break; } reportSb.Clear(); reportSb.AppendLine("== Texture Matching report =="); reportSb.AppendLine("== Texture Matching Started! =="); reportSb.AppendLine("Selected Format: " + dumpFormat.name); taskTime = Time.unscaledTime; progressCb(new ProgressInfo("Loading textures and BgInfos", 0, 0, 0f)); yield return(new WaitForEndOfFrame()); yield return(new WaitForEndOfFrame()); //Load all the bg infos for later bgInfoPath = Path.Combine(bgInfoPath, game.ToString()); var bgInfoCount = fm.LoadFiles(bgInfoPath, "json"); var bgInfos = fm.GetObjectsFromFiles <BgInfo>(); //Load all the file info of the textures to match var mcTexCount = fm.LoadFiles(Path.Combine(dumpTexPath, dumpFormat.name), "png", SearchOption.AllDirectories); var candidatesList = new List <MatchCandidate>(); var texDuplicatesCount = 0; var unmatchedTexCount = 0; var unmatchedMcCount = 0; //1. Prepare the MatchCandidates for (var i = 0; i < mcTexCount; i++) { if (i % 50 == 0) { progressCb( new ProgressInfo("Preparing candidates", i + 1, mcTexCount, i / (float)(mcTexCount - 1))); yield return(new WaitForEndOfFrame()); } var mcTex = fm.GetTextureFromFileIndex(i); var mc = new MatchCandidate(); mc.md5 = fm.GetMd5(mcTex.GetRawTextureData()); mc.fileInfoIndices = new int[] { i }; mc.texSize.x = mcTex.width; mc.texSize.y = mcTex.height; mc.bgInfoMatchIndex = new List <int>(); mc.bgInfoMatchValue = new List <float>(); //Check for duplicates var isDuplicate = false; for (var j = 0; j < candidatesList.Count; j++) { if (candidatesList[j].md5 == mc.md5) { reportSb.AppendLine(string.Concat(mcTex.name, " is a duplicate of ", fm.fileInfos[candidatesList[j].fileInfoIndices[0]].Name)); texDuplicatesCount++; //int[] indices = new int[candidatesList[j].fileInfoIndices.Length + 1]; //for (int k = 0; k < candidatesList[j].fileInfoIndices.Length; k++) //{ // indices[k] = candidatesList[j].fileInfoIndices[k]; //} //indices[indices.Length - 1] = j; var indices = candidatesList[j].fileInfoIndices.Concat(new int[] { i }).ToArray(); var duplicatedMc = candidatesList[j]; duplicatedMc.SetFileInfoIndices(indices); candidatesList[j] = duplicatedMc; isDuplicate = true; //It can't have another duplicate, impossible since I checked for every new element. break; } } if (isDuplicate) { continue; } //Identify the candidate IdentifyCandidate(dumpFormat, ref mc, mcTex); //RE3 WARNING - That doesn't WORK AT ALL ON RE3 GAMECUBE //Create a special patch for mask texture, thus It will only pick colors into the non fully transparent area. var partPatch = new Patch(); if (mc.isMask) { //For some dumping format like Peixoto Before, starting the matching process, // transparency needs to be added by replacing pure black pixels with transparent ones. if (dumpFormat.useBlackAsTransparent) { var colors = mcTex.GetPixels32(); for (int j = 0; j < colors.Length; j++) { var c = colors[j]; if (c.r + c.g + c.b <= byte.MinValue) { c.a = byte.MinValue; colors[j] = c; } } mcTex.SetPixels32(colors); mcTex.Apply(); // Save the texture with proper transparent pixels. // var fi = fm.fileInfos[i]; // Debug.Log(fi.FullName); // var testName = $"{Path.GetFileNameWithoutExtension(fi.Name)}"; // fm.SaveTextureToPng(mcTex, fi.DirectoryName, testName); } if (config.inconsistentMaskSize) { partPatch = new Patch(0, mcTex.height - 23, 0, mcTex.height - 23, 128, 23); } else { var line = mcTex.GetPixels(12, 0, 1, mcTex.height); var foundFlag = false; for (var k = 0; k < line.Length; k++) { if (line[k].a > 0.9f) { partPatch.srcPos.y = k; partPatch.dstPos.y = k; partPatch.size.y = mcTex.height - k; foundFlag = true; break; } } if (foundFlag == false || partPatch.size.y < config.histogramPatchSize.y) { unmatchedMcCount++; unmatchedTexCount += mc.fileInfoIndices.Length; reportSb.AppendLine(string.Concat(mcTex.name, " seems fully transparent mask. Match it manually.")); continue; } line = mcTex.GetPixels(0, mcTex.height - 4, mcTex.width, 1); foundFlag = false; for (var k = mcTex.width - 1; k >= 0; k--) { if (line[k].a > 0.9f) { partPatch.size.x = k + 1; foundFlag = true; break; } } if (foundFlag == false || partPatch.size.x < config.histogramPatchSize.x) { unmatchedMcCount++; unmatchedTexCount += mc.fileInfoIndices.Length; reportSb.AppendLine(string.Concat(mcTex.name, " seems fully transparent mask. Match it manually.")); continue; } } } //Generate the histogram data var histGenAttemptsCount = 0; var isMonochromatic = false; mc.histograms = new Histogram[config.histogramPatchCount]; mc.HistPatches = new Patch[config.histogramPatchCount]; //mc.histBgPartPatchIndices = new int[config.histogramPatchCount]; for (var j = 0; j < config.histogramPatchCount; j++) { if (mc.isMask == false) { if (mc.bgPartIndex != -1) { var bgPartPatchIndex = Random.Range(0, dumpFormat.bgParts[mc.bgPartIndex].patches.Length); partPatch = dumpFormat.bgParts[mc.bgPartIndex].patches[bgPartPatchIndex]; } else { partPatch = new Patch(0, 0, 0, 0, mc.texSize.x, mc.texSize.y); } } var p = partPatch; p.size = config.histogramPatchSize; var patchPos = partPatch.size - config.histogramPatchSize; patchPos.x = Random.Range(0, patchPos.x); patchPos.y = Random.Range(0, patchPos.y); p.Move(patchPos); var histogram = new Histogram(mc.isMask ? 4 : 3, config.histogramStepCount, 1f); var patchColors = mcTex.GetPixels(p.srcPos.x, p.srcPos.y, p.size.x, p.size.y); histogram.AddValues(patchColors, mc.isMask); //Compare the new histogram with the previous one, if there are the same the texture might be monochromatic... if (j > 0) { if (histogram.Compare(mc.histograms[mc.histograms.Length - 1]) >= 0.999f) { histGenAttemptsCount++; if (histGenAttemptsCount > config.histGenAttemptsMaxCount) { isMonochromatic = true; break; } j--; continue; } } if (config.savePatchTexures) { var test = new Texture2D(p.size.x, p.size.y); test.SetPixels(patchColors); fm.SaveTextureToPng(test, "./test", mcTex.name + "_" + j); Object.Destroy(test); } mc.tempHistogram = new Histogram(mc.isMask ? 4 : 3, config.histogramStepCount, 1f); mc.histograms[j] = histogram; mc.HistPatches[j] = p; } if (isMonochromatic) { unmatchedMcCount++; unmatchedTexCount += mc.fileInfoIndices.Length; reportSb.AppendLine(string.Concat(mcTex.name, " is too consistent visually to be analyzed properly. Match it manually.")); } else { if (mc.bgPartIndex == -1) { mc.bgPartIndex = 0; } //Finally add the candidate to the list candidatesList.Add(mc); } Object.Destroy(mcTex); } progressCb(new ProgressInfo("Preparing candidates", mcTexCount, mcTexCount, 1f)); yield return(new WaitForEndOfFrame()); //2. The match candidates are now all generated, we can go through all the BgInfo and for each BgInfo, going through all the MCs to find the best match. //For sure this way increase risk of false positives but it is also much faster than loading entire textures exponentially... var candidates = candidatesList.ToArray(); var baseDumpPath = Path.Combine(dumpTexPath, baseDumpFormat.name); //int mcMatchesCount = 0; var texMatchesCount = 0; for (var i = 0; i < bgInfoCount; i++) { //if (i % 10 == 0) //{ progressCb(new ProgressInfo("Looking for matches", i + 1, bgInfoCount, i / (float)(bgInfoCount - 1))); yield return(new WaitForEndOfFrame()); //} if (config.resetDumpMatches) { bgInfos[i].ResetDumpMatches(dumpFormat.name); } //Every candidate is matched, stop searching. //TODO - Track the best BgInfo into the MCandidate. This could be perfect. But requires to get rid of that and the isMatched flag stored into the MC. //if (mcMatchesCount >= candidates.Length) // break; var bgTex = fm.GetTextureFromPath(Path.Combine(baseDumpPath, bgInfos[i].texDumpMatches[0].texNames[0])); var maskTex = bgInfos[i].hasMask ? fm.GetTextureFromPath(Path.Combine(baseDumpPath, bgInfos[i].texDumpMatches[0].texNames[1])) : null; //TODO - max possible match for one bg info and one bg part, List suck ass and realistically I never saw 3 textures exactly the same (and there is no duplicate on GC) var bestMatchValues = new List <float> [dumpFormat.bgParts.Length == 0 ? 2: dumpFormat.bgParts.Length + 1]; var bestMatchCandidateIndices = new List <int> [dumpFormat.bgParts.Length == 0 ? 2: dumpFormat.bgParts.Length + 1]; for (var j = 0; j < bestMatchValues.Length; j++) { bestMatchValues[j] = new List <float>(); bestMatchCandidateIndices[j] = new List <int>(); } for (var j = 0; j < candidates.Length; j++) { //if (candidates[j].isMatched) // continue; var mc = candidates[j]; //Quick Pruning for mask, compare the size. Masks tend to have different size. if (mc.isMask) { if (maskTex == null) { continue; } //In RE3, the masks have a fixed resolution on GC and a variable on PC (cropped) if (config.inconsistentMaskSize) { if (maskTex.width < 128 || maskTex.height < 23) { continue; } } else { if (maskTex.width != mc.texSize.x || maskTex.height != mc.texSize.y) { continue; } } } else { //This BG texture size comparison was certainly added for re3 but why? //It's actually horrible for RE2 and 1. Thus I added a BG parts length check. if (dumpFormat.bgParts.Length <= 1 && (bgTex.width != mc.texSize.x || bgTex.height != mc.texSize.y)) { continue; } } var mcMatchValue = 0f; var isImpossibleMatch = false; for (var k = 0; k < mc.HistPatches.Length; k++) { var p = mc.HistPatches[k]; mc.tempHistogram.Reset(); Color[] pixels; if (mc.isMask) { if (config.inconsistentMaskSize) { var dstPosY = maskTex.height - 23 + (p.dstPos.y - (256 - 23)); pixels = maskTex.GetPixels(p.dstPos.x, dstPosY, p.size.x, p.size.y); mc.tempHistogram.AddValues(pixels, true); } else { pixels = maskTex.GetPixels(p.dstPos.x, p.dstPos.y, p.size.x, p.size.y); mc.tempHistogram.AddValues(pixels, true); } } else { pixels = bgTex.GetPixels(p.dstPos.x, p.dstPos.y, p.size.x, p.size.y); mc.tempHistogram.AddValues(pixels); } if (config.savePatchTexures) { var test = new Texture2D(p.size.x, p.size.y); test.SetPixels(pixels); fm.SaveTextureToPng(test, "./test", (mc.isMask ? maskTex.name : bgTex.name) + "_" + k); Object.Destroy(test); } var patchMatchValue = mc.histograms[k].Compare(mc.tempHistogram); //if the match value of one patch is SO BAD, you can stop here and move to the next MC. if (patchMatchValue <= config.patchMinMatchValue) { isImpossibleMatch = true; break; } mcMatchValue += patchMatchValue; } if (isImpossibleMatch) { continue; } mcMatchValue = mcMatchValue / mc.HistPatches.Length; //bool isFirstValue = bestMatchValues[mc.bgPartIndex].Count <= 0; if (mcMatchValue >= config.candidateMinMatchValue ) // && (isFirstValue || mcMatchValue >= bestMatchValues[mc.bgPartIndex][0])) { //To deal with multiple perfect match... but I haven't already identified duplicates via checksum? //if (!isFirstValue && mcMatchValue != bestMatchValues[mc.bgPartIndex][0]) //{ // bestMatchValues[mc.bgPartIndex].Clear(); //} bestMatchValues[mc.bgPartIndex].Add(mcMatchValue); bestMatchCandidateIndices[mc.bgPartIndex].Add(j); } } //Finally we store all the best matches found for that BG info //Again going through the bg info (and then the candidates) produces more false positives. //But the opposite is crazy bad in term of performance. An exponential amount of disk fetching. //For each part (typically 0 = Left, 1 = right, 2 mask) var partCount = bestMatchValues.Length; //bool hasMatches = false; for (var j = 0; j < partCount; j++) { var matchCount = bestMatchValues[j].Count; for (var k = 0; k < matchCount; k++) { //it should be defaulted to 0 anyway. if (bestMatchValues[j][k] >= config.candidateMinMatchValue) { //candidates[bestMatchCandidateIndices[j][k]].isMatched = true; var mc = candidates[bestMatchCandidateIndices[j][k]]; //for (int l = 0; l < mc.fileInfoIndices.Length; l++) //{ // string texName = fm.RemoveExtensionFromFileInfo(fm.fileInfos[mc.fileInfoIndices[l]]); // bgInfos[i].AddDumpMatch(dumpFormat.name, texName, mc.bgPartIndex); // //hasMatches = true; // texMatchesCount++; //} ////mcMatchesCount++; mc.bgInfoMatchValue.Add(bestMatchValues[j][k]); mc.bgInfoMatchIndex.Add(i); } } } //if (hasMatches) // fm.SaveToJson(bgInfos[i], bgInfoPath, bgInfos[i].GetFileName(), prettifyJsonOnSave); Object.Destroy(bgTex); Object.Destroy(maskTex); } //Analyze the Match candidates' possible bg info matches for (var i = 0; i < candidates.Length; i++) { var mc = candidates[i]; if (mc.bgInfoMatchIndex.Count <= 0) { //Report the unmatchable candidates unmatchedMcCount++; reportSb.Append(string.Concat("Candidate ", i, " can't find a match: ")); for (var j = 0; j < candidates[i].fileInfoIndices.Length; j++) { unmatchedTexCount++; reportSb.Append(string.Concat("[", fm.fileInfos[candidates[i].fileInfoIndices[j]].Name, "]")); } reportSb.AppendLine(); } else { var bestBgInfoMatchValue = 0f; var bestBgInfoIndex = 0; for (var j = 0; j < mc.bgInfoMatchIndex.Count; j++) { if (mc.bgInfoMatchValue[j] > bestBgInfoMatchValue) { bestBgInfoMatchValue = mc.bgInfoMatchValue[j]; bestBgInfoIndex = mc.bgInfoMatchIndex[j]; } } //Update and save the best bg info match for (var j = 0; j < mc.fileInfoIndices.Length; j++) { var texName = fm.RemoveExtensionFromFileInfo(fm.fileInfos[mc.fileInfoIndices[j]]); bgInfos[bestBgInfoIndex].AddDumpMatch(dumpFormat.name, texName, mc.bgPartIndex); texMatchesCount++; } reportSb.Append(string.Concat("Candidate ", i, " matched: ")); for (var j = 0; j < candidates[i].fileInfoIndices.Length; j++) { reportSb.Append(string.Concat("[", fm.fileInfos[candidates[i].fileInfoIndices[j]].Name, "]")); } reportSb.Append(string.Concat(" - ", bgInfos[bestBgInfoIndex].namePrefix)); reportSb.Append(string.Concat(" - ", bestBgInfoMatchValue.ToString("0.00"))); reportSb.AppendLine(); fm.SaveToJson(bgInfos[bestBgInfoIndex], bgInfoPath, bgInfos[bestBgInfoIndex].GetFileName(), prettifyJsonOnSave); } } reportSb.AppendLine(string.Format( "{0} matches for {1} textures ({2} duplicates). {3} unmatchable textures from {4} candidates.", texMatchesCount, mcTexCount, texDuplicatesCount, unmatchedTexCount, unmatchedMcCount)); reportSb.AppendLine(string.Format("== Texture Matching done! ({0} seconds) ==", (Time.unscaledTime - taskTime).ToString("#.0"))); fm.OpenFolder(bgInfoPath); doneCb(); }