Fix frozen rim pixels via GPU coverage stamp; add brush masks

Replace the CPU island-mask dilation guard on the GPU backend with a
coverage pass rendered through the same brush rasterizer, so sub-pixel
rim texels that the GPU brush can never reach fall into the gutter and
stay paintable (no more frozen loaded pixels at island edges).

Also includes user content: additional brush masks (dirt) with an
expanded mask selector in the HUD prefab, and CharacterController /
wall-climb inspector tuning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-25 07:11:27 +02:00
1 parent c14525a2c8
commit fa7082817e
8 files changed
+986 -16

No files matched your search

+33 -8
View File
@@ -78,18 +78,46 @@ public partial class MeshPainter
if (mi == MAP_MSO)
{
// Canonical (R=metallic, G=occlusion, A=smoothness) -> work layout
// (R=metallic, G=smoothness, B=occlusion); UploadColorsToRT fills A = island coverage.
// (R=metallic, G=smoothness, B=occlusion). Alpha = coverage, stamped below.
var work = new Color32[src.Length];
for (int i = 0; i < src.Length; i++) work[i] = new Color32(src[i].r, src[i].a, src[i].g, 0);
UploadColorsToRT(work, gm.display, linear: true);
StampCoverage(gm.display);
PackMs(gm); // build the final mask RT once so the material shows the seeded values
}
else
{
UploadColorsToRT((Color32[])src.Clone(), gm.display, linear: gm.sRGB == false);
StampCoverage(gm.display);
}
}
/// <summary>
/// Stamps the dilation coverage guard into a map's alpha: rasterizes the mesh with the brush
/// shader's coverage pass (pass 1), writing alpha = 1 on exactly the texels the GPU brush can
/// paint. This replaces the CPU island-mask approximation, whose generous edge tolerance marked
/// sub-pixel island borders as protected even though the brush never reaches them - so they
/// could never be overpainted (stayed frozen at their seeded value) while the dilation guarded
/// them. With a GPU-accurate guard those border texels fall in the gutter (alpha 0) and dilation
/// overpaints them normally. Texels stay alpha 0 until reached, identical to the brush.
/// </summary>
private void StampCoverage(RenderTexture rt)
{
if (!_brushMat || !_bakedMesh) return;
var prev = RenderTexture.active;
Graphics.SetRenderTarget(rt);
if (_brushMat.SetPass(1))
{
int sub = _bakedMesh.subMeshCount;
for (int i = 0; i < sub; i++) Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i);
}
else
{
Debug.LogError("[MeshPainter] Brush coverage pass (pass 1) missing - dilation guard not seeded.", this);
}
RenderTexture.active = prev;
}
private void TeardownGpu()
{
if (_gpu != null)
@@ -124,16 +152,13 @@ public partial class MeshPainter
return rt;
}
/// <summary>Writes Color32 data into a RenderTexture via a temporary texture.</summary>
/// <summary>Writes Color32 RGB into a RenderTexture via a temporary texture. Alpha (the
/// dilation coverage marker) is cleared to 0 here and stamped afterwards by StampCoverage,
/// so the guard matches the GPU rasterizer exactly (see SeedGpuMap).</summary>
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool linear)
{
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
// Alpha is the dilation coverage marker: island texels (real content) = 255 so the
// dilate pass keeps them; the gutter = 0 so it stays paddable. Driving this from the UV
// islands (not a flat "unpainted") protects seeded content from the first stroke's bleed.
var island = GetIslandMask();
bool useMask = island.Length == colors.Length;
for (int i = 0; i < colors.Length; i++) colors[i].a = (useMask && island[i]) ? (byte)255 : (byte)0;
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
tmp.SetPixels32(colors);
tmp.Apply(false);