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:
1 parent
4ed74a5005
commit
b4e4c19e54
2 files changed
+408
-262
No files matched your search
@@ -23,6 +23,14 @@ namespace Chameleon.Components
|
|||||||
[SerializeField] Material _furMaterial;
|
[SerializeField] Material _furMaterial;
|
||||||
[SerializeField, Range(1, 64)] int _shellCount = 32;
|
[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")]
|
[Header("Shape")]
|
||||||
[SerializeField, Range(0f, 10f)] float _shellLength = 0.15f;
|
[SerializeField, Range(0f, 10f)] float _shellLength = 0.15f;
|
||||||
[SerializeField, Range(1f, 1000f)] float _density = 250f;
|
[SerializeField, Range(1f, 1000f)] float _density = 250f;
|
||||||
@@ -65,10 +73,9 @@ namespace Chameleon.Components
|
|||||||
[SerializeField, Range(0.01f, 1f)] float _furSoftness = 0.5f;
|
[SerializeField, Range(0.01f, 1f)] float _furSoftness = 0.5f;
|
||||||
// Per-strand edge anti-aliasing (soft strand silhouette + tip instead of hard clipped discs).
|
// Per-strand edge anti-aliasing (soft strand silhouette + tip instead of hard clipped discs).
|
||||||
[SerializeField, Range(0f, 1f)] float _strandSoftness = 0.3f;
|
[SerializeField, Range(0f, 1f)] float _strandSoftness = 0.3f;
|
||||||
// Fragments at/below this coverage are discarded so they don't write depth (the shader has
|
// Alpha-cutout threshold: fragments at/below this coverage are discarded, everything above draws
|
||||||
// ZWrite ON to get a correct per-pixel z-order between overlapping fur objects - transparents
|
// fully opaque (the fur renders on the AlphaTest queue - opaque + ZWrite gives per-pixel z-order
|
||||||
// otherwise sort per object and pop). Higher = harder strand edges, less halo where two fur
|
// between fur objects AND early-Z overdraw rejection). Higher = thinner/harder strands.
|
||||||
// objects overlap.
|
|
||||||
[SerializeField, Range(0f, 1f)] float _depthSortClip = 0.3f;
|
[SerializeField, Range(0f, 1f)] float _depthSortClip = 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.
|
||||||
@@ -82,14 +89,16 @@ namespace Chameleon.Components
|
|||||||
[SerializeField] Vector2 _windDirection = new Vector2(1f, 0f);
|
[SerializeField] Vector2 _windDirection = new Vector2(1f, 0f);
|
||||||
|
|
||||||
[Header("X-Ray Reveal")]
|
[Header("X-Ray Reveal")]
|
||||||
// The fur draws up to _shellCount stacked TRANSPARENT shells, so the mirrored X-ray glow composites
|
// Fur-vs-body glow intensity match. Since the fur went opaque cutout only ONE fur fragment
|
||||||
// once per overlapping layer and blows out; this scales the per-shell glow down so the total reads
|
// survives per pixel (no shell stacking anymore), so this typically wants ~1 to match the body
|
||||||
// at roughly the body's intensity. Lower = subtler.
|
// instead of the old stack-taming ~0.2.
|
||||||
[SerializeField, Range(0f, 1f)] float _xrayFurStrength = 0.2f;
|
[SerializeField, Range(0f, 1f)] float _xrayFurStrength = 1f;
|
||||||
|
|
||||||
[Header("Rendering")]
|
[Header("Rendering")]
|
||||||
[SerializeField] ShadowCastingMode _shadowMode = ShadowCastingMode.Off;
|
[SerializeField] ShadowCastingMode _shadowMode = ShadowCastingMode.Off;
|
||||||
[SerializeField] bool _receiveShadows = true;
|
[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
|
// 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.
|
// shell already covers the surface). Leave off if you want a separately-shaded base mesh.
|
||||||
[SerializeField] bool _hideSourceRenderer = true;
|
[SerializeField] bool _hideSourceRenderer = true;
|
||||||
@@ -197,9 +206,19 @@ namespace Chameleon.Components
|
|||||||
// to the on-disk state after OnEnable ran, and RenderMeshInstanced throws without it.
|
// to the on-disk state after OnEnable ran, and RenderMeshInstanced throws without it.
|
||||||
if (!_furMaterial.enableInstancing) _furMaterial.enableInstancing = true;
|
if (!_furMaterial.enableInstancing) _furMaterial.enableInstancing = true;
|
||||||
|
|
||||||
int count = Mathf.Max(1, _shellCount);
|
// Distance LOD: scale the shell count down with camera distance (full count inside Start,
|
||||||
if (_instances == null || _instances.Length != count)
|
// Min Shells at/after End). Per camera on purpose - the draw is per camera anyway, so the
|
||||||
_instances = new Matrix4x4[count];
|
// 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
|
// all shells share the object's transform; they differ only by SV_InstanceID in the shader
|
||||||
Matrix4x4 m = transform.localToWorldMatrix;
|
Matrix4x4 m = transform.localToWorldMatrix;
|
||||||
@@ -214,6 +233,7 @@ namespace Chameleon.Components
|
|||||||
matProps = _mpb,
|
matProps = _mpb,
|
||||||
shadowCastingMode = _shadowMode,
|
shadowCastingMode = _shadowMode,
|
||||||
receiveShadows = _receiveShadows,
|
receiveShadows = _receiveShadows,
|
||||||
|
renderingLayerMask = _renderingLayerMask,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int sub = 0; sub < mesh.subMeshCount; sub++)
|
for (int sub = 0; sub < mesh.subMeshCount; sub++)
|
||||||
|
|||||||
+377
-251
@@ -48,14 +48,14 @@ Shader "Custom/FurShell"
|
|||||||
// Low painted alpha => low opacity, so a lightly-painted texel fades in as faint short fuzz
|
// 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.
|
// 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
|
_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.
|
// Per-strand anti-aliasing: softens the coverage falloff BEFORE the cutout clip, so it now mostly
|
||||||
|
// shapes where the cutoff lands rather than blending an edge (the fur is opaque cutout).
|
||||||
_StrandSoftness("Strand Edge Softness", Range(0.0, 1.0)) = 0.3
|
_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
|
// Alpha-cutout threshold: fragments at/below this coverage are discarded, everything else draws
|
||||||
// pass). Depth writes are what give two overlapping fur objects a correct per-pixel z-order
|
// FULLY OPAQUE. Opaque + ZWrite means early-Z rejects hidden fur fragments before the fragment
|
||||||
// (transparents otherwise sort per OBJECT and pop); the clip keeps faint edge pixels from
|
// shader runs (the main fur cost is overdraw), and overlapping fur objects sort per PIXEL via the
|
||||||
// occluding the other object with near-invisible fragments. Higher = harder strand edges but
|
// depth buffer. Higher = thinner/harder strands, lower = fuller strands.
|
||||||
// less edge halo between overlapping fur objects.
|
_DepthSortClip("Alpha Cutoff (coverage)", Range(0.0, 1.0)) = 0.3
|
||||||
_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
|
// 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
|
||||||
@@ -90,26 +90,298 @@ Shader "Custom/FurShell"
|
|||||||
{
|
{
|
||||||
Tags
|
Tags
|
||||||
{
|
{
|
||||||
"RenderType" = "Transparent"
|
"RenderType" = "TransparentCutout"
|
||||||
"RenderPipeline" = "UniversalPipeline"
|
"RenderPipeline" = "UniversalPipeline"
|
||||||
"Queue" = "Transparent"
|
"Queue" = "AlphaTest"
|
||||||
|
// ComplexLit pattern (same as PlayerCamoLit / PictureTessellationLit): on the Deferred+
|
||||||
|
// renderer URP keeps SHADING the fur in its forward-only pass and stencils these pixels
|
||||||
|
// out of the deferred lighting - the GBuffer pass below only feeds data consumers
|
||||||
|
// (HTrace WSGI), it never causes double lighting.
|
||||||
|
"UniversalMaterialType" = "ComplexLit"
|
||||||
}
|
}
|
||||||
LOD 200
|
LOD 200
|
||||||
|
|
||||||
|
// Shared by the FurForward and GBuffer passes so both extrude the SAME shells and clip the
|
||||||
|
// SAME strand silhouette - any drift between them would show as GI ghosting (HTrace reads the
|
||||||
|
// GBuffer pass, the eye sees the forward pass) or depth mismatches.
|
||||||
|
HLSLINCLUDE
|
||||||
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||||
|
|
||||||
|
CBUFFER_START(UnityPerMaterial)
|
||||||
|
float4 _BaseMap_ST;
|
||||||
|
half4 _BaseColor;
|
||||||
|
float _ShellCount;
|
||||||
|
float _ShellLength;
|
||||||
|
float _Density;
|
||||||
|
float _Thickness;
|
||||||
|
float _Taper;
|
||||||
|
float _TipFill;
|
||||||
|
float _Curvature;
|
||||||
|
float _DirRandomness;
|
||||||
|
float _SilhouetteStretch;
|
||||||
|
half _RootAO;
|
||||||
|
half _TipShade;
|
||||||
|
half _RimPower;
|
||||||
|
half _RimStrength;
|
||||||
|
float _FurThreshold;
|
||||||
|
float _FurSoftness;
|
||||||
|
float _StrandSoftness;
|
||||||
|
float _DepthSortClip;
|
||||||
|
float _SolidRoot;
|
||||||
|
float _WindStrength;
|
||||||
|
float _WindSpeed;
|
||||||
|
float _WindFrequency;
|
||||||
|
float4 _WindDirection;
|
||||||
|
float _FurObjScale;
|
||||||
|
half4 _XRayColor;
|
||||||
|
half _XRayPulseSpeed;
|
||||||
|
half _XRayPulseStrength;
|
||||||
|
half _XRayPulseShape;
|
||||||
|
half _XRayFurStrength;
|
||||||
|
CBUFFER_END
|
||||||
|
|
||||||
|
TEXTURE2D(_BaseMap);
|
||||||
|
SAMPLER(sampler_BaseMap);
|
||||||
|
TEXTURE2D(_FurMap);
|
||||||
|
SAMPLER(sampler_FurMap);
|
||||||
|
|
||||||
|
struct Attributes
|
||||||
|
{
|
||||||
|
float4 positionOS : POSITION;
|
||||||
|
float3 normalOS : NORMAL;
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
UNITY_VERTEX_INPUT_INSTANCE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Varyings
|
||||||
|
{
|
||||||
|
float4 positionCS : SV_POSITION;
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
float3 normalWS : TEXCOORD1;
|
||||||
|
float3 positionWS : TEXCOORD2;
|
||||||
|
float shellH : TEXCOORD3; // 0 = root, 1 = tip
|
||||||
|
};
|
||||||
|
|
||||||
|
// cheap deterministic 2D hash -> [0,1)
|
||||||
|
float Hash21(float2 p)
|
||||||
|
{
|
||||||
|
p = frac(p * float2(123.34, 345.45));
|
||||||
|
p += dot(p, p + 34.345);
|
||||||
|
return frac(p.x * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Varyings vert(Attributes input)
|
||||||
|
{
|
||||||
|
Varyings output = (Varyings)0;
|
||||||
|
UNITY_SETUP_INSTANCE_ID(input);
|
||||||
|
|
||||||
|
// shell layer index -> normalized height (guarded so the non-instanced variant compiles)
|
||||||
|
#if UNITY_ANY_INSTANCING_ENABLED
|
||||||
|
uint shellIndex = input.instanceID;
|
||||||
|
#else
|
||||||
|
uint shellIndex = 0u;
|
||||||
|
#endif
|
||||||
|
// Instance 0 = TIP, last instance = root: instances rasterize in submission order, so
|
||||||
|
// tip-first is front-to-back on front faces - the nearest (tip) shells fill the depth
|
||||||
|
// buffer first and early-Z rejects the root shells behind them. Order is free to choose
|
||||||
|
// because the fur is opaque cutout (no blending to keep back-to-front).
|
||||||
|
float count = max(_ShellCount, 1.0);
|
||||||
|
float h = (count <= 1.0) ? 0.0 : 1.0 - (float)shellIndex / (count - 1.0);
|
||||||
|
|
||||||
|
// The instance matrix multiplies the object-space fur displacement by the avatar's world scale
|
||||||
|
// (objScale), so a naive extrusion shrinks/grows 1:1 with the player - too short on the small
|
||||||
|
// hider, too long on the big hunter. To keep the fur a CONSTANT world length (same absolute
|
||||||
|
// length at any size) we pre-divide the object-space offset by objScale; after the matrix's
|
||||||
|
// *objScale the net world displacement is then 1x. Wind sway shares this so it stays a constant
|
||||||
|
// world size too. (The gravity droop below intentionally does NOT - it scales with objScale.)
|
||||||
|
float objScale = max(_FurObjScale, 1e-4);
|
||||||
|
float dispComp = 1.0 / objScale;
|
||||||
|
|
||||||
|
// extrude along the normal; bend the tips down with h^2 for a gravity/curl droop.
|
||||||
|
// The (h + 0.02) lifts the whole stack a hair off the surface: the root (h=0) would else
|
||||||
|
// sit exactly on the opaque body and z-fight it - the fur draws a CPU-baked mesh while the
|
||||||
|
// body uses GPU skinning, so their depths are not bit-identical. 0.02 of the shell length
|
||||||
|
// is invisible but separates the root cleanly.
|
||||||
|
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * (h + 0.02) * dispComp);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// and the h^2 weight keeps roots planted while tips travel most => the stacked shells read
|
||||||
|
// as strands bending in the wind.
|
||||||
|
float2 wdir = _WindDirection.xy;
|
||||||
|
float wlen = length(wdir);
|
||||||
|
wdir = (wlen > 1e-4) ? (wdir / wlen) : float2(1.0, 0.0);
|
||||||
|
float phase = _Time.y * _WindSpeed + (input.positionOS.x + input.positionOS.z) * _WindFrequency;
|
||||||
|
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase) * dispComp);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
float3 positionWS = TransformObjectToWorld(posOS);
|
||||||
|
// Curl droop in world down, scaled by the avatar's world size (objScale) so the bend tracks the
|
||||||
|
// player: a bigger avatar gets a proportionally bigger droop, a smaller one a smaller one. The
|
||||||
|
// fur length above is a CONSTANT world size, so scaling only the droop keeps the bend reading
|
||||||
|
// right relative to the body at every player scale (hider 0.5 / hunter 1.5). Applied after
|
||||||
|
// moving to world space, so the droop follows true world down regardless of player rotation.
|
||||||
|
positionWS.y -= _Curvature * _ShellLength * h * h * objScale;
|
||||||
|
|
||||||
|
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
||||||
|
|
||||||
|
output.positionCS = TransformWorldToHClip(positionWS);
|
||||||
|
output.positionWS = positionWS;
|
||||||
|
output.normalWS = vn.normalWS;
|
||||||
|
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
|
||||||
|
output.shellH = h;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The full strand coverage (radial + height + painted-length opacity), shared so the forward
|
||||||
|
// and GBuffer passes threshold IDENTICAL silhouettes. Returns the coverage scalar the cutout
|
||||||
|
// clip thresholds; fragment stage only (uses screen-space derivatives).
|
||||||
|
// gradeH: the root->tip grading height for FurAlbedo. RELATIVE to this strand's own length -
|
||||||
|
// grading by the absolute shell height painted short strands root-dark across their whole
|
||||||
|
// visible run, which read as black strand streaks that scaled with _RootAO ("root AO is
|
||||||
|
// buggy"). Relative grading makes every strand bright at ITS tip and dark only where actual
|
||||||
|
// roots are visible.
|
||||||
|
half FurCoverage(Varyings input, float3 N, float3 V, out float gradeH)
|
||||||
|
{
|
||||||
|
float h = input.shellH;
|
||||||
|
|
||||||
|
// fur map: R = fur length (G/B/A unused). White default (no map) => full-length fur
|
||||||
|
// everywhere, so the standalone prototype is unchanged. The strand colour comes from the
|
||||||
|
// albedo map (_BaseMap) below, not from here.
|
||||||
|
float furLen = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv).r;
|
||||||
|
|
||||||
|
// --- strand mask: one strand per cell, random length, radially tapered toward the tip ---
|
||||||
|
float2 g = input.uv * _Density;
|
||||||
|
float2 cell = floor(g);
|
||||||
|
float strandLen = Hash21(cell); // this strand's random max height (0..1)
|
||||||
|
|
||||||
|
// per-strand lean: shift the cell-local strand centre by a random direction that grows
|
||||||
|
// with height, so each strand tilts its own way. This is a fragment-space lean - geometry
|
||||||
|
// is shared per vertex while strands are per cell, so the tilt has to live here, not in the
|
||||||
|
// extrusion.
|
||||||
|
float rndAngle = Hash21(cell + 3.7) * 6.2831853;
|
||||||
|
float2 leanDir = float2(cos(rndAngle), sin(rndAngle));
|
||||||
|
float2 local = (frac(g) * 2.0 - 1.0) - leanDir * (_DirRandomness * h);
|
||||||
|
|
||||||
|
// silhouette stretch: at grazing angles the flat shell slices are viewed edge-on, so a
|
||||||
|
// strand breaks into squished, separated dots. Elongate the strand footprint along the
|
||||||
|
// tangent-space view direction - the axis the extruded shells drift apart on screen - so
|
||||||
|
// consecutive slices overlap into a continuous strand near the rim. The tangent frame is
|
||||||
|
// rebuilt from screen-space derivatives (Schueler's cotangent trick), so this stays generic:
|
||||||
|
// no per-mesh tangents required. graze^2 confines the effect to the silhouette, leaving the
|
||||||
|
// head-on look untouched. The branch is on a uniform, so the derivatives stay quad-uniform
|
||||||
|
// (safe) and the cost is skipped when the feature is off; at stretch==1 the basis rotation is
|
||||||
|
// a pure rotation => identity, so ramping it in is continuous.
|
||||||
|
if (_SilhouetteStretch > 0.0)
|
||||||
|
{
|
||||||
|
float graze = 1.0 - saturate(dot(N, V));
|
||||||
|
graze *= graze;
|
||||||
|
|
||||||
|
float3 dp1 = ddx(input.positionWS);
|
||||||
|
float3 dp2 = ddy(input.positionWS);
|
||||||
|
float2 duv1 = ddx(input.uv);
|
||||||
|
float2 duv2 = ddy(input.uv);
|
||||||
|
float3 dp2perp = cross(dp2, N);
|
||||||
|
float3 dp1perp = cross(N, dp1);
|
||||||
|
float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
|
||||||
|
float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
|
||||||
|
float invmax = rsqrt(max(dot(T, T), dot(B, B)));
|
||||||
|
T *= invmax; B *= invmax;
|
||||||
|
|
||||||
|
float2 vt = float2(dot(V, T), dot(V, B));
|
||||||
|
float vlen = length(vt);
|
||||||
|
float2 sdir = (vlen > 1e-4) ? (vt / vlen) : float2(1.0, 0.0);
|
||||||
|
|
||||||
|
float stretch = 1.0 + _SilhouetteStretch * graze;
|
||||||
|
float along = dot(local, sdir) / stretch; // compress coord => elongate footprint along sdir
|
||||||
|
float perp = dot(local, float2(-sdir.y, sdir.x));
|
||||||
|
local = float2(along, perp);
|
||||||
|
}
|
||||||
|
|
||||||
|
float dist = length(local);
|
||||||
|
|
||||||
|
// allowed radius shrinks with height => the strand narrows toward its tip. _TipFill is a
|
||||||
|
// floor on that radius (as a fraction of the base thickness): the taper only thins a strand
|
||||||
|
// down to _TipFill, never to a point. With few shells the pointy tip breaks into floating
|
||||||
|
// discs ("dots") because consecutive shell slices stop overlapping; holding a minimum radius
|
||||||
|
// keeps neighbouring slices wide enough to bridge the inter-shell gap. 0 = sharp needles
|
||||||
|
// (dots return), higher = fuller, slightly rounded tips.
|
||||||
|
float taperedR = pow(saturate(1.0 - h), _Taper * 0.5);
|
||||||
|
float radius = _Thickness * max(taperedR, _TipFill);
|
||||||
|
|
||||||
|
// --- coverage: three factors multiply into a scalar that the CUTOUT clip thresholds.
|
||||||
|
// radialCov : strand silhouette falloff (soft ramp -> the cutoff lands softly).
|
||||||
|
// heightCov : each strand thins out toward its own random tip (_TipFill caps the height).
|
||||||
|
// lenOpacity : LOW painted alpha => LOW coverage, so lightly-painted texels lose their
|
||||||
|
// strands at the cutoff earlier -> short, sparse fuzz at paint borders.
|
||||||
|
// The fur map's alpha still also caps the strand HEIGHT (effectiveLen).
|
||||||
|
// The fur is opaque cutout: coverage no longer blends - it only decides clip-or-draw.
|
||||||
|
float effectiveLen = strandLen * furLen;
|
||||||
|
float radialCov = 1.0 - smoothstep(radius - max(radius * _StrandSoftness, 1e-4), radius, dist);
|
||||||
|
float heightCov = 1.0 - smoothstep(effectiveLen * (1.0 - _StrandSoftness), effectiveLen + 1e-4, h);
|
||||||
|
float lenOpacity = saturate((furLen - _FurThreshold) / max(_FurSoftness, 1e-4));
|
||||||
|
|
||||||
|
half alpha = (half)(radialCov * heightCov * lenOpacity);
|
||||||
|
|
||||||
|
gradeH = saturate(h / max(effectiveLen, 1e-3));
|
||||||
|
|
||||||
|
// _SolidRoot keeps the base shell fully covering (full-coverage prototype), but still honours
|
||||||
|
// 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;
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
// base colour is the PAINTED ALBEDO (the camo under the fur), sampled per-texel so each strand
|
||||||
|
// wears the colour beneath it, times the global tint (white by default). Root/tip grading is
|
||||||
|
// shared so HTrace's injected GI (from GBuffer albedo) matches the visible strand colour.
|
||||||
|
half3 FurAlbedo(float2 uv, float h)
|
||||||
|
{
|
||||||
|
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, 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);
|
||||||
|
return albedo * (ao * lerp(1.0h, 1.0h + _TipShade, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shading normals of at/beyond-the-horizon shell fragments point away from (or barely toward)
|
||||||
|
// the camera and shade darker than the fur right next to them - the silhouette band showed
|
||||||
|
// scattered dark strand streaks (verified with an N*V debug render). Nudge such normals toward
|
||||||
|
// the view until they face it by at least MIN_NV, so limb strands light like the surface they
|
||||||
|
// visually belong to. Shading-only; coverage/silhouette math keeps the raw normal.
|
||||||
|
float3 ClampNormalToHorizon(float3 N, float3 V)
|
||||||
|
{
|
||||||
|
const float MIN_NV = 0.2;
|
||||||
|
float nv = dot(N, V);
|
||||||
|
return (nv >= MIN_NV) ? N : normalize(N + (MIN_NV - nv) * V);
|
||||||
|
}
|
||||||
|
ENDHLSL
|
||||||
|
|
||||||
Pass
|
Pass
|
||||||
{
|
{
|
||||||
Name "FurForward"
|
Name "FurForward"
|
||||||
Tags { "LightMode" = "UniversalForward" }
|
// UniversalForwardOnly, NOT UniversalForward: since the cutout rework the fur renders in the
|
||||||
|
// OPAQUE pass (AlphaTest queue), and the Deferred+ renderer draws opaques via the GBuffer
|
||||||
|
// pass - a shader with only a UniversalForward pass is skipped entirely there (invisible).
|
||||||
|
// ForwardOnly is drawn by both the deferred renderer's forward-only pass AND the plain
|
||||||
|
// forward renderer, so this works on either path (same ComplexLit pattern as PlayerCamoLit).
|
||||||
|
Tags { "LightMode" = "UniversalForwardOnly" }
|
||||||
|
|
||||||
Cull Back
|
Cull Back
|
||||||
Blend SrcAlpha OneMinusSrcAlpha
|
// Opaque alpha-cutout (no blending): every surviving fragment writes depth, so
|
||||||
// ZWrite ON although this is a transparent blend: URP sorts transparents per OBJECT, so two
|
// 1) overlapping fur objects sort per PIXEL via the depth buffer (no per-object popping), and
|
||||||
// overlapping fur objects would swap their whole-object z-order as the camera moves (visible
|
// 2) early-Z rejects hidden fur fragments BEFORE the fragment shader runs - shell fur's main
|
||||||
// popping where the fur balls overlap). Writing depth resolves the occlusion per PIXEL instead:
|
// cost is overdraw (shell behind shell, fur behind walls), which this cuts massively.
|
||||||
// whichever object draws first, the second one's hidden fragments fail ZTest. Fragments below
|
// To maximise the early-Z win the shells are submitted TIP->ROOT (front-to-back on front
|
||||||
// _DepthSortClip coverage are discarded (see frag) so faint edge pixels don't occlude. Within
|
// faces, see the h mapping in vert); with no blending the draw order is free to choose.
|
||||||
// one object the shells are submitted root->tip (= back-to-front on front faces), so its own
|
|
||||||
// blending still composites correctly.
|
|
||||||
ZWrite On
|
ZWrite On
|
||||||
ZTest LEqual
|
ZTest LEqual
|
||||||
// Depth bias toward the camera so the fur wins the depth test against the body. The fur
|
// Depth bias toward the camera so the fur wins the depth test against the body. The fur
|
||||||
@@ -131,136 +403,8 @@ Shader "Custom/FurShell"
|
|||||||
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
|
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
|
||||||
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
|
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
|
||||||
|
|
||||||
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
|
||||||
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
|
||||||
|
|
||||||
CBUFFER_START(UnityPerMaterial)
|
|
||||||
float4 _BaseMap_ST;
|
|
||||||
half4 _BaseColor;
|
|
||||||
float _ShellCount;
|
|
||||||
float _ShellLength;
|
|
||||||
float _Density;
|
|
||||||
float _Thickness;
|
|
||||||
float _Taper;
|
|
||||||
float _TipFill;
|
|
||||||
float _Curvature;
|
|
||||||
float _DirRandomness;
|
|
||||||
float _SilhouetteStretch;
|
|
||||||
half _RootAO;
|
|
||||||
half _TipShade;
|
|
||||||
half _RimPower;
|
|
||||||
half _RimStrength;
|
|
||||||
float _FurThreshold;
|
|
||||||
float _FurSoftness;
|
|
||||||
float _StrandSoftness;
|
|
||||||
float _DepthSortClip;
|
|
||||||
float _SolidRoot;
|
|
||||||
float _WindStrength;
|
|
||||||
float _WindSpeed;
|
|
||||||
float _WindFrequency;
|
|
||||||
float4 _WindDirection;
|
|
||||||
float _FurObjScale;
|
|
||||||
half4 _XRayColor;
|
|
||||||
half _XRayPulseSpeed;
|
|
||||||
half _XRayPulseStrength;
|
|
||||||
half _XRayPulseShape;
|
|
||||||
half _XRayFurStrength;
|
|
||||||
CBUFFER_END
|
|
||||||
|
|
||||||
TEXTURE2D(_BaseMap);
|
|
||||||
SAMPLER(sampler_BaseMap);
|
|
||||||
TEXTURE2D(_FurMap);
|
|
||||||
SAMPLER(sampler_FurMap);
|
|
||||||
|
|
||||||
struct Attributes
|
|
||||||
{
|
|
||||||
float4 positionOS : POSITION;
|
|
||||||
float3 normalOS : NORMAL;
|
|
||||||
float2 uv : TEXCOORD0;
|
|
||||||
UNITY_VERTEX_INPUT_INSTANCE_ID
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Varyings
|
|
||||||
{
|
|
||||||
float4 positionCS : SV_POSITION;
|
|
||||||
float2 uv : TEXCOORD0;
|
|
||||||
float3 normalWS : TEXCOORD1;
|
|
||||||
float3 positionWS : TEXCOORD2;
|
|
||||||
float shellH : TEXCOORD3; // 0 = root, 1 = tip
|
|
||||||
};
|
|
||||||
|
|
||||||
// cheap deterministic 2D hash -> [0,1)
|
|
||||||
float Hash21(float2 p)
|
|
||||||
{
|
|
||||||
p = frac(p * float2(123.34, 345.45));
|
|
||||||
p += dot(p, p + 34.345);
|
|
||||||
return frac(p.x * p.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
Varyings vert(Attributes input)
|
|
||||||
{
|
|
||||||
Varyings output = (Varyings)0;
|
|
||||||
UNITY_SETUP_INSTANCE_ID(input);
|
|
||||||
|
|
||||||
// shell layer index -> normalized height (guarded so the non-instanced variant compiles)
|
|
||||||
#if UNITY_ANY_INSTANCING_ENABLED
|
|
||||||
uint shellIndex = input.instanceID;
|
|
||||||
#else
|
|
||||||
uint shellIndex = 0u;
|
|
||||||
#endif
|
|
||||||
float count = max(_ShellCount, 1.0);
|
|
||||||
float h = (count <= 1.0) ? 0.0 : (float)shellIndex / (count - 1.0);
|
|
||||||
|
|
||||||
// The instance matrix multiplies the object-space fur displacement by the avatar's world scale
|
|
||||||
// (objScale), so a naive extrusion shrinks/grows 1:1 with the player - too short on the small
|
|
||||||
// hider, too long on the big hunter. To keep the fur a CONSTANT world length (same absolute
|
|
||||||
// length at any size) we pre-divide the object-space offset by objScale; after the matrix's
|
|
||||||
// *objScale the net world displacement is then 1x. Wind sway shares this so it stays a constant
|
|
||||||
// world size too. (The gravity droop below intentionally does NOT - it scales with objScale.)
|
|
||||||
float objScale = max(_FurObjScale, 1e-4);
|
|
||||||
float dispComp = 1.0 / objScale;
|
|
||||||
|
|
||||||
// extrude along the normal; bend the tips down with h^2 for a gravity/curl droop.
|
|
||||||
// The (h + 0.02) lifts the whole stack a hair off the surface: the root (h=0) would else
|
|
||||||
// sit exactly on the opaque body and z-fight it - the fur draws a CPU-baked mesh while the
|
|
||||||
// body uses GPU skinning, so their depths are not bit-identical, and now that fur is
|
|
||||||
// transparent (ZWrite Off) it can't rely on a depth write to win the tie. 0.02 of the shell
|
|
||||||
// length is invisible but separates the root cleanly.
|
|
||||||
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * (h + 0.02) * dispComp);
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
// and the h^2 weight keeps roots planted while tips travel most => the stacked shells read
|
|
||||||
// as strands bending in the wind.
|
|
||||||
float2 wdir = _WindDirection.xy;
|
|
||||||
float wlen = length(wdir);
|
|
||||||
wdir = (wlen > 1e-4) ? (wdir / wlen) : float2(1.0, 0.0);
|
|
||||||
float phase = _Time.y * _WindSpeed + (input.positionOS.x + input.positionOS.z) * _WindFrequency;
|
|
||||||
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase) * dispComp);
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
float3 positionWS = TransformObjectToWorld(posOS);
|
|
||||||
// Curl droop in world down, scaled by the avatar's world size (objScale) so the bend tracks the
|
|
||||||
// player: a bigger avatar gets a proportionally bigger droop, a smaller one a smaller one. The
|
|
||||||
// fur length above is a CONSTANT world size, so scaling only the droop keeps the bend reading
|
|
||||||
// right relative to the body at every player scale (hider 0.5 / hunter 1.5). Applied after
|
|
||||||
// moving to world space, so the droop follows true world down regardless of player rotation.
|
|
||||||
positionWS.y -= _Curvature * _ShellLength * h * h * objScale;
|
|
||||||
|
|
||||||
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
|
||||||
|
|
||||||
output.positionCS = TransformWorldToHClip(positionWS);
|
|
||||||
output.positionWS = positionWS;
|
|
||||||
output.normalWS = vn.normalWS;
|
|
||||||
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
|
|
||||||
output.shellH = h;
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
half4 frag(Varyings input) : SV_Target
|
half4 frag(Varyings input) : SV_Target
|
||||||
{
|
{
|
||||||
float h = input.shellH;
|
float h = input.shellH;
|
||||||
@@ -268,109 +412,20 @@ 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: R = fur length (G/B/A unused). White default (no map) => full-length fur
|
float gradeH;
|
||||||
// everywhere, so the standalone prototype is unchanged. The strand colour comes from the
|
half alpha = FurCoverage(input, N, V, gradeH);
|
||||||
// albedo map (_BaseMap) below, not from here.
|
// Alpha cutout: below the threshold the fragment is gone (writes neither color nor depth),
|
||||||
float furLen = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv).r;
|
// above it the fragment draws fully opaque. The 0.003 floor keeps zero-coverage fragments
|
||||||
|
// dropping even at cutoff 0.
|
||||||
// --- strand mask: one strand per cell, random length, radially tapered toward the tip ---
|
|
||||||
float2 g = input.uv * _Density;
|
|
||||||
float2 cell = floor(g);
|
|
||||||
float strandLen = Hash21(cell); // this strand's random max height (0..1)
|
|
||||||
|
|
||||||
// per-strand lean: shift the cell-local strand centre by a random direction that grows
|
|
||||||
// with height, so each strand tilts its own way. This is a fragment-space lean - geometry
|
|
||||||
// is shared per vertex while strands are per cell, so the tilt has to live here, not in the
|
|
||||||
// extrusion.
|
|
||||||
float rndAngle = Hash21(cell + 3.7) * 6.2831853;
|
|
||||||
float2 leanDir = float2(cos(rndAngle), sin(rndAngle));
|
|
||||||
float2 local = (frac(g) * 2.0 - 1.0) - leanDir * (_DirRandomness * h);
|
|
||||||
|
|
||||||
// silhouette stretch: at grazing angles the flat shell slices are viewed edge-on, so a
|
|
||||||
// strand breaks into squished, separated dots. Elongate the strand footprint along the
|
|
||||||
// tangent-space view direction - the axis the extruded shells drift apart on screen - so
|
|
||||||
// consecutive slices overlap into a continuous strand near the rim. The tangent frame is
|
|
||||||
// rebuilt from screen-space derivatives (Schueler's cotangent trick), so this stays generic:
|
|
||||||
// no per-mesh tangents required. graze^2 confines the effect to the silhouette, leaving the
|
|
||||||
// head-on look untouched. The branch is on a uniform, so the derivatives stay quad-uniform
|
|
||||||
// (safe) and the cost is skipped when the feature is off; at stretch==1 the basis rotation is
|
|
||||||
// a pure rotation => identity, so ramping it in is continuous.
|
|
||||||
if (_SilhouetteStretch > 0.0)
|
|
||||||
{
|
|
||||||
float graze = 1.0 - saturate(dot(N, V));
|
|
||||||
graze *= graze;
|
|
||||||
|
|
||||||
float3 dp1 = ddx(input.positionWS);
|
|
||||||
float3 dp2 = ddy(input.positionWS);
|
|
||||||
float2 duv1 = ddx(input.uv);
|
|
||||||
float2 duv2 = ddy(input.uv);
|
|
||||||
float3 dp2perp = cross(dp2, N);
|
|
||||||
float3 dp1perp = cross(N, dp1);
|
|
||||||
float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
|
|
||||||
float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
|
|
||||||
float invmax = rsqrt(max(dot(T, T), dot(B, B)));
|
|
||||||
T *= invmax; B *= invmax;
|
|
||||||
|
|
||||||
float2 vt = float2(dot(V, T), dot(V, B));
|
|
||||||
float vlen = length(vt);
|
|
||||||
float2 sdir = (vlen > 1e-4) ? (vt / vlen) : float2(1.0, 0.0);
|
|
||||||
|
|
||||||
float stretch = 1.0 + _SilhouetteStretch * graze;
|
|
||||||
float along = dot(local, sdir) / stretch; // compress coord => elongate footprint along sdir
|
|
||||||
float perp = dot(local, float2(-sdir.y, sdir.x));
|
|
||||||
local = float2(along, perp);
|
|
||||||
}
|
|
||||||
|
|
||||||
float dist = length(local);
|
|
||||||
|
|
||||||
// allowed radius shrinks with height => the strand narrows toward its tip. _TipFill is a
|
|
||||||
// floor on that radius (as a fraction of the base thickness): the taper only thins a strand
|
|
||||||
// down to _TipFill, never to a point. With few shells the pointy tip breaks into floating
|
|
||||||
// discs ("dots") because consecutive shell slices stop overlapping; holding a minimum radius
|
|
||||||
// keeps neighbouring slices wide enough to bridge the inter-shell gap. 0 = sharp needles
|
|
||||||
// (dots return), higher = fuller, slightly rounded tips.
|
|
||||||
float taperedR = pow(saturate(1.0 - h), _Taper * 0.5);
|
|
||||||
float radius = _Thickness * max(taperedR, _TipFill);
|
|
||||||
|
|
||||||
// --- soft coverage (replaces the old binary alpha-clip) so the fur fades IN with alpha
|
|
||||||
// instead of a hard on/off edge. Three factors multiply into the output alpha:
|
|
||||||
// radialCov : soft, anti-aliased strand edge instead of a hard disc.
|
|
||||||
// heightCov : each strand fades out toward its own random tip (_TipFill caps the height).
|
|
||||||
// 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 radialCov = 1.0 - smoothstep(radius - max(radius * _StrandSoftness, 1e-4), radius, dist);
|
|
||||||
float heightCov = 1.0 - smoothstep(effectiveLen * (1.0 - _StrandSoftness), effectiveLen + 1e-4, h);
|
|
||||||
float lenOpacity = saturate((furLen - _FurThreshold) / max(_FurSoftness, 1e-4));
|
|
||||||
|
|
||||||
half alpha = (half)(radialCov * heightCov * lenOpacity);
|
|
||||||
|
|
||||||
// _SolidRoot keeps the base shell fully covering (full-coverage prototype), but still honours
|
|
||||||
// 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;
|
|
||||||
// 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));
|
clip(alpha - max((half)_DepthSortClip, 0.003h));
|
||||||
|
|
||||||
|
// Keep the raw grazing term for the rim highlight below (the clamp would dim it),
|
||||||
|
// then clamp the shading normal so silhouette strands don't go dark (see helper).
|
||||||
|
half rawGraze = saturate(1.0h - saturate(dot(N, V)));
|
||||||
|
N = ClampNormalToHorizon(N, V);
|
||||||
|
|
||||||
// --- shading (lightweight URP) ---
|
// --- shading (lightweight URP) ---
|
||||||
// base colour is the PAINTED ALBEDO (the camo under the fur), sampled per-texel so each
|
half3 albedo = FurAlbedo(input.uv, gradeH);
|
||||||
// strand wears the colour beneath it, times the global tint (white by default). The fur map
|
|
||||||
// no longer carries colour - only length.
|
|
||||||
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);
|
||||||
@@ -391,7 +446,8 @@ Shader "Custom/FurShell"
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// rim / backlight scatter: brighter fuzz at grazing angles, strongest at the tips
|
// rim / backlight scatter: brighter fuzz at grazing angles, strongest at the tips
|
||||||
half rim = pow(saturate(1.0h - saturate(dot(N, V))), _RimPower) * _RimStrength * h;
|
// (rawGraze = pre-clamp grazing so the horizon clamp doesn't dim the rim glow)
|
||||||
|
half rim = pow(rawGraze, _RimPower) * _RimStrength * h;
|
||||||
color += rim * mainLight.color * albedo;
|
color += rim * mainLight.color * albedo;
|
||||||
|
|
||||||
// X-Ray reveal glow on the VISIBLE fur, mirroring the body's forward half (PlayerCamo.hlsl)
|
// X-Ray reveal glow on the VISIBLE fur, mirroring the body's forward half (PlayerCamo.hlsl)
|
||||||
@@ -401,12 +457,82 @@ Shader "Custom/FurShell"
|
|||||||
// MeshPainter drives on the body, so on/occluded stays consistent between fur and body.
|
// MeshPainter drives on the body, so on/occluded stays consistent between fur and body.
|
||||||
half xrayCurve = pow(0.5h - 0.5h * cos(_Time.y * _XRayPulseSpeed), _XRayPulseShape);
|
half xrayCurve = pow(0.5h - 0.5h * cos(_Time.y * _XRayPulseSpeed), _XRayPulseShape);
|
||||||
half xrayPulse = 1.0h - _XRayPulseStrength * (1.0h - xrayCurve);
|
half xrayPulse = 1.0h - _XRayPulseStrength * (1.0h - xrayCurve);
|
||||||
// _XRayFurStrength tames the per-shell accumulation: the body adds this term once (opaque),
|
// Now that the fur is opaque cutout, only ONE fur fragment survives per pixel - the glow no
|
||||||
// but the fur stacks _ShellCount transparent shells that each composite it, so the raw term
|
// longer stacks per shell. _XRayFurStrength stays as the fur-vs-body intensity match knob;
|
||||||
// over-brightens. Scaling it per shell brings the stacked total back near the body's glow.
|
// with no stacking to tame it typically wants ~1 instead of the old ~0.2.
|
||||||
color += _XRayColor.rgb * (_XRayColor.a * xrayPulse * _XRayFurStrength);
|
color += _XRayColor.rgb * (_XRayColor.a * xrayPulse * _XRayFurStrength);
|
||||||
|
|
||||||
return half4(color, alpha);
|
// opaque cutout: surviving fragments are fully opaque, alpha only fed the clip above
|
||||||
|
return half4(color, 1.0h);
|
||||||
|
}
|
||||||
|
ENDHLSL
|
||||||
|
}
|
||||||
|
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
// The project renders Deferred+ but the fur is SHADED forward-only (ComplexLit stencil);
|
||||||
|
// the ONLY consumer of this pass is HTrace WSGI, which renders its thin GBuffer through the
|
||||||
|
// "UniversalGBuffer" tag and composites its traced irradiance from GBuffer0.rgb (albedo)
|
||||||
|
// AFTER post-processing. Without it the fur pixels keep the GBuffer data of whatever is
|
||||||
|
// BEHIND the fur, so HTrace injected the background's GI over the strands - the fur read as
|
||||||
|
// half transparent (same failure class as the HeroCutout / PictureTessellationLit fixes).
|
||||||
|
// Same shells, same cutout clip, same graded albedo as the forward pass via the shared code.
|
||||||
|
Name "FurGBuffer"
|
||||||
|
Tags { "LightMode" = "UniversalGBuffer" }
|
||||||
|
|
||||||
|
Cull Back
|
||||||
|
ZWrite On
|
||||||
|
ZTest LEqual
|
||||||
|
// Same depth bias as FurForward so both passes agree on depth against the body.
|
||||||
|
Offset -1, -1
|
||||||
|
|
||||||
|
HLSLPROGRAM
|
||||||
|
#pragma target 4.5
|
||||||
|
#pragma exclude_renderers gles3 glcore
|
||||||
|
|
||||||
|
#pragma vertex vert
|
||||||
|
#pragma fragment GBufferFrag
|
||||||
|
|
||||||
|
#pragma multi_compile_instancing
|
||||||
|
#pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT
|
||||||
|
#include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/RenderingLayers.hlsl"
|
||||||
|
|
||||||
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
|
||||||
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/GBufferOutput.hlsl"
|
||||||
|
|
||||||
|
GBufferFragOutput GBufferFrag(Varyings input)
|
||||||
|
{
|
||||||
|
float3 N = normalize(input.normalWS);
|
||||||
|
float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS);
|
||||||
|
|
||||||
|
float gradeH;
|
||||||
|
half alpha = FurCoverage(input, N, V, gradeH);
|
||||||
|
clip(alpha - max((half)_DepthSortClip, 0.003h));
|
||||||
|
|
||||||
|
// Same horizon clamp as the forward pass - away-facing normals in the GBuffer would
|
||||||
|
// make HTrace inject correspondingly dark GI onto the same silhouette strands.
|
||||||
|
N = ClampNormalToHorizon(N, V);
|
||||||
|
|
||||||
|
// GBuffer0 is UNORM; HTrace multiplies its GI by it, so the graded strand albedo here
|
||||||
|
// makes the injected indirect light match the visible fur colour.
|
||||||
|
half3 albedo = saturate(FurAlbedo(input.uv, gradeH));
|
||||||
|
|
||||||
|
InputData inputData = (InputData)0;
|
||||||
|
inputData.positionWS = input.positionWS;
|
||||||
|
inputData.positionCS = input.positionCS;
|
||||||
|
inputData.normalWS = N;
|
||||||
|
inputData.viewDirectionWS = V;
|
||||||
|
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
|
||||||
|
|
||||||
|
// Fur is pure diffuse (metallic 0, smoothness 0) - matches the forward shading model.
|
||||||
|
BRDFData brdfData;
|
||||||
|
half brdfAlpha = 1.0h;
|
||||||
|
InitializeBRDFData(albedo, 0.0h, half3(0.0h, 0.0h, 0.0h), 0.0h, brdfAlpha, brdfData);
|
||||||
|
|
||||||
|
// Baked-GI slot mirrors the forward ambient term (SampleSH); deferred lighting never
|
||||||
|
// reads it here (stenciled out) - it only completes the GBuffer for HTrace.
|
||||||
|
half3 gi = SampleSH(N) * brdfData.diffuse;
|
||||||
|
return PackGBuffersBRDFData(brdfData, inputData, 0.0h, gi, 1.0h);
|
||||||
}
|
}
|
||||||
ENDHLSL
|
ENDHLSL
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user