334 lines
12 KiB
HLSL
334 lines
12 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 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 rim = pow(1.0 - NdotV, _RimLightPower);
|
|
color += rim * _RimLightColor.rgb * _RimLightIntensity * 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);
|
|
|
|
// 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 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 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);
|
|
|
|
// 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 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);
|
|
|
|
// 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
|