FurShell: per-pixel z-order between fur objects (ZWrite On + coverage clip)
Transparent queue sorted fur draws per object, so overlapping fur renderers swapped in front of each other frame to frame. FurForward now writes depth; new Depth Sort Clip tunable discards low-coverage fragments before they write depth (avoids halo punch-through). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
38b9369051
commit
874ddccb4f
2 files changed
+30
-6
No files matched your search
@@ -65,6 +65,11 @@ namespace Chameleon.Components
|
||||
[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;
|
||||
// Fragments at/below this coverage are discarded so they don't write depth (the shader has
|
||||
// ZWrite ON to get a correct per-pixel z-order between overlapping fur objects - transparents
|
||||
// otherwise sort per object and pop). Higher = harder strand edges, less halo where two fur
|
||||
// objects overlap.
|
||||
[SerializeField, Range(0f, 1f)] float _depthSortClip = 0.3f;
|
||||
// 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.
|
||||
[SerializeField] bool _solidRoot = true;
|
||||
@@ -108,6 +113,7 @@ namespace Chameleon.Components
|
||||
static readonly int FurThresholdId = Shader.PropertyToID("_FurThreshold");
|
||||
static readonly int FurSoftnessId = Shader.PropertyToID("_FurSoftness");
|
||||
static readonly int StrandSoftnessId = Shader.PropertyToID("_StrandSoftness");
|
||||
static readonly int DepthSortClipId = Shader.PropertyToID("_DepthSortClip");
|
||||
static readonly int SolidRootId = Shader.PropertyToID("_SolidRoot");
|
||||
static readonly int WindStrengthId = Shader.PropertyToID("_WindStrength");
|
||||
static readonly int WindSpeedId = Shader.PropertyToID("_WindSpeed");
|
||||
@@ -246,6 +252,7 @@ namespace Chameleon.Components
|
||||
_mpb.SetFloat(FurThresholdId, _furThreshold);
|
||||
_mpb.SetFloat(FurSoftnessId, _furSoftness);
|
||||
_mpb.SetFloat(StrandSoftnessId, _strandSoftness);
|
||||
_mpb.SetFloat(DepthSortClipId, _depthSortClip);
|
||||
_mpb.SetFloat(SolidRootId, _solidRoot ? 1f : 0f);
|
||||
|
||||
_mpb.SetFloat(WindStrengthId, _windStrength);
|
||||
@@ -306,6 +313,7 @@ namespace Chameleon.Components
|
||||
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(DepthSortClipId)) _depthSortClip = mat.GetFloat(DepthSortClipId);
|
||||
if (mat.HasProperty(SolidRootId)) _solidRoot = mat.GetFloat(SolidRootId) > 0.5f;
|
||||
if (mat.HasProperty(WindStrengthId)) _windStrength = mat.GetFloat(WindStrengthId);
|
||||
if (mat.HasProperty(WindSpeedId)) _windSpeed = mat.GetFloat(WindSpeedId);
|
||||
|
||||
@@ -50,6 +50,12 @@ Shader "Custom/FurShell"
|
||||
_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
|
||||
// Fragments at/below this coverage are discarded so they don't WRITE DEPTH (ZWrite is on, see
|
||||
// pass). Depth writes are what give two overlapping fur objects a correct per-pixel z-order
|
||||
// (transparents otherwise sort per OBJECT and pop); the clip keeps faint edge pixels from
|
||||
// occluding the other object with near-invisible fragments. Higher = harder strand edges but
|
||||
// less edge halo between overlapping fur objects.
|
||||
_DepthSortClip("Depth Sort Coverage Clip", Range(0.0, 1.0)) = 0.3
|
||||
// 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).
|
||||
[ToggleUI] _SolidRoot("Solid Root", Float) = 1
|
||||
@@ -97,15 +103,21 @@ Shader "Custom/FurShell"
|
||||
|
||||
Cull Back
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
ZWrite Off
|
||||
// ZWrite ON although this is a transparent blend: URP sorts transparents per OBJECT, so two
|
||||
// overlapping fur objects would swap their whole-object z-order as the camera moves (visible
|
||||
// popping where the fur balls overlap). Writing depth resolves the occlusion per PIXEL instead:
|
||||
// whichever object draws first, the second one's hidden fragments fail ZTest. Fragments below
|
||||
// _DepthSortClip coverage are discarded (see frag) so faint edge pixels don't occlude. Within
|
||||
// one object the shells are submitted root->tip (= back-to-front on front faces), so its own
|
||||
// blending still composites correctly.
|
||||
ZWrite On
|
||||
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.
|
||||
// fails ZTest and the body flickers through as white speckles. Offset shifts the fragment's
|
||||
// depth, 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
|
||||
@@ -141,6 +153,7 @@ Shader "Custom/FurShell"
|
||||
float _FurThreshold;
|
||||
float _FurSoftness;
|
||||
float _StrandSoftness;
|
||||
float _DepthSortClip;
|
||||
float _SolidRoot;
|
||||
float _WindStrength;
|
||||
float _WindSpeed;
|
||||
@@ -344,7 +357,10 @@ Shader "Custom/FurShell"
|
||||
alpha = (half)lenOpacity;
|
||||
|
||||
if (furLen <= _FurThreshold) alpha = 0.0h;
|
||||
clip(alpha - 0.003h); // drop fully-transparent fragments (perf + avoids stray blend)
|
||||
// Discard low-coverage fragments BEFORE they write depth (ZWrite is on for per-pixel
|
||||
// sorting between overlapping fur objects): a near-invisible edge pixel must not occlude
|
||||
// the fur behind it. The 0.003 floor keeps the old fully-transparent drop as a minimum.
|
||||
clip(alpha - max((half)_DepthSortClip, 0.003h));
|
||||
|
||||
// --- shading (lightweight URP) ---
|
||||
// base colour is the PAINTED ALBEDO (the camo under the fur), sampled per-texel so each
|
||||
|
||||
Reference in new issue
Block a user