Shader "Toon" { Properties { _BaseMap ("Base Map", 2D) = "white" [Semantic(BaseColorTexture)] _BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)] _Cutoff ("Alpha Cutoff", Range) = 0.5 [Semantic(AlphaCutoff)] _IsDay ("Is Day", Float) = 1 _DoubleSided ("Double Sided", Float) = 0 _LightMap ("Light Map", 2D) = "white" _LightDirectionMultiplier ("Light Direction Multiplier", Vector) = (1,0.5,1,0) _ShadowOffset ("Shadow Offset", Float) = 0.1 _ShadowSmoothness ("Shadow Smoothness", Float) = 0.4 _ShadowColor ("Shadow Color", Color) = (1.1,1.1,1.1,1) _ShadowRamp ("Shadow Ramp", 2D) = "white" _UseCustomMaterialType ("Use Custom Material Type", Float) = 0 _CustomMaterialType ("Custom Material Type", Float) = 1 _UseEmission ("Use Emission", Float) = 0 _EmissionIntensity ("Emission Intensity", Float) = 0.2 _UseNormalMap ("Use Normal Map", Float) = 0 _NormalMap ("Normal Map", 2D) = "bump" _IsFace ("Is Face", Float) = 0 _FaceDirection ("Face Direction", Vector) = (0,0,1,0) _FaceShadowOffset ("Face Shadow Offset", Float) = 0 _FaceBlushColor ("Face Blush Color", Color) = (1,0.72156864,0.69803923,1) _FaceBlushStrength ("Face Blush Strength", Float) = 0 _FaceLightMap ("Face Light Map", 2D) = "white" _FaceShadow ("Face Shadow", 2D) = "white" _UseSpecular ("Use Specular", Float) = 0 _SpecularSmoothness ("Specular Smoothness", Float) = 5 _NonmetallicIntensity ("Nonmetallic Intensity", Float) = 0.3 _MetallicIntensity ("Metallic Intensity", Float) = 8 _MetalMap ("Metal Map", 2D) = "white" _UseRim ("Use Rim", Float) = 0 _RimOffset ("Rim Offset", Float) = 5 _RimThreshold ("Rim Threshold", Float) = 0.5 _RimIntensity ("Rim Intensity", Float) = 0.5 _UseSmoothNormal ("Use Smooth Normal", Float) = 0 _OutlineWidth ("Outline Width", Float) = 1.6 _OutlineWidthParams ("Outline Width Params", Vector) = (0,6,0.1,0.6) _OutlineZOffset ("Outline Z Offset", Float) = 0.1 _ScreenOffset ("Screen Offset", Vector) = (0,0,0,0) _OutlineColor ("Outline Color", Color) = (0.5176471,0.35686275,0.34117648,1) _OutlineColor2 ("Outline Color 2", Color) = (0.3529412,0.3529412,0.3529412,1) _OutlineColor3 ("Outline Color 3", Color) = (0.47058824,0.47058824,0.5647059,1) _OutlineColor4 ("Outline Color 4", Color) = (0.5176471,0.35686275,0.34117648,1) _OutlineColor5 ("Outline Color 5", Color) = (0.35,0.35,0.35,1) } HLSLINCLUDE cbuffer PerObjectConstants { float4x4 gProjectionMatrix; float4x4 gViewMatrix; float4x4 gModelMatrix; float4x4 gNormalMatrix; }; static const int XC_MAX_ADDITIONAL_LIGHTS = 8; struct AdditionalLightData { float4 colorAndIntensity; float4 positionAndRange; float4 directionAndType; float4 spotAnglesAndFlags; }; struct ShadowMapMetrics { float2 inverseMapSize; float worldTexelSize; float padding; }; struct ShadowSamplingData { float enabled; float receiverDepthBias; float normalBiasScale; float shadowStrength; }; cbuffer LightingConstants { float4 gMainLightDirectionAndIntensity; float4 gMainLightColorAndFlags; float4 gLightingParams; AdditionalLightData gAdditionalLights[XC_MAX_ADDITIONAL_LIGHTS]; }; cbuffer MaterialConstants { float4 _BaseColor; float4 _Cutoff; float4 _IsDay; float4 _DoubleSided; float4 _LightDirectionMultiplier; float4 _ShadowOffset; float4 _ShadowSmoothness; float4 _ShadowColor; float4 _UseCustomMaterialType; float4 _CustomMaterialType; float4 _UseEmission; float4 _EmissionIntensity; float4 _UseNormalMap; float4 _IsFace; float4 _FaceDirection; float4 _FaceShadowOffset; float4 _FaceBlushColor; float4 _FaceBlushStrength; float4 _UseSpecular; float4 _SpecularSmoothness; float4 _NonmetallicIntensity; float4 _MetallicIntensity; float4 _UseRim; float4 _RimOffset; float4 _RimThreshold; float4 _RimIntensity; float4 _UseSmoothNormal; float4 _OutlineWidth; float4 _OutlineWidthParams; float4 _OutlineZOffset; float4 _ScreenOffset; float4 _OutlineColor; float4 _OutlineColor2; float4 _OutlineColor3; float4 _OutlineColor4; float4 _OutlineColor5; }; cbuffer ShadowReceiverConstants { float4x4 gWorldToShadowMatrix; ShadowMapMetrics gShadowMapMetrics; ShadowSamplingData gShadowSampling; }; Texture2D BaseColorTexture; Texture2D _LightMap; Texture2D _ShadowRamp; Texture2D _NormalMap; Texture2D _FaceLightMap; Texture2D _FaceShadow; Texture2D _MetalMap; SamplerState LinearClampSampler; Texture2D ShadowMapTexture; SamplerState ShadowMapSampler; struct VSInput { float3 position : POSITION; float3 normal : NORMAL; float4 color : COLOR; float2 texcoord : TEXCOORD0; float2 backTexcoord : TEXCOORD1; float3 tangent : TEXCOORD2; float3 bitangent : TEXCOORD3; }; struct PSInput { float4 position : SV_POSITION; float2 texcoord : TEXCOORD0; float2 backTexcoord : TEXCOORD1; float3 positionWS : TEXCOORD2; float3 positionVS : TEXCOORD3; float3 normalWS : TEXCOORD4; float3 tangentWS : TEXCOORD5; float3 bitangentWS : TEXCOORD6; float4 color : COLOR; }; float3 NormalizeSafe(float3 value, float3 fallbackValue) { const float lengthSq = dot(value, value); if (lengthSq <= 1e-6f) { return fallbackValue; } return value * rsqrt(lengthSq); } float3 UnpackNormalRG(float4 packedNormal) { const float2 normalXY = packedNormal.xy * 2.0f - 1.0f; const float normalZSq = saturate(1.0f - dot(normalXY, normalXY)); return float3(normalXY, sqrt(normalZSq)); } bool UseEmissionFeature() { #ifdef _EMISSION return true; #else return _UseEmission.x > 0.5f; #endif } bool UseNormalMapFeature() { #ifdef _NORMAL_MAP return true; #else return _UseNormalMap.x > 0.5f; #endif } bool UseFaceFeature() { #ifdef _IS_FACE return true; #else return _IsFace.x > 0.5f; #endif } bool UseSpecularFeature() { #ifdef _SPECULAR return true; #else return _UseSpecular.x > 0.5f; #endif } bool UseRimFeature() { #ifdef _RIM return true; #else return _UseRim.x > 0.5f; #endif } bool UseDoubleSidedFeature() { #ifdef _DOUBLE_SIDED return true; #else return _DoubleSided.x > 0.5f; #endif } float2 ResolveMaterialTexcoord(PSInput input, bool isFrontFace) { if (!UseDoubleSidedFeature()) { return input.texcoord; } return lerp(input.texcoord, input.backTexcoord, isFrontFace ? 0.0f : 1.0f); } PSInput MainVS(VSInput input) { PSInput output; const float4 positionWS = mul(gModelMatrix, float4(input.position, 1.0f)); const float4 positionVS = mul(gViewMatrix, positionWS); output.position = mul(gProjectionMatrix, positionVS); output.texcoord = input.texcoord; output.backTexcoord = input.backTexcoord; output.positionWS = positionWS.xyz; output.positionVS = positionVS.xyz; output.normalWS = normalize(mul((float3x3)gNormalMatrix, input.normal)); output.tangentWS = normalize(mul((float3x3)gNormalMatrix, input.tangent)); output.bitangentWS = normalize(mul((float3x3)gNormalMatrix, input.bitangent)); output.color = input.color; return output; } float SampleShadowVisibility(float2 sampleUv, float receiverDepth) { if (sampleUv.x < 0.0f || sampleUv.x > 1.0f || sampleUv.y < 0.0f || sampleUv.y > 1.0f) { return 1.0f; } const float shadowDepth = ShadowMapTexture.Sample(ShadowMapSampler, sampleUv).r; return receiverDepth <= shadowDepth ? 1.0f : 0.0f; } float ResolveShadowPcfAxisWeight(int offset) { const int absoluteOffset = abs(offset); if (absoluteOffset == 0) { return 6.0f; } if (absoluteOffset == 1) { return 4.0f; } return 1.0f; } float ComputeShadowPcfVisibility(float2 shadowUv, float receiverDepth, float2 shadowTexelSize) { float weightedVisibility = 0.0f; float weightSum = 0.0f; [unroll] for (int offsetY = -2; offsetY <= 2; ++offsetY) { const float weightY = ResolveShadowPcfAxisWeight(offsetY); [unroll] for (int offsetX = -2; offsetX <= 2; ++offsetX) { const float weight = weightY * ResolveShadowPcfAxisWeight(offsetX); const float2 sampleUv = shadowUv + float2((float)offsetX, (float)offsetY) * shadowTexelSize; weightedVisibility += SampleShadowVisibility(sampleUv, receiverDepth) * weight; weightSum += weight; } } return weightSum > 0.0f ? weightedVisibility / weightSum : 1.0f; } float ComputeShadowAttenuation(float3 positionWS, float3 normalWS, float3 lightDirectionWS) { #ifndef XC_MAIN_LIGHT_SHADOWS return 1.0f; #else if (gShadowSampling.enabled < 0.5f) { return 1.0f; } const float3 resolvedNormalWS = NormalizeSafe(normalWS, float3(0.0f, 1.0f, 0.0f)); const float3 resolvedLightDirectionWS = NormalizeSafe(lightDirectionWS, float3(0.0f, -1.0f, 0.0f)); const float nDotL = saturate(dot(resolvedNormalWS, resolvedLightDirectionWS)); const float normalBiasWorld = gShadowMapMetrics.worldTexelSize * gShadowSampling.normalBiasScale * (1.0f - nDotL); const float3 shadowPositionWS = positionWS + resolvedNormalWS * normalBiasWorld; const float4 shadowClip = mul(gWorldToShadowMatrix, float4(shadowPositionWS, 1.0f)); if (shadowClip.w <= 0.0f) { return 1.0f; } const float3 shadowNdc = shadowClip.xyz / shadowClip.w; #if UNITY_UV_STARTS_AT_TOP const float shadowUvY = shadowNdc.y * -0.5f + 0.5f; #else const float shadowUvY = shadowNdc.y * 0.5f + 0.5f; #endif const float2 shadowUv = float2(shadowNdc.x * 0.5f + 0.5f, shadowUvY); #if UNITY_NEAR_CLIP_VALUE < 0 if (shadowUv.x < 0.0f || shadowUv.x > 1.0f || shadowUv.y < 0.0f || shadowUv.y > 1.0f || shadowNdc.z < -1.0f || shadowNdc.z > 1.0f) { return 1.0f; } const float receiverDepth = shadowNdc.z * 0.5f + 0.5f - gShadowSampling.receiverDepthBias; #else if (shadowUv.x < 0.0f || shadowUv.x > 1.0f || shadowUv.y < 0.0f || shadowUv.y > 1.0f || shadowNdc.z < 0.0f || shadowNdc.z > 1.0f) { return 1.0f; } const float receiverDepth = shadowNdc.z - gShadowSampling.receiverDepthBias; #endif const float2 shadowTexelSize = gShadowMapMetrics.inverseMapSize; const float visibility = ComputeShadowPcfVisibility( shadowUv, receiverDepth, shadowTexelSize); const float shadowStrength = saturate(gShadowSampling.shadowStrength); return lerp(1.0f - shadowStrength, 1.0f, visibility); #endif } float ComputeRangeAttenuation(float distanceSq, float range) { if (range <= 0.0f) { return 0.0f; } const float clampedRange = max(range, 0.0001f); const float rangeSq = clampedRange * clampedRange; if (distanceSq >= rangeSq) { return 0.0f; } const float distance = sqrt(max(distanceSq, 0.0f)); const float normalized = saturate(1.0f - distance / clampedRange); return normalized * normalized; } float ComputeSpotAttenuation(AdditionalLightData light, float3 directionToLightWS) { const float cosOuter = light.spotAnglesAndFlags.x; const float cosInner = light.spotAnglesAndFlags.y; const float3 spotAxisToLightWS = NormalizeSafe(light.directionAndType.xyz, float3(0.0f, -1.0f, 0.0f)); const float cosTheta = dot(spotAxisToLightWS, directionToLightWS); return saturate((cosTheta - cosOuter) / max(cosInner - cosOuter, 1e-4f)); } float3 EvaluateAdditionalLight(AdditionalLightData light, float3 normalWS, float3 positionWS) { const float lightType = light.directionAndType.w; const float3 lightColor = light.colorAndIntensity.rgb; const float lightIntensity = light.colorAndIntensity.w; float3 directionToLightWS = float3(0.0f, 0.0f, 0.0f); float attenuation = 1.0f; if (lightType < 0.5f) { directionToLightWS = NormalizeSafe(light.directionAndType.xyz, float3(0.0f, -1.0f, 0.0f)); } else { const float3 lightVectorWS = light.positionAndRange.xyz - positionWS; const float distanceSq = dot(lightVectorWS, lightVectorWS); if (distanceSq <= 1e-6f) { return float3(0.0f, 0.0f, 0.0f); } directionToLightWS = lightVectorWS * rsqrt(distanceSq); attenuation = ComputeRangeAttenuation(distanceSq, light.positionAndRange.w); if (attenuation <= 0.0f) { return float3(0.0f, 0.0f, 0.0f); } if (lightType > 1.5f) { attenuation *= ComputeSpotAttenuation(light, directionToLightWS); if (attenuation <= 0.0f) { return float3(0.0f, 0.0f, 0.0f); } } } const float diffuse = saturate(dot(normalWS, directionToLightWS)); if (diffuse <= 0.0f) { return float3(0.0f, 0.0f, 0.0f); } return lightColor * (diffuse * lightIntensity * attenuation); } float3 ComputeSurfaceNormalWS(PSInput input, float2 texcoord) { float3 normalWS = NormalizeSafe(input.normalWS, float3(0.0f, 1.0f, 0.0f)); if (!UseNormalMapFeature()) { return normalWS; } const float3 tangentWS = NormalizeSafe(input.tangentWS, float3(1.0f, 0.0f, 0.0f)); const float3 bitangentWS = NormalizeSafe(input.bitangentWS, float3(0.0f, 0.0f, 1.0f)); const float3 normalTS = UnpackNormalRG(_NormalMap.Sample(LinearClampSampler, texcoord)); const float3 mappedNormalWS = tangentWS * normalTS.x + bitangentWS * normalTS.y + normalWS * normalTS.z; return NormalizeSafe(mappedNormalWS, normalWS); } float3 GetAdjustedMainLightDirection() { const float3 scaledDirection = gMainLightDirectionAndIntensity.xyz * _LightDirectionMultiplier.xyz; return NormalizeSafe( scaledDirection, NormalizeSafe(gMainLightDirectionAndIntensity.xyz, float3(0.0f, -1.0f, 0.0f))); } float ResolveMaterialSelector(float lightMapAlpha) { const float useCustomMaterialType = _UseCustomMaterialType.x > 0.5f ? 1.0f : 0.0f; return lerp(lightMapAlpha, _CustomMaterialType.x, useCustomMaterialType); } float GetGenericShadow(float3 normalWS, float3 lightDirectionWS, float aoFactor) { const float ndotl = dot(normalWS, lightDirectionWS); const float halfLambert = ndotl * 0.5f + 0.5f; const float shadow = saturate(2.0f * halfLambert * aoFactor); return lerp(shadow, 1.0f, step(0.9f, aoFactor)); } float GetFaceShadow(PSInput input, float2 texcoord, float3 lightDirectionWS) { const float3 faceDirection = NormalizeSafe( float3(_FaceDirection.x, 0.0f, _FaceDirection.z), float3(0.0f, 0.0f, 1.0f)); const float3 flatLightDirection = NormalizeSafe( float3(lightDirectionWS.x, 0.0f, lightDirectionWS.z), float3(0.0f, 0.0f, 1.0f)); const float faceDotLight = dot(faceDirection, flatLightDirection); const float faceCrossLight = cross(faceDirection, flatLightDirection).y; float2 shadowUv = texcoord; shadowUv.x = lerp(shadowUv.x, 1.0f - shadowUv.x, step(0.0f, faceCrossLight)); const float faceShadowMap = _FaceLightMap.Sample(LinearClampSampler, shadowUv).r; const float faceShadow = step(-0.5f * faceDotLight + 0.5f + _FaceShadowOffset.x, faceShadowMap); const float faceMask = _FaceShadow.Sample(LinearClampSampler, texcoord).a; return lerp(faceShadow, 1.0f, faceMask); } float ResolveShadowRampIndex(float materialSelector) { float rampIndex = 4.0f; rampIndex = lerp(rampIndex, 1.0f, step(0.2f, materialSelector)); rampIndex = lerp(rampIndex, 2.0f, step(0.4f, materialSelector)); rampIndex = lerp(rampIndex, 0.0f, step(0.6f, materialSelector)); rampIndex = lerp(rampIndex, 3.0f, step(0.8f, materialSelector)); return rampIndex; } float3 GetShadowColor(float shadow, float materialSelector) { const float rampIndex = ResolveShadowRampIndex(materialSelector); const float rangeMin = 0.5f + _ShadowOffset.x - _ShadowSmoothness.x; const float rangeMax = 0.5f + _ShadowOffset.x; const float rampV = rampIndex / 10.0f + 0.5f * saturate(_IsDay.x) + 0.05f; const float2 rampUv = float2( smoothstep(rangeMin, rangeMax, shadow), rampV); const float3 shadowRamp = _ShadowRamp.Sample(LinearClampSampler, rampUv).rgb; float3 shadowColor = shadowRamp * lerp(_ShadowColor.rgb, float3(1.0f, 1.0f, 1.0f), smoothstep(0.9f, 1.0f, rampUv.x)); shadowColor = lerp(shadowColor, float3(1.0f, 1.0f, 1.0f), step(rangeMax, shadow)); return shadowColor; } float3 GetSpecular( PSInput input, float3 normalWS, float3 lightDirectionWS, float3 albedo, float3 lightMap) { const float3 viewDirectionVS = NormalizeSafe(-input.positionVS, float3(0.0f, 0.0f, 1.0f)); const float3 lightDirectionVS = NormalizeSafe( mul((float3x3)gViewMatrix, lightDirectionWS), float3(0.0f, 0.0f, 1.0f)); const float3 normalVS = NormalizeSafe(mul((float3x3)gViewMatrix, normalWS), float3(0.0f, 0.0f, 1.0f)); const float3 halfDirectionVS = NormalizeSafe(lightDirectionVS + viewDirectionVS, lightDirectionVS); const float nDotH = dot(normalVS, halfDirectionVS); const float blinnPhong = pow(saturate(nDotH), max(_SpecularSmoothness.x, 1.0f)); const float2 matcapUv = normalVS.xy * 0.5f + 0.5f; const float3 metalMap = _MetalMap.Sample(LinearClampSampler, matcapUv).rgb; const float3 nonMetallic = step(1.1f, lightMap.b + blinnPhong) * lightMap.r * _NonmetallicIntensity.x; const float3 metallic = blinnPhong * lightMap.b * albedo * metalMap * _MetallicIntensity.x; return lerp(nonMetallic, metallic, step(0.9f, lightMap.r)); } float GetRim(PSInput input, float3 normalWS) { const float3 viewDirectionVS = NormalizeSafe(-input.positionVS, float3(0.0f, 0.0f, 1.0f)); const float3 normalVS = NormalizeSafe(mul((float3x3)gViewMatrix, normalWS), float3(0.0f, 0.0f, 1.0f)); const float nDotV = dot(normalVS, viewDirectionVS); const float fresnel = pow(saturate(1.0f - nDotV), max(_RimOffset.x, 0.001f)); return smoothstep(0.0f, max(_RimThreshold.x, 0.001f), fresnel) * _RimIntensity.x; } float4 MainPS(PSInput input, bool isFrontFace : SV_IsFrontFace) : SV_TARGET { const float2 texcoord = ResolveMaterialTexcoord(input, isFrontFace); float4 baseMap = BaseColorTexture.Sample(LinearClampSampler, texcoord); float3 albedo = baseMap.rgb * _BaseColor.rgb; const float alphaMask = baseMap.a; if (UseFaceFeature()) { albedo = lerp(albedo, _FaceBlushColor.rgb, _FaceBlushStrength.x * alphaMask); } const float3 normalWS = ComputeSurfaceNormalWS(input, texcoord); const float4 lightMap = _LightMap.Sample(LinearClampSampler, texcoord); float3 color = albedo; if (gMainLightColorAndFlags.a >= 0.5f) { const float3 lightDirectionWS = GetAdjustedMainLightDirection(); const float materialSelector = ResolveMaterialSelector(lightMap.a); const float aoFactor = lightMap.g * input.color.x; float shadow = UseFaceFeature() ? GetFaceShadow(input, texcoord, lightDirectionWS) : GetGenericShadow(normalWS, lightDirectionWS, aoFactor); const float3 shadowColor = GetShadowColor(shadow, materialSelector); float3 specular = float3(0.0f, 0.0f, 0.0f); if (UseSpecularFeature()) { specular = GetSpecular(input, normalWS, lightDirectionWS, albedo, lightMap.rgb); } float3 emission = float3(0.0f, 0.0f, 0.0f); if (UseEmissionFeature()) { emission = albedo * _EmissionIntensity.x * alphaMask; } float3 rim = float3(0.0f, 0.0f, 0.0f); if (UseRimFeature()) { rim = albedo * GetRim(input, normalWS); } color = albedo * shadowColor + specular + emission + rim; } return float4(color, 1.0f); } ENDHLSL SubShader { Cull Back ZWrite On ZTest LEqual Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardLit" } HLSLPROGRAM #pragma target 4.5 #pragma vertex MainVS #pragma fragment MainPS #pragma multi_compile _ XC_MAIN_LIGHT_SHADOWS #pragma shader_feature_local _ XC_ALPHA_TEST #pragma shader_feature_local _ _EMISSION #pragma shader_feature_local _ _NORMAL_MAP #pragma shader_feature_local _ _IS_FACE #pragma shader_feature_local _ _SPECULAR #pragma shader_feature_local _ _RIM ENDHLSL } UsePass "Builtin Depth Only/DepthOnly" UsePass "Builtin Shadow Caster/ShadowCaster" } }