// NiloToon Character Fur - Fragment Shader // Full-featured NiloToon style cel shading + Fur specific effects #ifndef NILOTOON_CHARACTER_FUR_FRAGMENT_INCLUDED #define NILOTOON_CHARACTER_FUR_FRAGMENT_INCLUDED //------------------------------------------------------------------------------------------------------------------------------ // Get Normal from Normal Map //------------------------------------------------------------------------------------------------------------------------------ half3 GetNormalFromMap(float2 uv, float3 normalWS, float4 tangentWS) { #if defined(_NORMALMAP) half4 normalMap = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, uv); half3 normalTS = UnpackNormalWithScale(normalMap, _BumpScale); float3 bitangent = cross(normalWS, tangentWS.xyz) * tangentWS.w; float3x3 TBN = float3x3(tangentWS.xyz, bitangent, normalWS); return normalize(mul(normalTS, TBN)); #else return normalize(normalWS); #endif } //------------------------------------------------------------------------------------------------------------------------------ // Apply Dissolve (NiloToon Style) //------------------------------------------------------------------------------------------------------------------------------ void ApplyDissolve(inout half3 color, float2 uv, float3 positionWS, float4 positionCS) { #if _NILOTOON_DISSOLVE // Matching to NiloToonPerCharacterRenderController.cs's enum DissolveMode #define DISSOLVEMODE_UV1 1 #define DISSOLVEMODE_UV2 2 #define DISSOLVEMODE_WorldSpaceNoise 3 #define DISSOLVEMODE_WorldSpaceVerticalUpward 4 #define DISSOLVEMODE_WorldSpaceVerticalDownward 5 #define DISSOLVEMODE_ScreenSpaceNoise 6 #define DISSOLVEMODE_ScreenSpaceVerticalUpward 7 #define DISSOLVEMODE_ScreenSpaceVerticalDownward 8 half finalDissolveAmount = _DissolveAmount * _AllowPerCharacterDissolve; if(finalDissolveAmount <= 0) return; float noiseStrength = _DissolveNoiseStrength * 0.1; float2 uvTiling = float2(_DissolveThresholdMapTilingX, _DissolveThresholdMapTilingY); half dissolveMapThresholdMapValue = 0; // UV1 - sample .g channel (matching NiloToonCharacter) if(_DissolveMode == DISSOLVEMODE_UV1) { float2 dissolveUV = uv * uvTiling; dissolveMapThresholdMapValue = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g; } // WorldSpaceNoise - 3-axis multiplication (matching NiloToonCharacter) else if(_DissolveMode == DISSOLVEMODE_WorldSpaceNoise) { float2 dissolveUV; dissolveUV = positionWS.xz * uvTiling; dissolveMapThresholdMapValue = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g; dissolveUV = positionWS.xy * uvTiling; dissolveMapThresholdMapValue *= SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g; dissolveUV = positionWS.yz * uvTiling; dissolveMapThresholdMapValue *= SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g; } // WorldSpaceVerticalUpward - use character bound data from global arrays else if(_DissolveMode == DISSOLVEMODE_WorldSpaceVerticalUpward) { float2 dissolveUV = positionWS.xz * uvTiling; float noise = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g * noiseStrength; // Calculate character bound from center pos and radius (same as NiloToonCharacter) float3 characterBoundCenterPosWS = _NiloToonGlobalPerCharBoundCenterPosWSArray[_CharacterID]; float characterBoundBottom = characterBoundCenterPosWS.y - _CharacterBoundRadius; float characterBoundTop = characterBoundCenterPosWS.y + _CharacterBoundRadius; dissolveMapThresholdMapValue = invLerpClamp(characterBoundBottom, characterBoundTop, positionWS.y + noise); } // WorldSpaceVerticalDownward - use character bound data from global arrays else if(_DissolveMode == DISSOLVEMODE_WorldSpaceVerticalDownward) { float2 dissolveUV = positionWS.xz * uvTiling; float noise = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g * noiseStrength; // Calculate character bound from center pos and radius (same as NiloToonCharacter) float3 characterBoundCenterPosWS = _NiloToonGlobalPerCharBoundCenterPosWSArray[_CharacterID]; float characterBoundBottom = characterBoundCenterPosWS.y - _CharacterBoundRadius; float characterBoundTop = characterBoundCenterPosWS.y + _CharacterBoundRadius; dissolveMapThresholdMapValue = invLerpClamp(characterBoundTop, characterBoundBottom, positionWS.y + noise); } // ScreenSpaceNoise - use character bound 2D rect UV (matching NiloToonCharacter) else if(_DissolveMode == DISSOLVEMODE_ScreenSpaceNoise) { // Calculate character bound 2D rect UV using Calc3DSphereTo2DUV (same as NiloToonCharacter) float3 characterBoundCenterPosWS = _NiloToonGlobalPerCharBoundCenterPosWSArray[_CharacterID]; float2 characterBound2DRectUV01 = Calc3DSphereTo2DUV(positionWS, characterBoundCenterPosWS, _CharacterBoundRadius); // since the default bounding sphere is 2.5m, when compared to world space, it should have 2.5x Tiling float2 dissolveUV = characterBound2DRectUV01 * uvTiling * 2.5; dissolveMapThresholdMapValue = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g; } // ScreenSpaceVerticalUpward else if(_DissolveMode == DISSOLVEMODE_ScreenSpaceVerticalUpward) { // Calculate character bound 2D rect UV using Calc3DSphereTo2DUV (same as NiloToonCharacter) float3 characterBoundCenterPosWS = _NiloToonGlobalPerCharBoundCenterPosWSArray[_CharacterID]; float2 characterBound2DRectUV01 = Calc3DSphereTo2DUV(positionWS, characterBoundCenterPosWS, _CharacterBoundRadius); // since the default bounding sphere is 2.5m, when compared to world space, it should have 2.5x Tiling float2 dissolveUV = characterBound2DRectUV01 * uvTiling * 2.5; float noise = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g * noiseStrength; dissolveMapThresholdMapValue = saturate(characterBound2DRectUV01.y + noise); } // ScreenSpaceVerticalDownward else if(_DissolveMode == DISSOLVEMODE_ScreenSpaceVerticalDownward) { // Calculate character bound 2D rect UV using Calc3DSphereTo2DUV (same as NiloToonCharacter) float3 characterBoundCenterPosWS = _NiloToonGlobalPerCharBoundCenterPosWSArray[_CharacterID]; float2 characterBound2DRectUV01 = Calc3DSphereTo2DUV(positionWS, characterBoundCenterPosWS, _CharacterBoundRadius); // since the default bounding sphere is 2.5m, when compared to world space, it should have 2.5x Tiling float2 dissolveUV = characterBound2DRectUV01 * uvTiling * 2.5; float noise = SAMPLE_TEXTURE2D(_DissolveThresholdMap, sampler_DissolveThresholdMap, dissolveUV).g * noiseStrength; dissolveMapThresholdMapValue = saturate((1 - characterBound2DRectUV01.y) + noise); } half dissolve = dissolveMapThresholdMapValue - finalDissolveAmount; // Clip threshold clip(dissolve - 0.0001); // HDR color tint to "near threshold area" color = lerp(color, _DissolveBorderTintColor.rgb, smoothstep(finalDissolveAmount + _DissolveBorderRange, finalDissolveAmount, dissolveMapThresholdMapValue)); #endif } //------------------------------------------------------------------------------------------------------------------------------ // Apply Per-Character Color Controls (Tint, Add, Desaturation, Lerp) //------------------------------------------------------------------------------------------------------------------------------ half3 ApplyPerCharacterColorControls(half3 color) { // Base Color Multiply & Tint color *= _PerCharacterBaseColorMultiply; color *= _PerCharacterBaseColorTint.rgb; // Per-Character Tint & Add Color (set by NiloToonPerCharacterRenderController) // Matching NiloToonCharacter_Shared.hlsl: color.rgb = color.rgb * _PerCharEffectTintColor + _PerCharEffectAddColor; color.rgb = color.rgb * _PerCharEffectTintColor + _PerCharEffectAddColor; // Desaturation (matching NiloToonCharacter_Shared.hlsl line 4099) color = lerp(color, Luminance(color), _PerCharEffectDesaturatePercentage); // Replace by Color / Lerp Color (matching NiloToonCharacter_Shared.hlsl line 4105) // Uses alpha channel of _PerCharEffectLerpColor as the lerp amount color.rgb = lerp(color.rgb, _PerCharEffectLerpColor.rgb, _PerCharEffectLerpColor.a); return color; } //------------------------------------------------------------------------------------------------------------------------------ // Apply BaseMap Override (matching NiloToonPerCharacterRenderController) // Based on ApplyPostLightingPerCharacterBaseMapOverride from NiloToonCharacter_Shared.hlsl //------------------------------------------------------------------------------------------------------------------------------ half3 ApplyBaseMapOverride(half3 baseColor, float2 uv, float3 positionWS, float4 positionCS) { // Sample texture with tiling/offset float2 overrideUV = uv * _PerCharacterBaseMapOverrideMap_ST.xy + _PerCharacterBaseMapOverrideMap_ST.zw; half4 texValue = SAMPLE_TEXTURE2D(_PerCharacterBaseMapOverrideMap, sampler_PerCharacterBaseMapOverrideMap, overrideUV); // Apply tint color texValue.rgb *= _PerCharacterBaseMapOverrideTintColor; // KEY FIX: Multiply alpha by Amount (0-1 range) // When Amount is 0, alpha becomes 0, making BlendColor() return baseColor unchanged texValue.a *= _PerCharacterBaseMapOverrideAmount; // Create RGBA versions for BlendColor function half4 originalColorRGBA = half4(baseColor.r, baseColor.g, baseColor.b, 1); half4 resultColorRGBA = BlendColor(originalColorRGBA, texValue, _PerCharacterBaseMapOverrideBlendMode); // Return only RGB (don't modify alpha) return resultColorRGBA.rgb; } //------------------------------------------------------------------------------------------------------------------------------ // Apply Character Area Color Fill // Based on NiloToonCharacterAreaColorFillFragmentFunction from NiloToonCharacter.shader //------------------------------------------------------------------------------------------------------------------------------ half3 ApplyCharacterAreaColorFill(half3 color, float2 uv, float3 positionWS, float4 positionCS) { // Sample texture with tiling/offset float2 fillUV = uv * _CharacterAreaColorFillTexture_ST.xy + _CharacterAreaColorFillTexture_ST.zw; half4 fillTexSample = SAMPLE_TEXTURE2D(_CharacterAreaColorFillTexture, sampler_CharacterAreaColorFillTexture, fillUV); // Apply color tint half4 resultColor = _CharacterAreaColorFillColor * fillTexSample; // KEY FIX: Multiply alpha by _ShouldRenderCharacterAreaColorFill (acts as enabled flag) // When disabled (0), alpha becomes 0, so the color addition has no effect // Note: Original shader also multiplies by renderArea, but we don't have prepass buffer in fur shader, // so we'll just use the enabled flag resultColor.a *= _ShouldRenderCharacterAreaColorFill; // Add the color based on alpha (when disabled, alpha is 0 so nothing is added) color += resultColor.rgb * resultColor.a; return color; } //------------------------------------------------------------------------------------------------------------------------------ // Apply Dither Opacity (screen-space dithering for transparency) //------------------------------------------------------------------------------------------------------------------------------ void ApplyDitherOpacity(float4 positionCS, half opacity) { if (opacity >= 1.0) return; // 4x4 Bayer matrix for dithering const float4x4 thresholdMatrix = float4x4( 1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0, 13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0, 4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0, 16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0 ); int2 pixelPos = int2(positionCS.xy) % 4; float threshold = thresholdMatrix[pixelPos.x][pixelPos.y]; clip(opacity - threshold); } //------------------------------------------------------------------------------------------------------------------------------ // Apply All Effects (MatCap, Rim, Emission) //------------------------------------------------------------------------------------------------------------------------------ half3 ApplyEffects( half3 color, float2 uv, float3 normalWS, float3 viewDirWS, half3 lightColor, half furLayer ) { // MatCap UV float2 matCapUV = GetMatCapUV(normalWS, viewDirWS); // MatCap Additive #if defined(_MATCAP_ADD) half matCapAddMask = SAMPLE_TEXTURE2D(_MatCapAddMask, sampler_MatCapAddMask, uv).r; half3 matCapAdd = SAMPLE_TEXTURE2D(_MatCapAddMap, sampler_MatCapAddMap, matCapUV).rgb; matCapAdd *= _MatCapAddColor.rgb * _MatCapAddIntensity * matCapAddMask; color += matCapAdd; #endif // MatCap Multiply #if defined(_MATCAP_MUL) half3 matCapMul = SAMPLE_TEXTURE2D(_MatCapMulMap, sampler_MatCapMulMap, matCapUV).rgb; color *= lerp(half3(1, 1, 1), matCapMul, _MatCapMulIntensity); #endif // Rim Light (General) - not applied to fur shells to avoid double rim #if defined(_RIMLIGHT) if (furLayer < 0) // Only for base pass { half NdotV = saturate(dot(normalWS, viewDirWS)); half rimPower = _RimLightPower; half rimIntensity = _RimLightIntensity; half3 rimColor = _RimLightColor.rgb; // Apply per-character rim light override if enabled if (_UsePerCharacterRimLightIntensity > 0.5) { rimPower = _PerCharacterRimLightSharpnessPower; rimIntensity = _PerCharacterRimLightIntensity; rimColor = _PerCharacterRimLightColor.rgb; } half rim = pow(1.0 - NdotV, rimPower); color += rim * rimColor * rimIntensity * lightColor; } #endif // Emission #if defined(_EMISSION) half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, uv).rgb; emission *= _EmissionColor.rgb * _EmissionIntensity; color += emission; #endif return color; } //------------------------------------------------------------------------------------------------------------------------------ // Base Pass Fragment Shader //------------------------------------------------------------------------------------------------------------------------------ #if defined(NILOTOON_FUR_BASE_PASS) half4 frag(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); // Sample base texture half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); half4 color = baseMap * _BaseColor; // Alpha cutoff clip(color.a - _Cutoff); // Apply Dither Fadeout (FIRST - affects visibility) #if _NILOTOON_DITHER_FADEOUT NiloDoDitherFadeoutClip(input.positionCS.xy, 1.0 - _DitherFadeoutAmount * _AllowPerCharacterDitherFadeout); #endif // Apply dissolve (after dither fadeout) // This will clip pixels and apply border glow #if _NILOTOON_DISSOLVE ApplyDissolve(color.rgb, input.uv, input.positionWS, input.positionCS); #endif // Get normal (with normal map if enabled) float3 normalWS = GetNormalFromMap(input.uv, input.normalWS, input.tangentWS); // Get view direction float3 viewDirWS = GetWorldSpaceViewDirSafe(input.positionWS); // Get main light with NiloToon override support Light mainLight = GetMainLightWithNiloToonOverride(); // Try to get shadow attenuation if shadow coord is valid half shadowAttenuation = 1.0; #if defined(_MAIN_LIGHT_SHADOWS) || defined(_MAIN_LIGHT_SHADOWS_CASCADE) || defined(_MAIN_LIGHT_SHADOWS_SCREEN) shadowAttenuation = MainLightRealtimeShadow(input.shadowCoord); #endif mainLight.shadowAttenuation = shadowAttenuation; // Occlusion half occlusion = 1.0; #if defined(_OCCLUSIONMAP) occlusion = lerp(1.0, SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, input.uv).r, _OcclusionStrength); #endif // Apply BaseMap Override (before shading) color.rgb = ApplyBaseMapOverride(color.rgb, input.uv, input.positionWS, input.positionCS); // Apply NiloToon cel shading color.rgb = ApplyNiloToonCelShading( color.rgb, mainLight.color, mainLight.direction, normalWS, viewDirWS, shadowAttenuation, occlusion ); // Apply additional effects color.rgb = ApplyEffects(color.rgb, input.uv, normalWS, viewDirWS, mainLight.color, input.furLayer); // Apply per-character color controls color.rgb = ApplyPerCharacterColorControls(color.rgb); // Apply character area color fill color.rgb = ApplyCharacterAreaColorFill(color.rgb, input.uv, input.positionWS, input.positionCS); // Apply dither opacity ApplyDitherOpacity(input.positionCS, _DitherOpacity); // Apply fog color.rgb = MixFog(color.rgb, input.fogFactor); return color; } #endif //------------------------------------------------------------------------------------------------------------------------------ // Fur Shell Pass Fragment Shader //------------------------------------------------------------------------------------------------------------------------------ #if defined(NILOTOON_FUR_SHELL_PASS) // MRT output structure for simultaneous color + prepass buffer rendering struct FurMRTOutput { half4 color : SV_Target0; // Main color buffer half4 prepass : SV_Target1; // PrepassBuffer (character mask) }; // Internal function to compute fur color (shared between standard and MRT versions) half4 ComputeFurColor(Varyings input, out half furAlpha) { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); // Sample base texture half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); half4 color = baseMap * _BaseColor; // Get main light with NiloToon override support Light mainLight = GetMainLightWithNiloToonOverride(); half3 lightColor = mainLight.color; half3 lightDir = mainLight.direction; // Sample fur noise mask for alpha (default white = full fur) float2 furNoiseUV = input.uv * _FurNoiseMask_ST.xy + _FurNoiseMask_ST.zw; half furNoise = SAMPLE_TEXTURE2D(_FurNoiseMask, sampler_FurNoiseMask, furNoiseUV).r; // Sample fur mask (where fur appears, default white = everywhere) float2 furMaskUV = input.uv * _FurMask_ST.xy + _FurMask_ST.zw; half furMask = SAMPLE_TEXTURE2D(_FurMask, sampler_FurMask, furMaskUV).r; // furLayer: 0 = root/base, 1 = tip half furLayer = saturate(input.furLayer); // Calculate fur alpha using lilToon-style non-linear curve // This creates natural-looking fur tips instead of obvious hair cards // furLayerShift with root offset adjustment (lilToon style) // _FurRootOffset range is -1 to 0: -1 = hide roots completely, 0 = show all half furLayerShift = furLayer - furLayer * _FurRootOffset + _FurRootOffset; half furLayerAbs = abs(furLayerShift); // Non-linear alpha curve: creates sharp tip cutoff // Using cubic falloff for natural-looking fur tips furAlpha = saturate(furNoise - furLayerShift * furLayerAbs * furLayerAbs * furLayerAbs + 0.25); // Apply fur mask furAlpha *= furMask; // Minimum alpha threshold clip(furAlpha - 0.05); // Apply Dither Fadeout (FIRST - affects visibility) #if _NILOTOON_DITHER_FADEOUT NiloDoDitherFadeoutClip(input.positionCS.xy, 1.0 - _DitherFadeoutAmount * _AllowPerCharacterDitherFadeout); #endif // Apply dissolve (after dither fadeout) // This will clip pixels and apply border glow #if _NILOTOON_DISSOLVE ApplyDissolve(color.rgb, input.uv, input.positionWS, input.positionCS); #endif // Apply BaseMap Override (before shading) color.rgb = ApplyBaseMapOverride(color.rgb, input.uv, input.positionWS, input.positionCS); // Get normal (with normal map if enabled) float3 normalWS = GetNormalFromMap(input.uv, input.normalWS, input.tangentWS); // Get view direction float3 viewDirWS = GetWorldSpaceViewDirSafe(input.positionWS); // Occlusion half occlusion = 1.0; #if defined(_OCCLUSIONMAP) occlusion = lerp(1.0, SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, input.uv).r, _OcclusionStrength); #endif // Cel shading half NdotL = dot(normalWS, lightDir); half halfLambert = NdotL * 0.5 + 0.5; half celShadeResult = smoothstep( _CelShadeMidPoint + 0.5 - _CelShadeSoftness, _CelShadeMidPoint + 0.5 + _CelShadeSoftness, halfLambert ); celShadeResult *= occlusion; #if defined(_SHADOW_COLOR) // Apply HSV adjustment to shadow half3 shadowAlbedo = ApplyHSVChange( color.rgb, _ShadowHueShift, _ShadowSaturationBoost, _ShadowValueMultiplier ); shadowAlbedo *= _ShadowColor.rgb * _ShadowBrightness; color.rgb = lerp(shadowAlbedo, color.rgb, celShadeResult); #else color.rgb = lerp(color.rgb * 0.5, color.rgb, celShadeResult); #endif // Apply light color color.rgb *= lightColor; // Apply fur ambient occlusion (lilToon style) // Uses fwidth to reduce aliasing at layer boundaries half furAOFactor = _FurAO * saturate(1.0 - fwidth(input.furLayer)); color.rgb *= furLayer * furAOFactor * 2.0 + 1.0 - furAOFactor; // Apply fur rim lighting half NdotV = abs(dot(normalWS, viewDirWS)); half rimFresnel = pow(saturate(1.0 - NdotV), _FurRimFresnelPower); half antiLightFactor = lerp(1.0, 1.0 - Grayscale(lightColor), _FurRimAntiLight); half3 rimColor = furLayer * rimFresnel * antiLightFactor * _FurRimColor.rgb; color.rgb += rimColor; // Apply additional effects (MatCap, Emission) // MatCap UV float2 matCapUV = GetMatCapUV(normalWS, viewDirWS); #if defined(_MATCAP_ADD) half matCapAddMask = SAMPLE_TEXTURE2D(_MatCapAddMask, sampler_MatCapAddMask, input.uv).r; half3 matCapAdd = SAMPLE_TEXTURE2D(_MatCapAddMap, sampler_MatCapAddMap, matCapUV).rgb; matCapAdd *= _MatCapAddColor.rgb * _MatCapAddIntensity * matCapAddMask * (1.0 - furLayer * 0.5); color.rgb += matCapAdd; #endif #if defined(_MATCAP_MUL) half3 matCapMul = SAMPLE_TEXTURE2D(_MatCapMulMap, sampler_MatCapMulMap, matCapUV).rgb; color.rgb *= lerp(half3(1, 1, 1), matCapMul, _MatCapMulIntensity * (1.0 - furLayer * 0.5)); #endif #if defined(_EMISSION) half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, input.uv).rgb; emission *= _EmissionColor.rgb * _EmissionIntensity * (1.0 - furLayer * 0.5); color.rgb += emission; #endif // Apply per-character color controls color.rgb = ApplyPerCharacterColorControls(color.rgb); // Apply character area color fill color.rgb = ApplyCharacterAreaColorFill(color.rgb, input.uv, input.positionWS, input.positionCS); // Apply dither opacity ApplyDitherOpacity(input.positionCS, _DitherOpacity); // Apply fog color.rgb = MixFog(color.rgb, input.fogFactor); // Final alpha color.a = furAlpha; return color; } // Standard single render target version half4 frag_fur(Varyings input) : SV_Target { half furAlpha; return ComputeFurColor(input, furAlpha); } // MRT version: outputs to both color buffer and Fur Mask buffer simultaneously // This creates the jagged mask effect where only actual rendered fur pixels are marked FurMRTOutput frag_fur_mrt(Varyings input) { FurMRTOutput output; half furAlpha; output.color = ComputeFurColor(input, furAlpha); // Write to Fur Mask Buffer (_NiloToonFurMaskTex): // Write white (1,1,1,1) where fur pixels are rendered // This creates a mask that shows exactly where fur is visible output.prepass = half4(1, 1, 1, 1); return output; } // Mask-only version: outputs to PrepassBuffer format // R: face (not used by fur), G: character area (fur adds here), B: fur area only // Used for two-pass approach (more compatible than MRT) half4 frag_fur_mask(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); // Sample fur noise mask for alpha (default white = full fur) float2 furNoiseUV = input.uv * _FurNoiseMask_ST.xy + _FurNoiseMask_ST.zw; half furNoise = SAMPLE_TEXTURE2D(_FurNoiseMask, sampler_FurNoiseMask, furNoiseUV).r; // Sample fur mask (where fur appears, default white = everywhere) float2 furMaskUV = input.uv * _FurMask_ST.xy + _FurMask_ST.zw; half furMask = SAMPLE_TEXTURE2D(_FurMask, sampler_FurMask, furMaskUV).r; // furLayer: 0 = root/base, 1 = tip half furLayer = saturate(input.furLayer); // Calculate fur alpha using lilToon-style non-linear curve half furLayerShift = furLayer - furLayer * _FurRootOffset + _FurRootOffset; half furLayerAbs = abs(furLayerShift); half furAlpha = saturate(furNoise - furLayerShift * furLayerAbs * furLayerAbs * furLayerAbs + 0.25); // Apply fur mask furAlpha *= furMask; // Clip pixels that don't pass threshold (same as main fur rendering) clip(furAlpha - 0.05); // Apply Dither Fadeout (mask pass must also respect dither fadeout) #if _NILOTOON_DITHER_FADEOUT NiloDoDitherFadeoutClip(input.positionCS.xy, 1.0 - _DitherFadeoutAmount * _AllowPerCharacterDitherFadeout); #endif // Apply dissolve clipping (mask pass must also respect dissolve) #if _NILOTOON_DISSOLVE half3 dummyColor = half3(1, 1, 1); ApplyDissolve(dummyColor, input.uv, input.positionWS, float4(0, 0, 0, 1)); #endif // Output to PrepassBuffer format: // G channel = character visible area (unified mask for face, body, and fur) // All character areas use G channel for consistent masking return half4(0, 1, 0, 0); } #endif #endif // NILOTOON_CHARACTER_FUR_FRAGMENT_INCLUDED