// Generic shell-texturing fur/hair/grass shader (URP). STANDALONE prototype - NOT wired into the // MeshPainter paint pipeline yet. The illusion of strands comes from rendering the mesh many times // ("shells"), each extruded a little further along its normals, and alpha-clipping a per-cell strand // mask that thins toward the tip. It is generic because it needs nothing but positions + normals + a // UV set - it works on any mesh. // // DISPATCH: the shells are drawn by FurShellRenderer.cs via Graphics.RenderMeshInstanced. Each // instance is one shell layer; the layer index arrives as SV_InstanceID and drives the extrusion // height. Put this material on FurShellRenderer, NOT on a plain MeshRenderer (a MeshRenderer draws a // single instance => only the root shell would show). The renderer is the single source of truth: it // pushes EVERY tunable parameter below via a MaterialPropertyBlock, so the values authored on the // material are only template defaults (they get overridden live at render time). // // hair vs fur vs grass = the same technique, only parameters differ (length, density, taper, bend). Shader "Custom/FurShell" { Properties { [Header(Fur Shape)] _ShellCount("Shell Count (driven by FurShellRenderer)", Range(1, 64)) = 32 _ShellLength("Fur Length", Range(0.0, 2.0)) = 0.15 _Density("Strand Density", Range(1.0, 1000.0)) = 250 _Thickness("Strand Thickness", Range(0.0, 1.0)) = 0.5 _Taper("Tip Taper", Range(0.1, 8.0)) = 2.0 _TipFill("Tip Fill (bridge shell gaps)", Range(0.0, 1.0)) = 0.35 _Curvature("Gravity / Curl Bend", Range(0.0, 1.0)) = 0.25 _DirRandomness("Direction Randomness (per strand)", Range(0.0, 1.0)) = 0.3 _SilhouetteStretch("Silhouette Strand Stretch", Range(0.0, 5.0)) = 2.0 [Header(Fur Shading)] // The strand colour: the painted ALBEDO map (the camo under the fur). FurShellRenderer binds the // sibling painter's live albedo RT here, so each strand wears the colour beneath it. [MainTexture] _BaseMap("Albedo (strand colour)", 2D) = "white" {} // 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 _RimStrength("Rim / Backlight Strength", Range(0.0, 2.0)) = 0.4 [Header(Fur Map)] // R = fur length (0 = bald here, 1 = full length); G/B/A unused/reserved. The strand COLOUR now // comes from the albedo map (_BaseMap), so fur wears the painted camo. White default = full-length // 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 // 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: 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 // Alpha-cutout threshold: fragments at/below this coverage are discarded, everything else draws // FULLY OPAQUE. Opaque + ZWrite means early-Z rejects hidden fur fragments before the fragment // shader runs (the main fur cost is overdraw), and overlapping fur objects sort per PIXEL via the // depth buffer. Higher = thinner/harder strands, lower = fuller strands. _DepthSortClip("Alpha Cutoff (coverage)", 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 [Header(Wind)] _WindStrength("Wind Strength", Range(0.0, 1.0)) = 0.1 _WindSpeed("Wind Speed", Range(0.0, 10.0)) = 2.0 _WindFrequency("Wind Spatial Frequency", Range(0.0, 10.0)) = 1.0 _WindDirection("Wind Direction (object XZ)", Vector) = (1, 0, 0, 0) // Avatar world scale, pushed by FurShellRenderer (transform.lossyScale). The fur keeps a CONSTANT // world length at any player size, but the gravity droop scales with this so the bend tracks the // avatar: small hider => small droop, big hunter => big droop. 1 = authored size (vertex shader). [HideInInspector] _FurObjScale("Object Scale (driven by FurShellRenderer)", Float) = 1 [Header(XRay Reveal)] // See-through reveal glow mirrored from the sibling MeshPainter's body X-ray so the fur strands glow // WITH the reveal instead of hiding it under opaque fur. rgb = HDR glow colour, a = fade master. // Default (0,0,0,0) => additive term is zero, so the fur looks unchanged until a reveal fades in. [HDR] _XRayColor("X-Ray Overlay Color", Color) = (0, 0, 0, 0) // Breathing pulse kept identical to the body halves (PlayerCamo.hlsl / XRayOverlay) so they stay in phase. _XRayPulseSpeed("X-Ray Pulse Speed", Float) = 3 _XRayPulseStrength("X-Ray Pulse Strength", Range(0, 1)) = 0.5 _XRayPulseShape("X-Ray Pulse Shape", Range(1, 8)) = 3 // The fur draws _ShellCount stacked TRANSPARENT shells; the additive glow below would composite once // PER overlapping layer and blow out (the body writes it once, opaque). This scales the per-shell glow // down so the stacked total reads at roughly the body's intensity. Tune to match the body glow. _XRayFurStrength("X-Ray Fur Strength (tames shell stacking)", Range(0, 1)) = 0.2 } SubShader { Tags { "RenderType" = "TransparentCutout" "RenderPipeline" = "UniversalPipeline" "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 // 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 { Name "FurForward" // 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 // Opaque alpha-cutout (no blending): every surviving fragment writes depth, so // 1) overlapping fur objects sort per PIXEL via the depth buffer (no per-object popping), and // 2) early-Z rejects hidden fur fragments BEFORE the fragment shader runs - shell fur's main // cost is overdraw (shell behind shell, fur behind walls), which this cuts massively. // To maximise the early-Z win the shells are submitted TIP->ROOT (front-to-back on front // faces, see the h mapping in vert); with no blending the draw order is free to choose. 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 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 #pragma target 3.5 #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile_fragment _ _SHADOWS_SOFT #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" half4 frag(Varyings input) : SV_Target { float h = input.shellH; float3 N = normalize(input.normalWS); float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS); float gradeH; half alpha = FurCoverage(input, N, V, gradeH); // Alpha cutout: below the threshold the fragment is gone (writes neither color nor depth), // above it the fragment draws fully opaque. The 0.003 floor keeps zero-coverage fragments // dropping even at cutoff 0. 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) --- half3 albedo = FurAlbedo(input.uv, gradeH); float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS); Light mainLight = GetMainLight(shadowCoord); half ndotl = saturate(dot(N, mainLight.direction)) * 0.5h + 0.5h; // soft half-lambert half3 lighting = mainLight.color * (ndotl * mainLight.shadowAttenuation); half3 ambient = SampleSH(N); half3 color = albedo * (lighting + ambient); #ifdef _ADDITIONAL_LIGHTS uint addCount = GetAdditionalLightsCount(); for (uint i = 0u; i < addCount; i++) { Light lt = GetAdditionalLight(i, input.positionWS); half nl = saturate(dot(N, lt.direction)) * 0.5h + 0.5h; color += albedo * lt.color * (nl * lt.distanceAttenuation * lt.shadowAttenuation); } #endif // rim / backlight scatter: brighter fuzz at grazing angles, strongest at the tips // (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; // X-Ray reveal glow on the VISIBLE fur, mirroring the body's forward half (PlayerCamo.hlsl) // EXACTLY so the strands glow IN PHASE with the body instead of hiding its reveal under // opaque fur. Additive HDR term; _XRayColor.a is the eased fade master (0 when off => fur // unchanged). FurShellRenderer pushes the same colour/pulse/flash state the sibling // 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 xrayPulse = 1.0h - _XRayPulseStrength * (1.0h - xrayCurve); // Now that the fur is opaque cutout, only ONE fur fragment survives per pixel - the glow no // longer stacks per shell. _XRayFurStrength stays as the fur-vs-body intensity match knob; // with no stacking to tame it typically wants ~1 instead of the old ~0.2. color += _XRayColor.rgb * (_XRayColor.a * xrayPulse * _XRayFurStrength); // 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 } } FallBack Off }