diff --git a/Assets/Scripts/Components/MeshPainter/MeshPainter.Gpu.cs b/Assets/Scripts/Components/MeshPainter/MeshPainter.Gpu.cs
index 98474f1..e1aa12e 100644
--- a/Assets/Scripts/Components/MeshPainter/MeshPainter.Gpu.cs
+++ b/Assets/Scripts/Components/MeshPainter/MeshPainter.Gpu.cs
@@ -54,9 +54,9 @@ public partial class MeshPainter
var gm = new GpuMap
{
sRGB = !linear,
- display = CreateRT("MeshPainter_DisplayRT_" + mi, linear),
- temp = CreateRT("MeshPainter_TempRT_" + mi, linear),
- final = (mi == MAP_MSO) ? CreateRT("MeshPainter_FinalRT_" + mi, linear) : null
+ display = CreateRT("MeshPainter_DisplayRT_" + mi),
+ temp = CreateRT("MeshPainter_TempRT_" + mi),
+ final = (mi == MAP_MSO) ? CreateRT("MeshPainter_FinalRT_" + mi) : null
};
_gpu[mi] = gm;
@@ -134,10 +134,17 @@ public partial class MeshPainter
if (_packMat) { Destroy(_packMat); _packMat = null; }
}
- private RenderTexture CreateRT(string rtName, bool linear)
+ private RenderTexture CreateRT(string rtName)
{
- var rt = new RenderTexture(textureSize, textureSize, 0, RenderTextureFormat.ARGB32,
- linear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.sRGB)
+ // 16-bit half-float, NOT ARGB32: repeated low-opacity source-over only converges to the
+ // target colour if the accumulation buffer has sub-8-bit precision. At 8 bpp the per-stroke
+ // decrement dst*alpha rounds back to dst once it drops below half an LSB, so painting e.g.
+ // black at low opacity freezes at a grey floor (~0.5/alpha in 255ths) and never reaches 0.
+ // Half-float removes that floor. Storage is linear; the sRGB round-trip for colour maps
+ // happens at the byte boundary (seed decodes on upload, ReadRtPixels re-encodes on readback),
+ // so canonical bytes and CPU/GPU parity are unchanged - only the accumulation gained bits.
+ var rt = new RenderTexture(textureSize, textureSize, 0, RenderTextureFormat.ARGBHalf,
+ RenderTextureReadWrite.Linear)
{
name = rtName,
wrapMode = TextureWrapMode.Clamp,
@@ -161,21 +168,39 @@ public partial class MeshPainter
tmp.Apply(false);
bool prev = GL.sRGBWrite;
- GL.sRGBWrite = !linear;
+ // Paint RTs store LINEAR half-float, so never sRGB-encode on write. The source tex's own
+ // colour space (sRGB for colour maps) still decodes to linear when the blit samples it.
+ GL.sRGBWrite = false;
Graphics.Blit(tmp, rt);
GL.sRGBWrite = prev;
Destroy(tmp);
}
- /// Reads a RenderTexture back as Color32 (for the backend switch).
+ ///
+ /// Reads a paint RT back into 8-bit canonical bytes. The paint RTs are linear 16-bit half (for
+ /// blend precision), so this first blits down into a throwaway 8-bit RT using the SAME encoding
+ /// the old ARGB32 RTs had - sRGB for colour maps (GL.sRGBWrite re-encodes), linear for data maps -
+ /// then reads that. The resulting bytes are bit-for-bit what the old 8-bit path (and the CPU
+ /// backend) produced for the same colour; only the accumulation gained precision.
+ ///
private Color32[] ReadRtPixels(RenderTexture rt, bool linear)
{
- var prev = RenderTexture.active;
- RenderTexture.active = rt;
+ var prevActive = RenderTexture.active;
+
+ var tmp = RenderTexture.GetTemporary(textureSize, textureSize, 0, RenderTextureFormat.ARGB32,
+ linear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.sRGB);
+ bool prevSRGB = GL.sRGBWrite;
+ GL.sRGBWrite = !linear; // colour maps: re-encode linear->sRGB on write; data maps: linear
+ Graphics.Blit(rt, tmp);
+ GL.sRGBWrite = prevSRGB;
+
+ RenderTexture.active = tmp;
var tex = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
tex.ReadPixels(new Rect(0, 0, textureSize, textureSize), 0, 0, false);
tex.Apply(false);
- RenderTexture.active = prev;
+
+ RenderTexture.active = prevActive;
+ RenderTexture.ReleaseTemporary(tmp);
var px = tex.GetPixels32();
Destroy(tex);
return px;
@@ -206,7 +231,7 @@ public partial class MeshPainter
var gm = _gpu[mi];
Graphics.SetRenderTarget(gm.display);
- GL.sRGBWrite = gm.sRGB; // sRGB color map vs linear data map (MS)
+ GL.sRGBWrite = false; // linear half RTs: blend natively in linear, no sRGB encode
// Stamp placement is shared by every draw on this map.
_brushMat.SetTexture(MaskTexId, _stampMask);
@@ -279,7 +304,7 @@ public partial class MeshPainter
// Edge padding against UV seams: dilate the display RT into temp, then swap.
if (gm.drew && paddingTexels > 0)
{
- GL.sRGBWrite = gm.sRGB;
+ GL.sRGBWrite = false;
_dilateMat.SetInt(PadId, paddingTexels);
Graphics.Blit(gm.display, gm.temp, _dilateMat, 0);