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:
1 parent
7536e8ec8b
commit
1b783a39be
15 files changed
+250
-302
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Unity.Cinemachine;
|
using Unity.Cinemachine;
|
||||||
|
using Chameleon.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// First-person camera. Drives its OWN dedicated <see cref="CinemachineCamera"/>: each frame it snaps
|
/// First-person camera. Drives its OWN dedicated <see cref="CinemachineCamera"/>: each frame it snaps
|
||||||
@@ -24,6 +25,9 @@ public class FirstPersonController : MonoBehaviour
|
|||||||
[Tooltip("Renderers hidden while first-person is active (so you don't see your own body). Empty = the player's SkinnedMeshRenderers, auto-resolved.")]
|
[Tooltip("Renderers hidden while first-person is active (so you don't see your own body). Empty = the player's SkinnedMeshRenderers, auto-resolved.")]
|
||||||
public Renderer[] hiddenRenderers;
|
public Renderer[] hiddenRenderers;
|
||||||
|
|
||||||
|
[Tooltip("Fur renderers disabled while first-person is active (they draw via GPU instancing, not a Renderer, so they need their own toggle). Empty = the player's FurShellRenderers, auto-resolved.")]
|
||||||
|
public FurShellRenderer[] hiddenFurRenderers;
|
||||||
|
|
||||||
[Header("Look")]
|
[Header("Look")]
|
||||||
[Tooltip("Horizontal look sensitivity (degrees of yaw per pixel).")]
|
[Tooltip("Horizontal look sensitivity (degrees of yaw per pixel).")]
|
||||||
public float horizontalSensitivity = 0.2f;
|
public float horizontalSensitivity = 0.2f;
|
||||||
@@ -139,9 +143,15 @@ public class FirstPersonController : MonoBehaviour
|
|||||||
|
|
||||||
void SetMeshHidden(bool hidden)
|
void SetMeshHidden(bool hidden)
|
||||||
{
|
{
|
||||||
if (hiddenRenderers == null) return;
|
if (hiddenRenderers != null)
|
||||||
foreach (var r in hiddenRenderers)
|
foreach (var r in hiddenRenderers)
|
||||||
if (r) r.enabled = !hidden;
|
if (r) r.enabled = !hidden;
|
||||||
|
|
||||||
|
// The fur draws from RenderPipelineManager.beginCameraRendering, not through a Renderer, so hiding
|
||||||
|
// the SkinnedMeshRenderer above doesn't stop it - disabling the component unsubscribes that hook.
|
||||||
|
if (hiddenFurRenderers != null)
|
||||||
|
foreach (var f in hiddenFurRenderers)
|
||||||
|
if (f) f.enabled = !hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -157,6 +167,7 @@ public class FirstPersonController : MonoBehaviour
|
|||||||
Transform t = p.transform.Find("FirstPersonTarget");
|
Transform t = p.transform.Find("FirstPersonTarget");
|
||||||
if (t) cameraTarget = t;
|
if (t) cameraTarget = t;
|
||||||
hiddenRenderers = p.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
hiddenRenderers = p.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||||
|
hiddenFurRenderers = p.GetComponentsInChildren<FurShellRenderer>(true);
|
||||||
_resolved = true;
|
_resolved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,5 +191,8 @@ public class FirstPersonController : MonoBehaviour
|
|||||||
|
|
||||||
if (hiddenRenderers == null || hiddenRenderers.Length == 0)
|
if (hiddenRenderers == null || hiddenRenderers.Length == 0)
|
||||||
hiddenRenderers = player.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
hiddenRenderers = player.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||||
|
|
||||||
|
if (hiddenFurRenderers == null || hiddenFurRenderers.Length == 0)
|
||||||
|
hiddenFurRenderers = player.GetComponentsInChildren<FurShellRenderer>(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,23 +43,31 @@ namespace Chameleon.Components
|
|||||||
[SerializeField, Range(0f, 5f)] float _silhouetteStretch = 2f;
|
[SerializeField, Range(0f, 5f)] float _silhouetteStretch = 2f;
|
||||||
|
|
||||||
[Header("Shading")]
|
[Header("Shading")]
|
||||||
// Optional per-instance albedo override; leave null to keep the material's own texture.
|
// Optional albedo (strand-colour) override for the standalone prototype; a sibling MeshPainter's
|
||||||
|
// live albedo RT overrides this when present, so the fur wears the painted camo per texel.
|
||||||
[SerializeField] Texture _baseMap;
|
[SerializeField] Texture _baseMap;
|
||||||
[SerializeField] Color _rootColor = new Color(0.42f, 0.32f, 0.20f, 1f);
|
// Overall tint multiplied onto the PAINTED fur colour; white leaves the painted colour untouched.
|
||||||
[SerializeField] Color _tipColor = new Color(0.62f, 0.52f, 0.36f, 1f);
|
[SerializeField] Color _furTint = Color.white;
|
||||||
[SerializeField, Range(0f, 1f)] float _rootAO = 0.45f;
|
// Root/tip are two shades of the same painted colour: roots a touch darker, tips a touch lighter.
|
||||||
|
[SerializeField, Range(0f, 1f)] float _rootAO = 0.3f;
|
||||||
|
[SerializeField, Range(0f, 1f)] float _tipShade = 0.2f;
|
||||||
[SerializeField, Range(0.5f, 8f)] float _rimPower = 3f;
|
[SerializeField, Range(0.5f, 8f)] float _rimPower = 3f;
|
||||||
[SerializeField, Range(0f, 2f)] float _rimStrength = 0.4f;
|
[SerializeField, Range(0f, 2f)] float _rimStrength = 0.4f;
|
||||||
|
|
||||||
[Header("Fur Map")]
|
[Header("Fur Map")]
|
||||||
// RGBA fur map: RGB = per-texel strand colour, A = fur length (0 = bald, 1 = full length). Leave
|
// Fur map: R = fur length (0 = bald, 1 = full length); G/B/A unused. The strand colour comes from
|
||||||
// null on the standalone prototype - the shader's white default is uniform full-length fur. A
|
// the albedo map, not here. Leave null on the standalone prototype - the shader's white default is
|
||||||
// sibling MeshPainter's live fur RT overrides this when present, so a painted avatar grows fur in
|
// uniform full-length fur. A sibling MeshPainter's live fur RT overrides this when present, so a
|
||||||
// real time (see PushProperties).
|
// painted avatar grows fur in real time (see PushProperties).
|
||||||
[SerializeField] Texture _furMap;
|
[SerializeField] Texture _furMap;
|
||||||
// Fur is fully cut where the map's alpha is at/below this - keeps stray dilation fuzz from growing
|
// 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.
|
// hair. 0 keeps the exact "alpha 0 = no fur" contract.
|
||||||
[SerializeField, Range(0f, 1f)] float _furThreshold = 0f;
|
[SerializeField, Range(0f, 1f)] float _furThreshold = 0f;
|
||||||
|
// Low painted alpha => low fur opacity, so a lightly-painted region fades in as faint short fuzz
|
||||||
|
// instead of hard-cutting to full-opacity fur. Larger = softer/longer fade.
|
||||||
|
[SerializeField, Range(0.01f, 1f)] float _furSoftness = 0.5f;
|
||||||
|
// Per-strand edge anti-aliasing (soft strand silhouette + tip instead of hard clipped discs).
|
||||||
|
[SerializeField, Range(0f, 1f)] float _strandSoftness = 0.3f;
|
||||||
// Opaque base shell. On for the standalone prototype (full coverage); turn OFF on the painted
|
// Opaque base shell. On for the standalone prototype (full coverage); turn OFF on the painted
|
||||||
// avatar so the camo body shows between strands instead of being sealed under a solid root.
|
// avatar so the camo body shows between strands instead of being sealed under a solid root.
|
||||||
[SerializeField] bool _solidRoot = true;
|
[SerializeField] bool _solidRoot = true;
|
||||||
@@ -89,12 +97,14 @@ namespace Chameleon.Components
|
|||||||
static readonly int SilhouetteStretchId = Shader.PropertyToID("_SilhouetteStretch");
|
static readonly int SilhouetteStretchId = Shader.PropertyToID("_SilhouetteStretch");
|
||||||
static readonly int BaseMapId = Shader.PropertyToID("_BaseMap");
|
static readonly int BaseMapId = Shader.PropertyToID("_BaseMap");
|
||||||
static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
|
static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
|
||||||
static readonly int TipColorId = Shader.PropertyToID("_TipColor");
|
|
||||||
static readonly int RootAOId = Shader.PropertyToID("_RootAO");
|
static readonly int RootAOId = Shader.PropertyToID("_RootAO");
|
||||||
|
static readonly int TipShadeId = Shader.PropertyToID("_TipShade");
|
||||||
static readonly int RimPowerId = Shader.PropertyToID("_RimPower");
|
static readonly int RimPowerId = Shader.PropertyToID("_RimPower");
|
||||||
static readonly int RimStrengthId = Shader.PropertyToID("_RimStrength");
|
static readonly int RimStrengthId = Shader.PropertyToID("_RimStrength");
|
||||||
static readonly int FurMapId = Shader.PropertyToID("_FurMap");
|
static readonly int FurMapId = Shader.PropertyToID("_FurMap");
|
||||||
static readonly int FurThresholdId = Shader.PropertyToID("_FurThreshold");
|
static readonly int FurThresholdId = Shader.PropertyToID("_FurThreshold");
|
||||||
|
static readonly int FurSoftnessId = Shader.PropertyToID("_FurSoftness");
|
||||||
|
static readonly int StrandSoftnessId = Shader.PropertyToID("_StrandSoftness");
|
||||||
static readonly int SolidRootId = Shader.PropertyToID("_SolidRoot");
|
static readonly int SolidRootId = Shader.PropertyToID("_SolidRoot");
|
||||||
static readonly int WindStrengthId = Shader.PropertyToID("_WindStrength");
|
static readonly int WindStrengthId = Shader.PropertyToID("_WindStrength");
|
||||||
static readonly int WindSpeedId = Shader.PropertyToID("_WindSpeed");
|
static readonly int WindSpeedId = Shader.PropertyToID("_WindSpeed");
|
||||||
@@ -211,16 +221,22 @@ namespace Chameleon.Components
|
|||||||
_mpb.SetFloat(DirRandomnessId, _directionRandomness);
|
_mpb.SetFloat(DirRandomnessId, _directionRandomness);
|
||||||
_mpb.SetFloat(SilhouetteStretchId, _silhouetteStretch);
|
_mpb.SetFloat(SilhouetteStretchId, _silhouetteStretch);
|
||||||
|
|
||||||
if (_baseMap) _mpb.SetTexture(BaseMapId, _baseMap);
|
// Strand colour = the painted albedo: prefer the sibling painter's live albedo RT over the
|
||||||
// Prefer the sibling painter's live fur RT (painted colour + length) over the serialized map.
|
// serialized override, so the fur wears the camo per texel. Re-fetched each frame - the RT
|
||||||
|
// reference ping-pongs on the dilation swap.
|
||||||
|
Texture albedoTex = _painter && _painter.AlbedoMapTexture ? _painter.AlbedoMapTexture : _baseMap;
|
||||||
|
if (albedoTex) _mpb.SetTexture(BaseMapId, albedoTex);
|
||||||
|
// Prefer the sibling painter's live fur RT (R = painted length) over the serialized map.
|
||||||
Texture furTex = _painter && _painter.FurMapTexture ? _painter.FurMapTexture : _furMap;
|
Texture furTex = _painter && _painter.FurMapTexture ? _painter.FurMapTexture : _furMap;
|
||||||
if (furTex) _mpb.SetTexture(FurMapId, furTex);
|
if (furTex) _mpb.SetTexture(FurMapId, furTex);
|
||||||
_mpb.SetColor(BaseColorId, ToRenderColor(_rootColor));
|
_mpb.SetColor(BaseColorId, ToRenderColor(_furTint));
|
||||||
_mpb.SetColor(TipColorId, ToRenderColor(_tipColor));
|
|
||||||
_mpb.SetFloat(RootAOId, _rootAO);
|
_mpb.SetFloat(RootAOId, _rootAO);
|
||||||
|
_mpb.SetFloat(TipShadeId, _tipShade);
|
||||||
_mpb.SetFloat(RimPowerId, _rimPower);
|
_mpb.SetFloat(RimPowerId, _rimPower);
|
||||||
_mpb.SetFloat(RimStrengthId, _rimStrength);
|
_mpb.SetFloat(RimStrengthId, _rimStrength);
|
||||||
_mpb.SetFloat(FurThresholdId, _furThreshold);
|
_mpb.SetFloat(FurThresholdId, _furThreshold);
|
||||||
|
_mpb.SetFloat(FurSoftnessId, _furSoftness);
|
||||||
|
_mpb.SetFloat(StrandSoftnessId, _strandSoftness);
|
||||||
_mpb.SetFloat(SolidRootId, _solidRoot ? 1f : 0f);
|
_mpb.SetFloat(SolidRootId, _solidRoot ? 1f : 0f);
|
||||||
|
|
||||||
_mpb.SetFloat(WindStrengthId, _windStrength);
|
_mpb.SetFloat(WindStrengthId, _windStrength);
|
||||||
@@ -253,12 +269,14 @@ namespace Chameleon.Components
|
|||||||
if (mat.HasProperty(CurvatureId)) _curvature = mat.GetFloat(CurvatureId);
|
if (mat.HasProperty(CurvatureId)) _curvature = mat.GetFloat(CurvatureId);
|
||||||
if (mat.HasProperty(DirRandomnessId)) _directionRandomness = mat.GetFloat(DirRandomnessId);
|
if (mat.HasProperty(DirRandomnessId)) _directionRandomness = mat.GetFloat(DirRandomnessId);
|
||||||
if (mat.HasProperty(SilhouetteStretchId)) _silhouetteStretch = mat.GetFloat(SilhouetteStretchId);
|
if (mat.HasProperty(SilhouetteStretchId)) _silhouetteStretch = mat.GetFloat(SilhouetteStretchId);
|
||||||
if (mat.HasProperty(BaseColorId)) _rootColor = FromRenderColor(mat.GetColor(BaseColorId));
|
if (mat.HasProperty(BaseColorId)) _furTint = FromRenderColor(mat.GetColor(BaseColorId));
|
||||||
if (mat.HasProperty(TipColorId)) _tipColor = FromRenderColor(mat.GetColor(TipColorId));
|
|
||||||
if (mat.HasProperty(RootAOId)) _rootAO = mat.GetFloat(RootAOId);
|
if (mat.HasProperty(RootAOId)) _rootAO = mat.GetFloat(RootAOId);
|
||||||
|
if (mat.HasProperty(TipShadeId)) _tipShade = mat.GetFloat(TipShadeId);
|
||||||
if (mat.HasProperty(RimPowerId)) _rimPower = mat.GetFloat(RimPowerId);
|
if (mat.HasProperty(RimPowerId)) _rimPower = mat.GetFloat(RimPowerId);
|
||||||
if (mat.HasProperty(RimStrengthId)) _rimStrength = mat.GetFloat(RimStrengthId);
|
if (mat.HasProperty(RimStrengthId)) _rimStrength = mat.GetFloat(RimStrengthId);
|
||||||
if (mat.HasProperty(FurThresholdId)) _furThreshold = mat.GetFloat(FurThresholdId);
|
if (mat.HasProperty(FurThresholdId)) _furThreshold = mat.GetFloat(FurThresholdId);
|
||||||
|
if (mat.HasProperty(FurSoftnessId)) _furSoftness = mat.GetFloat(FurSoftnessId);
|
||||||
|
if (mat.HasProperty(StrandSoftnessId)) _strandSoftness = mat.GetFloat(StrandSoftnessId);
|
||||||
if (mat.HasProperty(SolidRootId)) _solidRoot = mat.GetFloat(SolidRootId) > 0.5f;
|
if (mat.HasProperty(SolidRootId)) _solidRoot = mat.GetFloat(SolidRootId) > 0.5f;
|
||||||
if (mat.HasProperty(WindStrengthId)) _windStrength = mat.GetFloat(WindStrengthId);
|
if (mat.HasProperty(WindStrengthId)) _windStrength = mat.GetFloat(WindStrengthId);
|
||||||
if (mat.HasProperty(WindSpeedId)) _windSpeed = mat.GetFloat(WindSpeedId);
|
if (mat.HasProperty(WindSpeedId)) _windSpeed = mat.GetFloat(WindSpeedId);
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ public partial class MeshPainter
|
|||||||
return (Color32[])_cpu[mi].pixels.Clone();
|
return (Color32[])_cpu[mi].pixels.Clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool MapIsLinear(int mi) => mi == MAP_MSO;
|
// MSO and fur are DATA maps (raw values, no sRGB): MSO packs metallic/occlusion/smoothness, fur's R is
|
||||||
|
// painted strand length. Storing them linearly keeps the values undistorted for the shaders that read them.
|
||||||
|
private static bool MapIsLinear(int mi) => mi == MAP_MSO || mi == MAP_FUR;
|
||||||
|
|
||||||
// Material slots per map, as pre-hashed IDs (this runs per stroke end - no per-call allocations).
|
// Material slots per map, as pre-hashed IDs (this runs per stroke end - no per-call allocations).
|
||||||
// MSO: one mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) +
|
// MSO: one mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) +
|
||||||
@@ -114,7 +116,7 @@ public partial class MeshPainter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The live fur map (RGB = strand colour, A = painted length), for a sibling FurShellRenderer to sample
|
/// The live fur map (R = painted strand length, linear data), for a sibling FurShellRenderer to sample
|
||||||
/// as _FurMap. GPU backend: the display RT (stable - fur skips the dilation ping-pong swap); CPU backend:
|
/// 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.
|
/// the Texture2D mirror. Null until the backend is set up.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -127,4 +129,20 @@ public partial class MeshPainter
|
|||||||
return _cpu != null ? _cpu[MAP_FUR]?.tex : null;
|
return _cpu != null ? _cpu[MAP_FUR]?.tex : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The live albedo map (the painted camo), for a sibling FurShellRenderer to sample as the fur strand
|
||||||
|
/// colour so the fur wears the camo per texel. GPU backend: the current display RT (ping-pongs on the
|
||||||
|
/// dilation swap, so callers must re-fetch each frame); CPU backend: the Texture2D mirror. Null until the
|
||||||
|
/// backend is set up.
|
||||||
|
/// </summary>
|
||||||
|
public Texture AlbedoMapTexture
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_setupValid) return null;
|
||||||
|
if (_gpuActive) return _gpu != null ? _gpu[MAP_ALBEDO]?.display : null;
|
||||||
|
return _cpu != null ? _cpu[MAP_ALBEDO]?.tex : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ public partial class MeshPainter
|
|||||||
{
|
{
|
||||||
var src = ResolveSeed(mi, canonical);
|
var src = ResolveSeed(mi, canonical);
|
||||||
var px = (Color32[])src.Clone();
|
var px = (Color32[])src.Clone();
|
||||||
// Albedo / emission are opaque. The mask map keeps alpha = smoothness; fur keeps alpha = painted
|
// Albedo / emission are opaque. The mask map keeps alpha = smoothness; fur is a data map (R =
|
||||||
// length (0 = bald), which is data, not coverage.
|
// length), so leave its channels as seeded rather than forcing alpha opaque.
|
||||||
if (mi != MAP_MSO && mi != MAP_FUR)
|
if (mi != MAP_MSO && mi != MAP_FUR)
|
||||||
for (int i = 0; i < px.Length; i++) px[i].a = 255;
|
for (int i = 0; i < px.Length; i++) px[i].a = 255;
|
||||||
return px;
|
return px;
|
||||||
@@ -102,6 +102,8 @@ public partial class MeshPainter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void PaintStamp3D(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
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;
|
bool aAlb = paintAlbedo, aMS = paintMetallicSmoothness, aEM = paintEmission, aAO = paintOcclusion, aFur = fur;
|
||||||
if (!aAlb && !aMS && !aEM && !aAO && !aFur) return;
|
if (!aAlb && !aMS && !aEM && !aAO && !aFur) return;
|
||||||
|
|
||||||
@@ -129,13 +131,9 @@ public partial class MeshPainter
|
|||||||
float emOpMul = Mathf.Clamp01(emissionColor.a);
|
float emOpMul = Mathf.Clamp01(emissionColor.a);
|
||||||
CpuMap em = _cpu[MAP_EM];
|
CpuMap em = _cpu[MAP_EM];
|
||||||
|
|
||||||
// Fur target (linear RGB like albedo; alpha = painted length that EASES toward furTargetA
|
// Fur target: only R = length, eased toward furLength*255 at the brush's alpha (albOpMul) as the
|
||||||
// at the brush strength furColor.a, not accumulating to full - mirrors the GPU fade+deposit).
|
// deposit weight - a linear data lerp (no sRGB), mirroring the GPU source-over on R.
|
||||||
float frLin = Mathf.GammaToLinearSpace(furColor.r);
|
float furTargetR = Mathf.Clamp01(furLength) * 255f;
|
||||||
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];
|
CpuMap furm = _cpu[MAP_FUR];
|
||||||
|
|
||||||
float invR = 1f / radiusLocal;
|
float invR = 1f / radiusLocal;
|
||||||
@@ -258,20 +256,14 @@ public partial class MeshPainter
|
|||||||
|
|
||||||
if (aFur)
|
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)
|
if (op > 0f)
|
||||||
{
|
{
|
||||||
Color32 dst = furm.pixels[idx];
|
Color32 dst = furm.pixels[idx];
|
||||||
float it = 1f - op;
|
byte nr = (byte)(dst.r * (1f - op) + furTargetR * op + 0.5f);
|
||||||
// RGB source-over toward the fur colour (linear); alpha EASES toward the target
|
furm.pixels[idx] = new Color32(nr, 0, 0, 0);
|
||||||
// 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;
|
furm.painted[idx] = true;
|
||||||
ExpandDirty(furm, x, y);
|
ExpandDirty(furm, x, y);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ public partial class MeshPainter
|
|||||||
public bool sRGB; // sRGB color map (albedo/emission) vs linear data map (MSO)
|
public bool sRGB; // sRGB color map (albedo/emission) vs linear data map (MSO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MeshPaintBrush.shader pass indices: 0 = source-over brush, 1 = coverage seed,
|
// 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 brushShader; // optional; else Shader.Find("Hidden/MeshPaintBrush")
|
||||||
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
|
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
|
||||||
@@ -91,10 +88,10 @@ public partial class MeshPainter
|
|||||||
}
|
}
|
||||||
else if (mi == MAP_FUR)
|
else if (mi == MAP_FUR)
|
||||||
{
|
{
|
||||||
// Fur: alpha IS data (strand length), not coverage. Preserve the seeded/loaded alpha and skip
|
// Fur R IS data (strand length), not coverage. Upload the seeded/loaded R verbatim (linear
|
||||||
// the coverage stamp, so unpainted texels stay bald (alpha 0) instead of being marked full
|
// data map) and skip the coverage stamp, so unpainted texels stay bald (R 0) instead of being
|
||||||
// length. (Dilation is also skipped for fur in EndGpuStroke, so the alpha guard never matters.)
|
// marked full length. (Dilation is also skipped for fur in EndGpuStroke.)
|
||||||
UploadColorsToRT((Color32[])src.Clone(), gm.display, linear: gm.sRGB == false, preserveAlpha: true);
|
UploadColorsToRT((Color32[])src.Clone(), gm.display, linear: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -169,14 +166,13 @@ public partial class MeshPainter
|
|||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Writes Color32 RGB into a RenderTexture via a temporary texture. Alpha (the
|
/// <summary>Writes Color32 RGB into a RenderTexture via a temporary texture. Alpha (the dilation
|
||||||
/// dilation coverage marker) is cleared to 0 here and stamped afterwards by StampCoverage,
|
/// coverage marker) is cleared to 0 here and stamped afterwards by StampCoverage, so the guard matches
|
||||||
/// so the guard matches the GPU rasterizer exactly (see SeedGpuMap). preserveAlpha keeps the
|
/// the GPU rasterizer exactly (see SeedGpuMap). Fur carries no coverage (its length lives in R), so a
|
||||||
/// source alpha instead - used by the fur map, whose alpha is painted length, not coverage.</summary>
|
/// zero alpha is correct there too.</summary>
|
||||||
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool linear, bool preserveAlpha = false)
|
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool linear)
|
||||||
{
|
{
|
||||||
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, 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.SetPixels32(colors);
|
||||||
tmp.Apply(false);
|
tmp.Apply(false);
|
||||||
@@ -267,16 +263,15 @@ public partial class MeshPainter
|
|||||||
}
|
}
|
||||||
else if (mi == MAP_FUR)
|
else if (mi == MAP_FUR)
|
||||||
{
|
{
|
||||||
// Fur alpha is DATA (strand length) that must ease toward furOpacity, not accumulate to 1.
|
// Fur stores ONLY strand length in R (the colour comes from the albedo map at render). Gated by
|
||||||
// Fixed-function can't lerp-to-target in one draw, so it is split into two stamps into the
|
// its own toggle: the length eases toward furLength with the brush's alpha as the deposit
|
||||||
// SAME display RT (reference stays stable for FurMapTexture - no ping-pong):
|
// weight. Reuse the source-over pass (0) on R only, so R = lerp(dst.R, furLength, op) with
|
||||||
// pass FurFade: RGB source-over toward furColor + alpha *= (1-op)
|
// op = luma*mask.a*brushColor.a. SetVector + EncodeData (not SetColor) keeps furLength as raw
|
||||||
// pass FurDeposit: alpha += op * furOpacity
|
// linear data - the fur RT is a linear data map the shell samples directly for length.
|
||||||
// Net alpha = (1-op)*dst + op*target = lerp(dst, target, op); op = luma*mask.a*furColor.a.
|
float fl = EncodeData(furLength);
|
||||||
_brushMat.SetColor(BrushColorId, furColor);
|
_brushMat.SetVector(BrushColorId, new Vector4(fl, fl, fl, Mathf.Clamp01(brushColor.a)));
|
||||||
_brushMat.SetFloat(FurTargetId, Mathf.Clamp01(furOpacity));
|
_brushMat.SetFloat(ColorWriteMaskId, 8f); // R only (bits: R=8, G=4, B=2, A=1)
|
||||||
if (_brushMat.SetPass(FurFadePass)) DrawBakedMeshNow();
|
DrawStamp();
|
||||||
if (_brushMat.SetPass(FurDepositPass)) DrawBakedMeshNow();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public struct PaintStampNet : INetworkSerializable
|
|||||||
public const byte FlagOcclusion = 1 << 2;
|
public const byte FlagOcclusion = 1 << 2;
|
||||||
public const byte FlagEmission = 1 << 3;
|
public const byte FlagEmission = 1 << 3;
|
||||||
public const byte FlagSpray = 1 << 4;
|
public const byte FlagSpray = 1 << 4;
|
||||||
public const byte FlagFur = 1 << 5;
|
public const byte FlagFur = 1 << 5; // fur has its own toggle -> replay must carry it explicitly
|
||||||
|
|
||||||
// Placement (object-local space).
|
// Placement (object-local space).
|
||||||
public Vector3 P; // hit point
|
public Vector3 P; // hit point
|
||||||
@@ -29,10 +29,9 @@ public struct PaintStampNet : INetworkSerializable
|
|||||||
public int BrushIndex; // active brush mask index (resolved-mask painting)
|
public int BrushIndex; // active brush mask index (resolved-mask painting)
|
||||||
public int SpraySeed; // RNG seed so a spray stamp scatters identically on every peer
|
public int SpraySeed; // RNG seed so a spray stamp scatters identically on every peer
|
||||||
|
|
||||||
public Color32 BrushColor; // albedo color (alpha = opacity)
|
public Color32 BrushColor; // albedo color (alpha = opacity; also the fur length deposit weight)
|
||||||
public Color32 EmissionColor; // emission color (alpha = opacity)
|
public Color32 EmissionColor; // emission color (alpha = opacity)
|
||||||
public Color32 FurColor; // fur colour (alpha = brush strength easing toward FurTarget)
|
public byte FurLength; // target fur length the stroke eases toward (0..255 = 0..1)
|
||||||
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 Metallic, Smoothness, Occlusion; // 0..255 = 0..1
|
||||||
public byte MSOpacity, OccOpacity; // 0..255 = 0..1
|
public byte MSOpacity, OccOpacity; // 0..255 = 0..1
|
||||||
|
|
||||||
@@ -48,8 +47,7 @@ public struct PaintStampNet : INetworkSerializable
|
|||||||
s.SerializeValue(ref SpraySeed);
|
s.SerializeValue(ref SpraySeed);
|
||||||
s.SerializeValue(ref BrushColor);
|
s.SerializeValue(ref BrushColor);
|
||||||
s.SerializeValue(ref EmissionColor);
|
s.SerializeValue(ref EmissionColor);
|
||||||
s.SerializeValue(ref FurColor);
|
s.SerializeValue(ref FurLength);
|
||||||
s.SerializeValue(ref FurTarget);
|
|
||||||
s.SerializeValue(ref Metallic);
|
s.SerializeValue(ref Metallic);
|
||||||
s.SerializeValue(ref Smoothness);
|
s.SerializeValue(ref Smoothness);
|
||||||
s.SerializeValue(ref Occlusion);
|
s.SerializeValue(ref Occlusion);
|
||||||
@@ -97,8 +95,7 @@ public partial class MeshPainter
|
|||||||
SpraySeed = _lastSpraySeed,
|
SpraySeed = _lastSpraySeed,
|
||||||
BrushColor = brushColor,
|
BrushColor = brushColor,
|
||||||
EmissionColor = emissionColor,
|
EmissionColor = emissionColor,
|
||||||
FurColor = furColor,
|
FurLength = ToByte01(furLength),
|
||||||
FurTarget = ToByte01(furOpacity),
|
|
||||||
Metallic = ToByte01(metallic),
|
Metallic = ToByte01(metallic),
|
||||||
Smoothness = ToByte01(smoothness),
|
Smoothness = ToByte01(smoothness),
|
||||||
Occlusion = ToByte01(occlusion),
|
Occlusion = ToByte01(occlusion),
|
||||||
@@ -138,8 +135,7 @@ public partial class MeshPainter
|
|||||||
|
|
||||||
brushColor = s.BrushColor;
|
brushColor = s.BrushColor;
|
||||||
emissionColor = s.EmissionColor;
|
emissionColor = s.EmissionColor;
|
||||||
furColor = s.FurColor;
|
furLength = s.FurLength / 255f;
|
||||||
furOpacity = s.FurTarget / 255f;
|
|
||||||
metallic = s.Metallic / 255f;
|
metallic = s.Metallic / 255f;
|
||||||
smoothness = s.Smoothness / 255f;
|
smoothness = s.Smoothness / 255f;
|
||||||
occlusion = s.Occlusion / 255f;
|
occlusion = s.Occlusion / 255f;
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ public partial class MeshPainter
|
|||||||
// Paintings/<key>/metallic.png greyscale = canonical MSO.R
|
// Paintings/<key>/metallic.png greyscale = canonical MSO.R
|
||||||
// Paintings/<key>/occlusion.png greyscale = canonical MSO.G
|
// Paintings/<key>/occlusion.png greyscale = canonical MSO.G
|
||||||
// Paintings/<key>/smoothness.png greyscale = canonical MSO.A
|
// Paintings/<key>/smoothness.png greyscale = canonical MSO.A
|
||||||
// Paintings/<key>/fur.png RGBA: RGB = strand colour, A = fur length (the ONE image whose
|
// Paintings/<key>/fur.png R = fur length (painted length data, not coverage); G/B/A unused
|
||||||
// alpha is meaningful - painted length data, not coverage)
|
|
||||||
// Paintings/<key>/painting.json display name + map size + the non-baked material scalars
|
// 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
|
// 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: the colour maps are forced opaque and the mask
|
// 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
|
// channels become opaque greyscale, so there is no PNG alpha / premultiply ambiguity. fur.png is a data
|
||||||
// exception - its alpha is length data, stored verbatim). Canonical MSO.B is always 0 (see MeshPaintPackMS.shader)
|
// map (R = length), stored verbatim. Canonical MSO.B is always 0 (see MeshPaintPackMS.shader) and is
|
||||||
// and is reconstructed on load. The downstream reseed path (ReseedCanonical) is unchanged, so pixel
|
// reconstructed on load. The downstream reseed path (ReseedCanonical) is unchanged, so pixel fidelity
|
||||||
// fidelity equals the previous .paint format plus exact PNG transport.
|
// equals the previous .paint format plus exact PNG transport.
|
||||||
|
|
||||||
// v6 adds fur.png (RGBA: strand colour + painted length). v5 = split PNG maps + JSON scalars. Loads are
|
// v6 adds fur.png. v5 = split PNG maps + JSON scalars. Loads are not version-gated (only the resolution
|
||||||
// not version-gated (only the resolution is), so pre-v6 saves without a fur.png load fine - the fur map
|
// is), so saves without a fur.png load fine - the fur map stays seeded (bald). NOTE: the fur.png layout
|
||||||
// stays seeded (bald).
|
// changed to R = length (was RGB colour + A length); pre-change fur.png files load stale length data and
|
||||||
|
// should be repainted.
|
||||||
private const int PaintMetaVersion = 6;
|
private const int PaintMetaVersion = 6;
|
||||||
|
|
||||||
private const string MetaFileName = "painting.json";
|
private const string MetaFileName = "painting.json";
|
||||||
@@ -133,8 +133,8 @@ public partial class MeshPainter
|
|||||||
WriteMapPng(Path.Combine(dir, MetallicFileName), mso, c => new Color32(c.r, c.r, c.r, 255));
|
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, 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));
|
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
|
// Fur is data, not colour: R = painted length (0 = bald), G/B/A unused. Store verbatim so the
|
||||||
// data, so store the full RGBA verbatim instead of forcing it opaque.
|
// length round-trips exactly instead of being forced opaque.
|
||||||
WriteMapPng(Path.Combine(dir, FurFileName), fur, c => c);
|
WriteMapPng(Path.Combine(dir, FurFileName), fur, c => c);
|
||||||
|
|
||||||
// The painter is ready here (_setupValid), so the material-backed camo/height getters return the
|
// The painter is ready here (_setupValid), so the material-backed camo/height getters return the
|
||||||
@@ -268,8 +268,8 @@ public partial class MeshPainter
|
|||||||
if (ReadMapPng(Path.Combine(dir, EmissionFileName), out Color32[] emission))
|
if (ReadMapPng(Path.Combine(dir, EmissionFileName), out Color32[] emission))
|
||||||
{ result[MAP_EM] = emission; loaded++; }
|
{ result[MAP_EM] = emission; loaded++; }
|
||||||
|
|
||||||
// Fur carries a real alpha (painted length); loaded verbatim. Missing on pre-v6 saves -> the fur
|
// Fur carries painted length in R; loaded verbatim. Missing on saves without a fur map -> it stays
|
||||||
// map stays seeded (bald), the correct backward-compatible default.
|
// seeded (bald), the correct backward-compatible default.
|
||||||
if (ReadMapPng(Path.Combine(dir, FurFileName), out Color32[] fur))
|
if (ReadMapPng(Path.Combine(dir, FurFileName), out Color32[] fur))
|
||||||
{ result[MAP_FUR] = fur; loaded++; }
|
{ result[MAP_FUR] = fur; loaded++; }
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ public partial class MeshPainter
|
|||||||
paintMetallicSmoothness = true;
|
paintMetallicSmoothness = true;
|
||||||
paintOcclusion = pickedHeight;
|
paintOcclusion = pickedHeight;
|
||||||
paintEmission = true;
|
paintEmission = true;
|
||||||
|
// Fur toggle always turns on with a pick; the length is only adopted off our own paint mesh
|
||||||
|
// (pickedHeight), so a foreign-surface pick enables fur but leaves furLength at its current value.
|
||||||
|
fur = true;
|
||||||
|
|
||||||
pipette = false;
|
pipette = false;
|
||||||
|
|
||||||
@@ -125,7 +128,8 @@ public partial class MeshPainter
|
|||||||
Color32[] alb = ReadCanonical(MAP_ALBEDO);
|
Color32[] alb = ReadCanonical(MAP_ALBEDO);
|
||||||
Color32[] mso = ReadCanonical(MAP_MSO);
|
Color32[] mso = ReadCanonical(MAP_MSO);
|
||||||
Color32[] em = ReadCanonical(MAP_EM);
|
Color32[] em = ReadCanonical(MAP_EM);
|
||||||
if (idx < 0 || idx >= alb.Length || idx >= mso.Length || idx >= em.Length) return false;
|
Color32[] fur = ReadCanonical(MAP_FUR);
|
||||||
|
if (idx < 0 || idx >= alb.Length || idx >= mso.Length || idx >= em.Length || idx >= fur.Length) return false;
|
||||||
|
|
||||||
Color32 a = alb[idx];
|
Color32 a = alb[idx];
|
||||||
Color32 m = mso[idx];
|
Color32 m = mso[idx];
|
||||||
@@ -138,6 +142,7 @@ public partial class MeshPainter
|
|||||||
smoothness = m.a / 255f;
|
smoothness = m.a / 255f;
|
||||||
occlusion = m.g / 255f; // the painted HEIGHT - adopted only off our own mesh (FinalizePick)
|
occlusion = m.g / 255f; // the painted HEIGHT - adopted only off our own mesh (FinalizePick)
|
||||||
emissionColor = new Color(e.r / 255f, e.g / 255f, e.b / 255f, emissionColor.a);
|
emissionColor = new Color(e.r / 255f, e.g / 255f, e.b / 255f, emissionColor.a);
|
||||||
|
furLength = fur[idx].r / 255f; // fur map carries only R = painted length (linear data)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,12 +181,9 @@ public partial class MeshPainter
|
|||||||
(byte)(Mathf.Clamp01(emission0.g) * 255f),
|
(byte)(Mathf.Clamp01(emission0.g) * 255f),
|
||||||
(byte)(Mathf.Clamp01(emission0.b) * 255f), 255));
|
(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 -
|
// Fur: R = 0 so a fresh avatar has NO fur (R is painted-in length, not coverage, so it is never
|
||||||
// the alpha is painted-in length, not coverage, so it is never island-stamped (see SeedGpuMap).
|
// island-stamped - see SeedGpuMap). G/B/A unused. The strand colour comes from the albedo map.
|
||||||
_seedCanonical[MAP_FUR] = FillCanonical(new Color32(
|
_seedCanonical[MAP_FUR] = FillCanonical(new Color32(0, 0, 0, 0));
|
||||||
(byte)(Mathf.Clamp01(furColor.r) * 255f),
|
|
||||||
(byte)(Mathf.Clamp01(furColor.g) * 255f),
|
|
||||||
(byte)(Mathf.Clamp01(furColor.b) * 255f), 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color32[] FillCanonical(Color32 c)
|
private Color32[] FillCanonical(Color32 c)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public partial class MeshPainter
|
|||||||
if (paintAlbedo) _strokeDirtyMaps |= 1 << MAP_ALBEDO;
|
if (paintAlbedo) _strokeDirtyMaps |= 1 << MAP_ALBEDO;
|
||||||
if (paintMetallicSmoothness || paintOcclusion) _strokeDirtyMaps |= 1 << MAP_MSO;
|
if (paintMetallicSmoothness || paintOcclusion) _strokeDirtyMaps |= 1 << MAP_MSO;
|
||||||
if (paintEmission) _strokeDirtyMaps |= 1 << MAP_EM;
|
if (paintEmission) _strokeDirtyMaps |= 1 << MAP_EM;
|
||||||
if (fur) _strokeDirtyMaps |= 1 << MAP_FUR;
|
if (fur) _strokeDirtyMaps |= 1 << MAP_FUR; // fur has its own toggle gate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -195,21 +195,19 @@ public partial class MeshPainter : MonoBehaviour
|
|||||||
public float emissionIntensity = 1f;
|
public float emissionIntensity = 1f;
|
||||||
|
|
||||||
[Header("Brush - fur")]
|
[Header("Brush - fur")]
|
||||||
[Tooltip("Paint the fur map: RGB = strand colour, A = fur presence/length (accumulates toward full " +
|
[Tooltip("Enables fur painting. When on, each stroke eases the fur map's length (channel R) toward " +
|
||||||
"length where you paint, 0 = bald). Drives a sibling FurShellRenderer via FurMapTexture; the " +
|
"furLength using the brush's alpha as the deposit weight; when off the fur map is left untouched. " +
|
||||||
"base PlayerCamoLit material is untouched.")]
|
"The strand COLOUR comes from the albedo map, so the fur wears the painted camo. The pipette turns " +
|
||||||
|
"this on when it samples fur length.")]
|
||||||
public bool fur = false;
|
public bool fur = false;
|
||||||
|
|
||||||
[Tooltip("Fur brush colour: RGB = strand colour, alpha = brush strength that eases the painted length " +
|
[Tooltip("Target fur length the stroke eases toward (0 = bald .. 1 = full length). Painting over-writes the " +
|
||||||
"toward furOpacity (higher = reaches the target faster). The map's own alpha is the painted " +
|
"map's length toward THIS value (raise it to grow fur, drop to 0 to shave it), weighted by the " +
|
||||||
"length (0 = bald .. 1 = full length), read by the FurShell shader as effectiveLen.")]
|
"brush's alpha. Drives a sibling FurShellRenderer via FurMapTexture; the base PlayerCamoLit " +
|
||||||
public Color furColor = new Color(0.42f, 0.32f, 0.20f, 0.6f);
|
"material is untouched.")]
|
||||||
|
|
||||||
[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)]
|
[Range(0f, 1f)]
|
||||||
public float furOpacity = 1f;
|
[UnityEngine.Serialization.FormerlySerializedAs("furOpacity")]
|
||||||
|
public float furLength = 0.5f;
|
||||||
|
|
||||||
[Header("Brush - shape")]
|
[Header("Brush - shape")]
|
||||||
[Tooltip("Brush half-extent in texels (= half the square side). Converted into a 3D size per hit.")]
|
[Tooltip("Brush half-extent in texels (= half the square side). Converted into a 3D size per hit.")]
|
||||||
@@ -308,10 +306,10 @@ public partial class MeshPainter : MonoBehaviour
|
|||||||
// _MetallicGlossMap (reads R/A) and _OcclusionMap (reads G).
|
// _MetallicGlossMap (reads R/A) and _OcclusionMap (reads G).
|
||||||
private const int MAP_MSO = 1;
|
private const int MAP_MSO = 1;
|
||||||
private const int MAP_EM = 2; // emission (RGB)
|
private const int MAP_EM = 2; // emission (RGB)
|
||||||
// Fur: RGB = strand colour, A = fur presence/length (0 = bald, 1 = full). A COLOUR-style map (sRGB),
|
// Fur: R = fur length (0 = bald, 1 = full); G/B/A unused. A DATA map (linear, like MSO): seeded to 0,
|
||||||
// but its alpha is DATA (length), not coverage - so it is seeded to alpha 0, never coverage-stamped,
|
// never coverage-stamped, and skips edge-padding dilation. Length is painted whenever the fur toggle is
|
||||||
// saved with real alpha, and skips edge-padding dilation. Not bound to the base material (no _FurMap
|
// on (own gate); the strand colour comes from the albedo map. Not bound to the base material; a sibling
|
||||||
// slot there); a sibling FurShellRenderer reads the live RT via FurMapTexture.
|
// FurShellRenderer reads the live RTs via FurMapTexture (length) + AlbedoMapTexture (colour).
|
||||||
private const int MAP_FUR = 3;
|
private const int MAP_FUR = 3;
|
||||||
private const int MAP_COUNT = 4;
|
private const int MAP_COUNT = 4;
|
||||||
|
|
||||||
@@ -355,7 +353,7 @@ public partial class MeshPainter : MonoBehaviour
|
|||||||
// albedo -> RGB color
|
// albedo -> RGB color
|
||||||
// MSO -> R = metallic, G = occlusion/height (0.5 = neutral: no relief, AO = 1), A = smoothness
|
// MSO -> R = metallic, G = occlusion/height (0.5 = neutral: no relief, AO = 1), A = smoothness
|
||||||
// emission -> RGB emission
|
// emission -> RGB emission
|
||||||
// fur -> RGB colour, A = 0 (bald; length is painted in, never coverage-stamped)
|
// fur -> R = length (0 = bald; painted in, never coverage-stamped), G/B/A unused
|
||||||
private Color32[][] _seedCanonical;
|
private Color32[][] _seedCanonical;
|
||||||
|
|
||||||
// --- backends ---
|
// --- backends ---
|
||||||
@@ -411,7 +409,6 @@ public partial class MeshPainter : MonoBehaviour
|
|||||||
private static readonly int BrushVId = Shader.PropertyToID("_BrushV");
|
private static readonly int BrushVId = Shader.PropertyToID("_BrushV");
|
||||||
private static readonly int BrushNormalId = Shader.PropertyToID("_BrushNormal");
|
private static readonly int BrushNormalId = Shader.PropertyToID("_BrushNormal");
|
||||||
private static readonly int MaskTexId = Shader.PropertyToID("_MaskTex");
|
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 PadId = Shader.PropertyToID("_Pad");
|
||||||
private static readonly int PreviewOpacityId = Shader.PropertyToID("_PreviewOpacity");
|
private static readonly int PreviewOpacityId = Shader.PropertyToID("_PreviewOpacity");
|
||||||
private static readonly int PreviewModeId = Shader.PropertyToID("_PreviewMode");
|
private static readonly int PreviewModeId = Shader.PropertyToID("_PreviewMode");
|
||||||
@@ -1070,7 +1067,7 @@ public partial class MeshPainter : MonoBehaviour
|
|||||||
case MAP_ALBEDO: return paintAlbedo;
|
case MAP_ALBEDO: return paintAlbedo;
|
||||||
case MAP_MSO: return paintMetallicSmoothness || paintOcclusion;
|
case MAP_MSO: return paintMetallicSmoothness || paintOcclusion;
|
||||||
case MAP_EM: return paintEmission;
|
case MAP_EM: return paintEmission;
|
||||||
case MAP_FUR: return fur;
|
case MAP_FUR: return fur; // fur has its own toggle gate (strand colour still comes from the albedo map)
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ public partial class PaintHud
|
|||||||
"defaults every session.")]
|
"defaults every session.")]
|
||||||
public bool persistHud = true;
|
public bool persistHud = true;
|
||||||
|
|
||||||
private const int HudFileVersion = 1;
|
// v2: HudValues gained fur (toggle) + furLength. Bumping discards pre-v2 saves so the new fur fields
|
||||||
|
// fall back to the painter's prefab defaults instead of deserializing as 0/false.
|
||||||
|
private const int HudFileVersion = 2;
|
||||||
|
|
||||||
// Auto-save bookkeeping. Brush edits are debounced (sliders fire every frame while dragged): the due
|
// Auto-save bookkeeping. Brush edits are debounced (sliders fire every frame while dragged): the due
|
||||||
// time is pushed forward and written once they settle.
|
// time is pushed forward and written once they settle.
|
||||||
@@ -83,11 +85,12 @@ public partial class PaintHud
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
private class HudValues
|
private class HudValues
|
||||||
{
|
{
|
||||||
public bool paintAlbedo, paintMetallicSmoothness, paintOcclusion, paintEmission, spray;
|
public bool paintAlbedo, paintMetallicSmoothness, paintOcclusion, paintEmission, spray, fur;
|
||||||
public Color brushColor;
|
public Color brushColor;
|
||||||
public float metallic, smoothness, metallicSmoothnessOpacity, occlusion, occlusionOpacity;
|
public float metallic, smoothness, metallicSmoothnessOpacity, occlusion, occlusionOpacity;
|
||||||
public Color emissionColor;
|
public Color emissionColor;
|
||||||
public float emissionIntensity;
|
public float emissionIntensity;
|
||||||
|
public float furLength;
|
||||||
public int brushSize;
|
public int brushSize;
|
||||||
public int activeBrushIndex;
|
public int activeBrushIndex;
|
||||||
public int sprayDots;
|
public int sprayDots;
|
||||||
@@ -275,6 +278,8 @@ public partial class PaintHud
|
|||||||
&& p.paintOcclusion == v.paintOcclusion
|
&& p.paintOcclusion == v.paintOcclusion
|
||||||
&& p.paintEmission == v.paintEmission
|
&& p.paintEmission == v.paintEmission
|
||||||
&& p.spray == v.spray
|
&& p.spray == v.spray
|
||||||
|
&& p.fur == v.fur
|
||||||
|
&& p.furLength == v.furLength
|
||||||
&& p.brushColor == v.brushColor
|
&& p.brushColor == v.brushColor
|
||||||
&& p.metallic == v.metallic
|
&& p.metallic == v.metallic
|
||||||
&& p.smoothness == v.smoothness
|
&& p.smoothness == v.smoothness
|
||||||
@@ -335,6 +340,8 @@ public partial class PaintHud
|
|||||||
v.paintOcclusion = from.paintOcclusion;
|
v.paintOcclusion = from.paintOcclusion;
|
||||||
v.paintEmission = from.paintEmission;
|
v.paintEmission = from.paintEmission;
|
||||||
v.spray = from.spray;
|
v.spray = from.spray;
|
||||||
|
v.fur = from.fur;
|
||||||
|
v.furLength = from.furLength;
|
||||||
v.brushColor = from.brushColor;
|
v.brushColor = from.brushColor;
|
||||||
v.metallic = from.metallic;
|
v.metallic = from.metallic;
|
||||||
v.smoothness = from.smoothness;
|
v.smoothness = from.smoothness;
|
||||||
@@ -371,6 +378,8 @@ public partial class PaintHud
|
|||||||
to.paintOcclusion = v.paintOcclusion;
|
to.paintOcclusion = v.paintOcclusion;
|
||||||
to.paintEmission = v.paintEmission;
|
to.paintEmission = v.paintEmission;
|
||||||
to.spray = v.spray;
|
to.spray = v.spray;
|
||||||
|
to.fur = v.fur;
|
||||||
|
to.furLength = v.furLength;
|
||||||
to.brushColor = v.brushColor;
|
to.brushColor = v.brushColor;
|
||||||
to.metallic = v.metallic;
|
to.metallic = v.metallic;
|
||||||
to.smoothness = v.smoothness;
|
to.smoothness = v.smoothness;
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
private const string ToggleFur = "Toggle_Fur";
|
private const string ToggleFur = "Toggle_Fur";
|
||||||
private const string PickerAlbedo = "Picker_Albedo";
|
private const string PickerAlbedo = "Picker_Albedo";
|
||||||
private const string PickerEmission = "Picker_Emission";
|
private const string PickerEmission = "Picker_Emission";
|
||||||
private const string PickerFur = "Picker_Fur";
|
|
||||||
private const string SliderAlbedoAlpha = "Slider_AlbedoAlpha";
|
private const string SliderAlbedoAlpha = "Slider_AlbedoAlpha";
|
||||||
private const string SliderEmissionAlpha = "Slider_EmissionAlpha";
|
private const string SliderEmissionAlpha = "Slider_EmissionAlpha";
|
||||||
private const string SliderMetallic = "Slider_Metallic";
|
private const string SliderMetallic = "Slider_Metallic";
|
||||||
@@ -65,8 +64,7 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
private const string SliderOcclusionThreshold = "Slider_OcclusionThreshold";
|
private const string SliderOcclusionThreshold = "Slider_OcclusionThreshold";
|
||||||
private const string SliderNormalIntensity = "Slider_NormalIntensity";
|
private const string SliderNormalIntensity = "Slider_NormalIntensity";
|
||||||
private const string SliderParallaxIntensity = "Slider_ParallaxIntensity";
|
private const string SliderParallaxIntensity = "Slider_ParallaxIntensity";
|
||||||
private const string SliderFurAlpha = "Slider_FurAlpha";
|
private const string SliderFurLength = "Slider_FurLength";
|
||||||
private const string SliderFurOpacity = "Slider_FurOpacity";
|
|
||||||
private const string MaskSelector = "MaskSelector";
|
private const string MaskSelector = "MaskSelector";
|
||||||
private const string MaskSwatchPrefix = "MaskSwatch_";
|
private const string MaskSwatchPrefix = "MaskSwatch_";
|
||||||
|
|
||||||
@@ -263,12 +261,10 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
WireSlider(panel, SliderNormalIntensity, painter ? painter.HeightNormalIntensity : 1.4f, v => { if (painter) painter.HeightNormalIntensity = v; });
|
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; });
|
WireSlider(panel, SliderParallaxIntensity, painter ? painter.ParallaxIntensity : 1f, v => { if (painter) painter.ParallaxIntensity = v; });
|
||||||
|
|
||||||
WirePicker(panel, PickerFur, painter ? painter.furColor : Color.blue,
|
// Fur has its own Toggle_Fur gate (wired above); the colour comes from the albedo map and the deposit
|
||||||
c => { if (painter) painter.furColor = WithRgb(painter.furColor, c); });
|
// weight from the brush alpha, so the only extra fur control is the target length.
|
||||||
WireSlider(panel, SliderFurAlpha, painter ? painter.furColor.a : .6f,
|
WireSlider(panel, SliderFurLength, painter ? painter.furLength : 0.5f,
|
||||||
v => { if (painter) painter.furColor = WithAlpha(painter.furColor, v); });
|
v => { if (painter) painter.furLength = v; });
|
||||||
WireSlider(panel, SliderFurOpacity, painter ? painter.furOpacity : 1f,
|
|
||||||
v => { if (painter) painter.furOpacity = v; });
|
|
||||||
|
|
||||||
foreach (var name in DraggablePanels)
|
foreach (var name in DraggablePanels)
|
||||||
MakeDraggable(panel, name);
|
MakeDraggable(panel, name);
|
||||||
@@ -295,11 +291,9 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
|
|
||||||
SetPicker(panel, PickerAlbedo, painter.brushColor);
|
SetPicker(panel, PickerAlbedo, painter.brushColor);
|
||||||
SetPicker(panel, PickerEmission, painter.emissionColor);
|
SetPicker(panel, PickerEmission, painter.emissionColor);
|
||||||
SetPicker(panel, PickerFur, painter.furColor);
|
|
||||||
SetSlider(panel, SliderAlbedoAlpha, painter.brushColor.a);
|
SetSlider(panel, SliderAlbedoAlpha, painter.brushColor.a);
|
||||||
SetSlider(panel, SliderEmissionAlpha, painter.emissionColor.a);
|
SetSlider(panel, SliderEmissionAlpha, painter.emissionColor.a);
|
||||||
SetSlider(panel, SliderFurAlpha, painter.furColor.a);
|
SetSlider(panel, SliderFurLength, painter.furLength);
|
||||||
SetSlider(panel, SliderFurOpacity, painter.furOpacity);
|
|
||||||
|
|
||||||
SetSlider(panel, SliderMetallic, painter.metallic);
|
SetSlider(panel, SliderMetallic, painter.metallic);
|
||||||
SetSlider(panel, SliderSmoothness, painter.smoothness);
|
SetSlider(panel, SliderSmoothness, painter.smoothness);
|
||||||
|
|||||||
@@ -28,18 +28,28 @@ Shader "Custom/FurShell"
|
|||||||
_SilhouetteStretch("Silhouette Strand Stretch", Range(0.0, 5.0)) = 2.0
|
_SilhouetteStretch("Silhouette Strand Stretch", Range(0.0, 5.0)) = 2.0
|
||||||
|
|
||||||
[Header(Fur Shading)]
|
[Header(Fur Shading)]
|
||||||
[MainTexture] _BaseMap("Albedo", 2D) = "white" {}
|
// The strand colour: the painted ALBEDO map (the camo under the fur). FurShellRenderer binds the
|
||||||
[MainColor] _BaseColor("Root Tint", Color) = (0.42, 0.32, 0.20, 1)
|
// sibling painter's live albedo RT here, so each strand wears the colour beneath it.
|
||||||
_TipColor("Tip Tint", Color) = (0.62, 0.52, 0.36, 1)
|
[MainTexture] _BaseMap("Albedo (strand colour)", 2D) = "white" {}
|
||||||
_RootAO("Root Darkening (fake AO)", Range(0.0, 1.0)) = 0.45
|
// Overall tint multiplied onto the painted fur colour; white leaves the painted colour untouched.
|
||||||
|
[MainColor] _BaseColor("Fur Tint", Color) = (1, 1, 1, 1)
|
||||||
|
// Root/tip are two shades of the SAME painted colour: roots a touch darker, tips a touch lighter.
|
||||||
|
_RootAO("Root Darkening", Range(0.0, 1.0)) = 0.3
|
||||||
|
_TipShade("Tip Lightening", Range(0.0, 1.0)) = 0.2
|
||||||
_RimPower("Rim / Backlight Power", Range(0.5, 8.0)) = 3.0
|
_RimPower("Rim / Backlight Power", Range(0.5, 8.0)) = 3.0
|
||||||
_RimStrength("Rim / Backlight Strength", Range(0.0, 2.0)) = 0.4
|
_RimStrength("Rim / Backlight Strength", Range(0.0, 2.0)) = 0.4
|
||||||
|
|
||||||
[Header(Fur Map)]
|
[Header(Fur Map)]
|
||||||
// RGB = per-texel strand colour, A = fur length (0 = bald here, 1 = full length). White default
|
// R = fur length (0 = bald here, 1 = full length); G/B/A unused/reserved. The strand COLOUR now
|
||||||
// means "full-length fur everywhere" so the standalone prototype behaves as before with no map.
|
// comes from the albedo map (_BaseMap), so fur wears the painted camo. White default = full-length
|
||||||
_FurMap("Fur Map (RGB color, A length)", 2D) = "white" {}
|
// fur everywhere, so the standalone prototype (no map) is unchanged.
|
||||||
|
_FurMap("Fur Map (R = length)", 2D) = "white" {}
|
||||||
_FurThreshold("Fur Cutoff (alpha)", Range(0.0, 1.0)) = 0.0
|
_FurThreshold("Fur Cutoff (alpha)", Range(0.0, 1.0)) = 0.0
|
||||||
|
// Low painted alpha => low opacity, so a lightly-painted texel fades in as faint short fuzz
|
||||||
|
// instead of full-opacity fur with a hard border. Larger = softer/longer fade.
|
||||||
|
_FurSoftness("Edge Softness (length fade)", Range(0.01, 1.0)) = 0.5
|
||||||
|
// Per-strand anti-aliasing: soft strand silhouette + tip instead of hard alpha-clipped discs.
|
||||||
|
_StrandSoftness("Strand Edge Softness", Range(0.0, 1.0)) = 0.3
|
||||||
// Opaque base shell. On = solid root (full-coverage prototype); Off = the root also clips to the
|
// Opaque base shell. On = solid root (full-coverage prototype); Off = the root also clips to the
|
||||||
// map so the painted body shows between strands (avatar integration).
|
// map so the painted body shows between strands (avatar integration).
|
||||||
[ToggleUI] _SolidRoot("Solid Root", Float) = 1
|
[ToggleUI] _SolidRoot("Solid Root", Float) = 1
|
||||||
@@ -55,9 +65,9 @@ Shader "Custom/FurShell"
|
|||||||
{
|
{
|
||||||
Tags
|
Tags
|
||||||
{
|
{
|
||||||
"RenderType" = "Opaque"
|
"RenderType" = "Transparent"
|
||||||
"RenderPipeline" = "UniversalPipeline"
|
"RenderPipeline" = "UniversalPipeline"
|
||||||
"Queue" = "Geometry"
|
"Queue" = "Transparent"
|
||||||
}
|
}
|
||||||
LOD 200
|
LOD 200
|
||||||
|
|
||||||
@@ -67,8 +77,17 @@ Shader "Custom/FurShell"
|
|||||||
Tags { "LightMode" = "UniversalForward" }
|
Tags { "LightMode" = "UniversalForward" }
|
||||||
|
|
||||||
Cull Back
|
Cull Back
|
||||||
ZWrite On
|
Blend SrcAlpha OneMinusSrcAlpha
|
||||||
|
ZWrite Off
|
||||||
ZTest LEqual
|
ZTest LEqual
|
||||||
|
// Depth bias toward the camera so the fur wins the depth test against the body. The fur
|
||||||
|
// extrudes a CPU-baked mesh while the body uses GPU skinning, so their surfaces are not
|
||||||
|
// bit-identical: where the body pokes a hair in front of a shell layer, that shell fragment
|
||||||
|
// fails ZTest (ZWrite Off => no depth write to win the tie) and the body flickers through as
|
||||||
|
// white speckles. Offset shifts the fragment's tested depth (respected even with ZWrite Off),
|
||||||
|
// slope-scaled + a constant unit, so the fur reliably passes without moving geometry (no
|
||||||
|
// visible root gap). Small enough not to punch through objects genuinely in front.
|
||||||
|
Offset -1, -1
|
||||||
|
|
||||||
HLSLPROGRAM
|
HLSLPROGRAM
|
||||||
#pragma target 3.5
|
#pragma target 3.5
|
||||||
@@ -87,7 +106,6 @@ Shader "Custom/FurShell"
|
|||||||
CBUFFER_START(UnityPerMaterial)
|
CBUFFER_START(UnityPerMaterial)
|
||||||
float4 _BaseMap_ST;
|
float4 _BaseMap_ST;
|
||||||
half4 _BaseColor;
|
half4 _BaseColor;
|
||||||
half4 _TipColor;
|
|
||||||
float _ShellCount;
|
float _ShellCount;
|
||||||
float _ShellLength;
|
float _ShellLength;
|
||||||
float _Density;
|
float _Density;
|
||||||
@@ -98,9 +116,12 @@ Shader "Custom/FurShell"
|
|||||||
float _DirRandomness;
|
float _DirRandomness;
|
||||||
float _SilhouetteStretch;
|
float _SilhouetteStretch;
|
||||||
half _RootAO;
|
half _RootAO;
|
||||||
|
half _TipShade;
|
||||||
half _RimPower;
|
half _RimPower;
|
||||||
half _RimStrength;
|
half _RimStrength;
|
||||||
float _FurThreshold;
|
float _FurThreshold;
|
||||||
|
float _FurSoftness;
|
||||||
|
float _StrandSoftness;
|
||||||
float _SolidRoot;
|
float _SolidRoot;
|
||||||
float _WindStrength;
|
float _WindStrength;
|
||||||
float _WindSpeed;
|
float _WindSpeed;
|
||||||
@@ -152,14 +173,13 @@ Shader "Custom/FurShell"
|
|||||||
float count = max(_ShellCount, 1.0);
|
float count = max(_ShellCount, 1.0);
|
||||||
float h = (count <= 1.0) ? 0.0 : (float)shellIndex / (count - 1.0);
|
float h = (count <= 1.0) ? 0.0 : (float)shellIndex / (count - 1.0);
|
||||||
|
|
||||||
// extrude along the normal; bend the tips down with h^2 for a gravity/curl droop
|
// extrude along the normal; bend the tips down with h^2 for a gravity/curl droop.
|
||||||
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * h);
|
// The (h + 0.02) lifts the whole stack a hair off the surface: the root (h=0) would else
|
||||||
// gravity pulls toward WORLD down, so strands hang downward no matter how the mesh is
|
// sit exactly on the opaque body and z-fight it - the fur draws a CPU-baked mesh while the
|
||||||
// oriented - a rotated player mesh would otherwise droop along its own local down. Convert
|
// body uses GPU skinning, so their depths are not bit-identical, and now that fur is
|
||||||
// world down into object space (the extrusion space) so the droop magnitude stays consistent
|
// transparent (ZWrite Off) it can't rely on a depth write to win the tie. 0.02 of the shell
|
||||||
// with _ShellLength under the object-to-world transform.
|
// length is invisible but separates the root cleanly.
|
||||||
float3 downOS = normalize(mul((float3x3)unity_WorldToObject, float3(0.0, -1.0, 0.0)));
|
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * (h + 0.02));
|
||||||
posOS += downOS * (_Curvature * _ShellLength * h * h);
|
|
||||||
|
|
||||||
// wind sway: nudge each shell along the wind direction (object XZ) by a time-varying
|
// wind sway: nudge each shell along the wind direction (object XZ) by a time-varying
|
||||||
// amount. The phase is offset by object-space position so a gust rolls across the surface,
|
// amount. The phase is offset by object-space position so a gust rolls across the surface,
|
||||||
@@ -171,11 +191,20 @@ Shader "Custom/FurShell"
|
|||||||
float phase = _Time.y * _WindSpeed + (input.positionOS.x + input.positionOS.z) * _WindFrequency;
|
float phase = _Time.y * _WindSpeed + (input.positionOS.x + input.positionOS.z) * _WindFrequency;
|
||||||
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase));
|
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase));
|
||||||
|
|
||||||
VertexPositionInputs vp = GetVertexPositionInputs(posOS);
|
// Gravity/curl bends toward WORLD down. Applied AFTER moving to world space and using ONLY
|
||||||
|
// unity_ObjectToWorld (positions/normals already prove that matrix correct). The previous
|
||||||
|
// object-space approach relied on unity_WorldToObject, which is NOT reliably populated in the
|
||||||
|
// RenderMeshInstanced path - when it falls back to identity the droop follows the mesh's own
|
||||||
|
// local down, so on the rotated player the fur hung sideways instead of toward true down.
|
||||||
|
// objScale keeps the droop magnitude consistent with the normal extrusion (object units).
|
||||||
|
float3 positionWS = TransformObjectToWorld(posOS);
|
||||||
|
float objScale = length(mul((float3x3)unity_ObjectToWorld, float3(0.0, 1.0, 0.0)));
|
||||||
|
positionWS.y -= _Curvature * _ShellLength * h * h * objScale;
|
||||||
|
|
||||||
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
||||||
|
|
||||||
output.positionCS = vp.positionCS;
|
output.positionCS = TransformWorldToHClip(positionWS);
|
||||||
output.positionWS = vp.positionWS;
|
output.positionWS = positionWS;
|
||||||
output.normalWS = vn.normalWS;
|
output.normalWS = vn.normalWS;
|
||||||
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
|
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
|
||||||
output.shellH = h;
|
output.shellH = h;
|
||||||
@@ -189,10 +218,10 @@ Shader "Custom/FurShell"
|
|||||||
float3 N = normalize(input.normalWS);
|
float3 N = normalize(input.normalWS);
|
||||||
float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS);
|
float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS);
|
||||||
|
|
||||||
// fur map: RGB = strand colour, A = fur length. White default (no map) => full-length fur
|
// fur map: R = fur length (G/B/A unused). White default (no map) => full-length fur
|
||||||
// everywhere, so the standalone prototype is unchanged.
|
// everywhere, so the standalone prototype is unchanged. The strand colour comes from the
|
||||||
half4 furSample = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv);
|
// albedo map (_BaseMap) below, not from here.
|
||||||
float furLen = furSample.a;
|
float furLen = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv).r;
|
||||||
|
|
||||||
// --- strand mask: one strand per cell, random length, radially tapered toward the tip ---
|
// --- strand mask: one strand per cell, random length, radially tapered toward the tip ---
|
||||||
float2 g = input.uv * _Density;
|
float2 g = input.uv * _Density;
|
||||||
@@ -253,26 +282,42 @@ Shader "Custom/FurShell"
|
|||||||
float taperedR = pow(saturate(1.0 - h), _Taper * 0.5);
|
float taperedR = pow(saturate(1.0 - h), _Taper * 0.5);
|
||||||
float radius = _Thickness * max(taperedR, _TipFill);
|
float radius = _Thickness * max(taperedR, _TipFill);
|
||||||
|
|
||||||
// The fur map's alpha caps this strand's height: 0 => bald, so the whole shell stack
|
// --- soft coverage (replaces the old binary alpha-clip) so the fur fades IN with alpha
|
||||||
// (root included) clips and the body shows through; 1 => the strand reaches its full
|
// instead of a hard on/off edge. Three factors multiply into the output alpha:
|
||||||
// random length. _SolidRoot keeps the base shell opaque for the full-coverage prototype;
|
// radialCov : soft, anti-aliased strand edge instead of a hard disc.
|
||||||
// on the painted avatar it is turned OFF so the camo body stays visible between strands
|
// heightCov : each strand fades out toward its own random tip (_TipFill caps the height).
|
||||||
// instead of being sealed under a solid root (which would also Z-fight the body).
|
// lenOpacity : LOW painted alpha => LOW opacity. A lightly-painted texel reads as faint,
|
||||||
|
// short fuzz that blends over the body -> the soft transition asked for,
|
||||||
|
// instead of full-opacity fur with a hard border. The fur map's alpha still
|
||||||
|
// also caps the strand HEIGHT (effectiveLen), so low alpha is short AND faint.
|
||||||
|
// The mesh is drawn on the Transparent queue and the shells are submitted root->tip, i.e.
|
||||||
|
// back-to-front on front faces, so these alphas composite correctly over the camo body.
|
||||||
float effectiveLen = strandLen * furLen;
|
float effectiveLen = strandLen * furLen;
|
||||||
half alpha;
|
float radialCov = 1.0 - smoothstep(radius - max(radius * _StrandSoftness, 1e-4), radius, dist);
|
||||||
if (furLen <= _FurThreshold)
|
float heightCov = 1.0 - smoothstep(effectiveLen * (1.0 - _StrandSoftness), effectiveLen + 1e-4, h);
|
||||||
alpha = 0.0h;
|
float lenOpacity = saturate((furLen - _FurThreshold) / max(_FurSoftness, 1e-4));
|
||||||
else if (h <= 0.0 && _SolidRoot > 0.5)
|
|
||||||
alpha = 1.0h;
|
half alpha = (half)(radialCov * heightCov * lenOpacity);
|
||||||
else
|
|
||||||
alpha = (dist < radius && h <= effectiveLen) ? 1.0h : 0.0h;
|
// _SolidRoot keeps the base shell fully covering (full-coverage prototype), but still honours
|
||||||
clip(alpha - 0.5h);
|
// the painted length so the boundary keeps its soft fade instead of a hard opaque ring. On
|
||||||
|
// the painted avatar it is turned OFF so the root also thins to the strands and the camo body
|
||||||
|
// stays visible between them.
|
||||||
|
if (_SolidRoot > 0.5 && h <= 0.0)
|
||||||
|
alpha = (half)lenOpacity;
|
||||||
|
|
||||||
|
if (furLen <= _FurThreshold) alpha = 0.0h;
|
||||||
|
clip(alpha - 0.003h); // drop fully-transparent fragments (perf + avoids stray blend)
|
||||||
|
|
||||||
// --- shading (lightweight URP) ---
|
// --- shading (lightweight URP) ---
|
||||||
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * furSample.rgb * _BaseColor.rgb;
|
// base colour is the PAINTED ALBEDO (the camo under the fur), sampled per-texel so each
|
||||||
albedo = lerp(albedo, _TipColor.rgb, h);
|
// strand wears the colour beneath it, times the global tint (white by default). The fur map
|
||||||
half ao = lerp(1.0h - _RootAO, 1.0h, h); // roots darker (fake self-occlusion)
|
// no longer carries colour - only length.
|
||||||
albedo *= ao;
|
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb;
|
||||||
|
// two shades of that one colour: roots a touch darker (fake self-occlusion), tips a touch
|
||||||
|
// lighter - a subtle root->tip gradient rather than two independent colours.
|
||||||
|
half ao = lerp(1.0h - _RootAO, 1.0h, h);
|
||||||
|
albedo *= ao * lerp(1.0h, 1.0h + _TipShade, h);
|
||||||
|
|
||||||
float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
|
float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
|
||||||
Light mainLight = GetMainLight(shadowCoord);
|
Light mainLight = GetMainLight(shadowCoord);
|
||||||
@@ -296,7 +341,7 @@ Shader "Custom/FurShell"
|
|||||||
half rim = pow(saturate(1.0h - saturate(dot(N, V))), _RimPower) * _RimStrength * h;
|
half rim = pow(saturate(1.0h - saturate(dot(N, V))), _RimPower) * _RimStrength * h;
|
||||||
color += rim * mainLight.color * albedo;
|
color += rim * mainLight.color * albedo;
|
||||||
|
|
||||||
return half4(color, 1.0h);
|
return half4(color, alpha);
|
||||||
}
|
}
|
||||||
ENDHLSL
|
ENDHLSL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,138 +139,6 @@ Shader "Hidden/MeshPaintBrush"
|
|||||||
fixed4 frag (v2f i) : SV_Target { return fixed4(0, 0, 0, 1); }
|
fixed4 frag (v2f i) : SV_Target { return fixed4(0, 0, 0, 1); }
|
||||||
ENDCG
|
ENDCG
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2 - fur fade: RGB source-over toward the strand colour AND alpha faded by (1-op),
|
|
||||||
// where op = luma(mask.rgb) * mask.a * _BrushColor.a is the per-texel deposit strength.
|
|
||||||
// Paired with pass 3 (deposit): together they make alpha ease toward a target instead of
|
|
||||||
// accumulating to 1, so the fur map's alpha (strand length) is an over-writable value, not
|
|
||||||
// a coverage marker. Both fur passes clip the exact same texels, so the (1-op) fade and the
|
|
||||||
// op*target add line up per texel. Writes into the fur display RT in place (no ping-pong).
|
|
||||||
Pass
|
|
||||||
{
|
|
||||||
ZTest Always
|
|
||||||
ZWrite Off
|
|
||||||
Cull Off
|
|
||||||
ColorMask RGBA
|
|
||||||
// RGB: op*strand + (1-op)*dst (source-over). A: 0*src + (1-op)*dst (multiply by 1-op).
|
|
||||||
Blend SrcAlpha OneMinusSrcAlpha, Zero OneMinusSrcAlpha
|
|
||||||
|
|
||||||
CGPROGRAM
|
|
||||||
#pragma vertex vert
|
|
||||||
#pragma fragment frag
|
|
||||||
#include "UnityCG.cginc"
|
|
||||||
|
|
||||||
float4 _BrushColor;
|
|
||||||
float3 _BrushCenter;
|
|
||||||
float3 _BrushU;
|
|
||||||
float3 _BrushV;
|
|
||||||
float3 _BrushNormal;
|
|
||||||
float _BrushRadius;
|
|
||||||
sampler2D _MaskTex;
|
|
||||||
|
|
||||||
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
|
|
||||||
struct v2f { float4 pos : SV_POSITION; float3 local : TEXCOORD0; };
|
|
||||||
|
|
||||||
v2f vert (appdata v)
|
|
||||||
{
|
|
||||||
v2f o;
|
|
||||||
float2 p = v.uv * 2.0 - 1.0;
|
|
||||||
#if UNITY_UV_STARTS_AT_TOP
|
|
||||||
p.y = -p.y;
|
|
||||||
#endif
|
|
||||||
o.pos = float4(p, 0.0, 1.0);
|
|
||||||
o.local = v.vertex.xyz;
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-texel deposit strength (clips texels outside the square stamp box).
|
|
||||||
float BrushOp (v2f i)
|
|
||||||
{
|
|
||||||
float3 d = i.local - _BrushCenter;
|
|
||||||
float du = dot(d, _BrushU);
|
|
||||||
float dv = dot(d, _BrushV);
|
|
||||||
float dn = dot(d, _BrushNormal);
|
|
||||||
clip(_BrushRadius - abs(du));
|
|
||||||
clip(_BrushRadius - abs(dv));
|
|
||||||
clip(_BrushRadius - abs(dn));
|
|
||||||
float2 muv = float2(du, dv) / _BrushRadius * 0.5 + 0.5;
|
|
||||||
fixed4 m = tex2D(_MaskTex, muv);
|
|
||||||
float luma = dot(m.rgb, fixed3(0.299, 0.587, 0.114));
|
|
||||||
return luma * m.a * _BrushColor.a;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed4 frag (v2f i) : SV_Target
|
|
||||||
{
|
|
||||||
float op = BrushOp(i);
|
|
||||||
clip(op - 1e-4);
|
|
||||||
return fixed4(_BrushColor.rgb, op);
|
|
||||||
}
|
|
||||||
ENDCG
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 3 - fur deposit: alpha += op * _FurTarget (RGB masked out). Run AFTER pass 2's fade,
|
|
||||||
// so the alpha channel becomes (1-op)*dst + op*target = lerp(dst, target, op). _FurTarget is
|
|
||||||
// the desired strand length; _BrushColor.a is the brush strength that eases toward it.
|
|
||||||
Pass
|
|
||||||
{
|
|
||||||
ZTest Always
|
|
||||||
ZWrite Off
|
|
||||||
Cull Off
|
|
||||||
ColorMask A
|
|
||||||
Blend One One
|
|
||||||
|
|
||||||
CGPROGRAM
|
|
||||||
#pragma vertex vert
|
|
||||||
#pragma fragment frag
|
|
||||||
#include "UnityCG.cginc"
|
|
||||||
|
|
||||||
float4 _BrushColor;
|
|
||||||
float _FurTarget;
|
|
||||||
float3 _BrushCenter;
|
|
||||||
float3 _BrushU;
|
|
||||||
float3 _BrushV;
|
|
||||||
float3 _BrushNormal;
|
|
||||||
float _BrushRadius;
|
|
||||||
sampler2D _MaskTex;
|
|
||||||
|
|
||||||
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
|
|
||||||
struct v2f { float4 pos : SV_POSITION; float3 local : TEXCOORD0; };
|
|
||||||
|
|
||||||
v2f vert (appdata v)
|
|
||||||
{
|
|
||||||
v2f o;
|
|
||||||
float2 p = v.uv * 2.0 - 1.0;
|
|
||||||
#if UNITY_UV_STARTS_AT_TOP
|
|
||||||
p.y = -p.y;
|
|
||||||
#endif
|
|
||||||
o.pos = float4(p, 0.0, 1.0);
|
|
||||||
o.local = v.vertex.xyz;
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
float BrushOp (v2f i)
|
|
||||||
{
|
|
||||||
float3 d = i.local - _BrushCenter;
|
|
||||||
float du = dot(d, _BrushU);
|
|
||||||
float dv = dot(d, _BrushV);
|
|
||||||
float dn = dot(d, _BrushNormal);
|
|
||||||
clip(_BrushRadius - abs(du));
|
|
||||||
clip(_BrushRadius - abs(dv));
|
|
||||||
clip(_BrushRadius - abs(dn));
|
|
||||||
float2 muv = float2(du, dv) / _BrushRadius * 0.5 + 0.5;
|
|
||||||
fixed4 m = tex2D(_MaskTex, muv);
|
|
||||||
float luma = dot(m.rgb, fixed3(0.299, 0.587, 0.114));
|
|
||||||
return luma * m.a * _BrushColor.a;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed4 frag (v2f i) : SV_Target
|
|
||||||
{
|
|
||||||
float op = BrushOp(i);
|
|
||||||
clip(op - 1e-4);
|
|
||||||
return fixed4(0, 0, 0, op * _FurTarget);
|
|
||||||
}
|
|
||||||
ENDCG
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Fallback Off
|
Fallback Off
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user