// SPDX-License-Identifier: (Not available for this version, you are only allowed to use this software if you have express permission from the copyright holder and agreed to the latest NiloToonURP EULA) // Copyright (c) 2021 Kuroneko ShaderLab Limited // For more information, visit -> https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample // #pragma once is a safeguard best practice in almost every .hlsl, // doing this can make sure your .hlsl's user can include this .hlsl anywhere anytime without producing any multi include conflict #pragma once #include "NiloToonCharacter_RiderSupport.hlsl" // calculate Albedo's shadow area's color, // any light's light color is NOT considered in this function yet! // this function only apply hsv edit then multiply tint color to rawAlbedo, then returns a shadow color(with out considering light color) half3 CalculateLightIndependentSelfShadowAlbedoColor(const ToonSurfaceData surfaceData, const ToonLightingData lightingData, const half finalShadowArea) { // "Shadow Color" main group's toggle if(!_EnableShadowColor) { return surfaceData.albedo; } const half3 rawAlbedo = surfaceData.albedo; const half isFace = lightingData.isFaceArea; const half isSkin = lightingData.isSkinArea; const float2 uv = lightingData.uv; // calculate isLitToShadowTransitionArea, // if a pixel is inside LitToShadowTransitionArea, later we will do an additional hsv edit and color tint for this area, for producing a more artistic color expression of the LitToShadowTransitionArea const half isLitToShadowTransitionArea = saturate((1-abs(finalShadowArea-0.5)*2)*_LitToShadowTransitionAreaIntensity); //const half isLitToShadowTransitionArea = saturate(pow(4 * finalShadowArea * (1-finalShadowArea), 2) * _LitToShadowTransitionAreaIntensity); // it is also possible to use a smooth function // [hsv] const half HueOffset = _SelfShadowAreaHueOffset * _SelfShadowAreaHSVStrength + _LitToShadowTransitionAreaHueOffset * isLitToShadowTransitionArea; // 1 MAD const half SaturationBoost = _SelfShadowAreaSaturationBoost * _SelfShadowAreaHSVStrength+ _LitToShadowTransitionAreaSaturationBoost * isLitToShadowTransitionArea; // 1 MAD const half ValueMul = lerp(1,_SelfShadowAreaValueMul,_SelfShadowAreaHSVStrength) * lerp(1,_LitToShadowTransitionAreaValueMul, isLitToShadowTransitionArea); half3 originalColorHSV; // for receiving an extra output from ApplyHSVChange(...) half3 result = ApplyHSVChange(rawAlbedo, HueOffset, SaturationBoost, ValueMul, originalColorHSV); // [if albedo saturation is low, allow user to optionally replace to a fallback color instead of hue shift, in order to suppress hsv result's 0/low saturation color's random hue artifact due to GPU texture compression] const half3 fallbackColor = rawAlbedo * _LowSaturationFallbackColor.rgb; result = lerp(fallbackColor,result, lerp(1,saturate(originalColorHSV.y * 5),_LowSaturationFallbackColor.a)); //only 0~20% saturation area affected, 0% saturation area use 100% fallback // [tint] result *= _SelfShadowTintColor; // [lit to shadow area transition tint] result *= lerp(1,_LitToShadowTransitionAreaTintColor, isLitToShadowTransitionArea); //////////////////////////////////////////// // override if skin //////////////////////////////////////////// // skin can optionally completely override to just a simple single color tint result = lerp(result, rawAlbedo * _SkinShadowTintColor * _SkinShadowTintColor2 * _SkinShadowBrightness, isSkin * _OverrideBySkinShadowTintColor); //////////////////////////////////////////// // override if face //////////////////////////////////////////// #if _ISFACE // face can optionally completely override to just a simple single color tint result = lerp(result, rawAlbedo * _FaceShadowTintColor * _FaceShadowTintColor2 * _FaceShadowBrightness, isFace * _OverrideByFaceShadowTintColor); #endif //////////////////////////////////////////// // override shadow color result by user's shadow color texture //////////////////////////////////////////// #if _OVERRIDE_SHADOWCOLOR_BY_TEXTURE half4 overrideShadowColorTexValue = tex2D(_OverrideShadowColorTex, uv) * _OverrideShadowColorTexTintColor; overrideShadowColorTexValue.a = lerp(overrideShadowColorTexValue.a,1,_OverrideShadowColorTexIgnoreAlphaChannel); // when user provide a shadow color map that is to replace the shadow color, all tint edits to the albedo are lost(since it is a replacement), so we need to do tints again here const half3 externalBaseColorMul = GetCombinedBaseColor().rgb * _BaseMapBrightness * _PerCharacterBaseColorTint.rgb * _GlobalVolumeBaseColorTintColor.rgb; const half3 replaceModeResultShadowColor = overrideShadowColorTexValue.rgb * externalBaseColorMul; const half3 multiplyModeResultShadowColor = overrideShadowColorTexValue.rgb * rawAlbedo; // _OverrideShadowColorByTexMode (0,Replace or 1,Multiply) const half3 finalOverriddenShadowColor = _OverrideShadowColorByTexMode ? multiplyModeResultShadowColor : replaceModeResultShadowColor; half mask = dot(tex2D(_OverrideShadowColorMaskMap, uv),_OverrideShadowColorMaskMapChannelMask); mask = _OverrideShadowColorMaskMapInvertColor ? 1-mask : mask; const half finalApplyStrength = overrideShadowColorTexValue.a * _OverrideShadowColorByTexIntensity * mask; result = lerp(result, finalOverriddenShadowColor, finalApplyStrength); #endif return result; } // [deprecated, this function is no longer used, the indirect light part is now happening in main light injection phase] // calculate scene's light probe contribution // will return _GlobalIndirectLightMinColor if light probe is completely black /* half3 ShadeGI(const ToonSurfaceData surfaceData, const ToonLightingData lightingData) { // occlusion, force target area (defined by occlusion texture) become shadow // separated control for direct/indirect occlusion half indirectOcclusion = 1; #if AnyOcclusionEnabled // default weaker occlusion for indirect indirectOcclusion = lerp(1, surfaceData.occlusion, _OcclusionStrengthIndirectMultiplier); // default 50% usage, but user can edit it #endif // max() can prevent result completely black, if light probe was not baked and no direct light is active const half3 indirectLight = min(max(lightingData.SH,_GlobalIndirectLightMinColor),_GlobalIndirectLightMaxColor) * indirectOcclusion; return indirectLight * surfaceData.albedo; } */ // Most important function: lighting equation for main directional light // Also this is the heaviest method! // Return: rgb = result color, a = final rim rimAttenuation(rim area) half4 ShadeMainLight(inout ToonSurfaceData surfaceData, Varyings input, ToonLightingData lightingData, Light light, UVData uvData) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Common data //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // unused result will be treated as dead code and removed by compiler, don't worry performance if you don't use it half3 N = lightingData.normalWS; half3 rawN = lightingData.normalWS_NoNormalMap; half3 L = light.direction; half3 V = lightingData.viewDirectionWS; half3 H = normalize(L+V); half NoL = dot(N,L); // don't saturate(), because we will remap NoL by smoothstep() later half saturateNoL = saturate(NoL); half RawNoL = dot(rawN,L); half NoV = saturate(dot(N,V)); half NoH = saturate(dot(N,H)); half LoH = saturate(dot(L,H)); half VoV = saturate(dot(V,V)); // unity_OrthoParams's x is orthographic camera’s width, y is orthographic camera’s height, z is unused and w is 1.0 when camera is orthographic, 0.0 when perspective. // (https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html) half orthographicCameraAmount = lerp(unity_OrthoParams.w,1,_PerspectiveRemovalAmount); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Lit (NoL cel diffuse, occlusion, URP shadow map) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// half celShadeMidPointOffset = 0; #if _SHADING_GRADEMAP half4 texValue = tex2D(_ShadingGradeMap, lightingData.uv); texValue = _ShadingGradeMapInvertColor ? 1 - texValue : texValue; half shadingGradeMapValue = dot(texValue, _ShadingGradeMapChannelMask); shadingGradeMapValue = invLerpClamp(_ShadingGradeMapRemapStart,_ShadingGradeMapRemapEnd, shadingGradeMapValue); // should remap first, shadingGradeMapValue = lerp(0.5, shadingGradeMapValue, _ShadingGradeMapStrength); // then apply per material fadeout. celShadeMidPointOffset = (shadingGradeMapValue - 0.5) * _ShadingGradeMapApplyRange + _ShadingGradeMapMidPointOffset; #endif // remapped N dot L // simplest 1 line cel shade, you can always replace this line by your method, like a grayscale ramp texture. // celShadeResult: 0 is in shadow, 1 is in light half finalCelShadeMidPoint = _CelShadeMidPoint + celShadeMidPointOffset; half skinDiffuseNoL = lerp(RawNoL,NoL, _MainLightSkinDiffuseNormalMapStrength); half nonSkinDiffuseNoL = lerp(RawNoL,NoL, _MainLightNonSkinDiffuseNormalMapStrength); half diffuseFinalNoL = lerp(nonSkinDiffuseNoL,skinDiffuseNoL,lightingData.isSkinArea); half smoothstepNoL = smoothstep(finalCelShadeMidPoint-_CelShadeSoftness,finalCelShadeMidPoint+_CelShadeSoftness, diffuseFinalNoL); // if you don't want direct lighting's NdotL cel shade effect looks too strong, set _MainLightIgnoreCelShade to a higher value half selfLightAttenuation = lerp(smoothstepNoL,1, _MainLightIgnoreCelShade); // selfLightAttenuation is a 0~1 value, 0 means completely in shadow, 1 means completely lit // later we will apply different types of shadow to selfLightAttenuation #if _ISFACE // [calculate another set of selfLightAttenuation for face area] half celShadeResultForFaceArea = smoothstep(_CelShadeMidPointForFaceArea-_CelShadeSoftnessForFaceArea,_CelShadeMidPointForFaceArea+_CelShadeSoftnessForFaceArea, diffuseFinalNoL); half ignoreCelShadeForFaceArea = _MainLightIgnoreCelShadeForFaceArea; #if _FACE_SHADOW_GRADIENTMAP ignoreCelShadeForFaceArea = max(ignoreCelShadeForFaceArea,_IgnoreDefaultMainLightFaceShadow); #endif half selfLightAttenuationForFaceArea = lerp(celShadeResultForFaceArea,1, ignoreCelShadeForFaceArea); // [if current fragment is face area, replace original selfLightAttenuation by face's selfLightAttenuation result] selfLightAttenuation = lerp(selfLightAttenuation, selfLightAttenuationForFaceArea, _OverrideCelShadeParamForFaceArea * lightingData.isFaceArea); #endif // occlusion, force target area become shadow // separated control for direct/indirect occlusion #if AnyOcclusionEnabled half directOcclusion = surfaceData.occlusion; // hardcode 100% usage, unlike indirectOcclusion in ShadeGI(), it is not adjustable selfLightAttenuation *= directOcclusion; #endif #if _ISFACE && (_FACE_SHADOW_GRADIENTMAP || _FACE_3D_RIMLIGHT_AND_SHADOW) // TODO: move this whole section to C# for optimization, when it is proven stable and bug free half3 faceForwardDirection = _NiloToonGlobalPerCharFaceForwardDirWSArray[_CharacterID]; half3 faceUpwardDirection = _NiloToonGlobalPerCharFaceUpwardDirWSArray[_CharacterID]; half3 faceRightDirWS = -cross(faceUpwardDirection, faceForwardDirection); // right-hand rule cross(). Since both inputs are unit vector, normalize() is not needed // we can't just discard y (simply call directionVectorWS.xz) in world space, because character's head can have any kind of rotation, // we must transform all direction vector to head space first, then discard y (simply call directionVectorHeadSpace.xz in head space) // (what is head space? head space is a space where x basis vector is face's right, y basis vector is face's up, and z basis vector is face forward) // Change of basis: https://www.3blue1brown.com/lessons/change-of-basis // fill in head space's basis vector x,y,z, similar to filling in a TBN matrix // since we only care rotation, 3x3 matrix is enough half3x3 worldSpaceToHeadSpaceMatrix = half3x3(faceRightDirWS,faceUpwardDirection,faceForwardDirection); // For world to head space with rotation and translation, we need the inverse transform /* float4x4 worldToHeadMatrix = float4x4( faceRightDirWS.x, faceRightDirWS.y, faceRightDirWS.z, -dot(faceRightDirWS, headCenterWS), faceUpwardDirection.x, faceUpwardDirection.y, faceUpwardDirection.z, -dot(faceUpwardDirection, headCenterWS), faceForwardDirection.x, faceForwardDirection.y, faceForwardDirection.z, -dot(faceForwardDirection, headCenterWS), 0, 0, 0, 1 ); */ // [transform all directions to head space] // after a "rotation only" matrix mul, unit vector is still unit vector, no normalize() is needed half3 faceForwardDirectionHeadSpace = mul(worldSpaceToHeadSpaceMatrix,faceForwardDirection); half3 faceRightDirectionHeadSpace = mul(worldSpaceToHeadSpaceMatrix,faceRightDirWS); half3 lightDirectionHeadSpace = mul(worldSpaceToHeadSpaceMatrix,L); // TODO: we need to handle a special case when "light(L) is parallel to _FaceUpDirection", // which means we are going to call normalize(0 length vector) that will produce undefined result in GPU. // Currently we will just SafeNormalize(lightDirectionHeadSpace.xz) to make result at least consistent lightDirectionHeadSpace.y = 0; float2 lightDirectionHeadSpaceXZ = SafeNormalize(lightDirectionHeadSpace).xz; float2 faceForwardDirectionHeadSpaceXZ = normalize(faceForwardDirectionHeadSpace.xz); float2 faceRightDirectionHeadSpaceXZ = normalize(faceRightDirectionHeadSpace.xz); float FaceForwardDotL = dot(faceForwardDirectionHeadSpaceXZ, lightDirectionHeadSpaceXZ); float FaceRightDotL = dot(faceRightDirectionHeadSpaceXZ, lightDirectionHeadSpaceXZ); #if _FACE_SHADOW_GRADIENTMAP //---------------------------------------------------------------------------------------- // [correct implementation ref] /* float thresholdCompareValue = 1 - (dot(L, faceForward) * 0.5 + 0.5); float filpUVu = sign(dot(L, faceLeft)); float shadowThresholdGradientFromTexture = tex2D(_FaceShadowGradientMap, uv * flaot2(filpUVu, 1)).g; float faceShadow = step(thresholdCompareValue, shadowThresholdGradientFromTexture); // or smoothstep depending on style preferred */ //---------------------------------------------------------------------------------------- float2 faceUV = uvData.GetUV(_FaceShadowGradientMapUVIndex); // uv tiling offset with pivot at center faceUV -= 0.5; faceUV = faceUV / _FaceShadowGradientMapUVCenterPivotScalePos.xy - _FaceShadowGradientMapUVCenterPivotScalePos.zw; faceUV += 0.5; // regular uv tiling offset faceUV = faceUV * _FaceShadowGradientMapUVScaleOffset.xy + _FaceShadowGradientMapUVScaleOffset.zw; // [old uv implementation] // assume texture _FaceShadowGradientMap can be mirrored horizontally(where nose is at the center of the texture), // so we just flip uv.x to get another side's gradient data //float2 faceShadowGradientMapValueUV = faceUV * float2(sign(FaceRightDotL), 1); //faceShadowGradientMapValueUV.x *= _FaceShadowGradientMapUVxInvert ? -1 : 1; // [new uv implementation] // _FaceShadowGradientMap can now place face at any position of the texture // but user need to define _FaceShadowGradientMapFaceMidPoint (default 0.5), so the shader can flip correctly no matter where the face is. // For old material, since _FaceShadowGradientMapFaceMidPoint is default 0.5, they can produce the same result as the old uv implementation float2 faceShadowGradientMapValueUV = faceUV; if(sign(FaceRightDotL) < 0) { faceShadowGradientMapValueUV.x -= _FaceShadowGradientMapFaceMidPoint; faceShadowGradientMapValueUV.x *= -1; faceShadowGradientMapValueUV.x += _FaceShadowGradientMapFaceMidPoint; } faceShadowGradientMapValueUV.x -= _FaceShadowGradientMapFaceMidPoint; faceShadowGradientMapValueUV.x *= _FaceShadowGradientMapUVxInvert ? -1 : 1; faceShadowGradientMapValueUV.x += _FaceShadowGradientMapFaceMidPoint; //---------------------------------------------------------------------------------------- half4 faceShadowGradientMapSampledValue = tex2D(_FaceShadowGradientMap, faceShadowGradientMapValueUV); half faceShadowGradientMapValue = dot(_FaceShadowGradientMapChannel, faceShadowGradientMapSampledValue); faceShadowGradientMapValue = _FaceShadowGradientMapInvertColor ? 1-faceShadowGradientMapValue : faceShadowGradientMapValue; // find apply area half faceShadowGradientMask = dot(tex2D(_FaceShadowGradientMaskMap, uvData.GetUV(_FaceShadowGradientMaskMapUVIndex)),_FaceShadowGradientMaskMapChannel); faceShadowGradientMask = _FaceShadowGradientMaskMapInvertColor? 1-faceShadowGradientMask : faceShadowGradientMask; half faceShadowGradientApplyArea = faceShadowGradientMask * lightingData.isFaceArea; // user debug if(_DebugFaceShadowGradientMap && faceShadowGradientApplyArea > 0) { return faceShadowGradientMapValue; } //---------------------------------------------------------------------------------------- // final threshold test faceShadowGradientMapValue += _FaceShadowGradientOffset; // user controlled offset float resultThresholdValue = 1 - (FaceForwardDotL * 0.5 + 0.5); resultThresholdValue = max(_FaceShadowGradientThresholdMin, resultThresholdValue); resultThresholdValue = min(_FaceShadowGradientThresholdMax, resultThresholdValue); half faceShadowResult = smoothstep(resultThresholdValue-_FaceShadowGradientResultSoftness,resultThresholdValue+_FaceShadowGradientResultSoftness, faceShadowGradientMapValue); //---------------------------------------------------------------------------------------- // intensity control (mask included) faceShadowResult = lerp(1,faceShadowResult,_FaceShadowGradientIntensity * faceShadowGradientApplyArea); selfLightAttenuation *= faceShadowResult; #endif #if _FACE_3D_RIMLIGHT_AND_SHADOW half3 smoothedN = input.smoothedNormalWS; // we want smoothed vertex face normal, not flattened face normal, for fresnel of the cheek/jawline half fresnel = 1-dot(smoothedN,V); half headSpaceLx = dot(L,faceRightDirWS); // TODO: light dir isn't flatten to head space xz, do we need to do it? it looks ok without doing it half headSpaceNx = dot(smoothedN,faceRightDirWS); // remap to show more rim & shadow half headSpaceLightingResult = smoothstep(0.4,0.6,((headSpaceNx * headSpaceLx) * 0.5 + 0.5)) * 2 - 1; // sample masks half noseRimAreaMask = dot(tex2D(_Face3DRimLightAndShadow_NoseRimLightMaskMap, lightingData.uv), _Face3DRimLightAndShadow_NoseRimLightMaskMapChannel); half noseShadowAreaMask = dot(tex2D(_Face3DRimLightAndShadow_NoseShadowMaskMap, lightingData.uv), _Face3DRimLightAndShadow_NoseShadowMaskMapChannel); half cheekRimAreaMask = dot(tex2D(_Face3DRimLightAndShadow_CheekRimLightMaskMap, lightingData.uv), _Face3DRimLightAndShadow_CheekRimLightMaskMapChannel); half cheekShadowAreaMask = dot(tex2D(_Face3DRimLightAndShadow_CheekShadowMaskMap, lightingData.uv), _Face3DRimLightAndShadow_CheekShadowMaskMapChannel); // calculate rim light (nose+cheek) half rimMidpoint = _Face3DRimLightAndShadow_CheekRimLightThreshold; // default 0.7 half rimSmoothness = _Face3DRimLightAndShadow_CheekRimLightSoftness; // default 0.25 half rimFresnel = smoothstep(rimMidpoint-rimSmoothness,rimMidpoint+rimSmoothness,fresnel); half rimLightComponent = saturate(headSpaceLightingResult) * lightingData.isFaceArea; half cheekRim = rimLightComponent * cheekRimAreaMask * rimFresnel * _Face3DRimLightAndShadow_CheekRimLightIntensity; half noseRim = rimLightComponent * noseRimAreaMask * _Face3DRimLightAndShadow_NoseRimLightIntensity; // calculate shadow half shadowMidpoint = _Face3DRimLightAndShadow_CheekShadowThreshold; // default 0.6 half shadowSoftness = _Face3DRimLightAndShadow_CheekShadowSoftness; // default 0.05 half shadowFresnel = smoothstep(shadowMidpoint-shadowSoftness,shadowMidpoint+shadowSoftness,fresnel); half shadowComponent = saturate(-headSpaceLightingResult) * lightingData.isFaceArea; half cheekShadow = shadowComponent * cheekShadowAreaMask * shadowFresnel * _Face3DRimLightAndShadow_CheekShadowIntensity; half noseShadow = shadowComponent * noseShadowAreaMask * _Face3DRimLightAndShadow_NoseShadowIntensity; // result half3 face3DRimAddColor = max(cheekRim * _Face3DRimLightAndShadow_CheekRimLightTintColor, noseRim * _Face3DRimLightAndShadow_NoseRimLightTintColor) * saturate(lightingData.mainLight.color) * 0.25; // *0.25 to make default value rim light looks correct half3 face3DShadowTintColor = min(lerp(1,_Face3DRimLightAndShadow_CheekShadowTintColor,cheekShadow), lerp(1,_Face3DRimLightAndShadow_NoseShadowTintColor, noseShadow)); #endif #endif #if ShouldReceiveURPShadow // regular URP shadowmap but with sample position offset (extra depth bias), this extra depth bias is usually for avoiding ugly shadow map on face // invLerpClamp() to let user optionally remap shadow from soft shadow to sharp shadow, but shadow may flicker selfLightAttenuation *= invLerpClamp(0.5-_GlobalNiloToonReceiveURPShadowblurriness,0.5+_GlobalNiloToonReceiveURPShadowblurriness,light.shadowAttenuation); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // "_CameraDepthTexture depth" vs "fragment self depth", linear view space depth difference as 2D rim light and 2D shadow 's show area // only renders if enableDepthTextureRimLightAndShadow enabled // (if enableDepthTextureRimLightAndShadow is off, 2D rim light will fall back to classic NoV rim light) // (if enableDepthTextureRimLightAndShadow is off, 2D shadow don't have any fallback) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// half depthDiffShadow = 1; half rimAttenuation = 0; #if NiloToonForwardLitPass || NiloToonSelfOutlinePass // [Why not using a faster multi_compile_local here, but choosing to use a slower shader if+else here?] // Before NiloToonURP 0.8.3, we use a global shader keyword _NILOTOON_ENABLE_DEPTH_TEXTURE_RIMLIGHT_AND_SHADOW for on/off this section to improve performance. // After NiloToonURP 0.8.3, we are using a shader if+else instead of shader keyword due to: // 1)shader memory will reduce 50% // 2)a shader if+else will allow per character on/off, while a global shader keyword can't // 3)we know that it is a static uniform branch(but with texture load inside), it is not fast due to texture load inside, but it is not extremely slow, which is usable. // 4)we tried turning _NILOTOON_ENABLE_DEPTH_TEXTURE_RIMLIGHT_AND_SHADOW to a local keyword, // but doing this will prevent user seeing correct result in edit mode, // since we can't enable/disable material keyword in editmode(which will pollute version control) //------------------------------------------------------------------------------------------------------------- // So we now rely on this bool enableDepthTextureRimLightAndShadow to control on/off // Only when "character script && material && global" all allows depth texture rim light, then we can render depth texture rim light safely // - _DitherFadeout will destroy DepthTextureRimLightAndShadow's result since it rely on a correct depth texture, so when dither fadeout is active, we disabled DepthTextureRimLightAndShadow bool enableDepthTextureRimLightAndShadow = _PerMaterialEnableDepthTextureRimLightAndShadow && _GlobalEnableDepthTextureRimLigthAndShadow && _AllowRenderDepthOnlyOrDepthNormalsPass && !_GlobalShouldDisableNiloToonDepthTextureRimLightAndShadow && ((_DitherFadeoutAmount == 0) || !_AllowPerCharacterDitherFadeout); if(enableDepthTextureRimLightAndShadow) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Get _CameraDepthTexture's linear view space depth at offseted screen position(screen space uv offset) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // for perspective camera: // Assume // - d is distance to camera, // - w is final width // when eyeDepth > 1, keep rim width constant(character relative constant) when camera move away, using w=1/d // when eyeDepth <= 1, let user pick a lerp between w=1/d and w=-d+2, these 2 curves meets at d=1 with the same slope // for orthographic camera: // disable camera distance fix(only return a constant 0.875 to match perspective's result) since distance or depth will not affect polygon NDC xy position on screen in orthographic camera // lightingData.selfLinearEyeDepth already contains the effect of ZOffset // but we don't want ZOffset affecting the cameraDistanceFix(= affecting uv = affecting rim&shadow width), so we "+ lightingData.ZOffsetFinalSum" to cancel out ZOffset for uv calculation float selfLinearEyeDepthWithoutZOffset = lightingData.selfLinearEyeDepth + lightingData.ZOffsetFinalSum; float eyeDepthRCP = rcp(selfLinearEyeDepthWithoutZOffset); // w=1/d, this method will make rim light width ALWAYS constant (width relative to character) float flatLineExitStartDistance = _DepthTexRimLightAndShadowSafeViewDistance; // exposed as slider, 1~10, default 1 because 1 is not breaking change float flatLineExit = 2.0/flatLineExitStartDistance - 1.0/(flatLineExitStartDistance*flatLineExitStartDistance) * selfLinearEyeDepthWithoutZOffset; //w = 2/c - 1/(c^2) * d, where c is flatLineExitStartDistance (see graph: https://www.desmos.com/calculator/rqssgc4ol7) // Note: for flatlineExit, it is also possible to simply flatLineExit = 1, it can prevent rim light width too large on closeup, but we believe it is too conservative, it will make rim width too small float cameraDistanceFix_perspective = selfLinearEyeDepthWithoutZOffset > flatLineExitStartDistance ? eyeDepthRCP : lerp(eyeDepthRCP, flatLineExit, _DepthTexRimLightAndShadowReduceWidthWhenCameraIsClose); float cameraDistanceFix = IsPerspectiveProjection() ? cameraDistanceFix_perspective : 0.875; // no need to care orthographicCameraAmount, this line is already correct // [calculate finalDepthTexRimLightAndShadowWidthMultiplier] // allow width edit by global/per material/per vertex/per fragment // (* global * per material width multiplier) float finalDepthTexRimLightAndShadowWidthMultiplier = _DepthTexRimLightAndShadowWidthMultiplier * _DepthTexRimLightAndShadowWidthExtraMultiplier * _GlobalDepthTexRimLightAndShadowWidthMultiplier; // (* vertex color width multiplier) float depthTexRimLightAndShadowWidthMultiplierFromVertexColor = dot(lightingData.vertexColor,_DepthTexRimLightAndShadowWidthMultiplierFromVertexColorChannelMask); finalDepthTexRimLightAndShadowWidthMultiplier *= _UseDepthTexRimLightAndShadowWidthMultiplierFromVertexColor ? depthTexRimLightAndShadowWidthMultiplierFromVertexColor : 1; // (* texture width multiplier) #if _DEPTHTEX_RIMLIGHT_SHADOW_WIDTHMAP { finalDepthTexRimLightAndShadowWidthMultiplier *= dot(tex2D(_DepthTexRimLightAndShadowWidthTex, lightingData.uv),_DepthTexRimLightAndShadowWidthTexChannelMask); } #endif #if _ISFACE { // face area shadow width is smaller since it is usually for receiving hair shadow // (in theory hair shadow caster is very close to face surface), so smaller shadow width is better finalDepthTexRimLightAndShadowWidthMultiplier *= lerp(1,0.66666,lightingData.isFaceArea); // hardcode 0.666. TODO: should we expose it to material UI? } #endif // [Calculate fovFix] // when compared between 0.1/1/10/30/60/90 fov cameras, // using UNITY_MATRIX_P[1][1] will produce a more consistent rim light width then using FOV, // since UNITY_MATRIX_P[1][1] = 1 / tan(verticalFOV / 2), which equals to the final scaling of all vertices in NDC space directly // note: UNITY_MATRIX_P[0][0] = 1 / (aspectRatio * tan(verticalFOV / 2)) float fovFix = abs(UNITY_MATRIX_P[1][1]) * 0.0054; //fovFix = _GlobalFOVorOrthoSizeFix; // fov is a usable but not a perfect value to use // group all float1 to calculate first for better performance float2 UvOffsetMultiplier = _GlobalAspectFix * (finalDepthTexRimLightAndShadowWidthMultiplier * fovFix * cameraDistanceFix); float2 originalUvOffsetMultiplier = UvOffsetMultiplier; UvOffsetMultiplier*= 0.707; // when rim light is off screen, it looks ugly with hard cutoff, so here we fadeout the rim light width before going outside of the screen(5% buffer) UvOffsetMultiplier.y *= smoothstep(1.0,0.95,lightingData.normalizedScreenSpaceUV.y); // offset according to view space light direction // light direction can be affected by additional light, so we can't use uniform directly float3 userOverridenMainLightDirVS = mul((float3x3)UNITY_MATRIX_V, lightingData.mainLight.direction); float3 DirVS360 = mul(input.smoothedNormalWS, (float3x3)UNITY_MATRIX_V); float3 userOverridenMainLightDirVSForShadow = mul((float3x3)UNITY_MATRIX_V, lerp(lightingData.mainLight.direction, _NiloToonGlobalPerCharFaceUpwardDirWSArray[_CharacterID], lightingData.isFaceArea * _DepthTexShadowFixedDirectionForFace)); float depthTexRimLightWidthMultiplier = _DepthTexRimLightWidthMultiplier; #if _ISFACE depthTexRimLightWidthMultiplier = lerp(depthTexRimLightWidthMultiplier, min(depthTexRimLightWidthMultiplier, _DepthTexRimLightWidthClampForFace), lightingData.isFaceArea); #endif float2 depthTexRimlightFinalUVOffset = UvOffsetMultiplier * normalize(lerp(userOverridenMainLightDirVS.xy, DirVS360.xy, _DepthTexRimLightIgnoreLightDir)) * depthTexRimLightWidthMultiplier; float2 depthTexShadowFinalUVOffset = UvOffsetMultiplier * normalize(userOverridenMainLightDirVSForShadow.xy) * _DepthTexShadowWidthMultiplier; float2 depthTexRimlightUvOffsetMultiplier = originalUvOffsetMultiplier * depthTexRimLightWidthMultiplier; // WIP(Danger) /* float2 UVOffset360 = .xy; float2 depthTexRimlightDir = normalize(lerp(defaultUVOffsetToLightDir,UVOffset360,_DepthTexRimLightIgnoreLightDir)); float2 depthTexShadowDir = normalize(lerp(defaultUVOffsetToLightDir, UVOffset360, _DepthTexShadowIgnoreLightDir)); float2 depthTexRimlightFinalUVOffset = depthTexRimlightDir * UvOffsetMultiplier * _DepthTexRimLightWidthMultiplier; float2 depthTexShadowFinalUVOffset = depthTexShadowDir * UvOffsetMultiplier * _DepthTexShadowWidthMultiplier; */ // [get _CameraDepthTexture's camera depth data for finding the edge of large depth difference to character] // here we load _CameraDepthTexture once only, and use it as both rim light and shadow's input, // very nice performance win if compared to load _CameraDepthTexture twice, but we lost the ability to edit width separately (e.g. rim and shadow separated width) // use LOAD instead of sample for better performance since we don't need mipmap of depth texture, and don't want bilinear filtering to depth data,which is meaningless int2 depthTexRimLightLoadTexPos = lightingData.SV_POSITIONxy + depthTexRimlightFinalUVOffset * GetScaledScreenWidthHeight(); float depthTexRimLightLinearDepthVS = LoadDepthTextureLinearDepthVS(depthTexRimLightLoadTexPos); int2 depthTexShadowLoadTexPos = lightingData.SV_POSITIONxy + depthTexShadowFinalUVOffset * GetScaledScreenWidthHeight(); float depthTexShadowLinearDepthVS = LoadDepthTextureLinearDepthVS(depthTexShadowLoadTexPos); #if _DEPTHTEX_RIMLIGHT_FIX_DOTTED_LINE_ARTIFACTS float2 offset = _DepthTexRimLightFixDottedLineArtifactsExtendMultiplier * depthTexRimlightUvOffsetMultiplier; int2 loadTexPosExtra1 = lightingData.SV_POSITIONxy + (depthTexRimlightFinalUVOffset + float2(-1.0,0) * offset) * GetScaledScreenWidthHeight(); float depthTextureLinearDepthVSExtra1 = LoadDepthTextureLinearDepthVS(loadTexPosExtra1); int2 loadTexPosExtra2 = lightingData.SV_POSITIONxy + (depthTexRimlightFinalUVOffset + float2(+1.0 ,0) * offset) * GetScaledScreenWidthHeight(); float depthTextureLinearDepthVSExtra2 = LoadDepthTextureLinearDepthVS(loadTexPosExtra2); int2 loadTexPosExtra3 = lightingData.SV_POSITIONxy + (depthTexRimlightFinalUVOffset + float2(0,+1.0) * offset) * GetScaledScreenWidthHeight(); float depthTextureLinearDepthVSExtra3 = LoadDepthTextureLinearDepthVS(loadTexPosExtra3); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Calculate depth texture screen space rim light //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - cancel out material ZOffset in depth tex rim's calculation, don't let material ZOffset affect rim result // - but still keep the per character ZOffset affecting rim result float selfLinearEyeDepthForDepthTexRimLight = lightingData.selfLinearEyeDepth + (lightingData.ZOffsetFinalSum - (-_PerCharZOffset)); float depthTexRimLightDepthDiffThreshold = 0.05; // give user per material control (default 0) float thresholdOffset = _DepthTexRimLightThresholdOffset; #if _ISFACE thresholdOffset = max(thresholdOffset, _DepthTexRimLightMinimumThresholdOffsetForFace * lightingData.isFaceArea); #endif depthTexRimLightDepthDiffThreshold += thresholdOffset; float rimLightdepthDiffThreshold = saturate(depthTexRimLightDepthDiffThreshold + _GlobalDepthTexRimLightDepthDiffThresholdOffset); // counter/cancel face area ZOffset rendered in _CameraDepthTexture(NiloToonDepthOnlyOrDepthNormalPass) rimLightdepthDiffThreshold += _FaceAreaCameraDepthTextureZWriteOffset * lightingData.isFaceArea; // let rim light fadeout softly and smoothly when depth difference is too small, // _DepthTexRimLightFadeoutRange will allow user controlling how the fadeout should look like rimAttenuation = saturate((depthTexRimLightLinearDepthVS - (selfLinearEyeDepthForDepthTexRimLight + rimLightdepthDiffThreshold)) * 10 / _DepthTexRimLightFadeoutRange); #if _DEPTHTEX_RIMLIGHT_FIX_DOTTED_LINE_ARTIFACTS float rimAttenuationExtraTest1 = saturate((depthTextureLinearDepthVSExtra1 - (selfLinearEyeDepthForDepthTexRimLight + rimLightdepthDiffThreshold)) * 10 / _DepthTexRimLightFadeoutRange); float rimAttenuationExtraTest2 = saturate((depthTextureLinearDepthVSExtra2 - (selfLinearEyeDepthForDepthTexRimLight + rimLightdepthDiffThreshold)) * 10 / _DepthTexRimLightFadeoutRange); float rimAttenuationExtraTest3 = saturate((depthTextureLinearDepthVSExtra3 - (selfLinearEyeDepthForDepthTexRimLight + rimLightdepthDiffThreshold)) * 10 / _DepthTexRimLightFadeoutRange); rimAttenuation = min(rimAttenuation,rimAttenuationExtraTest1); rimAttenuation = min(rimAttenuation,rimAttenuationExtraTest2); rimAttenuation = min(rimAttenuation,rimAttenuationExtraTest3); // test code to Anti Aliasing of rim light, a simple average //rimAttenuation = (rimAttenuation+rimAttenuationExtraTest1+rimAttenuationExtraTest2+rimAttenuationExtraTest3)/4; #endif // TODO: write a better fwidth() method to produce anti-aliased 2D rim light, which worth the performance cost // reference resources (need SDF input, which we don't have): // - https://forum.unity.com/threads/antialiasing-circle-shader.432119/#post-2796401 // the below example line = if fwidth() detected any difference within a 2x2 pixel block, replace rimAttenuation 1 to 0.6666, replace depthDiffRimAttenuation 0 to 0.3333 // but it doesn't look good, so disabled //rimAttenuation = fwidth(rimAttenuation) > 0 ? rimAttenuation * 0.3333 + 0.333 : rimAttenuation; // prevent small object like finger to have too much 2D rim light if(_DepthTexRimLight3DRimMaskEnable) { float blur = 0.025; rimAttenuation *= smoothstep(_DepthTexRimLight3DRimMaskThreshold-blur, _DepthTexRimLight3DRimMaskThreshold + blur, (saturate(NoL) * (1.0 - saturate(NoV)))); // 0.2(no blur) or 0.05~0.075(blur) threshold seems good } // possible method to make rim light not that uniform in color //rimAttenuation *= pow(saturateNoL,2); // rim light AA (test) //rimAttenuation = saturate(applyAA(rimAttenuation,0.33333,0.6666)); // assume rimAttenuation is 0~1 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // calculate depth texture screen space shadow //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // hardcode threshold to reduce material inspector complexity float depthTexShadowDepthDiffThreshold = 0.03; float depthTexShadowDepthDiffThresholdForFace = 0.01; // face area using a smaller threshold, since face area's depth was pushed in _CameraDepthTexture already #if _ISFACE depthTexShadowDepthDiffThreshold = lerp(depthTexShadowDepthDiffThreshold, depthTexShadowDepthDiffThresholdForFace, lightingData.isFaceArea); #endif // give user per material control (default 0) depthTexShadowDepthDiffThreshold += _DepthTexShadowThresholdOffset; // let shadow fadeout softly and smoothly when depth difference is too small // _DepthTexShadowFadeoutRange will allow user controlling how the fadeout should look like depthDiffShadow = saturate((depthTexShadowLinearDepthVS - (lightingData.selfLinearEyeDepth - depthTexShadowDepthDiffThreshold)) * 50.0 / _DepthTexShadowFadeoutRange); half depthTexShadowAmount = _DepthTexShadowUsage; #if _ISFACE depthTexShadowAmount *= lerp(1, _NiloToonGlobalAllowUnityCameraDepthTextureWriteFaceZOffset, lightingData.isFaceArea); #endif depthDiffShadow = lerp(1,depthDiffShadow,depthTexShadowAmount); } else { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // (if depth texture rim light is not enabled) // fall back to this classic NoV rim //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // extract camera forward from V matrix, it works because V matrix's scale is always 1, // so we can use Z basis vector inside the 3x3 part of V matrix as camera forward directly // https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab // https://answers.unity.com/questions/192553/camera-forward-vector-in-shader.html // *use UNITY_MATRIX_I_V instead of unity_CameraToWorld, since unity_CameraToWorld is not flipped between PC & PCVR mode half3 neg_cameraViewForwardDirWS = UNITY_MATRIX_I_V._m02_m12_m22; // [fresnel] // A note of V(view vector) for orthographics camera: // - for orthographic camera: use dot(N, -cameraForward) instead of dot(N,V), so rim result is the same no matter where the pixel is on screen // - for perspective camera: use dot(N,V), as usual // A note of N(normal vector): // - We should NOT consider normal map contribution in order to produce better rim light(more similar to depth texture rim light), so we use PolygonNdotV(rawN) instead of NdotV half NoVForRimOrtho = saturate(dot(rawN, neg_cameraViewForwardDirWS)); half NoVForRimPerspective = lightingData.NdotV_NoNormalMap; half NoVForRimResult = lerp(NoVForRimPerspective,NoVForRimOrtho, orthographicCameraAmount); rimAttenuation = 1 - NoVForRimResult; // rim light area *= NoL mask rimAttenuation *= saturateNoL * 0.25 + 0.75; rimAttenuation *= NoL > 0; // rim light area *= remove rim on face rimAttenuation *= 1-lightingData.isFaceArea; // remap rim, try to match result to depth tex screen space 2D rim light's default result // use fwidth() to remove big area flat polygon's rim light pop half midPoint = _DepthTexRimLight3DFallbackMidPoint; half softness = _DepthTexRimLight3DFallbackSoftness; half fwidthFix = _DepthTexRimLight3DFallbackRemoveFlatPolygonRimLight ? saturate(fwidth(rimAttenuation) * 50) : 1; rimAttenuation = smoothstep(midPoint-softness,midPoint+softness,rimAttenuation) * fwidthFix; } // let rim light fadeout softly when character is very far away from camera, to avoid rim light flicker artifact(due to pixel count not enough) // it is similar to a linear fog, where fog amount(0~1) replaced to rim light fadeout amount(0~1) rimAttenuation = lerp(0, rimAttenuation, smoothstep(_GlobalDepthTexRimLightCameraDistanceFadeoutEndDistance, _GlobalDepthTexRimLightCameraDistanceFadeoutStartDistance, lightingData.selfLinearEyeDepth)); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Char self shadow map //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// half selfShadowMapShadow = 1; // default no self shadow map effect #if _NILOTOON_RECEIVE_SELF_SHADOW // use an if() here to allow per material on/off if(_EnableNiloToonSelfShadowMapping && _ControlledByNiloToonPerCharacterRenderController) { float3 selfPositionWS = lightingData.positionWS.xyz; float3 smoothedNormalWSUnitVector = normalize(input.smoothedNormalWS); // TODO: cache in lightingData // lightDirection pointing from vertex to light // (1 slope base +0.1 constant) float biasMultiplier = 1.1-dot(smoothedNormalWSUnitVector, _NiloToonSelfShadowLightDirection); // apply shadow receiver's normal bias, slope based // to inflate the surface position for shadow test (very important to hide shadow acne) // it is similar to finding the outline vertex position selfPositionWS += smoothedNormalWSUnitVector * biasMultiplier * _NiloToonGlobalSelfShadowReceiverNormalBias; // apply shadow receiver's depth bias, slope based selfPositionWS += _NiloToonSelfShadowLightDirection * biasMultiplier * _NiloToonGlobalSelfShadowReceiverDepthBias; // calculate shadowmap uv(x,y,z) // matrix mul is heavy(= 3 dot()), but better than passing float4 from Varyings due to interpolation cost // this vector's xyz value is similar to "shadowCoord" of URP's Shadows.hlsl float4 positionSelfShadowCS = mul(_NiloToonSelfShadowWorldToClip, float4(selfPositionWS,1)); // similar to URP's Shadows.hlsl->SampleShadowMap()'s isPerspectiveProjection, // ortho camera shadow map can ignore w divide since positionSelfShadowCS.w is always 1 //float3 positionSelfShadowNDC = positionSelfShadowCS.xyz / positionSelfShadowCS.w; // ignored, no need this line's /w float3 positionSelfShadowNDC = positionSelfShadowCS.xyz; // this line is enough // TODO: see URP's Shadows.hlsl->SampleShadowmap(...), // it is possible to sample the shadowmap rightnow without any calculation, // since the following lines can be applied to _NiloToonSelfShadowWorldToClip in C# // see URP's MainLightShadowCasterPass.cs-> SetupMainLightShadowReceiverConstants(...) for calculate _WorldToShadow matrix // convert ndc.xy[-1,1] to uv[0,1] float2 shadowMapUV_XY = positionSelfShadowNDC.xy * 0.5 + 0.5; // calculate SAMPLE_TEXTURE2D_SHADOW(...)'s ndc.z compare value //--------------------------------------------------------- float ndcZCompareValue = positionSelfShadowNDC.z; // if OpenGL, convert ndc.z [-1,1] to shadowmap's [0,1], because shadowmap is within 1~0 range (flipped when compared to DirectX) // if DirectX, do nothing, it is 0~1 range already ndcZCompareValue = UNITY_NEAR_CLIP_VALUE < 0 ? ndcZCompareValue * 0.5 + 0.5 : ndcZCompareValue; // +z compare bias (depth bias) in ndc.z [0,1] space, also apply DirectX's reverse depth to bias //ndcZCompareValue += (_NiloToonGlobalSelfShadowDepthBias+_NiloToonSelfShadowMappingDepthBias) * UNITY_NEAR_CLIP_VALUE; //--------------------------------------------------------- // if DirectX, flip shadowMapUV's y (y = 1-y) // if OpenGL, do nothing #if UNITY_UV_STARTS_AT_TOP shadowMapUV_XY.y = 1 - shadowMapUV_XY.y; #endif float4 selfShadowmapUV = float4(shadowMapUV_XY,ndcZCompareValue,0); // packing for SAMPLE_TEXTURE2D_SHADOW // 4 shadow options(Off, low, medium, high) // the if-else chain pattern is similar to URP Shadows.hlsl -> SampleShadowmapFiltered(...) if(_NiloToonSelfShadowSoftShadowParam.x > 0) { // use URP's SampleShadowmapFiltered(...) directly ShadowSamplingData_Nilo shadowSamplingData; // just for low quality 4-tap, offset is half pixel uv size to 4 directions float offset = _NiloToonSelfShadowParam.x / 2.0; shadowSamplingData.shadowOffset0 = float4(-offset, -offset, -offset, +offset); shadowSamplingData.shadowOffset1 = float4(+offset, +offset, +offset, -offset); shadowSamplingData.shadowmapSize = _NiloToonSelfShadowParam; // (1/w,1/h,w,h) of shadow map RT shadowSamplingData.softShadowQuality = _NiloToonSelfShadowSoftShadowParam.x; // (1~3 quality) selfShadowMapShadow = SampleShadowmapFiltered_Nilo(TEXTURE2D_SHADOW_ARGS(_NiloToonCharSelfShadowMapRT, sampler_NiloToonCharSelfShadowMapRT_LinearClampCompare), selfShadowmapUV, shadowSamplingData); // NiloToon added: add a stupid method to temp fix shadow acne, as URP's SampleShadowmapFiltered doesn't apply shadow bias cone for uv away from the center sample selfShadowMapShadow = saturate(selfShadowMapShadow * 1.375); // re-sharp soft shadow if(_NiloToonSelfShadowSoftShadowParam.y) { selfShadowMapShadow = smoothstep(0.5-_NiloToonSelfShadowSoftShadowParam.z,0.5+_NiloToonSelfShadowSoftShadowParam.z,selfShadowMapShadow); } } else { // a simple shadow map sample. The hardware will perform a 1-tap bilinear filter, // which is a no-cost filtering operation. selfShadowMapShadow = SAMPLE_TEXTURE2D_SHADOW(_NiloToonCharSelfShadowMapRT, sampler_NiloToonCharSelfShadowMapRT_LinearClampCompare, selfShadowmapUV); } // fadeout self shadow map if reaching the end of shadow distance (always use 2m from start fade to end fade) float fadeTotalDistance = 1; // hardcode now, can expose to C# if needed selfShadowMapShadow = lerp(selfShadowMapShadow,1, saturate((1/fadeTotalDistance) * (abs(lightingData.selfLinearEyeDepth)-(_NiloToonSelfShadowRange-fadeTotalDistance)))); // use additional N dot ShadowLight's L to hide self shadowmap artifact // smoothstep values 0.1,0.2 are based on observation only just to hide the artifact, no meaning selfShadowMapShadow *= _NiloToonSelfShadowUseNdotLFix ? smoothstep(0.1,0.2,saturate(dot(lightingData.normalWS_NoNormalMap, _NiloToonSelfShadowLightDirection))) : 1; // let user control self shadow intensity per material // For non-face, intensity is default 1 // For face, intensity is default 0, because most of the time shadow map on face is ugly half selfShadowIntensity = lerp(_NiloToonSelfShadowIntensityForNonFace,_NiloToonSelfShadowIntensityForFace,lightingData.isFaceArea) * _NiloToonSelfShadowIntensity; selfShadowMapShadow = lerp(1,selfShadowMapShadow, selfShadowIntensity * _PerCharReceiveNiloToonSelfShadowMap * _GlobalReceiveNiloToonSelfShadowMap); #if _NILOTOON_SELFSHADOW_INTENSITY_MAP half SelfShadowIntensityMultiplierTexValue = tex2D(_NiloToonSelfShadowIntensityMultiplierTex, lightingData.uv).g; selfShadowMapShadow = lerp(1,selfShadowMapShadow,SelfShadowIntensityMultiplierTexValue); #endif } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Lit (Shadow area & color) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// half finalShadowArea = selfLightAttenuation * depthDiffShadow * selfShadowMapShadow; #if _ISFACE && _FACE_3D_RIMLIGHT_AND_SHADOW finalShadowArea *= min(1-noseShadow, 1-cheekShadow); #endif finalShadowArea = saturate(lerp(1,finalShadowArea,_GlobalCharacterOverallShadowStrength)); // [ramp lighting override] // if enabled ramp lighting texture, we will ignore ALU lighting color, and use _RampLightingTex as shadow color instead #if _RAMP_LIGHTING // 1.find ramp uv.x half rampUvX = ((NoL+celShadeMidPointOffset) * 0.5 + 0.5); rampUvX = invLerp(_RampLightingNdotLRemapStart,_RampLightingNdotLRemapEnd,rampUvX);// allow user to remap // for any shadow area, force uv.x = 0, so we will sample the shadow side of ramp tex rampUvX *= depthDiffShadow; #if AnyOcclusionEnabled rampUvX *= surfaceData.occlusion; #endif #if _NILOTOON_RECEIVE_SELF_SHADOW rampUvX *= selfShadowMapShadow; #endif #if ShouldReceiveURPShadow // regular URP shadowmap but with sample position offset (extra depth bias), usually for avoiding ugly shadow map on face rampUvX *= light.shadowAttenuation; #endif // 2.find ramp uv.y half rampLightingTexSamplingUvY; #if _RAMP_LIGHTING_SAMPLE_UVY_TEX rampLightingTexSamplingUvY = dot(tex2D(_RampLightingSampleUvYTex, lightingData.uv),_RampLightingSampleUvYTexChannelMask); // select channel from tex rampLightingTexSamplingUvY = _RampLightingSampleUvYTexInvertColor ? 1-rampLightingTexSamplingUvY : rampLightingTexSamplingUvY; // optional invert rampLightingTexSamplingUvY = rampLightingTexSamplingUvY * (_RampLightingUvYRemapEnd-_RampLightingUvYRemapStart) + _RampLightingUvYRemapStart; // optional remap target UvY range #else rampLightingTexSamplingUvY = _RampLightingTexSampleUvY; #endif // we need to sample ramp texture using clamp sampler, else uv.x == 0 may still sampled uv.x == 1's position // force LOD 0, not regular auto mip sample, because mixing mipmap of ramp texture is wrong half3 rampSampledColor = 0; if(_RampLightTexMode) { rampSampledColor = SAMPLE_TEXTURE2D_LOD(_RampLightingTex, sampler_linear_clamp, half2(rampUvX, rampLightingTexSamplingUvY),0).rgb; } else { rampSampledColor = SAMPLE_TEXTURE2D_LOD(_DynamicRampLightingTex, sampler_linear_clamp, half2(rampUvX, 0.5),0).rgb; } // if rampUvX > 1, it is out of _RampLightingTex's range, we treat it as no-op ramp (*= 1) half useRampPercentage = rampUvX <= 1; // remove ramp in face area, since usually it is not looking good #if _ISFACE useRampPercentage *= lerp(1,0, lightingData.isFaceArea * _RampLightingFaceAreaRemoveEffect); #endif // 5.apply "remove ramp" rampSampledColor = lerp(1,rampSampledColor,useRampPercentage); half3 lightColorIndependentLitColor = surfaceData.albedo * rampSampledColor; #else // if NOT enable ramp lighting texture, calculate shadow color by ALU (HSV edit, see CalculateLightIndependentSelfShadowAlbedoColor(...)) half3 inSelfShadowAlbedoColor = CalculateLightIndependentSelfShadowAlbedoColor(surfaceData, lightingData, finalShadowArea); half3 lightColorIndependentLitColor = lerp(inSelfShadowAlbedoColor,surfaceData.albedo, finalShadowArea); #endif // [extra user defined color tint control to shadow area] #if NiloToonForwardLitPass || NiloToonSelfOutlinePass if(enableDepthTextureRimLightAndShadow) { // make depth tex shadow a little bit darker // because depth tex shadow is similar to contact shadow, shadow caster and receiver position is close, which means shadow/occlusion is strong (not much indirect light can reach) // we want depth tex shadow to have a bit different to self shadow to produce richer shadow, so default * 0.85 half3 finalDepthTexShadowTintColor = _DepthTexShadowTintColor * _DepthTexShadowBrightness; #if _ISFACE // if face, override to constant high saturation red tint half3 faceDepthTexShadowTintColor = _DepthTexShadowTintColorForFace * _DepthTexShadowBrightnessForFace; //half3(1,0.85,0.85); finalDepthTexShadowTintColor = lerp(finalDepthTexShadowTintColor, faceDepthTexShadowTintColor, lightingData.isFaceArea); #endif lightColorIndependentLitColor *= lerp(finalDepthTexShadowTintColor,1,depthDiffShadow); } #if _NILOTOON_RECEIVE_SELF_SHADOW lightColorIndependentLitColor *= lerp(_NiloToonSelfShadowMappingTintColor,1,selfShadowMapShadow); //TODO:... + global non skin + skin tint #endif #if ShouldReceiveURPShadow lightColorIndependentLitColor *= lerp(_URPShadowMappingTintColor,1,light.shadowAttenuation); //TODO:... + global non skin + skin tint #endif // global volume -> shadow tint color lightColorIndependentLitColor *= lerp(_GlobalCharacterOverallShadowTintColor,1,finalShadowArea); lightColorIndependentLitColor *= lerp(lerp(_GlobalCharacterOverallShadowTintColorForNonSkinFace,_GlobalCharacterOverallShadowTintColorForSkinFace,max(lightingData.isSkinArea, lightingData.isFaceArea)),1,finalShadowArea); #endif // for all additive light logic to use in the following code(specular,rim light, hair strand specular....), // to reduce (to 25%) but not completely remove add light result if in shadow half inShadow25PercentMul = lerp(lightingData.averageShadowAttenuation,1,0.25); // face 3D rim light and shadow apply shadow #if _ISFACE && _FACE_3D_RIMLIGHT_AND_SHADOW lightColorIndependentLitColor *= face3DShadowTintColor; #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Specular //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if _SPECULARHIGHLIGHTS half3 specularLitAdd = surfaceData.specular * surfaceData.specularMask; // allow user select simple specular or GGX specular method half SpecularRawArea; bool reactToLightDir = _SpecularReactToLightDirMode == 0 ? _GlobalSpecularReactToLightDirectionChange : (_SpecularReactToLightDirMode == 1 ? true : false); if(_UseGGXDirectSpecular) { // [GGX specular method] half roughness = max(1-saturate(surfaceData.smoothness * _GGXDirectSpecularSmoothnessMultiplier),0.04); // don't let roughness = 0, it will produce bad bloom. Make it minimum 0.04 (at least HALF_MIN_SQRT, but 0.04 can prevent bad bloom also) half F0 = 0.04; // only support non-metal for now, hardcode 0.04 F0 if(reactToLightDir) { // normal GGX specular function (consider light direction), produce specular result like a PBR shader SpecularRawArea = GGXDirectSpecular_Optimized(H,saturateNoL,NoV,NoH,LoH, roughness, F0); } else { // L equals V GGX specular function (NOT consider light direction), produce specular result like a matcap specular SpecularRawArea = GGXDirectSpecular_LequalsV_Optimized(NoV,VoV, roughness, F0); } } else { // [simple specular] if(reactToLightDir) { // (consider light direction) SpecularRawArea = NoH; } else { // (NOT consider light direction) SpecularRawArea = NoV; } } half specularRemapStartPoint = max(0,_SpecularAreaRemapMidPoint-_SpecularAreaRemapRange); half specularRemapEndPoint = min(1,_SpecularAreaRemapMidPoint+_SpecularAreaRemapRange); half remappedSpecularRawArea = smoothstep(specularRemapStartPoint,specularRemapEndPoint, SpecularRawArea); remappedSpecularRawArea = lerp(SpecularRawArea, remappedSpecularRawArea, _SpecularAreaRemapUsage); // allow user to choose apply specular remap or not half3 specularResultRGBMultiplier = remappedSpecularRawArea; #if _RAMP_SPECULAR { // this section will override specularResultRGBMultiplier half rampSpecularTexSamplingUvY; #if _RAMP_SPECULAR_SAMPLE_UVY_TEX rampSpecularTexSamplingUvY = tex2D(_RampSpecularSampleUvYTex, lightingData.uv).g; #else rampSpecularTexSamplingUvY = _RampSpecularTexSampleUvY; #endif half rampUvX = saturate(remappedSpecularRawArea); half4 specularRampSampleValue = SAMPLE_TEXTURE2D(_RampSpecularTex, sampler_linear_clamp, half2(rampUvX, rampSpecularTexSamplingUvY)); specularRampSampleValue.rgb /= _RampSpecularWhitePoint; // default mid gray in ramp texture = do nothing specularResultRGBMultiplier = lerp(1, specularRampSampleValue.rgb, specularRampSampleValue.a); // when ramp map's alpha = 0, don't apply specular } #endif specularLitAdd *= specularResultRGBMultiplier; // if ramp specular enabled, no need to do the this section, because ramp specular don't respect light color anyway #if !_RAMP_SPECULAR { // saturate(x), to prevent over bright specularLitAdd *= saturate(light.color) * _GlobalSpecularTintColor; // keep 25% specular in 0% direct light(in shadow) enviroment specularLitAdd *= lerp(lightingData.averageShadowAttenuation,1,_GlobalSpecularMinIntensity); } #endif if(_SpecularUseReplaceBlending) { half applyArea = saturate(remappedSpecularRawArea * surfaceData.specularMask * lerp(finalShadowArea,1,_SpecularShowInShadowArea)); half3 applyColor = surfaceData.specular; // apply (replace blending) lightColorIndependentLitColor = lerp(lightColorIndependentLitColor, applyColor, applyArea); // cancel all additive specular specularLitAdd = 0; } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Glitter //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if _NILOGLITTER half3 glitterLitAdd = 0; //----------------------- // NiloToon Glitter half3 niloGlitterResult = NiloGlitter(GetUV(input, _NiloGlitterUVIndex), V, N, L, _NiloGlitterDensity, _NiloGlitterSize, _NiloGlitterIntensity, _NiloGlitterRndomNormalStrength); niloGlitterResult *= tex2D(_NiloGlitterTintColorTex, GetUV(input, _NiloGlitterTintColorTexUVIndex)) * _NiloGlitterTintColor; niloGlitterResult *= saturate(light.color); glitterLitAdd += niloGlitterResult; //----------------------- // lilToon Glitter //glitterLitAdd += lilGlitter(...); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Kajiya-Kay specular for hair //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if _KAJIYAKAY_SPECULAR half3 kajiyaSpecularAdd = 0; float2 targetUV = GetUV(input, _HairStrandSpecularUVIndex); float shiftValue = _HairStrandSpecularUVDirection == 0 ? targetUV.x : targetUV.y; float3 shiftedT = ShiftTangent(lightingData.TBN_WS[1], lightingData.normalWS, shiftValue,_HairStrandSpecularShapeFrequency,_HairStrandSpecularShapeShift,_HairStrandSpecularShapePositionOffset); float3 HforStrandSpecular = normalize(_NiloToonGlobalPerCharFaceUpwardDirWSArray[_CharacterID] + V); // L+V, where L = _FaceUpDirection because it looks more stable kajiyaSpecularAdd += StrandSpecular(shiftedT, HforStrandSpecular,_HairStrandSpecularMainExponent) * _HairStrandSpecularMainColor * _HairStrandSpecularMainIntensity; //sharp kajiyaSpecularAdd += StrandSpecular(shiftedT, HforStrandSpecular,_HairStrandSpecularSecondExponent) * _HairStrandSpecularSecondColor * _HairStrandSpecularSecondIntensity; //soft kajiyaSpecularAdd *= 0.02 * _HairStrandSpecularOverallIntensity; // * 0.02 to allow _HairStrandSpecularMainColor & _HairStrandSpecularSecondColor's default value is white kajiyaSpecularAdd *= lerp(1, surfaceData.albedo, _HairStrandSpecularMixWithBaseMapColor); #if _KAJIYAKAY_SPECULAR_TEX_TINT { half3 kajiyaSpecularTexTint = tex2D(_HairStrandSpecularTintMap, lightingData.uv * _HairStrandSpecularTintMapTilingXyOffsetZw.xy +_HairStrandSpecularTintMapTilingXyOffsetZw.zw).rgb; kajiyaSpecularAdd *= lerp(1, kajiyaSpecularTexTint, _HairStrandSpecularTintMapUsage); } #endif // not mul inShadow25PercentMul because it looks better in shadow area // ... // max(0.25,x), to prevent 0 specular if Directional Light is off // saturate(x), to prevent over bright kajiyaSpecularAdd *= saturate(max(0.25,light.color)); kajiyaSpecularAdd = max(0,kajiyaSpecularAdd); // prevent negative additive color #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Composite final main light color //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // apply light color to the whole body at the final stage, it is better for toon/anime shading style's need // min(LightMaxContribution,light.color) to prevent over bright, ideally we want to tonemap it, but for performance reason, just min looks good enough half3 result = min(_GlobalMainDirectionalLightMaxContribution, light.color) * lightColorIndependentLitColor; // darken direct light by URP shadow (control by volume) #if ShouldReceiveURPShadow // regular URP shadowmap but with sample position offset (extra depth bias), usually for avoiding ugly shadow map on face result *= lerp(_GlobalMainLightURPShadowAsDirectResultTintColor,1, lerp(light.shadowAttenuation, lightingData.rawURPShadowAttenuation, _GlobalURPShadowAsDirectLightTintIgnoreMaterialURPUsageSetting)); #endif // apply all specular light to lit pass (ignore outline pass) #if NiloToonForwardLitPass && _SPECULARHIGHLIGHTS half specularShadowMask = lerp(finalShadowArea,1,_SpecularShowInShadowArea); #if _RAMP_SPECULAR result = lerp(result,result * specularLitAdd, remappedSpecularRawArea * selfShadowMapShadow * lightingData.averageShadowAttenuation * specularShadowMask * surfaceData.specularMask); #else half3 specularAddResult = specularLitAdd * specularShadowMask; result += specularAddResult; surfaceData.alpha = saturate(surfaceData.alpha + Luminance(specularAddResult)); // semi-transparent's reflection should NOT be affected by alpha, so we add alpha on specular area to cancel it #endif #endif #if NiloToonForwardLitPass && _NILOGLITTER result += glitterLitAdd; #endif #if NiloToonForwardLitPass || NiloToonSelfOutlinePass // (1): // min(2,light.color) to prevent over bright light.color // (2): // _ZWrite is * to _DepthTexRimLightUsage, // because if _ZWrite is off (=0), this material should be a transparent material, // and transparent material wont write to _CameraDepthTexture, // which makes depth texture rim light not correct, // so disable depth texture rim light automatically if _Zwrite is off half3 rimLightColor = min(2,light.color) * _DepthTexRimLightTintColor * lerp(1,surfaceData.albedo,_DepthTexRimLightMixWithBaseMapColor); rimLightColor = (rimLightColor / 6.0); rimLightColor *= _DepthTexRimLightIntensity; half3 RimLightMultiplier = _GlobalRimLightMultiplier; #if NiloToonSelfOutlinePass { RimLightMultiplier = _GlobalRimLightMultiplierForOutlineArea; } #endif rimLightColor *= RimLightMultiplier; // rimAttenuation will affect additional light additive rim rimAttenuation *= _DepthTexRimLightUsage * _ZWrite; #if _DEPTHTEX_RIMLIGHT_OPACITY_MASKMAP half mask = dot(tex2D(_DepthTexRimLightMaskTex, lightingData.uv), _DepthTexRimLightMaskTexChannelMask); mask = _DepthTexRimLightMaskTexInvertColor ? 1-mask : mask; rimAttenuation *= mask; #endif rimAttenuation *= inShadow25PercentMul; // if in shadow area, reduce rim light rimAttenuation *= lerp(1,finalShadowArea,_DepthTexRimLightBlockByShadow); result += rimAttenuation * rimLightColor; #endif #if NiloToonForwardLitPass && _ISFACE && _FACE_3D_RIMLIGHT_AND_SHADOW result += face3DRimAddColor * finalShadowArea; // face 3D rim block by shadow // TODO: should we add to rimAttenuation? #endif #if NiloToonForwardLitPass && _KAJIYAKAY_SPECULAR result += kajiyaSpecularAdd; #endif return half4(lerp(result,surfaceData.albedo,_AsUnlit),rimAttenuation); } half3 CalculateAdditiveSingleAdditionalLight(Light light, ToonLightingData lightingData) { half NoL = dot(lightingData.normalWS,light.direction); // remapped N dot L // simplest 1 line cel shade // celShadeResult: 0 is in shadow, 1 is in light half smoothstepNoL = smoothstep(_AdditionalLightCelShadeMidPoint-_AdditionalLightCelShadeSoftness,_AdditionalLightCelShadeMidPoint+_AdditionalLightCelShadeSoftness, NoL); // if you don't want direct lighting's NdotL cel shade effect looks too strong, set _AdditionalLightIgnoreCelShade to a higher value half lightAttenuation = lerp(smoothstepNoL,1, _AdditionalLightIgnoreCelShade); #if _ISFACE // [calculate another set of selfLightAttenuation for face area] half celShadeResultForFaceArea = smoothstep(_AdditionalLightCelShadeMidPointForFaceArea-_AdditionalLightCelShadeSoftnessForFaceArea,_AdditionalLightCelShadeMidPointForFaceArea+_AdditionalLightCelShadeSoftnessForFaceArea, NoL); half lightAttenuationForFaceArea = lerp(celShadeResultForFaceArea,1, _AdditionalLightIgnoreCelShadeForFaceArea); // [if current fragment is face area, replace original selfLightAttenuation by face's selfLightAttenuation result] lightAttenuation = lerp(lightAttenuation, lightAttenuationForFaceArea, _OverrideAdditionalLightCelShadeParamForFaceArea * lightingData.isFaceArea); #endif // distance and shadow // distance attenuation clamp to prevent over bright if light is too close to vertex half receiveShadowMappingAmount = lerp(_ReceiveURPAdditionalLightShadowMappingAmountForNonFace, _ReceiveURPAdditionalLightShadowMappingAmountForFace,lightingData.isFaceArea) * _ReceiveURPAdditionalLightShadowMappingAmount * _ReceiveURPAdditionalLightShadowMapping; half shadowAttenuation = lerp(1,light.shadowAttenuation,receiveShadowMappingAmount); lightAttenuation *= min(_AdditionalLightDistanceAttenuationClamp,light.distanceAttenuation) * shadowAttenuation; ////////////////////////////////////////////////////////////////////////////////////////////////// // Cinematic rim mask 3D ////////////////////////////////////////////////////////////////////////////////////////////////// half3 L = light.direction; half3 V = lightingData.viewDirectionWS; half3 N = lightingData.normalWS; half3 H = normalize(L+V); half NdotL = saturate(NoL); half NdotV = saturate(dot(N,V)); half fresnelTerm = 1.0-NdotV; // LdotV = is light infront of the character? (is light comes from camera's direction?) half LdotV = dot(L,V); // ------------------------------------------------ // [2D rim gradient] // possible method to make rim light not that uniform in color //lightAttenuation *= lerp(1, saturate(NdotL), _GlobalCinematic2DRimMaskStrength); // ------------------------------------------------ // [dynamic style] UNITY_BRANCH if(_GlobalCinematic3DRimMaskStrength_DynamicStyle > 0) { // forcing the light comes from the "side direction" of the character // (make V shorter than unit vector to avoid 0 length result vector) // (No need to saturate() LdotV, since we want to edit L when light is behind the characters also) half3 editedL = normalize(L+(-V * 0.999 * LdotV)); // can remove LdotV to produce "less bright+more stable" rim light // perform dot(N,L), where L is always at the "side" of character half editedNdotL = saturate(dot(N,editedL)); half width = _GlobalCinematic3DRimMaskBlur_DynamicStyle; // default 0.5 half midPoint = _GlobalCinematic3DRimMaskWidth_DynamicStyle; // default 0.5 editedNdotL = invLerpClamp(saturate(midPoint-width),saturate(midPoint+width),editedNdotL); // possible to optimize to C# passing start & end half editedRim = pow(editedNdotL,_GlobalCinematic3DRimMaskSharpness_DynamicStyle); lightAttenuation *= lerp(1,editedRim * 5.0,_GlobalCinematic3DRimMaskStrength_DynamicStyle); } // ------------------------------------------------ // [stable style] // similar to PBR's DFG's Fresnel term ^2 UNITY_BRANCH if(_GlobalCinematic3DRimMaskStrength_StableStyle > 0) { // Fresnel reflection(F) = F0 + (1-Fo) * (1-costheta)^5, where costheta is dot(N,V) // Here, dot(V,H)'s H is important to produce a soft and back light only rim effect half F = pow(saturate(1.0-dot(V,H)),_GlobalCinematic3DRimMaskSharpness_StableStyle); // sharpness = default 10. Allow 5~15 half width = 0.02; half midPoint = 0.02; //lightAttenuation *= lerp(1,smoothstep(midPoint-width,midPoint+width,F * NdotL)* (5.0 * 5.0), _GlobalCinematic3DRimMaskStrength_StableStyle); // can replace NdotL with editedNdotL lightAttenuation *= lerp(1,F * NdotL * (5.0 * 5.0 * 5.0), _GlobalCinematic3DRimMaskStrength_StableStyle); // can replace NdotL with editedNdotL } // ------------------------------------------------ // [classic rim] UNITY_BRANCH if(_GlobalCinematic3DRimMaskStrength_ClassicStyle > 0) { half width =_GlobalCinematic3DRimMaskBlur_ClassicStyle; // default 0.02 half midPoint = _GlobalCinematic3DRimMaskWidth_ClassicStyle; // default 0.7 half rim = smoothstep(saturate(midPoint-width),saturate(midPoint+width), sqrt(NdotL) * fresnelTerm); // possible to optimize to C# passing start & end rim = pow(rim, _GlobalCinematic3DRimMaskSharpness_ClassicStyle); lightAttenuation *= lerp(1,rim,_GlobalCinematic3DRimMaskStrength_ClassicStyle); } // ------------------------------------------------ // finish all float multiply first, then do float3 multiply. (performance) return light.color * lightAttenuation; } half3 ShadeEmission(ToonSurfaceData surfaceData, ToonLightingData lightingData, Light mainLight) { half3 emissionResult = surfaceData.emission; emissionResult *= lerp(1,saturate(mainLight.color * mainLight.shadowAttenuation),_MultiplyLightColorToEmissionColor); // let user optionally mix light color to emission color return emissionResult; } half3 CompositeAllLightResults(half3 mainLightResult, half3 additionalLightSumResult, half3 emissionResult) { // [simply add them all] half3 lightResult = mainLightResult; #if NeedCalculateAdditionalLight lightResult += additionalLightSumResult; #endif #if _EMISSION lightResult += emissionResult; #endif return lightResult; }