FurShell: Performance-Umbau (Distanz-LOD, Cutout+Early-Z, Tip-to-Root) + Deferred+/HTrace-Fixes

- Distanz-LOD am Renderer (Start/End/Min Shells, pro Kamera), Rendering Layer Mask wie beim MeshRenderer
- Cutout in der AlphaTest-Queue statt Transparent-Blend (Early-Z killt Overdraw), Shells tip-to-root submitted
- LightMode UniversalForwardOnly (Deferred+ hatte den Opaque-Pass sonst nicht gezeichnet)
- FurGBuffer-Pass (ComplexLit-Muster): HTrace WSGI sieht das Fell, kein Durchscheinen mehr
- Root-AO-Fix: Grading straehnen-relativ (gradeH) statt absoluter Shell-Hoehe -- schwarze Streifen weg
- Distanz-Aggregations-Fade wieder entfernt (auf Wunsch, Fern-Straehnen bleiben voll)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-11 16:59:38 +02:00
1 parent 4ed74a5005
commit b4e4c19e54
2 files changed
+408 -262

No files matched your search

+31 -11
View File
@@ -23,6 +23,14 @@ namespace Chameleon.Components
[SerializeField] Material _furMaterial;
[SerializeField, Range(1, 64)] int _shellCount = 32;
[Header("Distance LOD")]
// Shell cost is linear in the shell count and nobody can count 32 shells from a few meters away:
// full _shellCount up to Start, then a linear fade down to Min Shells at End (per camera, computed
// at draw time). Min Shells 0 stops drawing the fur entirely beyond End.
[SerializeField, Min(0f)] float _lodStartDistance = 8f;
[SerializeField, Min(0f)] float _lodEndDistance = 30f;
[SerializeField, Range(0, 64)] int _lodMinShells = 8;
[Header("Shape")]
[SerializeField, Range(0f, 10f)] float _shellLength = 0.15f;
[SerializeField, Range(1f, 1000f)] float _density = 250f;
@@ -65,10 +73,9 @@ 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.
// Alpha-cutout threshold: fragments at/below this coverage are discarded, everything above draws
// fully opaque (the fur renders on the AlphaTest queue - opaque + ZWrite gives per-pixel z-order
// between fur objects AND early-Z overdraw rejection). Higher = thinner/harder strands.
[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.
@@ -82,14 +89,16 @@ namespace Chameleon.Components
[SerializeField] Vector2 _windDirection = new Vector2(1f, 0f);
[Header("X-Ray Reveal")]
// The fur draws up to _shellCount stacked TRANSPARENT shells, so the mirrored X-ray glow composites
// once per overlapping layer and blows out; this scales the per-shell glow down so the total reads
// at roughly the body's intensity. Lower = subtler.
[SerializeField, Range(0f, 1f)] float _xrayFurStrength = 0.2f;
// Fur-vs-body glow intensity match. Since the fur went opaque cutout only ONE fur fragment
// survives per pixel (no shell stacking anymore), so this typically wants ~1 to match the body
// instead of the old stack-taming ~0.2.
[SerializeField, Range(0f, 1f)] float _xrayFurStrength = 1f;
[Header("Rendering")]
[SerializeField] ShadowCastingMode _shadowMode = ShadowCastingMode.Off;
[SerializeField] bool _receiveShadows = true;
// Same dropdown/semantics as MeshRenderer's Rendering Layer Mask (decal/light layer filtering).
[SerializeField] RenderingLayerMask _renderingLayerMask = RenderingLayerMask.defaultRenderingLayerMask;
// Hide the plain MeshRenderer on this object so it doesn't double-draw under the fur (the root
// shell already covers the surface). Leave off if you want a separately-shaded base mesh.
[SerializeField] bool _hideSourceRenderer = true;
@@ -197,9 +206,19 @@ namespace Chameleon.Components
// to the on-disk state after OnEnable ran, and RenderMeshInstanced throws without it.
if (!_furMaterial.enableInstancing) _furMaterial.enableInstancing = true;
int count = Mathf.Max(1, _shellCount);
if (_instances == null || _instances.Length != count)
_instances = new Matrix4x4[count];
// Distance LOD: scale the shell count down with camera distance (full count inside Start,
// Min Shells at/after End). Per camera on purpose - the draw is per camera anyway, so the
// Scene View and a distant player camera each get their own appropriate count.
int maxCount = Mathf.Max(1, _shellCount);
float lodEnd = Mathf.Max(_lodEndDistance, _lodStartDistance + 0.001f);
float lodT = Mathf.InverseLerp(_lodStartDistance, lodEnd, Vector3.Distance(cam.transform.position, transform.position));
int count = Mathf.RoundToInt(Mathf.Lerp(maxCount, Mathf.Min(_lodMinShells, maxCount), lodT));
if (count <= 0) return; // Min Shells 0 => beyond End the fur is culled entirely
// The array stays at the max size; RenderMeshInstanced only reads the first `count` entries,
// so LOD changes don't reallocate per frame.
if (_instances == null || _instances.Length != maxCount)
_instances = new Matrix4x4[maxCount];
// all shells share the object's transform; they differ only by SV_InstanceID in the shader
Matrix4x4 m = transform.localToWorldMatrix;
@@ -214,6 +233,7 @@ namespace Chameleon.Components
matProps = _mpb,
shadowCastingMode = _shadowMode,
receiveShadows = _receiveShadows,
renderingLayerMask = _renderingLayerMask,
};
for (int sub = 0; sub < mesh.subMeshCount; sub++)