Fix low-opacity paint never reaching the target colour
Paint RTs were ARGB32 (8 bpp). Repeated source-over at low alpha rounds back to the previous value once the per-stroke decrement dst*alpha drops below half an LSB, so it freezes at a grey floor (~0.5/alpha in 255ths) - e.g. black at low opacity never reaches 0. Switch the accumulation buffers to ARGBHalf (16-bit linear). The sRGB round-trip stays 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. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f8b6524515
commit
fbbb4f7419
1 file changed
+38
-13
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Reads a RenderTexture back as Color32 (for the backend switch).</summary>
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user