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

@@ -36,7 +36,9 @@ public partial class MeshPainter
{
var src = ResolveSeed(mi, canonical);
var px = (Color32[])src.Clone();
if (mi != MAP_MSO) // albedo / emission: opaque. The mask map keeps alpha = smoothness.
// Albedo / emission are opaque. The mask map keeps alpha = smoothness; fur keeps alpha = painted
// length (0 = bald), which is data, not coverage.
if (mi != MAP_MSO && mi != MAP_FUR)
for (int i = 0; i < px.Length; i++) px[i].a = 255;
return px;
}
@@ -66,7 +68,9 @@ public partial class MeshPainter
{
if (!IsChannelActive(mi)) continue;
var cm = _cpu[mi];
if (paddingTexels > 0) DilatePadding(cm, paddingTexels);
// Fur skips edge padding: its alpha is length, not a coverage guard, so there is no valid
// dilation source mask (matches the GPU path, which also skips fur in EndGpuStroke).
if (paddingTexels > 0 && mi != MAP_FUR) DilatePadding(cm, paddingTexels);
FlushDirty(cm);
}
}
@@ -98,8 +102,8 @@ public partial class MeshPainter
/// </summary>
private void PaintStamp3D(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
{
bool aAlb = paintAlbedo, aMS = paintMetallicSmoothness, aEM = paintEmission, aAO = paintOcclusion;
if (!aAlb && !aMS && !aEM && !aAO) return;
bool aAlb = paintAlbedo, aMS = paintMetallicSmoothness, aEM = paintEmission, aAO = paintOcclusion, aFur = fur;
if (!aAlb && !aMS && !aEM && !aAO && !aFur) return;
// Albedo target (brush color in linear space; blended linear to match the GPU).
float brLin = Mathf.GammaToLinearSpace(brushColor.r);
@@ -125,6 +129,15 @@ public partial class MeshPainter
float emOpMul = Mathf.Clamp01(emissionColor.a);
CpuMap em = _cpu[MAP_EM];
// Fur target (linear RGB like albedo; alpha = painted length that EASES toward furTargetA
// at the brush strength furColor.a, not accumulating to full - mirrors the GPU fade+deposit).
float frLin = Mathf.GammaToLinearSpace(furColor.r);
float fgLin = Mathf.GammaToLinearSpace(furColor.g);
float fbLin = Mathf.GammaToLinearSpace(furColor.b);
float furOpMul = Mathf.Clamp01(furColor.a);
float furTargetA = Mathf.Clamp01(furOpacity) * 255f;
CpuMap furm = _cpu[MAP_FUR];
float invR = 1f / radiusLocal;
int triCount = _tris.Length / 3;
float ts = textureSize;
@@ -243,6 +256,27 @@ public partial class MeshPainter
}
}
if (aFur)
{
float op = maskOp * furOpMul;
if (op > 0f)
{
Color32 dst = furm.pixels[idx];
float it = 1f - op;
// RGB source-over toward the fur colour (linear); alpha EASES toward the target
// length at the deposit rate: lerp(dst.a, target, op) - over-writable, not
// accumulating. Mirrors the GPU fur fade (alpha *= 1-op) + deposit (+= op*target).
byte na = (byte)(dst.a * it + furTargetA * op + 0.5f);
furm.pixels[idx] = new Color32(
BlendSrgb(dst.r, frLin, it, op),
BlendSrgb(dst.g, fgLin, it, op),
BlendSrgb(dst.b, fbLin, it, op),
na);
furm.painted[idx] = true;
ExpandDirty(furm, x, y);
}
}
}
}
}