Fur: Albedo-gekoppelte Länge, Toggle-Gate, Soft-Edge, World-Gravity, Pipette & FP-Hide

- Fur-Map trägt nur noch R = Länge (linearer Datenkanal); Farbe kommt per-texel
  aus der Live-Albedo-RT (an _BaseMap des Shell-Shaders gebunden)
- Toggle_Fur als echtes Gate (IsChannelActive = fur), Default aus, furLength 0.5
- Weicher Alpha-Übergang (radial/tip/length coverage) statt Hard-Clip; Transparent-
  Pass mit Depth-Bias (Offset -1,-1) gegen Body-Durchflimmern
- Root dunkler / Tip heller aus gemalter Farbe abgeleitet (kein Fix-Tint)
- Gravity/Curl in World-Space nach Transform (unabhängig von Mesh-Rotation)
- Pipette aktiviert Fur-Toggle immer; liest R-Kanal als Länge
- First-Person blendet FurShellRenderer mit aus
- Netzwerk (FlagFur), Undo, Persistence (fur.png R=Länge) mitgezogen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-03 23:40:58 +02:00
1 parent 7536e8ec8b
commit 1b783a39be
15 files changed
+254 -306

No files matched your search

@@ -36,8 +36,8 @@ public partial class MeshPainter
{
var src = ResolveSeed(mi, canonical);
var px = (Color32[])src.Clone();
// Albedo / emission are opaque. The mask map keeps alpha = smoothness; fur keeps alpha = painted
// length (0 = bald), which is data, not coverage.
// Albedo / emission are opaque. The mask map keeps alpha = smoothness; fur is a data map (R =
// length), so leave its channels as seeded rather than forcing alpha opaque.
if (mi != MAP_MSO && mi != MAP_FUR)
for (int i = 0; i < px.Length; i++) px[i].a = 255;
return px;
@@ -102,6 +102,8 @@ public partial class MeshPainter
/// </summary>
private void PaintStamp3D(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
{
// Fur is gated by its own toggle (see IsChannelActive), reusing the brush's alpha as the length
// deposit weight; the strand colour still comes from the albedo map at render time.
bool aAlb = paintAlbedo, aMS = paintMetallicSmoothness, aEM = paintEmission, aAO = paintOcclusion, aFur = fur;
if (!aAlb && !aMS && !aEM && !aAO && !aFur) return;
@@ -129,13 +131,9 @@ 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;
// Fur target: only R = length, eased toward furLength*255 at the brush's alpha (albOpMul) as the
// deposit weight - a linear data lerp (no sRGB), mirroring the GPU source-over on R.
float furTargetR = Mathf.Clamp01(furLength) * 255f;
CpuMap furm = _cpu[MAP_FUR];
float invR = 1f / radiusLocal;
@@ -258,20 +256,14 @@ public partial class MeshPainter
if (aFur)
{
float op = maskOp * furOpMul;
// Length (R) eases toward furLength at the brush weight: lerp(dst.R, target, op) -
// over-writable, not accumulating. Linear data, so no sRGB blend. G/B/A stay 0.
float op = maskOp * albOpMul;
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);
byte nr = (byte)(dst.r * (1f - op) + furTargetR * op + 0.5f);
furm.pixels[idx] = new Color32(nr, 0, 0, 0);
furm.painted[idx] = true;
ExpandDirty(furm, x, y);
}