// An all in one high-level RendererFeature that contains all NiloToon related passes, user only need to add this RendererFeature in their renderer using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.Serialization; namespace NiloToon.NiloToonURP { [Serializable] public class NiloToonRendererFeatureSettings { [Header("Outline settings")] public NiloToonToonOutlinePass.Settings outlineSettings = new NiloToonToonOutlinePass.Settings(); [Header("Misc settings")] public NiloToonSetToonParamPass.Settings MiscSettings = new NiloToonSetToonParamPass.Settings(); [Header("Average URP shadow map")] public NiloToonAverageShadowTestRTPass.Settings sphereShadowTestSettings = new NiloToonAverageShadowTestRTPass.Settings(); [Header("Anime PostProcess")] public NiloToonAnimePostProcessPass.Settings animePostProcessSettings = new NiloToonAnimePostProcessPass.Settings(); [Header("Bloom & Tonemap")] public NiloToonUberPostProcessPass.Settings uberPostProcessSettings = new NiloToonUberPostProcessPass.Settings(); [Header("Char SelfShadow")] public NiloToonCharSelfShadowMapRTPass.Settings charSelfShadowSettings = new NiloToonCharSelfShadowMapRTPass.Settings(); [Header("Motion Blur")] public NiloToonMotionBlurPass.Settings motionBlurSettings = new NiloToonMotionBlurPass.Settings(); [Header("Prepass Buffer")] [Revertible] public bool forceRenderPrepassBuffer = false; [Header("Override Shader stripping")] [OverrideDisplayName("Shader Stripping Settings")] [Tooltip("This slot is useful when you are in the following situation:\n" + "- Want to reduce build time/build size/runtime shader memory usage\n" + "- Found that a feature is missing in build, and want to include that feature in build\n\n" + "If you haven't configured this slot, NiloToon will default to the platform-specific stripping setting. This may not provide the optimal configuration for your project." + "\n\n" + "To create a custom stripping setting for enhanced shader stripping control, follow these steps:\n" + "1.Right click in the project window\n" + "2.Navigate to 'Create > NiloToon > NiloToonShaderStrippingSettingSO', click it to create a custom stripping setting\n" + "3.Assign that setting to this slot.\n\n" + "Please note:\n" + "* You only need one instance of NiloToonShaderStrippingSettingSO in your project.\n" + "* Ensure that all NiloToon renderer features share the same NiloToonShaderStrippingSettingSO for consistency.")] public NiloToonShaderStrippingSettingSO shaderStrippingSettingSO; } [DisallowMultipleRendererFeature] public class NiloToonAllInOneRendererFeature : ScriptableRendererFeature { public NiloToonRendererFeatureSettings settings = new NiloToonRendererFeatureSettings(); NiloToonSetToonParamPass SetToonParamPass; NiloToonAverageShadowTestRTPass SphereShadowTestRTPass; NiloToonCharSelfShadowMapRTPass CharSelfShadowMapRTRenderPass; NiloToonDrawSkyboxPass SkyboxRedrawBeforeOpaquePass; NiloToonToonOutlinePass ToonOutlinePass; NiloToonToonOutlinePass ToonOutlinePass_RightAfterTransparent; NiloToonScreenSpaceOutlinePass ScreenSpaceOutlinePass; NiloToonExtraThickOutlinePass ExtraThickOutlinePass; NiloToonAnimePostProcessPass AnimePostProcessPass; NiloToonPrepassBufferRTPass PrepassBufferRTPass; NiloToonUberPostProcessPass UberPostProcessPass; #if UNITY_2022_3_OR_NEWER NiloToonMotionBlurPass MotionBlurPass; #endif // This method is used to initialize the ScriptableRenderPass and any required resources. // Unity calls this method in OnEnable/OnValidate - // so every time the project loads, enter/exit play mode, scripts recompile, or serialisation changes. // You should create all passes and set their renderPassEvent inside Create() public override void Create() { // this can only ensure the last used(Created) NiloToonAllInOneRendererFeature can be accessed by NiloToonAllInOneRendererFeature.Instance // (safe to use when :1 Universal Renderer asset per 1 quality setting) // (unsafe to use when :N Universal Renderer asset per 1 quality setting) // TODO: we should remove Singleton of ScriptableRendererFeature/ScriptableRenderPass completely, since it is not a safe design for ScriptableRendererFeature/ScriptableRenderPass due to URP's multi-renderer per URP asset design // TODO: how about we turn Instance(Singleton) to Instances? where "Instances" is a list containing all Created and not yet deleted instances? _instance = this; ReInitPassesIfNeeded(); } private void ReInitPassesIfNeeded() { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Create all passes //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (SetToonParamPass == null) SetToonParamPass = new NiloToonSetToonParamPass(settings); if (SphereShadowTestRTPass == null) SphereShadowTestRTPass = new NiloToonAverageShadowTestRTPass(settings); if (CharSelfShadowMapRTRenderPass == null) CharSelfShadowMapRTRenderPass = new NiloToonCharSelfShadowMapRTPass(settings); if (SkyboxRedrawBeforeOpaquePass == null) SkyboxRedrawBeforeOpaquePass = new NiloToonDrawSkyboxPass(RenderPassEvent.BeforeRenderingOpaques); // RenderQueueRange.opaque = render queue(0-2500) materials if (ToonOutlinePass == null) ToonOutlinePass = new NiloToonToonOutlinePass(settings, RenderQueueRange.opaque, "NiloToonToonOutlinePass(Classic outline - OpaqueQueue)"); // RenderQueueRange.transparent = render queue(2501-5000) materials if (ToonOutlinePass_RightAfterTransparent == null) ToonOutlinePass_RightAfterTransparent = new NiloToonToonOutlinePass(settings, RenderQueueRange.transparent, "NiloToonToonOutlinePass(Classic outline - TransparentQueue)"); if (ScreenSpaceOutlinePass == null) ScreenSpaceOutlinePass = new NiloToonScreenSpaceOutlinePass(settings); if (ExtraThickOutlinePass == null) ExtraThickOutlinePass = new NiloToonExtraThickOutlinePass(settings); if (AnimePostProcessPass == null) AnimePostProcessPass = new NiloToonAnimePostProcessPass(settings); if (PrepassBufferRTPass == null) PrepassBufferRTPass = new NiloToonPrepassBufferRTPass(settings); if (UberPostProcessPass == null) UberPostProcessPass = new NiloToonUberPostProcessPass(settings); #if UNITY_2022_3_OR_NEWER if (MotionBlurPass == null) MotionBlurPass = new NiloToonMotionBlurPass(settings); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Configures where the render pass should be injected. // sorted by RenderPassEvent order here // (*Be aware that camera matrices and stereo rendering is not set up until the BeforeRenderingPrePasses event (value of 150)) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // this pass must be in front of all other NiloToon passes // DO NOT set this at AfterRenderingPrePasses or later! SetToonParamPass.renderPassEvent = RenderPassEvent.BeforeRenderingPrePasses; // require after _CameraDepthTexture is ready, since character shader's NiloToonPrepassBuffer pass needs _CameraDepthTexture. (AfterRenderingPrePasses is still too early because _CameraDepthTexture is not ready at this timing) PrepassBufferRTPass.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques - 2; SphereShadowTestRTPass.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques - 1; CharSelfShadowMapRTRenderPass.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques - 1; ScreenSpaceOutlinePass.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques - 1; // use BeforeRenderingOpaques because we need depth and normal texture, URP's SSAO is BeforeRenderingOpaques also // SkyboxRedrawBeforeOpaquePass's renderPassEvent is BeforeRenderingOpaques - 0 (defined when calling the pass's constructor, so we didn't write it here) // ...(X) // [The chart below shows all NiloToon's skybox and outline draw timing & order] //--------------------------------------- // BeforeRenderingOpaques (= Before RenderQueue 0) -> NiloToon's SkyboxRedrawBeforeOpaquePass // URP RenderingOpaques, draw RenderQueue 0~2500's color pass "UniversalForwardOnly" // AfterRenderingOpaque (= After RenderQueue 2500) -> (X) //--------------------------------------- // BeforeRenderingSkybox (= Before Skybox) -> (X) // URP draw skybox // (ToonOutlinePass) AfterRenderingSkybox (= After Skybox) //--------------------------------------- // (X) Before Transparent (= before RenderQueue 2501) // URP draw RenderQueue 2501~5000's color pass "UniversalForwardOnly" // (ToonOutlinePass_RightAfterTransparent) After Transparent (= After RenderQueue 5000) //--------------------------------------- ToonOutlinePass.renderPassEvent = RenderPassEvent.AfterRenderingSkybox; // use AfterRenderingSkybox instead of BeforeRenderingSkybox, to make "semi-transparent(ZWrite) + outline" blend with skybox correctly ToonOutlinePass_RightAfterTransparent.renderPassEvent = RenderPassEvent.AfterRenderingTransparents + 0; // right after transparent materials finish drawing, draw this outline pass ExtraThickOutlinePass.renderPassEvent = settings.outlineSettings.extraThickOutlineRenderTiming; // default use AfterRenderingTransparents, because we want this outline not being blocked by transparent effects // AnimePostProcessPass's renderPassEvent will be decided by the pass itself UberPostProcessPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing + UberPostProcessPass.settings.renderPassEventTimingOffset; } // Here you can inject one or multiple render passes in the renderer. // This method is called when setting up the renderer once per-camera. // *be aware this is called every frame/update, so avoid creating/instantiating anything in here (can use the Create method for that). public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { // a fix to any possible missing/null pass ReInitPassesIfNeeded(); //---------------------------------------------------------------- // temp fix for supporting SetToonParamPass+RenderGraph // since we can't call ConfigureInput() inside RecordRenderGraph() //---------------------------------------------------------------- SetToonParamPass.ConfigureInputs(renderer); ScreenSpaceOutlinePass.ConfigureInput(renderingData.cameraData.cameraType); #if UNITY_2022_3_OR_NEWER MotionBlurPass.ConfigureInputs(renderingData.cameraData.camera); #endif // Also note that by default it would enqueue the pass for all cameras - // including ones used by the Unity Editor. // In order to avoid this, we can test the camera type and return before enqueueing // (or can check later during Execute to prevent that function running, if you prefer). // Example code: //if (renderingData.cameraData.isPreviewCamera) return; //if (renderingData.cameraData.isSceneViewCamera) return; #if !UNITY_2022_2_OR_NEWER RenderTextureDescriptor cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor; RenderTargetHandle cameraTarget = new RenderTargetHandle(); cameraTarget.Init("_CameraColorAttachmentA"); UberPostProcessPass.Setup(cameraTargetDescriptor, cameraTarget); #endif // the order of Enqueue matters if they have the same renderPassEvent renderer.EnqueuePass(SetToonParamPass); renderer.EnqueuePass(SphereShadowTestRTPass); renderer.EnqueuePass(CharSelfShadowMapRTRenderPass); renderer.EnqueuePass(ToonOutlinePass); renderer.EnqueuePass(ToonOutlinePass_RightAfterTransparent); renderer.EnqueuePass(ScreenSpaceOutlinePass); renderer.EnqueuePass(ExtraThickOutlinePass); renderer.EnqueuePass(AnimePostProcessPass); if (settings.MiscSettings.EnableSkyboxDrawBeforeOpaque && renderingData.cameraData.camera.clearFlags == CameraClearFlags.Skybox) renderer.EnqueuePass(SkyboxRedrawBeforeOpaquePass); //-------------------------------------------------------------------------- // skip prepass buffer when all condition meet: // - no NiloToonBloom // - no NiloToonTonemapping // - no character is using [Color Fill] feature // - no force render var tonemappingEffect = VolumeManager.instance.stack.GetComponent(); var bloomEffect = VolumeManager.instance.stack.GetComponent(); bool isAnyNiloPostEnabled = false; isAnyNiloPostEnabled |= tonemappingEffect.IsActive(); isAnyNiloPostEnabled |= bloomEffect.IsActive() && settings.uberPostProcessSettings.allowRenderNiloToonBloom; bool isAnyNiloToonPerCharacterScriptRequiresPrepass = false; foreach (var characterRenderController in characterList) { if (!characterRenderController) continue; if (characterRenderController.isActiveAndEnabled && characterRenderController.gameObject.activeInHierarchy && characterRenderController.shouldRenderCharacterAreaColorFill) { isAnyNiloToonPerCharacterScriptRequiresPrepass = true; break; } } bool shouldDrawPrepass = renderingData.cameraData.postProcessEnabled && isAnyNiloPostEnabled; shouldDrawPrepass |= isAnyNiloToonPerCharacterScriptRequiresPrepass; shouldDrawPrepass |= settings.forceRenderPrepassBuffer; if (shouldDrawPrepass) { renderer.EnqueuePass(PrepassBufferRTPass); } //-------------------------------------------------------------------------- renderer.EnqueuePass(UberPostProcessPass); #if UNITY_2022_3_OR_NEWER if (renderingData.cameraData.postProcessEnabled && renderingData.cameraData.cameraType == CameraType.Game) // only apply to game window { renderer.EnqueuePass(MotionBlurPass); } #endif } #if !UNITY_6000_4_OR_NEWER // TODO: it seems not correct to remove UberPostProcessPass.Setup(...) in Unity6.4? #if UNITY_2022_2_OR_NEWER // If you want to pass cameraColorTargetHandle or cameraDepthTargetHandle RTHandle into a pass // You must do it here instead of AddRenderPasses(...), since these RTHandle are not yet init in AddRenderPasses(...)! // see https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/upgrade-guide-2022-2.html public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) { UberPostProcessPass.Setup(renderingData.cameraData.cameraTargetDescriptor); } #endif #endif #if UNITY_2022_2_OR_NEWER // The method can be useful for releasing any resources that have been allocated. // In editor, the method is called when removing features, recompiling scripts, entering/exiting play mode. // (Not too sure when it gets called in builds, probably when changing scenes?) // You should call Dispose() of every pass here to avoid memory leak // Usually each pass's Dispose() will call: // - RTHandle?.Release(); // - Destroy materials/textures created protected override void Dispose(bool disposing) { // call Dispose to passes that alloc RT PrepassBufferRTPass?.Dispose(); CharSelfShadowMapRTRenderPass?.Dispose(); UberPostProcessPass?.Dispose(); SphereShadowTestRTPass?.Dispose(); MotionBlurPass?.Dispose(); // null all passes SetToonParamPass = null; SphereShadowTestRTPass = null; CharSelfShadowMapRTRenderPass = null; SkyboxRedrawBeforeOpaquePass = null; ToonOutlinePass = null; ToonOutlinePass_RightAfterTransparent = null; ScreenSpaceOutlinePass = null; ExtraThickOutlinePass = null; AnimePostProcessPass = null; PrepassBufferRTPass = null; UberPostProcessPass = null; MotionBlurPass = null; } #endif #region [Singleton to store list of active char (no matter visible by camera or not)] public NiloToonAllInOneRendererFeature() { CheckInit(); } // TODO: should we enable this? //[Obsolete("We may remove Singleton(.Instance) API from all NiloToon ScriptableRendererFeature/ScriptableRenderPass in the future, since multiple Universal Renderer(s) can be added to 1 URP asset, Singleton is not possible.", false)] public static NiloToonAllInOneRendererFeature Instance { get => _instance; } static NiloToonAllInOneRendererFeature _instance; public static List characterList; public static HashSet characterHashSet; public static void AddCharIfNotExist(NiloToonPerCharacterRenderController controller) { CheckInit(); // optimize .Contains() call, now use HashSet instead of List: https://stackoverflow.com/questions/823860/listt-contains-is-very-slow if (!characterHashSet.Contains(controller)) { characterHashSet.Add(controller); characterList.Add(controller); UpdateCharacterControllerIndex(); } } public static void Remove(NiloToonPerCharacterRenderController controller) { CheckInit(); // optimize .Contains() call, now use HashSet instead of List: https://stackoverflow.com/questions/823860/listt-contains-is-very-slow if (characterHashSet.Contains(controller)) { characterHashSet.Remove(controller); characterList.Remove(controller); UpdateCharacterControllerIndex(); } } static void UpdateCharacterControllerIndex() { for (int i = 0; i < characterList.Count; i++) { var c = characterList[i]; if (!c) continue; c.characterID = i; } } internal static void CheckInit() { if (characterList == null) characterList = new List(); // for UpdateCharacterControllerIndex() if (characterHashSet == null) characterHashSet = new HashSet(); // for .Contains() checks } #endregion } }