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

@@ -14,10 +14,7 @@ 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;
// MeshPaintBrush.shader pass indices: 0 = source-over brush, 1 = coverage seed.
public Shader brushShader; // optional; else Shader.Find("Hidden/MeshPaintBrush")
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
@@ -91,10 +88,10 @@ public partial class MeshPainter
}
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);
// Fur R IS data (strand length), not coverage. Upload the seeded/loaded R verbatim (linear
// data map) and skip the coverage stamp, so unpainted texels stay bald (R 0) instead of being
// marked full length. (Dilation is also skipped for fur in EndGpuStroke.)
UploadColorsToRT((Color32[])src.Clone(), gm.display, linear: true);
}
else
{
@@ -169,15 +166,14 @@ public partial class MeshPainter
return rt;
}
/// <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). 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)
/// <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). Fur carries no coverage (its length lives in R), so a
/// zero alpha is correct there too.</summary>
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool linear)
{
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
if (!preserveAlpha)
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
tmp.SetPixels32(colors);
tmp.Apply(false);
@@ -267,16 +263,15 @@ public partial class MeshPainter
}
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();
// Fur stores ONLY strand length in R (the colour comes from the albedo map at render). Gated by
// its own toggle: the length eases toward furLength with the brush's alpha as the deposit
// weight. Reuse the source-over pass (0) on R only, so R = lerp(dst.R, furLength, op) with
// op = luma*mask.a*brushColor.a. SetVector + EncodeData (not SetColor) keeps furLength as raw
// linear data - the fur RT is a linear data map the shell samples directly for length.
float fl = EncodeData(furLength);
_brushMat.SetVector(BrushColorId, new Vector4(fl, fl, fl, Mathf.Clamp01(brushColor.a)));
_brushMat.SetFloat(ColorWriteMaskId, 8f); // R only (bits: R=8, G=4, B=2, A=1)
DrawStamp();
}
else
{