MeshPainter: resample saved paint to avatar resolution on load
Drop the resolution gate in LoadPaintByName/ReadMapPng. A slot saved at any size is now bilinearly resampled per map to the current avatar's textureSize (new ResampleBilinear/Bilerp helpers, running on the stored bytes with no gamma conversion to match downstream consumption), so any painting loads onto any avatar regardless of the size it was saved at. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5aa1f976d3
commit
ee01b3b8ab
1 file changed
+56
-17
@@ -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); }
|
||||
}
|
||||
|
||||
/// <summary>Loads a map PNG into canonical (material) layout; false if missing / wrong resolution / undecodable.</summary>
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Bilinear blend of four byte samples (rounded, clamped to 0..255).</summary>
|
||||
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) ---
|
||||
|
||||
/// <summary>Writes every map's canonical Color32 bytes to the stream (shared by the net snapshot).</summary>
|
||||
|
||||
Reference in new issue
Block a user