// For more information, visit -> https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample // #pragma once is a safe guard best practice in almost every .hlsl (need Unity2020 or up), // doing this can make sure your .hlsl's user can include this .hlsl anywhere anytime without producing any multi include conflict #pragma once // We don't have "UnityCG.cginc" in SRP/URP's package anymore, so: // Including the following two hlsl files is enough for shading with Universal Pipeline. Everything is included in them. // Core.hlsl will include SRP shader library, all constant buffers not related to materials (perobject, percamera, perframe). // It also includes matrix/space conversion functions and fog. // Lighting.hlsl will include the light functions/data to abstract light constants. You should use GetMainLight and GetLight functions // that initialize Light struct. Lighting.hlsl also include GI, Light BDRF functions. It also includes Shadows. // Required by all Universal Render Pipeline shaders. // It will include Unity built-in shader variables (except the lighting variables) // (https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html // It will also include many utilitary functions. #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // Include this if you are doing a lit shader. This includes lighting shader variables, // lighting and shadow functions #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" // Material shader variables are not defined in SRP or URP shader library. // This means _BaseColor, _BaseMap, _BaseMap_ST, and all variables in the Properties section of a shader // must be defined by the shader itself. If you define all those properties in CBUFFER named // UnityPerMaterial, SRP can cache the material properties between frames and reduce significantly the cost // of each drawcall. // In this case, although URP's LitInput.hlsl contains the CBUFFER for the material // properties defined above. As one can see this is not part of the ShaderLibrary, it specific to the // URP Lit shader. // So we are not going to use LitInput.hlsl, we will implement everything by ourself. //#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" // we will include some utility .hlsl files to help us #include "NiloOutlineUtil.hlsl" #include "NiloZOffset.hlsl" #include "NiloInvLerpRemap.hlsl" // note: // subfix OS means object spaces (e.g. positionOS = position object space) // subfix WS means world space (e.g. positionWS = position world space) // subfix VS means view space (e.g. positionVS = position view space) // subfix CS means clip space (e.g. positionCS = position clip space) // all pass will share this Attributes struct (define data needed from Unity app to our vertex shader) struct Attributes { float3 positionOS : POSITION; half3 normalOS : NORMAL; half4 tangentOS : TANGENT; float2 uv : TEXCOORD0; }; // all pass will share this Varyings struct (define data needed from our vertex shader to our fragment shader) struct Varyings { float2 uv : TEXCOORD0; float4 positionWSAndFogFactor : TEXCOORD1; // xyz: positionWS, w: vertex fog factor half3 normalWS : TEXCOORD2; float4 positionCS : SV_POSITION; }; /////////////////////////////////////////////////////////////////////////////////////// // CBUFFER and Uniforms // (you should put all uniforms of all passes inside this single UnityPerMaterial CBUFFER! else SRP batching is not possible!) /////////////////////////////////////////////////////////////////////////////////////// // all sampler2D don't need to put inside CBUFFER sampler2D _BaseMap; sampler2D _EmissionMap; sampler2D _OcclusionMap; sampler2D _OutlineZOffsetMaskTex; // put all your uniforms(usually things inside .shader file's properties{}) inside this CBUFFER, in order to make SRP batcher compatible // see -> https://blogs.unity3d.com/2019/02/28/srp-batcher-speed-up-your-rendering/ CBUFFER_START(UnityPerMaterial) // high level settings float _IsFace; // base color float4 _BaseMap_ST; half4 _BaseColor; // alpha half _Cutoff; // emission float _UseEmission; half3 _EmissionColor; half _EmissionMulByBaseColor; half3 _EmissionMapChannelMask; // occlusion float _UseOcclusion; half _OcclusionStrength; half4 _OcclusionMapChannelMask; half _OcclusionRemapStart; half _OcclusionRemapEnd; // lighting half3 _IndirectLightMinColor; half _CelShadeMidPoint; half _CelShadeSoftness; // shadow mapping half _ReceiveShadowMappingAmount; float _ReceiveShadowMappingPosOffset; half3 _ShadowMapColor; // outline float _OutlineWidth; half3 _OutlineColor; float _OutlineZOffset; float _OutlineZOffsetMaskRemapStart; float _OutlineZOffsetMaskRemapEnd; CBUFFER_END //a special uniform for applyShadowBiasFixToHClipPos() only, it is not a per material uniform, //so it is fine to write it outside our UnityPerMaterial CBUFFER float3 _LightDirection; struct ToonSurfaceData { half3 albedo; half alpha; half3 emission; half occlusion; }; struct ToonLightingData { half3 normalWS; float3 positionWS; half3 viewDirectionWS; float4 shadowCoord; }; /////////////////////////////////////////////////////////////////////////////////////// // vertex shared functions /////////////////////////////////////////////////////////////////////////////////////// float3 TransformPositionWSToOutlinePositionWS(float3 positionWS, float positionVS_Z, float3 normalWS) { //you can replace it to your own method! Here we will write a simple world space method for tutorial reason, it is not the best method! float outlineExpandAmount = _OutlineWidth * GetOutlineCameraFovAndDistanceFixMultiplier(positionVS_Z); return positionWS + normalWS * outlineExpandAmount; } // if "ToonShaderIsOutline" is not defined = do regular MVP transform // if "ToonShaderIsOutline" is defined = do regular MVP transform + push vertex out a bit according to normal direction Varyings VertexShaderWork(Attributes input) { Varyings output; // VertexPositionInputs contains position in multiple spaces (world, view, homogeneous clip space, ndc) // Unity compiler will strip all unused references (say you don't use view space). // Therefore there is more flexibility at no additional cost with this struct. VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS); // Similar to VertexPositionInputs, VertexNormalInputs will contain normal, tangent and bitangent // in world space. If not used it will be stripped. VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); float3 positionWS = vertexInput.positionWS; #ifdef ToonShaderIsOutline positionWS = TransformPositionWSToOutlinePositionWS(vertexInput.positionWS, vertexInput.positionVS.z, vertexNormalInput.normalWS); #endif // Computes fog factor per-vertex. float fogFactor = ComputeFogFactor(vertexInput.positionCS.z); // TRANSFORM_TEX is the same as the old shader library. output.uv = TRANSFORM_TEX(input.uv,_BaseMap); // packing positionWS(xyz) & fog(w) into a vector4 output.positionWSAndFogFactor = float4(positionWS, fogFactor); output.normalWS = vertexNormalInput.normalWS; //normlaized already by GetVertexNormalInputs(...) output.positionCS = TransformWorldToHClip(positionWS); #ifdef ToonShaderIsOutline // [Read ZOffset mask texture] // we can't use tex2D() in vertex shader because ddx & ddy is unknown before rasterization, // so use tex2Dlod() with an explict mip level 0, put explict mip level 0 inside the 4th component of param uv) float outlineZOffsetMaskTexExplictMipLevel = 0; float outlineZOffsetMask = tex2Dlod(_OutlineZOffsetMaskTex, float4(input.uv,0,outlineZOffsetMaskTexExplictMipLevel)).r; //we assume it is a Black/White texture // [Remap ZOffset texture value] // flip texture read value so default black area = apply ZOffset, because usually outline mask texture are using this format(black = hide outline) outlineZOffsetMask = 1-outlineZOffsetMask; outlineZOffsetMask = invLerpClamp(_OutlineZOffsetMaskRemapStart,_OutlineZOffsetMaskRemapEnd,outlineZOffsetMask);// allow user to flip value or remap // [Apply ZOffset, Use remapped value as ZOffset mask] output.positionCS = NiloGetNewClipPosWithZOffset(output.positionCS, _OutlineZOffset * outlineZOffsetMask + 0.03 * _IsFace); #endif // ShadowCaster pass needs special process to positionCS, else shadow artifact will appear //-------------------------------------------------------------------------------------- #ifdef ToonShaderApplyShadowBiasFix // see GetShadowPositionHClip() in URP/Shaders/ShadowCasterPass.hlsl // https://github.com/Unity-Technologies/Graphics/blob/master/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, output.normalWS, _LightDirection)); #if UNITY_REVERSED_Z positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #else positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #endif output.positionCS = positionCS; #endif //-------------------------------------------------------------------------------------- return output; } /////////////////////////////////////////////////////////////////////////////////////// // fragment shared functions (Step1: prepare data structs for lighting calculation) /////////////////////////////////////////////////////////////////////////////////////// half4 GetFinalBaseColor(Varyings input) { return tex2D(_BaseMap, input.uv) * _BaseColor; } half3 GetFinalEmissionColor(Varyings input) { half3 result = 0; if(_UseEmission) { result = tex2D(_EmissionMap, input.uv).rgb * _EmissionMapChannelMask * _EmissionColor.rgb; } return result; } half GetFinalOcculsion(Varyings input) { half result = 1; if(_UseOcclusion) { half4 texValue = tex2D(_OcclusionMap, input.uv); half occlusionValue = dot(texValue, _OcclusionMapChannelMask); occlusionValue = lerp(1, occlusionValue, _OcclusionStrength); occlusionValue = invLerpClamp(_OcclusionRemapStart, _OcclusionRemapEnd, occlusionValue); result = occlusionValue; } return result; } void DoClipTestToTargetAlphaValue(half alpha) { #if _UseAlphaClipping clip(alpha - _Cutoff); #endif } ToonSurfaceData InitializeSurfaceData(Varyings input) { ToonSurfaceData output; // albedo & alpha float4 baseColorFinal = GetFinalBaseColor(input); output.albedo = baseColorFinal.rgb; output.alpha = baseColorFinal.a; DoClipTestToTargetAlphaValue(output.alpha);// early exit if possible // emission output.emission = GetFinalEmissionColor(input); // occlusion output.occlusion = GetFinalOcculsion(input); return output; } ToonLightingData InitializeLightingData(Varyings input) { ToonLightingData lightingData; lightingData.positionWS = input.positionWSAndFogFactor.xyz; lightingData.viewDirectionWS = SafeNormalize(GetCameraPositionWS() - lightingData.positionWS); lightingData.normalWS = normalize(input.normalWS); //interpolated normal is NOT unit vector, we need to normalize it return lightingData; } /////////////////////////////////////////////////////////////////////////////////////// // fragment shared functions (Step2: calculate lighting & final color) /////////////////////////////////////////////////////////////////////////////////////// // all lighting equation written inside this .hlsl, // just by editing this .hlsl can control most of the visual result. #include "SimpleURPToonLitOutlineExample_LightingEquation.hlsl" // this function contains no lighting logic, it just pass lighting results data around // the job done in this function is "do shadow mapping depth test positionWS offset" half3 ShadeAllLights(ToonSurfaceData surfaceData, ToonLightingData lightingData) { // Indirect lighting half3 indirectResult = ShadeGI(surfaceData, lightingData); ////////////////////////////////////////////////////////////////////////////////// // Light struct is provided by URP to abstract light shader variables. // It contains light's // - direction // - color // - distanceAttenuation // - shadowAttenuation // // URP take different shading approaches depending on light and platform. // You should never reference light shader variables in your shader, instead use the // -GetMainLight() // -GetLight() // funcitons to fill this Light struct. ////////////////////////////////////////////////////////////////////////////////// //============================================================================================== // Main light is the brightest directional light. // It is shaded outside the light loop and it has a specific set of variables and shading path // so we can be as fast as possible in the case when there's only a single directional light // You can pass optionally a shadowCoord. If so, shadowAttenuation will be computed. Light mainLight = GetMainLight(); float3 shadowTestPosWS = lightingData.positionWS + mainLight.direction * (_ReceiveShadowMappingPosOffset + _IsFace); #ifdef _MAIN_LIGHT_SHADOWS // compute the shadow coords in the fragment shader now due to this change // https://forum.unity.com/threads/shadow-cascades-weird-since-7-2-0.828453/#post-5516425 // _ReceiveShadowMappingPosOffset will control the offset the shadow comparsion position, // doing this is usually for hide ugly self shadow for shadow sensitive area like face float4 shadowCoord = TransformWorldToShadowCoord(shadowTestPosWS); mainLight.shadowAttenuation = MainLightRealtimeShadow(shadowCoord); #endif // Main light half3 mainLightResult = ShadeSingleLight(surfaceData, lightingData, mainLight, false); //============================================================================================== // All additional lights half3 additionalLightSumResult = 0; #ifdef _ADDITIONAL_LIGHTS // Returns the amount of lights affecting the object being renderer. // These lights are culled per-object in the forward renderer of URP. int additionalLightsCount = GetAdditionalLightsCount(); for (int i = 0; i < additionalLightsCount; ++i) { // Similar to GetMainLight(), but it takes a for-loop index. This figures out the // per-object light index and samples the light buffer accordingly to initialized the // Light struct. If ADDITIONAL_LIGHT_CALCULATE_SHADOWS is defined it will also compute shadows. int perObjectLightIndex = GetPerObjectLightIndex(i); Light light = GetAdditionalPerObjectLight(perObjectLightIndex, lightingData.positionWS); // use original positionWS for lighting light.shadowAttenuation = AdditionalLightRealtimeShadow(perObjectLightIndex, shadowTestPosWS); // use offseted positionWS for shadow test // Different function used to shade additional lights. additionalLightSumResult += ShadeSingleLight(surfaceData, lightingData, light, true); } #endif //============================================================================================== // emission half3 emissionResult = ShadeEmission(surfaceData, lightingData); return CompositeAllLightResults(indirectResult, mainLightResult, additionalLightSumResult, emissionResult, surfaceData, lightingData); } half3 ConvertSurfaceColorToOutlineColor(half3 originalSurfaceColor) { return originalSurfaceColor * _OutlineColor; } half3 ApplyFog(half3 color, Varyings input) { half fogFactor = input.positionWSAndFogFactor.w; // Mix the pixel color with fogColor. You can optionaly use MixFogColor to override the fogColor // with a custom one. color = MixFog(color, fogFactor); return color; } // only the .shader file will call this function by // #pragma fragment ShadeFinalColor half4 ShadeFinalColor(Varyings input) : SV_TARGET { ////////////////////////////////////////////////////////////////////////////////////////// // first prepare all data for lighting function ////////////////////////////////////////////////////////////////////////////////////////// // fillin ToonSurfaceData struct: ToonSurfaceData surfaceData = InitializeSurfaceData(input); // fillin ToonLightingData struct: ToonLightingData lightingData = InitializeLightingData(input); // apply all lighting calculation half3 color = ShadeAllLights(surfaceData, lightingData); #ifdef ToonShaderIsOutline color = ConvertSurfaceColorToOutlineColor(color); #endif color = ApplyFog(color, input); return half4(color, surfaceData.alpha); } ////////////////////////////////////////////////////////////////////////////////////////// // fragment shared functions (for ShadowCaster pass & DepthOnly pass to use only) ////////////////////////////////////////////////////////////////////////////////////////// void BaseColorAlphaClipTest(Varyings input) { DoClipTestToTargetAlphaValue(GetFinalBaseColor(input).a); }