diff --git a/README.md b/README.md index 12442c5..f201105 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ A BepInEx plugin that brings NVIDIA RTX Remix path tracing to most modern Unity - BepInEx 5.x - A Unity 2019+ game that uses MonoBleedingEdge (IL2CPP support will come later) +image + ## Installation 1. Download the latest [release](https://github.com/sambow23/UnityRTX/releases/latest) diff --git a/src/RemixMaterialManager.cs b/src/RemixMaterialManager.cs index a2a803b..32df88e 100644 --- a/src/RemixMaterialManager.cs +++ b/src/RemixMaterialManager.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.InteropServices; using System.Threading; +using BepInEx; using BepInEx.Logging; using UnityEngine; @@ -18,6 +20,7 @@ public class RemixMaterialManager private readonly BepInEx.Configuration.ConfigEntry captureMaterials; private readonly BepInEx.Configuration.ConfigEntry verboseTextureLogging; private readonly object apiLock; + private readonly string derivedTextureDumpDirectory; // Cached delegates private RemixAPI.PFN_remixapi_CreateTexture createTextureFunc; @@ -77,6 +80,7 @@ private static byte[] BuildZLookup() // Cache for tinted emissive textures - maps (texId ^ tintColorKey) to (handle, hash) private Dictionary tintedTextureCache = new Dictionary(); + private Dictionary derivedTextureCache = new Dictionary(); // Deferred texture upload queue — main thread prepares pixel data, render thread calls Remix API. // Prevents cross-thread DXVK lock contention between CreateTexture (s_mutex→devLock) and Present (devLock→submission). @@ -92,7 +96,9 @@ private struct PendingTextureUpload private readonly Queue pendingTextureUploads = new Queue(); private readonly HashSet pendingTextureIds = new HashSet(); private readonly HashSet pendingTintedKeys = new HashSet(); + private readonly HashSet pendingDerivedKeys = new HashSet(); private readonly object pendingTextureLock = new object(); + private readonly bool enableAlbedoDerivedPbr; // Alpha handling modes detected from Unity materials public enum AlphaMode @@ -123,6 +129,16 @@ public struct MaterialTextureData public Color emissiveColor; // HDR emission color (can exceed 1.0) public float emissiveIntensity; public bool useEmissiveBlend; // Use kAlphaEmissive blend instead of kAlpha + public IntPtr roughnessHandle; + public IntPtr metallicHandle; + public IntPtr heightHandle; + public ulong roughnessTextureHash; + public ulong metallicTextureHash; + public ulong heightTextureHash; + public float roughnessConstant; + public float metallicConstant; + public float displaceIn; + public float displaceOut; } private System.Collections.Concurrent.ConcurrentDictionary materialTextureData = new System.Collections.Concurrent.ConcurrentDictionary(); private HashSet loggedShaderProperties = new HashSet(); @@ -148,6 +164,9 @@ public RemixMaterialManager( this.captureMaterials = captureMaterials; this.verboseTextureLogging = verboseTextureLogging; this.apiLock = apiLock; + enableAlbedoDerivedPbr = Paths.GameRootPath.IndexOf("ULTRAKILL", StringComparison.OrdinalIgnoreCase) >= 0; + derivedTextureDumpDirectory = Path.Combine(Paths.BepInExRootPath, "UnityRemixDerivedTextures"); + Directory.CreateDirectory(derivedTextureDumpDirectory); // Cache delegates if (remixInterface.CreateTexture != IntPtr.Zero) @@ -284,7 +303,17 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp emissiveHandle = IntPtr.Zero, emissiveTextureHash = 0, emissiveColor = Color.black, - emissiveIntensity = 0f + emissiveIntensity = 0f, + roughnessHandle = IntPtr.Zero, + metallicHandle = IntPtr.Zero, + heightHandle = IntPtr.Zero, + roughnessTextureHash = 0, + metallicTextureHash = 0, + heightTextureHash = 0, + roughnessConstant = 0.5f, + metallicConstant = 0f, + displaceIn = 0f, + displaceOut = 0f }; // Get albedo color @@ -310,6 +339,16 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp { matData.alphaCutoff = material.GetFloat("_Cutoff"); } + + matData.metallicConstant = ReadFirstFloat(material, 0f, "_Metallic", "_Metalness", "_MetallicValue"); + float smoothness = ReadFirstFloat(material, -1f, "_Glossiness", "_Smoothness", "_SmoothnessValue", "_SpecularHighlights"); + if (smoothness >= 0f) + matData.roughnessConstant = 1f - Mathf.Clamp01(smoothness); + else + matData.roughnessConstant = Mathf.Clamp01(ReadFirstFloat(material, 0.5f, "_Roughness", "_RoughnessValue")); + matData.metallicConstant = Mathf.Clamp01(matData.metallicConstant); + matData.displaceIn = ReadFirstFloat(material, 0f, "_Parallax", "_HeightScale", "_DisplacementScale"); + matData.displaceOut = matData.displaceIn; // Detailed diagnostic log for every material if (verboseTextureLogging.Value) @@ -330,7 +369,7 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp // Read Unity wrap/filter modes from texture and convert to MDL values matData.wrapModeU = UnityWrapToMdl(tex.wrapModeU); matData.wrapModeV = UnityWrapToMdl(tex.wrapModeV); - matData.filterMode = (byte)(tex.filterMode == FilterMode.Point ? 0 : 1); + matData.filterMode = 0; matData.albedoHandle = UploadUnityTexture(tex, srgb: true); if (matData.albedoHandle != IntPtr.Zero) @@ -402,14 +441,15 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp bool hasActiveEmissionMap = material.HasProperty("_EmissionMap") && material.GetTexture("_EmissionMap") != null; if (material.IsKeywordEnabled("_EMISSION") || hasActiveEmissionMap) { + float maxChannel = 0f; if (material.HasProperty("_EmissionColor")) { matData.emissiveColor = material.GetColor("_EmissionColor"); - float maxChannel = Mathf.Max(matData.emissiveColor.r, Mathf.Max(matData.emissiveColor.g, matData.emissiveColor.b)); + maxChannel = Mathf.Max(matData.emissiveColor.r, Mathf.Max(matData.emissiveColor.g, matData.emissiveColor.b)); // Store 1.0 as intensity — CreateRemixMaterialSimple already folds maxChannel into // emCombinedIntensity (= maxCh * emissiveIntensity), so storing maxChannel here // would double it. The HDR brightness lives in the color, not the intensity. - matData.emissiveIntensity = maxChannel > 0f ? 1.0f : 0f; + matData.emissiveIntensity = maxChannel > 1.01f ? 1.0f : 0f; } if (hasActiveEmissionMap) { @@ -421,18 +461,20 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp matData.emissiveTextureHash = emHash; } } - hasEmission = matData.emissiveIntensity > 0f || matData.emissiveHandle != IntPtr.Zero; + hasEmission = matData.emissiveHandle != IntPtr.Zero || (matData.emissiveIntensity > 0f && maxChannel > 1.01f); } // ULTRAKILL/custom shader path: _EmissiveColor, _EmissiveTex, _EmissiveIntensity // Only emit if: toggle is on, OR a dedicated emission texture is assigned, OR MPB override is present if (!hasEmission && material.HasProperty("_EmissiveColor")) { - bool emissiveToggle = true; + bool emissiveToggle = false; if (material.HasProperty("EMISSIVE")) emissiveToggle = material.GetFloat("EMISSIVE") > 0.5f; - // A dedicated _EmissiveTex overrides the toggle (artist assigned a glow map) + bool likelyEmissiveMaterial = IsLikelyEmissiveMaterial(material, shaderName); + + // A dedicated _EmissiveTex overrides the toggle only for materials that are actually intended to emit bool hasEmissiveTex = false; if (material.HasProperty("_EmissiveTex")) hasEmissiveTex = material.GetTexture("_EmissiveTex") != null; @@ -440,19 +482,25 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp // MPB color override also triggers emission bool hasMpbOverride = mpbEmissiveColor.HasValue; - if (emissiveToggle || hasEmissiveTex || hasMpbOverride) + bool useAlbedoAsEmissive = material.HasProperty("_UseAlbedoAsEmissive") + && material.GetFloat("_UseAlbedoAsEmissive") > 0.5f + && matData.albedoHandle != IntPtr.Zero; + Color candidateEmissiveColor = mpbEmissiveColor ?? material.GetColor("_EmissiveColor"); + float candidateEmissiveIntensity = mpbEmissiveIntensity + ?? (material.HasProperty("_EmissiveIntensity") + ? material.GetFloat("_EmissiveIntensity") + : Mathf.Max(candidateEmissiveColor.r, Mathf.Max(candidateEmissiveColor.g, candidateEmissiveColor.b))); + float candidateEmissiveMax = Mathf.Max(candidateEmissiveColor.r, Mathf.Max(candidateEmissiveColor.g, candidateEmissiveColor.b)); + bool hasActiveEmissiveValues = candidateEmissiveMax > 1.01f || candidateEmissiveIntensity > 1.01f; + + if ((emissiveToggle && likelyEmissiveMaterial) || hasEmissiveTex || hasMpbOverride || useAlbedoAsEmissive || hasActiveEmissiveValues) { - matData.emissiveColor = mpbEmissiveColor ?? material.GetColor("_EmissiveColor"); - - if (mpbEmissiveIntensity.HasValue) - matData.emissiveIntensity = mpbEmissiveIntensity.Value; - else if (material.HasProperty("_EmissiveIntensity")) - matData.emissiveIntensity = material.GetFloat("_EmissiveIntensity"); - else - { - float maxCh = Mathf.Max(matData.emissiveColor.r, Mathf.Max(matData.emissiveColor.g, matData.emissiveColor.b)); - matData.emissiveIntensity = maxCh; - } + matData.emissiveColor = candidateEmissiveColor; + matData.emissiveIntensity = candidateEmissiveIntensity; + + float emissiveColorMax = Mathf.Max(matData.emissiveColor.r, Mathf.Max(matData.emissiveColor.g, matData.emissiveColor.b)); + if (!hasEmissiveTex && !hasMpbOverride && emissiveColorMax <= 1.01f && matData.emissiveIntensity <= 1.01f) + matData.emissiveIntensity = 0f; // Upload _EmissiveTex if present, pre-tinted by emission color if (hasEmissiveTex) @@ -467,10 +515,7 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp } // _UseAlbedoAsEmissive only when toggle is on (not just default property value) - if (emissiveToggle && matData.emissiveHandle == IntPtr.Zero - && material.HasProperty("_UseAlbedoAsEmissive") - && material.GetFloat("_UseAlbedoAsEmissive") > 0.5f - && matData.albedoHandle != IntPtr.Zero) + if (useAlbedoAsEmissive && matData.emissiveHandle == IntPtr.Zero) { if (albedoTex != null) { @@ -485,7 +530,13 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp } } - hasEmission = matData.emissiveIntensity > 0f && matData.emissiveHandle != IntPtr.Zero; + if (matData.emissiveHandle == IntPtr.Zero && matData.albedoHandle != IntPtr.Zero && hasActiveEmissiveValues) + { + matData.emissiveHandle = matData.albedoHandle; + matData.emissiveTextureHash = matData.albedoTextureHash; + } + + hasEmission = matData.emissiveIntensity > 0f && (matData.emissiveHandle != IntPtr.Zero || hasActiveEmissiveValues); } } @@ -613,25 +664,89 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp } // Upload normal map - if (captureTextures.Value && material.HasProperty("_BumpMap")) + if (captureTextures.Value) { - var tex = material.GetTexture("_BumpMap") as Texture2D; + var tex = GetFirstTexture(material, "_BumpMap", "_Normal", "_NormalMap", "_Normals"); if (tex != null) { - matData.normalHandle = UploadUnityTexture(tex, srgb: false, isNormalMap: true); + var (normalHandle, normalHash) = UploadOctahedralNormalTexture(tex); + matData.normalHandle = normalHandle; if (matData.normalHandle != IntPtr.Zero) { - int texId = tex.GetInstanceID(); - if (textureHashCache.TryGetValue(texId, out ulong hash)) - { - matData.normalTextureHash = hash; - } + matData.normalTextureHash = normalHash; if (verboseTextureLogging.Value) logger.LogInfo($"Captured normal texture for material '{material.name}' (hash: 0x{matData.normalTextureHash:X16})"); } } + else if (enableAlbedoDerivedPbr && albedoTex != null) + { + var (normalHandle, normalHash) = UploadAlbedoDerivedNormalTexture(albedoTex); + matData.normalHandle = normalHandle; + matData.normalTextureHash = normalHash; + } } + if (captureTextures.Value) + { + Texture2D metallicTex = GetFirstTexture(material, "_MetallicGlossMap", "_MetallicMap", "_MetalnessMap", "_MetallicTex"); + if (metallicTex != null) + { + matData.metallicHandle = UploadUnityTexture(metallicTex, srgb: false); + if (matData.metallicHandle != IntPtr.Zero) + { + int texId = metallicTex.GetInstanceID(); + if (textureHashCache.TryGetValue(texId, out ulong hash)) + matData.metallicTextureHash = hash; + } + } + else if (enableAlbedoDerivedPbr && albedoTex != null) + { + var (metalHandle, metalHash) = UploadAlbedoDerivedMetallicTexture(albedoTex, material.name); + matData.metallicHandle = metalHandle; + matData.metallicTextureHash = metalHash; + } + + Texture2D roughnessTex = GetFirstTexture(material, "_RoughnessMap", "_RoughnessTex"); + if (roughnessTex != null) + { + matData.roughnessHandle = UploadUnityTexture(roughnessTex, srgb: false); + if (matData.roughnessHandle != IntPtr.Zero) + { + int texId = roughnessTex.GetInstanceID(); + if (textureHashCache.TryGetValue(texId, out ulong hash)) + matData.roughnessTextureHash = hash; + } + } + else + { + Texture2D glossOrSpecTex = GetFirstTexture(material, "_SpecGlossMap", "_GlossMap", "_Specular", "_SpecularMap"); + if (glossOrSpecTex != null) + { + var (roughHandle, roughHash) = UploadDerivedRoughnessTexture(glossOrSpecTex); + matData.roughnessHandle = roughHandle; + matData.roughnessTextureHash = roughHash; + } + else if (enableAlbedoDerivedPbr && albedoTex != null) + { + var (roughHandle, roughHash) = UploadAlbedoDerivedRoughnessTexture(albedoTex, material.name); + matData.roughnessHandle = roughHandle; + matData.roughnessTextureHash = roughHash; + } + } + + Texture2D heightTex = GetFirstTexture(material, "_ParallaxMap", "_HeightMap", "_DisplacementMap", "_DisplacementTex"); + if (heightTex != null) + { + matData.heightHandle = UploadUnityTexture(heightTex, srgb: false); + if (matData.heightHandle != IntPtr.Zero) + { + int texId = heightTex.GetInstanceID(); + if (textureHashCache.TryGetValue(texId, out ulong hash)) + matData.heightTextureHash = hash; + } + } + } + // Store for later use materialTextureData[materialId] = matData; @@ -641,7 +756,54 @@ public void CaptureMaterialTextures(Material material, int materialId, Color? mp string albedoPath = GetTexturePathFromHandle(matData.albedoHandle); string normalPath = GetTexturePathFromHandle(matData.normalHandle); - logger.LogInfo($"[MatCapture] '{material.name}' shader='{material.shader?.name}' albedo={albedoPath ?? "NONE"} normal={normalPath ?? "none"}"); + string roughnessPath = GetTexturePathFromHandle(matData.roughnessHandle); + string metallicPath = GetTexturePathFromHandle(matData.metallicHandle); + string heightPath = GetTexturePathFromHandle(matData.heightHandle); + logger.LogInfo($"[MatCapture] '{material.name}' shader='{material.shader?.name}' albedo={albedoPath ?? "NONE"} normal={normalPath ?? "none"} roughnessMap={roughnessPath ?? "none"} metallicMap={metallicPath ?? "none"} heightMap={heightPath ?? "none"} roughness={matData.roughnessConstant:F3} metallic={matData.metallicConstant:F3}"); + } + + private static bool IsLikelyEmissiveMaterial(Material material, string shaderName) + { + string materialName = material.name ?? string.Empty; + if (shaderName.IndexOf("Emissive", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + if (shaderName.IndexOf("Emission", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + if (materialName.IndexOf("Emissive", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + if (materialName.IndexOf("Emission", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + if (materialName.IndexOf("Light", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + if (materialName.IndexOf("Lamp", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + if (materialName.IndexOf("Neon", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + return false; + } + + private static float ReadFirstFloat(Material material, float fallback, params string[] propertyNames) + { + foreach (string propertyName in propertyNames) + { + if (material.HasProperty(propertyName)) + return material.GetFloat(propertyName); + } + return fallback; + } + + private static Texture2D GetFirstTexture(Material material, params string[] propertyNames) + { + foreach (string propertyName in propertyNames) + { + if (!material.HasProperty(propertyName)) + continue; + + Texture2D texture = material.GetTexture(propertyName) as Texture2D; + if (texture != null) + return texture; + } + return null; } /// @@ -698,6 +860,7 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool { if (unityTexture == null || createTextureFunc == null) return IntPtr.Zero; + bool uploadAsSrgb = false; int texId = unityTexture.GetInstanceID(); @@ -788,9 +951,9 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool } hashSourceData = pixelData; - format = srgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_SRGB - : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM; - + format = uploadAsSrgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_SRGB + : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM; + actualMipLevels = 1; // GPU readback only gives top mip UnityEngine.Object.Destroy(readableTexture); } @@ -807,8 +970,8 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool pixelData[i * 4 + 3] = 255; } hashSourceData = pixelData; - format = srgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_SRGB - : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM; + format = uploadAsSrgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_SRGB + : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM; } else { @@ -819,20 +982,20 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool switch (unityTexture.format) { case TextureFormat.RGBA32: - format = srgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_SRGB - : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM; + format = uploadAsSrgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_SRGB + : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM; break; case TextureFormat.BGRA32: - format = srgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_B8G8R8A8_SRGB - : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_B8G8R8A8_UNORM; + format = uploadAsSrgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_B8G8R8A8_SRGB + : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_B8G8R8A8_UNORM; break; case TextureFormat.DXT1: - format = srgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_SRGB - : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_UNORM; + format = uploadAsSrgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_SRGB + : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_UNORM; break; case TextureFormat.DXT5: - format = srgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_SRGB - : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_UNORM; + format = uploadAsSrgb ? RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_SRGB + : RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_UNORM; break; default: logger.LogWarning($"Unsupported texture format: {unityTexture.format}"); @@ -857,7 +1020,11 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool { hasAlpha = SampleAlphaBC3(pixelData); } - // BC1 (DXT1) has only 1-bit punch-through alpha, treat as opaque + else if (format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_SRGB || + format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_UNORM) + { + hasAlpha = SampleAlphaBC1(pixelData); + } if (hasAlpha) { @@ -867,7 +1034,10 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool // This distinguishes real transparency (decal backgrounds at alpha=0) from // smoothness-as-alpha packing (Standard shader Opaque mode, values ~50-230). bool hasCutoutAlpha; - if (format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_SRGB || + if (format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_SRGB || + format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC1_RGB_UNORM) + hasCutoutAlpha = SampleCutoutAlphaBC1(pixelData); + else if (format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_SRGB || format == RemixAPI.remixapi_Format.REMIXAPI_FORMAT_BC3_UNORM) hasCutoutAlpha = SampleCutoutAlphaBC3(pixelData); else @@ -1114,6 +1284,473 @@ public IntPtr UploadUnityTexture(Texture2D unityTexture, bool srgb = true, bool return (IntPtr.Zero, 0); } } + + private (IntPtr handle, ulong hash) UploadDerivedRoughnessTexture(Texture2D tex) + { + if (tex == null || createTextureFunc == null) + return (IntPtr.Zero, 0); + + long cacheKey = ((long)tex.GetInstanceID() << 24) ^ 0x524F5547L; + ulong hash = BuildDerivedCacheHash(tex, "roughness"); + + lock (pendingTextureLock) + { + if (derivedTextureCache.TryGetValue(cacheKey, out var cached)) + return cached; + if (pendingDerivedKeys.Contains(cacheKey)) + return (new IntPtr((long)hash), hash); + } + + try + { + byte[] pixelData; + if (TryGetExistingDerivedTexture(tex, "roughness", hash, out string roughnessDumpPath, out ulong dumpedHash)) + { + hash = dumpedHash; + pixelData = ReadTgaRgba32(roughnessDumpPath, tex.width, tex.height); + if (verboseTextureLogging.Value) + logger.LogInfo($"Using existing dumped roughness texture for '{tex.name}' -> {roughnessDumpPath}"); + } + else + { + Color32[] pixels = ReadTexturePixels(tex); + pixelData = new byte[pixels.Length * 4]; + for (int i = 0; i < pixels.Length; i++) + { + float spec = Mathf.Max(pixels[i].r, Mathf.Max(pixels[i].g, pixels[i].b)) / 255f; + float smoothness = pixels[i].a < 250 ? pixels[i].a / 255f : spec; + float roughness = Mathf.Clamp(1f - smoothness, 0.18f, 0.92f); + byte r = (byte)(roughness * 255f); + pixelData[i * 4 + 0] = r; + pixelData[i * 4 + 1] = r; + pixelData[i * 4 + 2] = r; + pixelData[i * 4 + 3] = 255; + } + pixelData = DumpAndLoadDerivedTexture(tex.name, "roughness", hash, tex.width, tex.height, pixelData); + } + + QueueDerivedTextureUpload(cacheKey, hash, pixelData, tex.width, tex.height); + return (new IntPtr((long)hash), hash); + } + catch (Exception ex) + { + logger.LogError($"Exception deriving roughness from '{tex.name}': {ex.Message}"); + return (IntPtr.Zero, 0); + } + } + + private (IntPtr handle, ulong hash) UploadOctahedralNormalTexture(Texture2D tex) + { + if (tex == null || createTextureFunc == null) + return (IntPtr.Zero, 0); + + long cacheKey = ((long)tex.GetInstanceID() << 24) ^ 0x4F43544EL; + ulong hash = BuildDerivedCacheHash(tex, "octnormal"); + + lock (pendingTextureLock) + { + if (derivedTextureCache.TryGetValue(cacheKey, out var cached)) + return cached; + if (pendingDerivedKeys.Contains(cacheKey)) + return (new IntPtr((long)hash), hash); + } + + try + { + byte[] pixelData; + if (TryGetExistingDerivedTexture(tex, "octnormal", hash, out string normalDumpPath, out ulong dumpedHash)) + { + hash = dumpedHash; + pixelData = ReadTgaRgba32(normalDumpPath, tex.width, tex.height); + if (verboseTextureLogging.Value) + logger.LogInfo($"Using existing dumped octnormal texture for '{tex.name}' -> {normalDumpPath}"); + } + else + { + Color32[] pixels = ReadTexturePixels(tex); + bool dxt5nm = tex.format == TextureFormat.DXT5 || tex.format == TextureFormat.DXT5Crunched; + pixelData = new byte[pixels.Length * 4]; + + for (int i = 0; i < pixels.Length; i++) + { + float nx = dxt5nm ? pixels[i].a / 127.5f - 1f : pixels[i].r / 127.5f - 1f; + float ny = pixels[i].g / 127.5f - 1f; + nx = -nx; + ny = -ny; + + float nz = Mathf.Sqrt(Mathf.Max(0f, 1f - nx * nx - ny * ny)); + float invL1 = 1f / (Mathf.Abs(nx) + Mathf.Abs(ny) + Mathf.Abs(nz) + 1e-6f); + float ox = nx * invL1; + float oy = ny * invL1; + + if (nz < 0f) + { + float oldX = ox; + ox = (1f - Mathf.Abs(oy)) * Mathf.Sign(oldX); + oy = (1f - Mathf.Abs(oldX)) * Mathf.Sign(oy); + } + + pixelData[i * 4 + 0] = (byte)(Mathf.Clamp01(ox * 0.5f + 0.5f) * 255f); + pixelData[i * 4 + 1] = (byte)(Mathf.Clamp01(oy * 0.5f + 0.5f) * 255f); + pixelData[i * 4 + 2] = 255; + pixelData[i * 4 + 3] = 255; + } + pixelData = DumpAndLoadDerivedTexture(tex.name, "octnormal", hash, tex.width, tex.height, pixelData); + } + + QueueDerivedTextureUpload(cacheKey, hash, pixelData, tex.width, tex.height); + return (new IntPtr((long)hash), hash); + } + catch (Exception ex) + { + logger.LogError($"Exception deriving octahedral normal from '{tex.name}': {ex.Message}"); + return (IntPtr.Zero, 0); + } + } + + private (IntPtr handle, ulong hash) UploadAlbedoDerivedRoughnessTexture(Texture2D tex, string materialName) + { + if (tex == null || createTextureFunc == null) + return (IntPtr.Zero, 0); + + ulong nameHash = Fnv1a64(materialName ?? string.Empty); + long cacheKey = ((long)tex.GetInstanceID() << 24) ^ 0x414C4252L ^ (long)nameHash; + ulong hash = BuildDerivedCacheHash(tex, "albedorough") ^ nameHash; + + lock (pendingTextureLock) + { + if (derivedTextureCache.TryGetValue(cacheKey, out var cached)) + return cached; + if (pendingDerivedKeys.Contains(cacheKey)) + return (new IntPtr((long)hash), hash); + } + + try + { + Color32[] pixels = ReadTexturePixels(tex); + byte[] pixelData = new byte[pixels.Length * 4]; + bool environmentNamed = NameHas(materialName, "environment", "batch material environment"); + bool concreteNamed = NameHas(materialName, "concrete", "cement", "stone", "rock", "brick", "wall", "floor", "tile"); + bool metalNamed = NameHas(materialName, "metal", "steel", "iron", "silver", "chrome", "pipe", "nail", "saw", "blade", "weapon", "gun", "revolver", "shotgun", "railcannon", "barrel"); + bool viewModelNamed = NameHas(materialName, "v1", "arm", "hand", "view", "weapon", "gun", "nail", "saw", "blade", "revolver", "shotgun", "railcannon", "barrel"); + for (int i = 0; i < pixels.Length; i++) + { + float luminance = (pixels[i].r * 0.2126f + pixels[i].g * 0.7152f + pixels[i].b * 0.0722f) / 255f; + float maxCh = Mathf.Max(pixels[i].r, Mathf.Max(pixels[i].g, pixels[i].b)) / 255f; + float minCh = Mathf.Min(pixels[i].r, Mathf.Min(pixels[i].g, pixels[i].b)) / 255f; + float saturation = maxCh > 0.001f ? (maxCh - minCh) / maxCh : 0f; + float greyReflectMask = Mathf.Clamp01((maxCh - (viewModelNamed ? 0.22f : 0.42f)) / (viewModelNamed ? 0.28f : 0.38f)) * Mathf.Clamp01(((viewModelNamed ? 0.42f : 0.28f) - saturation) / (viewModelNamed ? 0.42f : 0.28f)); + float baseRoughness = Mathf.Clamp(0.85f - luminance * 0.25f - saturation * 0.10f, 0.35f, 0.95f); + if (environmentNamed) + baseRoughness = 0.85f; + if (concreteNamed) + baseRoughness = Mathf.Max(baseRoughness, 0.88f); + float reflectiveMask = metalNamed || viewModelNamed ? Mathf.Pow(greyReflectMask, 0.4f) : Mathf.Pow(greyReflectMask, 0.7f); + float roughness = Mathf.Lerp(baseRoughness, metalNamed || viewModelNamed ? 0.02f : 0.08f, concreteNamed || environmentNamed ? 0f : reflectiveMask); + byte r = (byte)(roughness * 255f); + pixelData[i * 4 + 0] = r; + pixelData[i * 4 + 1] = r; + pixelData[i * 4 + 2] = r; + pixelData[i * 4 + 3] = 255; + } + + pixelData = DumpAndLoadDerivedTexture($"{materialName}_{tex.name}", "albedorough", hash, tex.width, tex.height, pixelData); + QueueDerivedTextureUpload(cacheKey, hash, pixelData, tex.width, tex.height); + return (new IntPtr((long)hash), hash); + } + catch (Exception ex) + { + logger.LogError($"Exception deriving roughness from albedo '{tex.name}': {ex.Message}"); + return (IntPtr.Zero, 0); + } + } + + private (IntPtr handle, ulong hash) UploadAlbedoDerivedMetallicTexture(Texture2D tex, string materialName) + { + if (tex == null || createTextureFunc == null) + return (IntPtr.Zero, 0); + + ulong nameHash = Fnv1a64(materialName ?? string.Empty); + long cacheKey = ((long)tex.GetInstanceID() << 24) ^ 0x414C424DL ^ (long)nameHash; + ulong hash = BuildDerivedCacheHash(tex, "albedometal") ^ nameHash; + + lock (pendingTextureLock) + { + if (derivedTextureCache.TryGetValue(cacheKey, out var cached)) + return cached; + if (pendingDerivedKeys.Contains(cacheKey)) + return (new IntPtr((long)hash), hash); + } + + try + { + Color32[] pixels = ReadTexturePixels(tex); + byte[] pixelData = new byte[pixels.Length * 4]; + bool environmentNamed = NameHas(materialName, "environment", "batch material environment"); + bool concreteNamed = NameHas(materialName, "concrete", "cement", "stone", "rock", "brick", "wall", "floor", "tile"); + bool metalNamed = NameHas(materialName, "metal", "steel", "iron", "silver", "chrome", "pipe", "nail", "saw", "blade", "weapon", "gun", "revolver", "shotgun", "railcannon", "barrel"); + bool viewModelNamed = NameHas(materialName, "v1", "arm", "hand", "view", "weapon", "gun", "nail", "saw", "blade", "revolver", "shotgun", "railcannon", "barrel"); + for (int i = 0; i < pixels.Length; i++) + { + float maxCh = Mathf.Max(pixels[i].r, Mathf.Max(pixels[i].g, pixels[i].b)) / 255f; + float minCh = Mathf.Min(pixels[i].r, Mathf.Min(pixels[i].g, pixels[i].b)) / 255f; + float saturation = maxCh > 0.001f ? (maxCh - minCh) / maxCh : 0f; + float greyReflectMask = Mathf.Clamp01((maxCh - (viewModelNamed ? 0.22f : 0.42f)) / (viewModelNamed ? 0.28f : 0.38f)) * Mathf.Clamp01(((viewModelNamed ? 0.42f : 0.28f) - saturation) / (viewModelNamed ? 0.42f : 0.28f)); + float metallic = concreteNamed || environmentNamed ? 0f : Mathf.Pow(greyReflectMask, metalNamed || viewModelNamed ? 0.35f : 0.75f); + byte m = (byte)(metallic * 255f); + pixelData[i * 4 + 0] = m; + pixelData[i * 4 + 1] = m; + pixelData[i * 4 + 2] = m; + pixelData[i * 4 + 3] = 255; + } + + pixelData = DumpAndLoadDerivedTexture($"{materialName}_{tex.name}", "albedometal", hash, tex.width, tex.height, pixelData); + QueueDerivedTextureUpload(cacheKey, hash, pixelData, tex.width, tex.height); + return (new IntPtr((long)hash), hash); + } + catch (Exception ex) + { + logger.LogError($"Exception deriving metallic from albedo '{tex.name}': {ex.Message}"); + return (IntPtr.Zero, 0); + } + } + + private (IntPtr handle, ulong hash) UploadAlbedoDerivedNormalTexture(Texture2D tex) + { + if (tex == null || createTextureFunc == null) + return (IntPtr.Zero, 0); + + long cacheKey = ((long)tex.GetInstanceID() << 24) ^ 0x414C424EL; + ulong hash = BuildDerivedCacheHash(tex, "albedonormal"); + + lock (pendingTextureLock) + { + if (derivedTextureCache.TryGetValue(cacheKey, out var cached)) + return cached; + if (pendingDerivedKeys.Contains(cacheKey)) + return (new IntPtr((long)hash), hash); + } + + try + { + Color32[] pixels = ReadTexturePixels(tex); + byte[] pixelData = new byte[pixels.Length * 4]; + float strength = 2.0f; + + for (int y = 0; y < tex.height; y++) + { + for (int x = 0; x < tex.width; x++) + { + int i = y * tex.width + x; + float hL = AlbedoHeight(pixels[y * tex.width + Mathf.Max(0, x - 1)]); + float hR = AlbedoHeight(pixels[y * tex.width + Mathf.Min(tex.width - 1, x + 1)]); + float hD = AlbedoHeight(pixels[Mathf.Max(0, y - 1) * tex.width + x]); + float hU = AlbedoHeight(pixels[Mathf.Min(tex.height - 1, y + 1) * tex.width + x]); + float nx = (hL - hR) * strength; + float ny = (hD - hU) * strength; + float nz = 1f; + float len = Mathf.Sqrt(nx * nx + ny * ny + nz * nz); + nx /= len; + ny /= len; + nz /= len; + + float invL1 = 1f / (Mathf.Abs(nx) + Mathf.Abs(ny) + Mathf.Abs(nz) + 1e-6f); + float ox = nx * invL1; + float oy = ny * invL1; + pixelData[i * 4 + 0] = (byte)(Mathf.Clamp01(ox * 0.5f + 0.5f) * 255f); + pixelData[i * 4 + 1] = (byte)(Mathf.Clamp01(oy * 0.5f + 0.5f) * 255f); + pixelData[i * 4 + 2] = 255; + pixelData[i * 4 + 3] = 255; + } + } + + pixelData = DumpAndLoadDerivedTexture(tex.name, "albedonormal", hash, tex.width, tex.height, pixelData); + QueueDerivedTextureUpload(cacheKey, hash, pixelData, tex.width, tex.height); + return (new IntPtr((long)hash), hash); + } + catch (Exception ex) + { + logger.LogError($"Exception deriving normal from albedo '{tex.name}': {ex.Message}"); + return (IntPtr.Zero, 0); + } + } + + private byte[] DumpAndLoadDerivedTexture(string sourceName, string kind, ulong hash, int width, int height, byte[] pixelData) + { + try + { + string safeName = MakeSafeFileName(sourceName); + string path = Path.Combine(derivedTextureDumpDirectory, $"{hash:X16}_{kind}_{safeName}.tga"); + if (!File.Exists(path)) + WriteTgaRgba32(path, width, height, pixelData); + byte[] loadedPixels = ReadTgaRgba32(path, width, height); + if (verboseTextureLogging.Value) + logger.LogInfo($"Loaded derived {kind} texture '{sourceName}' -> {path}"); + return loadedPixels; + } + catch (Exception ex) + { + logger.LogWarning($"Failed to dump/load derived {kind} texture '{sourceName}': {ex.Message}"); + } + + return pixelData; + } + + private Color32[] ReadTexturePixels(Texture2D tex) + { + if (tex.isReadable) + return tex.GetPixels32(); + + RenderTexture tmp = RenderTexture.GetTemporary( + tex.width, tex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear); + RenderTexture previous = RenderTexture.active; + Graphics.Blit(tex, tmp); + RenderTexture.active = tmp; + Texture2D readable = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, false, true); + readable.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0); + readable.Apply(); + RenderTexture.active = previous; + RenderTexture.ReleaseTemporary(tmp); + Color32[] pixels = readable.GetPixels32(); + UnityEngine.Object.Destroy(readable); + return pixels; + } + + private static float AlbedoHeight(Color32 pixel) + { + return (pixel.r * 0.2126f + pixel.g * 0.7152f + pixel.b * 0.0722f) / 255f; + } + + private static bool NameHas(string value, params string[] terms) + { + if (string.IsNullOrEmpty(value)) + return false; + + for (int i = 0; i < terms.Length; i++) + { + if (value.IndexOf(terms[i], StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } + + return false; + } + + private static ulong Fnv1a64(string value) + { + ulong hash = 14695981039346656037UL; + for (int i = 0; i < value.Length; i++) + { + hash ^= char.ToLowerInvariant(value[i]); + hash *= 1099511628211UL; + } + return hash; + } + + private void QueueDerivedTextureUpload(long cacheKey, ulong hash, byte[] pixelData, int width, int height) + { + lock (pendingTextureLock) + { + if (derivedTextureCache.ContainsKey(cacheKey) || pendingDerivedKeys.Contains(cacheKey)) + return; + + pendingTextureUploads.Enqueue(new PendingTextureUpload + { + texId = -1, + tintedCacheKey = cacheKey, + pixelData = pixelData, + hash = hash, + width = (uint)width, + height = (uint)height, + mipLevels = 1, + format = RemixAPI.remixapi_Format.REMIXAPI_FORMAT_R8G8B8A8_UNORM + }); + pendingDerivedKeys.Add(cacheKey); + } + } + + private static void WriteTgaRgba32(string path, int width, int height, byte[] rgba) + { + byte[] data = new byte[18 + rgba.Length]; + data[2] = 2; + data[12] = (byte)(width & 0xFF); + data[13] = (byte)((width >> 8) & 0xFF); + data[14] = (byte)(height & 0xFF); + data[15] = (byte)((height >> 8) & 0xFF); + data[16] = 32; + data[17] = 0x28; + + for (int i = 0, o = 18; i < rgba.Length; i += 4, o += 4) + { + data[o + 0] = rgba[i + 2]; + data[o + 1] = rgba[i + 1]; + data[o + 2] = rgba[i + 0]; + data[o + 3] = rgba[i + 3]; + } + + File.WriteAllBytes(path, data); + } + + private static byte[] ReadTgaRgba32(string path, int width, int height) + { + byte[] data = File.ReadAllBytes(path); + byte[] rgba = new byte[width * height * 4]; + for (int i = 0, o = 18; i < rgba.Length; i += 4, o += 4) + { + rgba[i + 0] = data[o + 2]; + rgba[i + 1] = data[o + 1]; + rgba[i + 2] = data[o + 0]; + rgba[i + 3] = data[o + 3]; + } + return rgba; + } + + private static string MakeSafeFileName(string value) + { + if (string.IsNullOrEmpty(value)) + return "texture"; + + foreach (char c in Path.GetInvalidFileNameChars()) + value = value.Replace(c, '_'); + + return value.Length > 80 ? value.Substring(0, 80) : value; + } + + private bool TryGetExistingDerivedTexture(Texture2D tex, string kind, ulong hash, out string path, out ulong dumpedHash) + { + string safeName = MakeSafeFileName(tex != null ? tex.name : null); + path = Path.Combine(derivedTextureDumpDirectory, $"{hash:X16}_{kind}_{safeName}.tga"); + dumpedHash = hash; + if (File.Exists(path)) + return true; + + string pattern = $"*_{kind}_{safeName}.tga"; + string[] matches = Directory.GetFiles(derivedTextureDumpDirectory, pattern); + if (matches.Length > 0) + { + path = matches[0]; + string fileName = Path.GetFileName(path); + if (!string.IsNullOrEmpty(fileName) && fileName.Length >= 16 && ulong.TryParse(fileName.Substring(0, 16), System.Globalization.NumberStyles.HexNumber, null, out ulong existingHash)) + { + dumpedHash = existingHash; + return true; + } + return true; + } + + return false; + } + + private static ulong BuildDerivedCacheHash(Texture2D tex, string kind) + { + ulong hash = 14695981039346656037UL; + string key = $"{kind}|{tex.GetInstanceID()}|{tex.name}|{tex.width}x{tex.height}|{tex.format}"; + for (int i = 0; i < key.Length; i++) + { + hash ^= key[i]; + hash *= 1099511628211UL; + } + return hash == 0 ? 1UL : hash; + } /// /// Get texture hash string from handle (for material creation) @@ -1263,6 +1900,44 @@ private static bool SampleCutoutAlphaRGBA(byte[] pixelData, int alphaOffset, int // >= 10% of sampled pixels are near-transparent → genuine cutout return totalSampled > 0 && nearZeroCount * 10 >= totalSampled; } + + private static bool SampleAlphaBC1(byte[] rawData) + { + int blockCount = rawData.Length / 8; + int step = Math.Max(1, blockCount / 256); + for (int b = 0; b < blockCount; b += step) + { + int offset = b * 8; + ushort color0 = (ushort)(rawData[offset] | (rawData[offset + 1] << 8)); + ushort color1 = (ushort)(rawData[offset + 2] | (rawData[offset + 3] << 8)); + if (color0 <= color1) + return true; + } + return false; + } + + private static bool SampleCutoutAlphaBC1(byte[] rawData) + { + int blockCount = rawData.Length / 8; + if (blockCount == 0) return false; + + int sampleCount = Math.Min(blockCount, 1024); + int step = Math.Max(1, blockCount / sampleCount); + int cutoutBlockCount = 0; + int totalSampled = 0; + + for (int b = 0; b < blockCount; b += step) + { + int offset = b * 8; + ushort color0 = (ushort)(rawData[offset] | (rawData[offset + 1] << 8)); + ushort color1 = (ushort)(rawData[offset + 2] | (rawData[offset + 3] << 8)); + totalSampled++; + if (color0 <= color1) + cutoutBlockCount++; + } + + return totalSampled > 0 && cutoutBlockCount * 10 >= totalSampled; + } /// /// Check if a BC3/DXT5 texture has any non-trivial alpha by scanning alpha endpoint @@ -1359,7 +2034,11 @@ private IntPtr CreateRemixMaterialSimple(ref MaterialTextureData matData, int ma { if (createMaterialFunc == null) return IntPtr.Zero; - + + IntPtr roughnessPathPtr = IntPtr.Zero; + IntPtr metallicPathPtr = IntPtr.Zero; + IntPtr heightPathPtr = IntPtr.Zero; + try { string albedoPath = GetTexturePathFromHandle(matData.albedoHandle); @@ -1379,28 +2058,37 @@ private IntPtr CreateRemixMaterialSimple(ref MaterialTextureData matData, int ma } string emissivePath = GetTexturePathFromHandle(matData.emissiveHandle); + string roughnessPath = GetTexturePathFromHandle(matData.roughnessHandle); + string metallicPath = GetTexturePathFromHandle(matData.metallicHandle); + string heightPath = GetTexturePathFromHandle(matData.heightHandle); + if (roughnessPath != null) + roughnessPathPtr = Marshal.StringToHGlobalUni(roughnessPath); + if (metallicPath != null) + metallicPathPtr = Marshal.StringToHGlobalUni(metallicPath); + if (heightPath != null) + heightPathPtr = Marshal.StringToHGlobalUni(heightPath); if (verboseTextureLogging.Value) - logger.LogInfo($"[MaterialCreate] '{matData.materialName}': albedo={albedoPath ?? "none"}, normal={normalPath ?? "none"}, emissive={emissivePath ?? "none"}, emColor=({matData.emissiveColor.r:F3},{matData.emissiveColor.g:F3},{matData.emissiveColor.b:F3}), emIntensity={matData.emissiveIntensity:F3}, alphaMode={matData.alphaMode}"); + logger.LogInfo($"[MaterialCreate] '{matData.materialName}': albedo={albedoPath ?? "none"}, normal={normalPath ?? "none"}, roughness={roughnessPath ?? "none"}, metallic={metallicPath ?? "none"}, height={heightPath ?? "none"}, emissive={emissivePath ?? "none"}, emColor=({matData.emissiveColor.r:F3},{matData.emissiveColor.g:F3},{matData.emissiveColor.b:F3}), emIntensity={matData.emissiveIntensity:F3}, alphaMode={matData.alphaMode}"); // Build the OpaqueEXT using the blittable Raw struct for safe pNext chaining var opaqueExt = new RemixAPI.remixapi_MaterialInfoOpaqueEXT_Raw { sType = (int)RemixAPI.remixapi_StructType.REMIXAPI_STRUCT_TYPE_MATERIAL_INFO_OPAQUE_EXT, pNext = IntPtr.Zero, - roughnessTexture = IntPtr.Zero, - metallicTexture = IntPtr.Zero, + roughnessTexture = roughnessPathPtr, + metallicTexture = metallicPathPtr, anisotropy = 0.0f, albedoConstant_x = matData.albedoColor.r, albedoConstant_y = matData.albedoColor.g, albedoConstant_z = matData.albedoColor.b, opacityConstant = matData.albedoColor.a, - roughnessConstant = 0.5f, - metallicConstant = 0.0f, + roughnessConstant = matData.roughnessConstant, + metallicConstant = matData.metallicConstant, thinFilmThickness_hasvalue = 0, thinFilmThickness_value = 0.0f, alphaIsThinFilmThickness = 0, - heightTexture = IntPtr.Zero, - displaceIn = 0.0f, + heightTexture = heightPathPtr, + displaceIn = matData.displaceIn, useDrawCallAlphaState = 0, blendType_hasvalue = 0, blendType_value = 0, @@ -1408,7 +2096,7 @@ private IntPtr CreateRemixMaterialSimple(ref MaterialTextureData matData, int ma // VkCompareOp: 0=NEVER, 6=GREATER_OR_EQUAL, 7=ALWAYS alphaTestType = 7, // kAlways — default: pass all pixels (no alpha test) alphaReferenceValue = 0, - displaceOut = 0.0f + displaceOut = matData.displaceOut }; // Configure alpha based on detected mode @@ -1499,6 +2187,15 @@ private IntPtr CreateRemixMaterialSimple(ref MaterialTextureData matData, int ma logger.LogError($"Exception creating material: {ex.Message}"); return IntPtr.Zero; } + finally + { + if (roughnessPathPtr != IntPtr.Zero) + Marshal.FreeHGlobal(roughnessPathPtr); + if (metallicPathPtr != IntPtr.Zero) + Marshal.FreeHGlobal(metallicPathPtr); + if (heightPathPtr != IntPtr.Zero) + Marshal.FreeHGlobal(heightPathPtr); + } } /// @@ -1550,8 +2247,13 @@ public void ProcessPendingTextureUploads() } if (upload.tintedCacheKey != 0) { - tintedTextureCache[upload.tintedCacheKey] = (handle, upload.hash); - pendingTintedKeys.Remove(upload.tintedCacheKey); + if (pendingDerivedKeys.Remove(upload.tintedCacheKey)) + derivedTextureCache[upload.tintedCacheKey] = (handle, upload.hash); + else + { + tintedTextureCache[upload.tintedCacheKey] = (handle, upload.hash); + pendingTintedKeys.Remove(upload.tintedCacheKey); + } } } } @@ -1561,7 +2263,11 @@ public void ProcessPendingTextureUploads() lock (pendingTextureLock) { if (upload.texId >= 0) pendingTextureIds.Remove(upload.texId); - if (upload.tintedCacheKey != 0) pendingTintedKeys.Remove(upload.tintedCacheKey); + if (upload.tintedCacheKey != 0) + { + pendingTintedKeys.Remove(upload.tintedCacheKey); + pendingDerivedKeys.Remove(upload.tintedCacheKey); + } } } } @@ -1710,3 +2416,7 @@ public string[] GetPlaceholderMaterialNames() } } } + + + +