diff --git a/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs b/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs
index 01747e0..ad7d61d 100644
--- a/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs
+++ b/Assets/Scripts/Components/MeshPainter/MeshPainter.Persistence.cs
@@ -176,15 +176,9 @@ public partial class MeshPainter
PaintMeta meta = ReadMeta(dir);
- // Resolution change -> ignore rather than smear mismatched data (the brush re-seeds from the
- // material instead). A missing/old sidecar has width 0 -> skip the header check and trust the PNGs
- // (ReadMapPng validates each image's resolution on its own).
- if (meta != null && meta.width > 0 && (meta.width != textureSize || meta.height != textureSize))
- {
- Debug.LogWarning($"[MeshPainter] Saved paint ignored (got {meta.width}x{meta.height}, expected " +
- $"{textureSize}x{textureSize}).", this);
- return false;
- }
+ // No resolution gate: a slot saved at any size is bilinearly resampled to this avatar's textureSize
+ // per map (see ReadMapPng), so any painting loads onto any avatar regardless of the size it was
+ // saved at. meta.width/height are kept only as informational metadata.
Color32[][] canonical = ReadFolderMaps(dir);
if (canonical == null) return false;
@@ -314,7 +308,11 @@ public partial class MeshPainter
finally { Destroy(tex); }
}
- /// Loads a map PNG into canonical (material) layout; false if missing / wrong resolution / undecodable.
+ ///
+ /// Loads a map PNG into canonical (material) layout; false only if the file is missing / undecodable.
+ /// A PNG whose resolution differs from the current textureSize is bilinearly resampled up/down to
+ /// textureSize (see ResampleBilinear), so a slot saved at any resolution loads onto any avatar.
+ ///
private bool ReadMapPng(string path, out Color32[] px)
{
px = null;
@@ -324,13 +322,9 @@ public partial class MeshPainter
try
{
if (!tex.LoadImage(File.ReadAllBytes(path))) return false;
- if (tex.width != textureSize || tex.height != textureSize)
- {
- Debug.LogWarning($"[MeshPainter] Saved map ignored (got {tex.width}x{tex.height}, expected " +
- $"{textureSize}x{textureSize}): {Path.GetFileName(path)}", this);
- return false;
- }
- px = tex.GetPixels32();
+ px = (tex.width == textureSize && tex.height == textureSize)
+ ? tex.GetPixels32()
+ : ResampleBilinear(tex.GetPixels32(), tex.width, tex.height, textureSize, textureSize);
return true;
}
catch (System.Exception e)
@@ -341,6 +335,51 @@ public partial class MeshPainter
finally { Destroy(tex); }
}
+ ///
+ /// Bilinearly resamples a raw Color32 byte grid to a new resolution. Interpolation runs on the stored
+ /// bytes with no gamma/linear conversion, matching how the maps are consumed downstream: colour maps
+ /// (albedo/emission) rescale smoothly, and the greyscale mask channels + the fur length channel
+ /// interpolate as linear scalars. Clamped edge sampling; used to load a slot saved at a different
+ /// resolution than the current avatar's textureSize.
+ ///
+ private static Color32[] ResampleBilinear(Color32[] src, int srcW, int srcH, int dstW, int dstH)
+ {
+ var dst = new Color32[dstW * dstH];
+ for (int y = 0; y < dstH; y++)
+ {
+ float sy = (y + 0.5f) * srcH / dstH - 0.5f;
+ int y0 = Mathf.Clamp(Mathf.FloorToInt(sy), 0, srcH - 1);
+ int y1 = Mathf.Min(y0 + 1, srcH - 1);
+ float fy = Mathf.Clamp01(sy - y0);
+ for (int x = 0; x < dstW; x++)
+ {
+ float sx = (x + 0.5f) * srcW / dstW - 0.5f;
+ int x0 = Mathf.Clamp(Mathf.FloorToInt(sx), 0, srcW - 1);
+ int x1 = Mathf.Min(x0 + 1, srcW - 1);
+ float fx = Mathf.Clamp01(sx - x0);
+
+ int r0 = y0 * srcW, r1 = y1 * srcW;
+ Color32 c00 = src[r0 + x0], c10 = src[r0 + x1];
+ Color32 c01 = src[r1 + x0], c11 = src[r1 + x1];
+
+ dst[y * dstW + x] = new Color32(
+ Bilerp(c00.r, c10.r, c01.r, c11.r, fx, fy),
+ Bilerp(c00.g, c10.g, c01.g, c11.g, fx, fy),
+ Bilerp(c00.b, c10.b, c01.b, c11.b, fx, fy),
+ Bilerp(c00.a, c10.a, c01.a, c11.a, fx, fy));
+ }
+ }
+ return dst;
+ }
+
+ /// Bilinear blend of four byte samples (rounded, clamped to 0..255).
+ private static byte Bilerp(byte c00, byte c10, byte c01, byte c11, float fx, float fy)
+ {
+ float top = c00 + (c10 - c00) * fx;
+ float bot = c01 + (c11 - c01) * fx;
+ return (byte)Mathf.Clamp(Mathf.RoundToInt(top + (bot - top) * fy), 0, 255);
+ }
+
// --- Shared canonical map (de)serialization: also used by the network snapshot path (MeshPainter.Net) ---
/// Writes every map's canonical Color32 bytes to the stream (shared by the net snapshot).