609 lines
26 KiB
HLSL

// 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