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:
1 parent
43791abc79
commit
7536e8ec8b
11 files changed
+319
-20
No files matched your search
@@ -53,7 +53,9 @@ namespace Chameleon.Components
|
||||
|
||||
[Header("Fur Map")]
|
||||
// RGBA fur map: RGB = per-texel strand colour, A = fur length (0 = bald, 1 = full length). Leave
|
||||
// null on the standalone prototype - the shader's white default is uniform full-length fur.
|
||||
// null on the standalone prototype - the shader's white default is uniform full-length fur. A
|
||||
// sibling MeshPainter's live fur RT overrides this when present, so a painted avatar grows fur in
|
||||
// real time (see PushProperties).
|
||||
[SerializeField] Texture _furMap;
|
||||
// Fur is fully cut where the map's alpha is at/below this - keeps stray dilation fuzz from growing
|
||||
// hair. 0 keeps the exact "alpha 0 = no fur" contract.
|
||||
@@ -101,6 +103,9 @@ namespace Chameleon.Components
|
||||
|
||||
MeshFilter _meshFilter;
|
||||
SkinnedColliderBaker _baker;
|
||||
// Optional sibling paint source: when present, its live fur RT (RGB = colour, A = length) overrides
|
||||
// the serialized _furMap so painting the fur map drives the shells in real time.
|
||||
MeshPainter _painter;
|
||||
Matrix4x4[] _instances;
|
||||
MaterialPropertyBlock _mpb;
|
||||
|
||||
@@ -110,6 +115,7 @@ namespace Chameleon.Components
|
||||
// On a skinned avatar the fur borrows this component's per-frame baked pose instead of a
|
||||
// static MeshFilter mesh (see ResolveMesh).
|
||||
_baker = GetComponent<SkinnedColliderBaker>();
|
||||
_painter = GetComponent<MeshPainter>();
|
||||
_mpb = new MaterialPropertyBlock();
|
||||
// RenderMeshInstanced requires the material's GPU-instancing flag; guarantee it here so any
|
||||
// assigned FurShell material just works without the manual inspector checkbox.
|
||||
@@ -206,7 +212,9 @@ namespace Chameleon.Components
|
||||
_mpb.SetFloat(SilhouetteStretchId, _silhouetteStretch);
|
||||
|
||||
if (_baseMap) _mpb.SetTexture(BaseMapId, _baseMap);
|
||||
if (_furMap) _mpb.SetTexture(FurMapId, _furMap);
|
||||
// Prefer the sibling painter's live fur RT (painted colour + length) over the serialized map.
|
||||
Texture furTex = _painter && _painter.FurMapTexture ? _painter.FurMapTexture : _furMap;
|
||||
if (furTex) _mpb.SetTexture(FurMapId, furTex);
|
||||
_mpb.SetColor(BaseColorId, ToRenderColor(_rootColor));
|
||||
_mpb.SetColor(TipColorId, ToRenderColor(_tipColor));
|
||||
_mpb.SetFloat(RootAOId, _rootAO);
|
||||
|
||||
@@ -103,6 +103,7 @@ public partial class MeshPainter
|
||||
new[] { BaseMapId, MainTexId }, // MAP_ALBEDO
|
||||
new[] { MetallicGlossMapId, OcclusionMapId, ParallaxMapId }, // MAP_MSO
|
||||
new[] { EmissionMapId }, // MAP_EM
|
||||
System.Array.Empty<int>(), // MAP_FUR: read by FurShellRenderer, not the base material
|
||||
};
|
||||
|
||||
private void AssignMapTexture(int mi, Texture tex)
|
||||
@@ -111,4 +112,19 @@ public partial class MeshPainter
|
||||
foreach (int id in MapPropIds[mi])
|
||||
if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The live fur map (RGB = strand colour, A = painted length), for a sibling FurShellRenderer to sample
|
||||
/// as _FurMap. GPU backend: the display RT (stable - fur skips the dilation ping-pong swap); CPU backend:
|
||||
/// the Texture2D mirror. Null until the backend is set up.
|
||||
/// </summary>
|
||||
public Texture FurMapTexture
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_setupValid) return null;
|
||||
if (_gpuActive) return _gpu != null ? _gpu[MAP_FUR]?.display : null;
|
||||
return _cpu != null ? _cpu[MAP_FUR]?.tex : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,6 +18,7 @@ public struct PaintStampNet : INetworkSerializable
|
||||
public const byte FlagOcclusion = 1 << 2;
|
||||
public const byte FlagEmission = 1 << 3;
|
||||
public const byte FlagSpray = 1 << 4;
|
||||
public const byte FlagFur = 1 << 5;
|
||||
|
||||
// Placement (object-local space).
|
||||
public Vector3 P; // hit point
|
||||
@@ -30,6 +31,8 @@ public struct PaintStampNet : INetworkSerializable
|
||||
|
||||
public Color32 BrushColor; // albedo color (alpha = opacity)
|
||||
public Color32 EmissionColor; // emission color (alpha = opacity)
|
||||
public Color32 FurColor; // fur colour (alpha = brush strength easing toward FurTarget)
|
||||
public byte FurTarget; // target fur length the stroke eases toward (0..255 = 0..1)
|
||||
public byte Metallic, Smoothness, Occlusion; // 0..255 = 0..1
|
||||
public byte MSOpacity, OccOpacity; // 0..255 = 0..1
|
||||
|
||||
@@ -45,6 +48,8 @@ public struct PaintStampNet : INetworkSerializable
|
||||
s.SerializeValue(ref SpraySeed);
|
||||
s.SerializeValue(ref BrushColor);
|
||||
s.SerializeValue(ref EmissionColor);
|
||||
s.SerializeValue(ref FurColor);
|
||||
s.SerializeValue(ref FurTarget);
|
||||
s.SerializeValue(ref Metallic);
|
||||
s.SerializeValue(ref Smoothness);
|
||||
s.SerializeValue(ref Occlusion);
|
||||
@@ -82,6 +87,7 @@ public partial class MeshPainter
|
||||
if (paintOcclusion) ch |= PaintStampNet.FlagOcclusion;
|
||||
if (paintEmission) ch |= PaintStampNet.FlagEmission;
|
||||
if (spray) ch |= PaintStampNet.FlagSpray;
|
||||
if (fur) ch |= PaintStampNet.FlagFur;
|
||||
|
||||
handler(new PaintStampNet
|
||||
{
|
||||
@@ -91,6 +97,8 @@ public partial class MeshPainter
|
||||
SpraySeed = _lastSpraySeed,
|
||||
BrushColor = brushColor,
|
||||
EmissionColor = emissionColor,
|
||||
FurColor = furColor,
|
||||
FurTarget = ToByte01(furOpacity),
|
||||
Metallic = ToByte01(metallic),
|
||||
Smoothness = ToByte01(smoothness),
|
||||
Occlusion = ToByte01(occlusion),
|
||||
@@ -126,9 +134,12 @@ public partial class MeshPainter
|
||||
paintOcclusion = (s.Channels & PaintStampNet.FlagOcclusion) != 0;
|
||||
paintEmission = (s.Channels & PaintStampNet.FlagEmission) != 0;
|
||||
spray = (s.Channels & PaintStampNet.FlagSpray) != 0;
|
||||
fur = (s.Channels & PaintStampNet.FlagFur) != 0;
|
||||
|
||||
brushColor = s.BrushColor;
|
||||
emissionColor = s.EmissionColor;
|
||||
furColor = s.FurColor;
|
||||
furOpacity = s.FurTarget / 255f;
|
||||
metallic = s.Metallic / 255f;
|
||||
smoothness = s.Smoothness / 255f;
|
||||
occlusion = s.Occlusion / 255f;
|
||||
|
||||
@@ -17,15 +17,20 @@ public partial class MeshPainter
|
||||
// Paintings/<key>/metallic.png greyscale = canonical MSO.R
|
||||
// Paintings/<key>/occlusion.png greyscale = canonical MSO.G
|
||||
// Paintings/<key>/smoothness.png greyscale = canonical MSO.A
|
||||
// Paintings/<key>/fur.png RGBA: RGB = strand colour, A = fur length (the ONE image whose
|
||||
// alpha is meaningful - painted length data, not coverage)
|
||||
// Paintings/<key>/painting.json display name + map size + the non-baked material scalars
|
||||
// The packed mask map is split into three greyscale images so every file is viewable/editable. This
|
||||
// also makes the round-trip byte-exact by construction: none of the stored images carry a meaningful
|
||||
// alpha channel (colour maps are forced opaque, the mask channels become opaque greyscale), so there
|
||||
// is no PNG alpha / premultiply ambiguity. Canonical MSO.B is always 0 (see MeshPaintPackMS.shader)
|
||||
// also makes the round-trip byte-exact by construction: the colour maps are forced opaque and the mask
|
||||
// channels become opaque greyscale, so there is no PNG alpha / premultiply ambiguity (fur.png is the
|
||||
// exception - its alpha is length data, stored verbatim). Canonical MSO.B is always 0 (see MeshPaintPackMS.shader)
|
||||
// and is reconstructed on load. The downstream reseed path (ReseedCanonical) is unchanged, so pixel
|
||||
// fidelity equals the previous .paint format plus exact PNG transport.
|
||||
|
||||
private const int PaintMetaVersion = 5; // folder format: split PNG maps + JSON scalars
|
||||
// v6 adds fur.png (RGBA: strand colour + painted length). v5 = split PNG maps + JSON scalars. Loads are
|
||||
// not version-gated (only the resolution is), so pre-v6 saves without a fur.png load fine - the fur map
|
||||
// stays seeded (bald).
|
||||
private const int PaintMetaVersion = 6;
|
||||
|
||||
private const string MetaFileName = "painting.json";
|
||||
private const string AlbedoFileName = "albedo.png";
|
||||
@@ -33,6 +38,7 @@ public partial class MeshPainter
|
||||
private const string MetallicFileName = "metallic.png";
|
||||
private const string OcclusionFileName = "occlusion.png";
|
||||
private const string SmoothnessFileName = "smoothness.png";
|
||||
private const string FurFileName = "fur.png";
|
||||
|
||||
/// <summary>
|
||||
/// JSON sidecar for a slot: display name, map resolution, and the per-player material scalars that are
|
||||
@@ -117,6 +123,7 @@ public partial class MeshPainter
|
||||
Color32[] albedo = ReadCanonical(MAP_ALBEDO);
|
||||
Color32[] mso = ReadCanonical(MAP_MSO);
|
||||
Color32[] emission = ReadCanonical(MAP_EM);
|
||||
Color32[] fur = ReadCanonical(MAP_FUR);
|
||||
|
||||
// Colour maps are stored opaque (the reseed path re-derives coverage/alpha anyway). The packed
|
||||
// mask map is split into one opaque greyscale image per channel: R=metallic, G=occlusion,
|
||||
@@ -126,6 +133,9 @@ public partial class MeshPainter
|
||||
WriteMapPng(Path.Combine(dir, MetallicFileName), mso, c => new Color32(c.r, c.r, c.r, 255));
|
||||
WriteMapPng(Path.Combine(dir, OcclusionFileName), mso, c => new Color32(c.g, c.g, c.g, 255));
|
||||
WriteMapPng(Path.Combine(dir, SmoothnessFileName), mso, c => new Color32(c.a, c.a, c.a, 255));
|
||||
// Fur is the exception: RGB = strand colour, A = painted length (0 = bald). Its alpha is real
|
||||
// data, so store the full RGBA verbatim instead of forcing it opaque.
|
||||
WriteMapPng(Path.Combine(dir, FurFileName), fur, c => c);
|
||||
|
||||
// The painter is ready here (_setupValid), so the material-backed camo/height getters return the
|
||||
// live values rather than the not-ready 0.
|
||||
@@ -258,6 +268,11 @@ public partial class MeshPainter
|
||||
if (ReadMapPng(Path.Combine(dir, EmissionFileName), out Color32[] emission))
|
||||
{ result[MAP_EM] = emission; loaded++; }
|
||||
|
||||
// Fur carries a real alpha (painted length); loaded verbatim. Missing on pre-v6 saves -> the fur
|
||||
// map stays seeded (bald), the correct backward-compatible default.
|
||||
if (ReadMapPng(Path.Combine(dir, FurFileName), out Color32[] fur))
|
||||
{ result[MAP_FUR] = fur; loaded++; }
|
||||
|
||||
// Recombine the split greyscale channels into the packed canonical layout
|
||||
// (R=metallic, G=occlusion, B=0, A=smoothness). B is always 0 (see MeshPaintPackMS.shader).
|
||||
if (ReadMapPng(Path.Combine(dir, MetallicFileName), out Color32[] metallic) &&
|
||||
|
||||
@@ -180,6 +180,13 @@ public partial class MeshPainter
|
||||
(byte)(Mathf.Clamp01(emission0.r) * 255f),
|
||||
(byte)(Mathf.Clamp01(emission0.g) * 255f),
|
||||
(byte)(Mathf.Clamp01(emission0.b) * 255f), 255));
|
||||
|
||||
// Fur: RGB = the current brush colour (harmless where bald), A = 0 so a fresh avatar has NO fur -
|
||||
// the alpha is painted-in length, not coverage, so it is never island-stamped (see SeedGpuMap).
|
||||
_seedCanonical[MAP_FUR] = FillCanonical(new Color32(
|
||||
(byte)(Mathf.Clamp01(furColor.r) * 255f),
|
||||
(byte)(Mathf.Clamp01(furColor.g) * 255f),
|
||||
(byte)(Mathf.Clamp01(furColor.b) * 255f), 0));
|
||||
}
|
||||
|
||||
private Color32[] FillCanonical(Color32 c)
|
||||
|
||||
@@ -109,6 +109,7 @@ public partial class MeshPainter
|
||||
if (paintAlbedo) _strokeDirtyMaps |= 1 << MAP_ALBEDO;
|
||||
if (paintMetallicSmoothness || paintOcclusion) _strokeDirtyMaps |= 1 << MAP_MSO;
|
||||
if (paintEmission) _strokeDirtyMaps |= 1 << MAP_EM;
|
||||
if (fur) _strokeDirtyMaps |= 1 << MAP_FUR;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -194,6 +194,23 @@ public partial class MeshPainter : MonoBehaviour
|
||||
"Above 1 makes the emission HDR/bloom-bright.")]
|
||||
public float emissionIntensity = 1f;
|
||||
|
||||
[Header("Brush - fur")]
|
||||
[Tooltip("Paint the fur map: RGB = strand colour, A = fur presence/length (accumulates toward full " +
|
||||
"length where you paint, 0 = bald). Drives a sibling FurShellRenderer via FurMapTexture; the " +
|
||||
"base PlayerCamoLit material is untouched.")]
|
||||
public bool fur = false;
|
||||
|
||||
[Tooltip("Fur brush colour: RGB = strand colour, alpha = brush strength that eases the painted length " +
|
||||
"toward furOpacity (higher = reaches the target faster). The map's own alpha is the painted " +
|
||||
"length (0 = bald .. 1 = full length), read by the FurShell shader as effectiveLen.")]
|
||||
public Color furColor = new Color(0.42f, 0.32f, 0.20f, 0.6f);
|
||||
|
||||
[Tooltip("Target fur presence/length the brush works toward (0 = bald .. 1 = full length). The painted " +
|
||||
"map alpha eases toward THIS value at the brush strength (furColor.a), so repeated strokes " +
|
||||
"converge to the target and over-write it instead of always accumulating to full length.")]
|
||||
[Range(0f, 1f)]
|
||||
public float furOpacity = 1f;
|
||||
|
||||
[Header("Brush - shape")]
|
||||
[Tooltip("Brush half-extent in texels (= half the square side). Converted into a 3D size per hit.")]
|
||||
[Range(1, 256)]
|
||||
@@ -291,7 +308,12 @@ public partial class MeshPainter : MonoBehaviour
|
||||
// _MetallicGlossMap (reads R/A) and _OcclusionMap (reads G).
|
||||
private const int MAP_MSO = 1;
|
||||
private const int MAP_EM = 2; // emission (RGB)
|
||||
private const int MAP_COUNT = 3;
|
||||
// Fur: RGB = strand colour, A = fur presence/length (0 = bald, 1 = full). A COLOUR-style map (sRGB),
|
||||
// but its alpha is DATA (length), not coverage - so it is seeded to alpha 0, never coverage-stamped,
|
||||
// saved with real alpha, and skips edge-padding dilation. Not bound to the base material (no _FurMap
|
||||
// slot there); a sibling FurShellRenderer reads the live RT via FurMapTexture.
|
||||
private const int MAP_FUR = 3;
|
||||
private const int MAP_COUNT = 4;
|
||||
|
||||
/// <summary>CPU backend per map: a Texture2D mirror + rasterization/dilation state.</summary>
|
||||
private class CpuMap
|
||||
@@ -333,6 +355,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
// albedo -> RGB color
|
||||
// MSO -> R = metallic, G = occlusion/height (0.5 = neutral: no relief, AO = 1), A = smoothness
|
||||
// emission -> RGB emission
|
||||
// fur -> RGB colour, A = 0 (bald; length is painted in, never coverage-stamped)
|
||||
private Color32[][] _seedCanonical;
|
||||
|
||||
// --- backends ---
|
||||
@@ -388,6 +411,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private static readonly int BrushVId = Shader.PropertyToID("_BrushV");
|
||||
private static readonly int BrushNormalId = Shader.PropertyToID("_BrushNormal");
|
||||
private static readonly int MaskTexId = Shader.PropertyToID("_MaskTex");
|
||||
private static readonly int FurTargetId = Shader.PropertyToID("_FurTarget");
|
||||
private static readonly int PadId = Shader.PropertyToID("_Pad");
|
||||
private static readonly int PreviewOpacityId = Shader.PropertyToID("_PreviewOpacity");
|
||||
private static readonly int PreviewModeId = Shader.PropertyToID("_PreviewMode");
|
||||
@@ -1045,7 +1069,9 @@ public partial class MeshPainter : MonoBehaviour
|
||||
{
|
||||
case MAP_ALBEDO: return paintAlbedo;
|
||||
case MAP_MSO: return paintMetallicSmoothness || paintOcclusion;
|
||||
default: return paintEmission;
|
||||
case MAP_EM: return paintEmission;
|
||||
case MAP_FUR: return fur;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,14 +34,17 @@ public partial class PaintHud : MonoBehaviour
|
||||
private const string PanelEmission = "Panel_Emission";
|
||||
private const string PanelBrushSize = "Panel_BrushSize";
|
||||
private const string PanelPaintings = "Panel_Paintings";
|
||||
private const string PanelFur = "Panel_Fur";
|
||||
private const string ToggleAlbedo = "Toggle_Albedo";
|
||||
private const string ToggleMetallicSmoothness = "Toggle_MetallicSmoothness";
|
||||
private const string ToggleOcclusion = "Toggle_Occlusion";
|
||||
private const string ToggleEmission = "Toggle_Emission";
|
||||
private const string ToggleSpray = "Toggle_Spray";
|
||||
private const string TogglePipette = "Toggle_Pipette";
|
||||
private const string ToggleFur = "Toggle_Fur";
|
||||
private const string PickerAlbedo = "Picker_Albedo";
|
||||
private const string PickerEmission = "Picker_Emission";
|
||||
private const string PickerFur = "Picker_Fur";
|
||||
private const string SliderAlbedoAlpha = "Slider_AlbedoAlpha";
|
||||
private const string SliderEmissionAlpha = "Slider_EmissionAlpha";
|
||||
private const string SliderMetallic = "Slider_Metallic";
|
||||
@@ -62,6 +65,8 @@ public partial class PaintHud : MonoBehaviour
|
||||
private const string SliderOcclusionThreshold = "Slider_OcclusionThreshold";
|
||||
private const string SliderNormalIntensity = "Slider_NormalIntensity";
|
||||
private const string SliderParallaxIntensity = "Slider_ParallaxIntensity";
|
||||
private const string SliderFurAlpha = "Slider_FurAlpha";
|
||||
private const string SliderFurOpacity = "Slider_FurOpacity";
|
||||
private const string MaskSelector = "MaskSelector";
|
||||
private const string MaskSwatchPrefix = "MaskSwatch_";
|
||||
|
||||
@@ -75,7 +80,8 @@ public partial class PaintHud : MonoBehaviour
|
||||
PanelBrushSize,
|
||||
PanelSpray,
|
||||
PanelGeneral,
|
||||
PanelPaintings
|
||||
PanelPaintings,
|
||||
PanelFur
|
||||
};
|
||||
|
||||
private GameObject _panel;
|
||||
@@ -209,12 +215,14 @@ public partial class PaintHud : MonoBehaviour
|
||||
WireToggle(panel, ToggleEmission, painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; });
|
||||
WireToggle(panel, ToggleSpray, painter && painter.spray, v => { if (painter) painter.spray = v; });
|
||||
WireToggle(panel, TogglePipette, painter && painter.pipette, v => { if (painter) painter.pipette = v; });
|
||||
WireToggle(panel, ToggleFur, painter && painter.fur, v => { if (painter) painter.fur = v; });
|
||||
|
||||
WirePanelFade(panel, PanelAlbedo, ToggleAlbedo);
|
||||
WirePanelFade(panel, PanelMetallicSmoothness, ToggleMetallicSmoothness);
|
||||
WirePanelFade(panel, PanelOcclusion, ToggleOcclusion);
|
||||
WirePanelFade(panel, PanelEmission, ToggleEmission);
|
||||
WirePanelFade(panel, PanelSpray, ToggleSpray);
|
||||
WirePanelFade(panel, PanelFur, ToggleFur);
|
||||
|
||||
// Picker drives only RGB (keeping the painter's current alpha); a standalone slider drives alpha.
|
||||
WirePicker(panel, PickerAlbedo, painter ? painter.brushColor : Color.red,
|
||||
@@ -255,6 +263,13 @@ public partial class PaintHud : MonoBehaviour
|
||||
WireSlider(panel, SliderNormalIntensity, painter ? painter.HeightNormalIntensity : 1.4f, v => { if (painter) painter.HeightNormalIntensity = v; });
|
||||
WireSlider(panel, SliderParallaxIntensity, painter ? painter.ParallaxIntensity : 1f, v => { if (painter) painter.ParallaxIntensity = v; });
|
||||
|
||||
WirePicker(panel, PickerFur, painter ? painter.furColor : Color.blue,
|
||||
c => { if (painter) painter.furColor = WithRgb(painter.furColor, c); });
|
||||
WireSlider(panel, SliderFurAlpha, painter ? painter.furColor.a : .6f,
|
||||
v => { if (painter) painter.furColor = WithAlpha(painter.furColor, v); });
|
||||
WireSlider(panel, SliderFurOpacity, painter ? painter.furOpacity : 1f,
|
||||
v => { if (painter) painter.furOpacity = v; });
|
||||
|
||||
foreach (var name in DraggablePanels)
|
||||
MakeDraggable(panel, name);
|
||||
}
|
||||
@@ -276,11 +291,15 @@ public partial class PaintHud : MonoBehaviour
|
||||
SetToggle(panel, ToggleEmission, painter.paintEmission);
|
||||
SetToggle(panel, ToggleSpray, painter.spray);
|
||||
SetToggle(panel, TogglePipette, painter.pipette);
|
||||
SetToggle(panel, ToggleFur, painter.fur);
|
||||
|
||||
SetPicker(panel, PickerAlbedo, painter.brushColor);
|
||||
SetPicker(panel, PickerEmission, painter.emissionColor);
|
||||
SetPicker(panel, PickerFur, painter.furColor);
|
||||
SetSlider(panel, SliderAlbedoAlpha, painter.brushColor.a);
|
||||
SetSlider(panel, SliderEmissionAlpha, painter.emissionColor.a);
|
||||
SetSlider(panel, SliderFurAlpha, painter.furColor.a);
|
||||
SetSlider(panel, SliderFurOpacity, painter.furOpacity);
|
||||
|
||||
SetSlider(panel, SliderMetallic, painter.metallic);
|
||||
SetSlider(panel, SliderSmoothness, painter.smoothness);
|
||||
|
||||
Reference in new issue
Block a user