Fur als 4. Paint-Map + Ziel-Opacity-Blend

Fur wird die vierte kanonische Paint-Map (RGB=Farbe, A=Länge). Integration
über Seed (kahl), GPU/CPU-Paint, Undo, Netzwerk-Sync, Persistence (fur.png,
v6) und Live-RT-Hook im FurShellRenderer. HUD-Binding (Toggle/Picker/Alpha)
aktiviert. Fur-Alpha lerpt jetzt zu einer Ziel-Opacity (Slider_FurOpacity)
statt additiv Richtung 1 zu akkumulieren (zwei Brush-Passes: Fade + Deposit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-03 21:35:53 +02:00
1 parent 43791abc79
commit 7536e8ec8b
11 files changed
+319 -20

No files matched your search

@@ -14,6 +14,11 @@ public partial class MeshPainter
public bool sRGB; // sRGB color map (albedo/emission) vs linear data map (MSO)
}
// MeshPaintBrush.shader pass indices: 0 = source-over brush, 1 = coverage seed,
// 2 = fur fade (RGB + alpha*(1-op)), 3 = fur deposit (alpha += op*target).
private const int FurFadePass = 2;
private const int FurDepositPass = 3;
public Shader brushShader; // optional; else Shader.Find("Hidden/MeshPaintBrush")
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
public Shader packMsShader; // optional; else Shader.Find("Hidden/MeshPaintPackMS")
@@ -84,6 +89,13 @@ public partial class MeshPainter
StampCoverage(gm.display);
PackMs(gm); // build the final mask RT once so the material shows the seeded values
}
else if (mi == MAP_FUR)
{
// Fur: alpha IS data (strand length), not coverage. Preserve the seeded/loaded alpha and skip
// the coverage stamp, so unpainted texels stay bald (alpha 0) instead of being marked full
// length. (Dilation is also skipped for fur in EndGpuStroke, so the alpha guard never matters.)
UploadColorsToRT((Color32[])src.Clone(), gm.display, linear: gm.sRGB == false, preserveAlpha: true);
}
else
{
UploadColorsToRT((Color32[])src.Clone(), gm.display, linear: gm.sRGB == false);
@@ -159,11 +171,13 @@ public partial class MeshPainter
/// <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)
/// so the guard matches the GPU rasterizer exactly (see SeedGpuMap). preserveAlpha keeps the
/// source alpha instead - used by the fur map, whose alpha is painted length, not coverage.</summary>
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool linear, bool preserveAlpha = false)
{
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
if (!preserveAlpha)
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
tmp.SetPixels32(colors);
tmp.Apply(false);
@@ -251,10 +265,24 @@ public partial class MeshPainter
if (paintOcclusion)
StampMso(Mathf.Clamp01(occlusionOpacity), 3f); // B|A (occlusion only)
}
else if (mi == MAP_FUR)
{
// Fur alpha is DATA (strand length) that must ease toward furOpacity, not accumulate to 1.
// Fixed-function can't lerp-to-target in one draw, so it is split into two stamps into the
// SAME display RT (reference stays stable for FurMapTexture - no ping-pong):
// pass FurFade: RGB source-over toward furColor + alpha *= (1-op)
// pass FurDeposit: alpha += op * furOpacity
// Net alpha = (1-op)*dst + op*target = lerp(dst, target, op); op = luma*mask.a*furColor.a.
_brushMat.SetColor(BrushColorId, furColor);
_brushMat.SetFloat(FurTargetId, Mathf.Clamp01(furOpacity));
if (_brushMat.SetPass(FurFadePass)) DrawBakedMeshNow();
if (_brushMat.SetPass(FurDepositPass)) DrawBakedMeshNow();
}
else
{
// Albedo / emission: full RGBA, opacity carried by the colour's own alpha.
_brushMat.SetColor(BrushColorId, mi == MAP_ALBEDO ? brushColor : emissionColor);
// Albedo / emission: full RGBA, opacity carried by the colour's own alpha (source-over).
Color paintCol = mi == MAP_ALBEDO ? brushColor : emissionColor;
_brushMat.SetColor(BrushColorId, paintCol);
_brushMat.SetFloat(ColorWriteMaskId, 15f);
DrawStamp();
}
@@ -301,8 +329,10 @@ public partial class MeshPainter
if (!IsChannelActive(mi)) continue;
var gm = _gpu[mi];
// Edge padding against UV seams: dilate the display RT into temp, then swap.
if (gm.drew && paddingTexels > 0)
// Edge padding against UV seams: dilate the display RT into temp, then swap. Skipped for fur:
// its alpha is length (not a coverage guard), so dilation has no valid source mask and the
// display RT reference must stay stable for FurMapTexture (no ping-pong swap).
if (gm.drew && paddingTexels > 0 && mi != MAP_FUR)
{
GL.sRGBWrite = false;
_dilateMat.SetInt(PadId, paddingTexels);