Streamingle_URP/Assets/NiloToonURP/Shaders/NiloToonCharacter_HLSL/NiloToonCharacter_LightingEquation.hlsl
user 010beaea75 Chore: NiloToonURP 업데이트 및 배경 썸네일 갱신
- NiloToonURP 외부 에셋 업데이트
- 배경 씬 썸네일 16:9 해상도로 갱신
- 렌더 파이프라인 설정 업데이트
- 외부 셰이더 그래프 업데이트 (LEDScreen, PIDI Planar Reflections)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 01:28:02 +09:00

1185 lines
72 KiB
HLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 cameras width, y is orthographic cameras 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;
}