diff --git a/Assets/NiloToonURP/Editor/ShaderGUI/NiloToonCharacterFurShaderGUI.cs b/Assets/NiloToonURP/Editor/ShaderGUI/NiloToonCharacterFurShaderGUI.cs new file mode 100644 index 00000000..6772beed --- /dev/null +++ b/Assets/NiloToonURP/Editor/ShaderGUI/NiloToonCharacterFurShaderGUI.cs @@ -0,0 +1,298 @@ +// NiloToon Character Fur Shader - Custom ShaderGUI +// Clean, organized material inspector with foldable sections + +using UnityEngine; +using UnityEditor; +using System; + +namespace NiloToonURP.ShaderGUI +{ + public class NiloToonCharacterFurShaderGUI : UnityEditor.ShaderGUI + { + // Foldout states + private static bool showBaseColor = true; + private static bool showNormalMap = false; + private static bool showFurShape = true; + private static bool showFurRim = false; + private static bool showCelShading = true; + private static bool showShadowColor = false; + private static bool showMatCap = false; + private static bool showRimLight = false; + private static bool showEmission = false; + private static bool showOcclusion = false; + private static bool showOutline = false; + private static bool showRendering = false; + + // Styles + private static GUIStyle headerStyle; + private static GUIStyle foldoutStyle; + private static bool stylesInitialized = false; + + private static void InitStyles() + { + if (stylesInitialized) return; + + headerStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 12, + margin = new RectOffset(0, 0, 10, 5) + }; + + foldoutStyle = new GUIStyle(EditorStyles.foldout) + { + fontStyle = FontStyle.Bold, + fontSize = 11 + }; + + stylesInitialized = true; + } + + public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) + { + InitStyles(); + + Material material = materialEditor.target as Material; + + EditorGUILayout.Space(5); + + // Header + EditorGUILayout.LabelField("NiloToon Character Fur Shader", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "Single-material fur shader with Base + Fur Shell rendering.\n\n" + + "Setup:\n" + + "1. Add 'NiloToonFurRendererFeature' to your URP Renderer Asset\n" + + "2. Apply this shader to your mesh material (single material slot)\n\n" + + "The shader will render base mesh first, then fur shells on top.", + MessageType.Info); + DrawSeparator(); + + // Base Color Section + showBaseColor = DrawSection("Base Color", showBaseColor, () => + { + DrawProperty(materialEditor, properties, "_BaseMap", "Base Map"); + DrawProperty(materialEditor, properties, "_BaseColor", "Base Color"); + DrawProperty(materialEditor, properties, "_Cutoff", "Alpha Cutoff"); + }); + + // Normal Map Section + showNormalMap = DrawSection("Normal Map", showNormalMap, () => + { + DrawToggleProperty(materialEditor, properties, "_UseNormalMap", "Enable Normal Map", "_NORMALMAP", material); + if (material.IsKeywordEnabled("_NORMALMAP")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_BumpMap", "Normal Map"); + DrawProperty(materialEditor, properties, "_BumpScale", "Normal Scale"); + EditorGUI.indentLevel--; + } + }); + + // Fur Shape Section + showFurShape = DrawSection("Fur Shape", showFurShape, () => + { + DrawProperty(materialEditor, properties, "_FurNoiseMask", "Fur Noise Mask"); + DrawProperty(materialEditor, properties, "_FurMask", "Fur Mask"); + DrawProperty(materialEditor, properties, "_FurLengthMask", "Fur Length Mask"); + EditorGUILayout.Space(5); + DrawProperty(materialEditor, properties, "_FurVector", "Fur Direction (XYZ) + Length (W)"); + DrawProperty(materialEditor, properties, "_FurVectorTex", "Fur Direction Map"); + DrawProperty(materialEditor, properties, "_FurVectorScale", "Fur Direction Scale"); + EditorGUILayout.Space(5); + DrawProperty(materialEditor, properties, "_FurGravity", "Fur Gravity"); + DrawProperty(materialEditor, properties, "_FurRandomize", "Fur Randomize"); + DrawProperty(materialEditor, properties, "_FurAO", "Fur Ambient Occlusion"); + EditorGUILayout.Space(5); + DrawProperty(materialEditor, properties, "_FurLayerNum", "Fur Layer Count"); + DrawProperty(materialEditor, properties, "_FurRootOffset", "Fur Root Offset"); + }); + + // Fur Rim Section + showFurRim = DrawSection("Fur Rim Light", showFurRim, () => + { + DrawProperty(materialEditor, properties, "_FurRimColor", "Fur Rim Color"); + DrawProperty(materialEditor, properties, "_FurRimFresnelPower", "Fur Rim Fresnel Power"); + DrawProperty(materialEditor, properties, "_FurRimAntiLight", "Fur Rim Anti-Light"); + }); + + // Cel Shading Section + showCelShading = DrawSection("Cel Shading", showCelShading, () => + { + DrawProperty(materialEditor, properties, "_CelShadeMidPoint", "Cel Shade Mid Point"); + DrawProperty(materialEditor, properties, "_CelShadeSoftness", "Cel Shade Softness"); + }); + + // Shadow Color Section + showShadowColor = DrawSection("Shadow Color", showShadowColor, () => + { + DrawToggleProperty(materialEditor, properties, "_EnableShadowColor", "Enable Shadow Color", "_SHADOW_COLOR", material); + if (material.IsKeywordEnabled("_SHADOW_COLOR")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_ShadowColor", "Shadow Tint Color"); + DrawProperty(materialEditor, properties, "_ShadowBrightness", "Shadow Brightness"); + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("HSV Adjustment", EditorStyles.miniLabel); + DrawProperty(materialEditor, properties, "_ShadowHueShift", "Hue Shift"); + DrawProperty(materialEditor, properties, "_ShadowSaturationBoost", "Saturation Boost"); + DrawProperty(materialEditor, properties, "_ShadowValueMultiplier", "Value Multiplier"); + EditorGUI.indentLevel--; + } + }); + + // MatCap Section + showMatCap = DrawSection("MatCap", showMatCap, () => + { + EditorGUILayout.LabelField("MatCap Additive", EditorStyles.miniLabel); + DrawToggleProperty(materialEditor, properties, "_UseMatCapAdd", "Enable MatCap (Add)", "_MATCAP_ADD", material); + if (material.IsKeywordEnabled("_MATCAP_ADD")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_MatCapAddMap", "MatCap Add Map"); + DrawProperty(materialEditor, properties, "_MatCapAddColor", "MatCap Add Color"); + DrawProperty(materialEditor, properties, "_MatCapAddIntensity", "MatCap Add Intensity"); + DrawProperty(materialEditor, properties, "_MatCapAddMask", "MatCap Add Mask"); + EditorGUI.indentLevel--; + } + + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("MatCap Multiply", EditorStyles.miniLabel); + DrawToggleProperty(materialEditor, properties, "_UseMatCapMul", "Enable MatCap (Multiply)", "_MATCAP_MUL", material); + if (material.IsKeywordEnabled("_MATCAP_MUL")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_MatCapMulMap", "MatCap Multiply Map"); + DrawProperty(materialEditor, properties, "_MatCapMulIntensity", "MatCap Multiply Intensity"); + EditorGUI.indentLevel--; + } + }); + + // Rim Light Section + showRimLight = DrawSection("Rim Light (General)", showRimLight, () => + { + DrawToggleProperty(materialEditor, properties, "_UseRimLight", "Enable Rim Light", "_RIMLIGHT", material); + if (material.IsKeywordEnabled("_RIMLIGHT")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_RimLightColor", "Rim Light Color"); + DrawProperty(materialEditor, properties, "_RimLightPower", "Rim Light Power"); + DrawProperty(materialEditor, properties, "_RimLightIntensity", "Rim Light Intensity"); + EditorGUI.indentLevel--; + } + }); + + // Emission Section + showEmission = DrawSection("Emission", showEmission, () => + { + DrawToggleProperty(materialEditor, properties, "_UseEmission", "Enable Emission", "_EMISSION", material); + if (material.IsKeywordEnabled("_EMISSION")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_EmissionMap", "Emission Map"); + DrawProperty(materialEditor, properties, "_EmissionColor", "Emission Color"); + DrawProperty(materialEditor, properties, "_EmissionIntensity", "Emission Intensity"); + EditorGUI.indentLevel--; + } + }); + + // Occlusion Section + showOcclusion = DrawSection("Occlusion", showOcclusion, () => + { + DrawToggleProperty(materialEditor, properties, "_UseOcclusion", "Enable Occlusion Map", "_OCCLUSIONMAP", material); + if (material.IsKeywordEnabled("_OCCLUSIONMAP")) + { + EditorGUI.indentLevel++; + DrawProperty(materialEditor, properties, "_OcclusionMap", "Occlusion Map"); + DrawProperty(materialEditor, properties, "_OcclusionStrength", "Occlusion Strength"); + EditorGUI.indentLevel--; + } + }); + + // Outline Section + showOutline = DrawSection("Outline", showOutline, () => + { + DrawProperty(materialEditor, properties, "_OutlineWidth", "Outline Width"); + DrawProperty(materialEditor, properties, "_OutlineColor", "Outline Color"); + DrawProperty(materialEditor, properties, "_OutlineWidthMask", "Outline Width Mask"); + }); + + // Rendering Options Section + showRendering = DrawSection("Rendering Options", showRendering, () => + { + DrawProperty(materialEditor, properties, "_Cull", "Cull Mode"); + DrawProperty(materialEditor, properties, "_FurCull", "Fur Cull Mode"); + DrawProperty(materialEditor, properties, "_ZWrite", "ZWrite"); + DrawProperty(materialEditor, properties, "_FurZWrite", "Fur ZWrite"); + DrawProperty(materialEditor, properties, "_ZTest", "ZTest"); + }); + + EditorGUILayout.Space(10); + + // Render Queue + materialEditor.RenderQueueField(); + materialEditor.EnableInstancingField(); + materialEditor.DoubleSidedGIField(); + } + + private bool DrawSection(string title, bool foldout, Action drawContent) + { + EditorGUILayout.Space(2); + + // Section header with foldout + Rect headerRect = EditorGUILayout.GetControlRect(false, 20); + headerRect.x -= 2; + headerRect.width += 4; + + // Background + EditorGUI.DrawRect(headerRect, new Color(0.22f, 0.22f, 0.22f, 1f)); + + // Foldout + headerRect.x += 4; + foldout = EditorGUI.Foldout(headerRect, foldout, title, true, foldoutStyle); + + if (foldout) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUI.indentLevel++; + drawContent?.Invoke(); + EditorGUI.indentLevel--; + EditorGUILayout.EndVertical(); + } + + return foldout; + } + + private void DrawProperty(MaterialEditor editor, MaterialProperty[] properties, string propertyName, string label) + { + MaterialProperty prop = FindProperty(propertyName, properties, false); + if (prop != null) + { + editor.ShaderProperty(prop, label); + } + } + + private void DrawToggleProperty(MaterialEditor editor, MaterialProperty[] properties, string propertyName, string label, string keyword, Material material) + { + MaterialProperty prop = FindProperty(propertyName, properties, false); + if (prop != null) + { + EditorGUI.BeginChangeCheck(); + editor.ShaderProperty(prop, label); + if (EditorGUI.EndChangeCheck()) + { + if (prop.floatValue > 0.5f) + material.EnableKeyword(keyword); + else + material.DisableKeyword(keyword); + } + } + } + + private void DrawSeparator() + { + EditorGUILayout.Space(5); + Rect rect = EditorGUILayout.GetControlRect(false, 1); + EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.5f)); + EditorGUILayout.Space(5); + } + } +} diff --git a/Assets/NiloToonURP/Editor/ShaderGUI/NiloToonCharacterFurShaderGUI.cs.meta b/Assets/NiloToonURP/Editor/ShaderGUI/NiloToonCharacterFurShaderGUI.cs.meta new file mode 100644 index 00000000..74130d15 --- /dev/null +++ b/Assets/NiloToonURP/Editor/ShaderGUI/NiloToonCharacterFurShaderGUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3ad6027c1c29b71488586df1e8150fa7 \ No newline at end of file diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/NiloToonFurRendererFeature.cs b/Assets/NiloToonURP/Runtime/RendererFeatures/NiloToonFurRendererFeature.cs new file mode 100644 index 00000000..30de94e8 --- /dev/null +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/NiloToonFurRendererFeature.cs @@ -0,0 +1,60 @@ +// NiloToon Fur Renderer Feature +// Add this to your URP Renderer to enable fur shell rendering +// This allows single-material fur shaders to work properly +// +// Note: Fur mask rendering is now integrated into NiloToonPrepassBufferRTPass +// The fur mask is automatically rendered to G and B channels of _NiloToonPrepassBufferTex + +using System; +using UnityEngine; +using UnityEngine.Rendering.Universal; + +namespace NiloToon.NiloToonURP +{ + [Serializable] + public class NiloToonFurRendererFeatureSettings + { + [Header("Fur Shell Settings")] + public NiloToonFurShellPass.Settings furShellSettings = new NiloToonFurShellPass.Settings(); + } + + public class NiloToonFurRendererFeature : ScriptableRendererFeature + { + public NiloToonFurRendererFeatureSettings settings = new NiloToonFurRendererFeatureSettings(); + + NiloToonFurShellPass FurShellPass; + + public override void Create() + { + ReInitPassesIfNeeded(); + } + + private void ReInitPassesIfNeeded() + { + if (FurShellPass == null) + FurShellPass = new NiloToonFurShellPass(settings.furShellSettings); + + // Set render pass events + FurShellPass.renderPassEvent = settings.furShellSettings.renderTiming; + } + + public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) + { + ReInitPassesIfNeeded(); + + // Update render timing in case it changed + FurShellPass.renderPassEvent = settings.furShellSettings.renderTiming; + + // Enqueue fur shell pass for actual fur rendering + // Note: Fur mask is rendered via NiloToonPrepassBufferRTPass (NiloToonFurShellMask LightMode) + renderer.EnqueuePass(FurShellPass); + } + +#if UNITY_2022_2_OR_NEWER + protected override void Dispose(bool disposing) + { + FurShellPass = null; + } +#endif + } +} diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/NiloToonFurRendererFeature.cs.meta b/Assets/NiloToonURP/Runtime/RendererFeatures/NiloToonFurRendererFeature.cs.meta new file mode 100644 index 00000000..eb7d4cb1 --- /dev/null +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/NiloToonFurRendererFeature.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4076e5ecd1634a74eaabed924c9a971c \ No newline at end of file diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurMaskPass.cs b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurMaskPass.cs new file mode 100644 index 00000000..e50bf8af --- /dev/null +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurMaskPass.cs @@ -0,0 +1,220 @@ +// NiloToon Fur Mask Pass +// Renders fur shells to a dedicated mask buffer for fur area detection +// Separate from main fur rendering for cleaner architecture +// Based on NiloToonPrepassBufferRTPass pattern + +using System; +using UnityEngine; +using UnityEngine.Experimental.Rendering; +using UnityEngine.Rendering; +using UnityEngine.Rendering.RendererUtils; +#if UNITY_6000_0_OR_NEWER +using UnityEngine.Rendering.RenderGraphModule; +#endif +using UnityEngine.Rendering.Universal; + +namespace NiloToon.NiloToonURP +{ + public class NiloToonFurMaskPass : ScriptableRenderPass + { + static readonly ShaderTagId furShellMaskLightModeShaderTagId = new ShaderTagId("NiloToonFurShellMask"); + static readonly int FurMaskTexId = Shader.PropertyToID("_NiloToonFurMaskTex"); + + [Serializable] + public class Settings + { + [Tooltip("Create a mask buffer for fur area detection.\n" + + "This creates _NiloToonFurMaskTex which can be used for post-processing.\n\nDefault: ON")] + public bool Enabled = true; + } + + Settings settings; + RenderQueueRange renderQueueRange; + +#if UNITY_2022_2_OR_NEWER + RTHandle furMaskRTHColor; + RTHandle furMaskRTHDepth; +#else + RenderTargetHandle furMaskRTHandle; +#endif + + public NiloToonFurMaskPass(Settings settings) + { + this.settings = settings; + this.renderQueueRange = RenderQueueRange.all; + +#if !UNITY_2022_2_OR_NEWER + furMaskRTHandle.Init("_NiloToonFurMaskRT"); +#endif + + base.profilingSampler = new ProfilingSampler(nameof(NiloToonFurMaskPass)); + } + +#if UNITY_6000_0_OR_NEWER + [Obsolete] +#endif + public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) + { + if (!settings.Enabled) + return; + + var cameraData = renderingData.cameraData; + +#if UNITY_2022_2_OR_NEWER + // [color RTHandle's Descriptor] - same pattern as NiloToonPrepassBufferRTPass + var colorDesc = cameraData.cameraTargetDescriptor; + colorDesc.graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm; + colorDesc.depthBufferBits = 0; + colorDesc.msaaSamples = 1; + + // [depth RTHandle's Descriptor] - create own depth buffer like PrepassBufferRTPass + var depthDesc = cameraData.cameraTargetDescriptor; + depthDesc.graphicsFormat = GraphicsFormat.None; // Depth only rendering + depthDesc.depthStencilFormat = cameraData.cameraTargetDescriptor.depthStencilFormat; + depthDesc.msaaSamples = 1; + + RenderingUtils.ReAllocateIfNeeded(ref furMaskRTHColor, colorDesc, name: "_NiloToonFurMaskColor"); + RenderingUtils.ReAllocateIfNeeded(ref furMaskRTHDepth, depthDesc, name: "_NiloToonFurMaskDepth"); + + // Set global RT + cmd.SetGlobalTexture(FurMaskTexId, furMaskRTHColor); + + // Configure target with own color and depth buffers + ConfigureTarget(furMaskRTHColor, furMaskRTHDepth); + ConfigureClear(ClearFlag.All, Color.black); +#else + var furMaskDesc = cameraData.cameraTargetDescriptor; + furMaskDesc.colorFormat = RenderTextureFormat.ARGB32; + furMaskDesc.depthStencilFormat = cameraData.cameraTargetDescriptor.depthStencilFormat; + furMaskDesc.msaaSamples = 1; + + cmd.GetTemporaryRT(furMaskRTHandle.id, furMaskDesc); + cmd.SetGlobalTexture(FurMaskTexId, furMaskRTHandle.Identifier()); + + ConfigureTarget(new RenderTargetIdentifier(furMaskRTHandle.Identifier(), 0, CubemapFace.Unknown, -1)); + ConfigureClear(ClearFlag.All, Color.black); +#endif + } + +#if UNITY_6000_0_OR_NEWER + [Obsolete] +#endif + public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) + { + if (!settings.Enabled) + return; + + // Never draw in Preview + Camera camera = renderingData.cameraData.camera; + if (camera.cameraType == CameraType.Preview) + return; + + var cmd = CommandBufferPool.Get(); + using (new ProfilingScope(cmd, base.profilingSampler)) + { + // Execute command buffer before DrawRenderers (frame debugger requirement) + context.ExecuteCommandBuffer(cmd); + cmd.Clear(); + + // Draw all fur shells with NiloToonFurShellMask pass + var filterSetting = new FilteringSettings(renderQueueRange); + var sortFlags = SortingCriteria.CommonTransparent; + var drawSettings = CreateDrawingSettings(furShellMaskLightModeShaderTagId, ref renderingData, sortFlags); + drawSettings.perObjectData = PerObjectData.None; + context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSetting); + } + + context.ExecuteCommandBuffer(cmd); + cmd.Clear(); + CommandBufferPool.Release(cmd); + } + + public override void OnCameraCleanup(CommandBuffer cmd) + { + // To Release a RTHandle, do it in ScriptableRendererFeature's Dispose(), don't do it in OnCameraCleanup(...) +#if !UNITY_2022_2_OR_NEWER + if (settings.Enabled) + { + cmd.ReleaseTemporaryRT(furMaskRTHandle.id); + } +#endif + } + +#if UNITY_2022_2_OR_NEWER + public void Dispose() + { + furMaskRTHColor?.Release(); + furMaskRTHDepth?.Release(); + } +#endif + +#if UNITY_6000_0_OR_NEWER + private class FurMaskPassData + { + public UniversalCameraData cameraData; + public TextureHandle colorTarget; + public TextureHandle depthTarget; + public RendererListHandle rendererList; + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + if (!settings.Enabled) + return; + + var cameraData = frameData.Get(); + var resourceData = frameData.Get(); + var renderingData = frameData.Get(); + + // Skip Preview cameras - same check as Execute + if (cameraData.camera.cameraType == CameraType.Preview) + return; + + using (var builder = renderGraph.AddRasterRenderPass("NiloToon Fur Mask Buffer", out var passData, base.profilingSampler)) + { + passData.cameraData = cameraData; + + // Create render textures for RG - EXACT same descriptors as OnCameraSetup + var colorDesc = cameraData.cameraTargetDescriptor; + colorDesc.graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm; + colorDesc.depthBufferBits = 0; + colorDesc.msaaSamples = 1; + + var depthDesc = cameraData.cameraTargetDescriptor; + depthDesc.graphicsFormat = GraphicsFormat.None; // Depth only rendering + depthDesc.depthStencilFormat = cameraData.cameraTargetDescriptor.depthStencilFormat; + depthDesc.msaaSamples = 1; + + passData.colorTarget = UniversalRenderer.CreateRenderGraphTexture(renderGraph, colorDesc, "_NiloToonFurMaskColor", false); + passData.depthTarget = UniversalRenderer.CreateRenderGraphTexture(renderGraph, depthDesc, "_NiloToonFurMaskDepth", false); + + // Create renderer list - EXACT same settings as Execute + var renderListDesc = new RendererListDesc(furShellMaskLightModeShaderTagId, renderingData.cullResults, cameraData.camera) + { + sortingCriteria = SortingCriteria.CommonTransparent, + renderQueueRange = renderQueueRange, + }; + passData.rendererList = renderGraph.CreateRendererList(renderListDesc); + + // Set up render targets - Match ConfigureTarget behavior + builder.SetRenderAttachment(passData.colorTarget, 0, AccessFlags.Write); + builder.SetRenderAttachmentDepth(passData.depthTarget, AccessFlags.Write); + + builder.UseRendererList(passData.rendererList); + + // Set global textures - Match cmd.SetGlobalTexture behavior + builder.SetGlobalTextureAfterPass(passData.colorTarget, FurMaskTexId); + + builder.SetRenderFunc((FurMaskPassData data, RasterGraphContext context) => + { + // Match ConfigureClear(ClearFlag.All, Color.black) behavior + context.cmd.ClearRenderTarget(RTClearFlags.All, Color.black, 1.0f, 0); + + // Match context.DrawRenderers behavior + context.cmd.DrawRendererList(data.rendererList); + }); + } + } +#endif + } +} diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurMaskPass.cs.meta b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurMaskPass.cs.meta new file mode 100644 index 00000000..10d24359 --- /dev/null +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurMaskPass.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d398264d435e9314bae19d3e5532f775 \ No newline at end of file diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurShellPass.cs b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurShellPass.cs new file mode 100644 index 00000000..bde5ca1f --- /dev/null +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurShellPass.cs @@ -0,0 +1,119 @@ +// NiloToon Fur Shell Pass +// Renders fur shell pass for materials with "NiloToonFurShell" LightMode tag +// Only handles fur rendering - mask buffer is handled by NiloToonFurMaskPass + +using System; +using UnityEngine; +using UnityEngine.Rendering; +#if UNITY_6000_0_OR_NEWER +using UnityEngine.Rendering.RenderGraphModule; +#endif +using UnityEngine.Rendering.Universal; + +namespace NiloToon.NiloToonURP +{ + public class NiloToonFurShellPass : ScriptableRenderPass + { + static readonly ShaderTagId furShellLightModeShaderTagId = new ShaderTagId("NiloToonFurShell"); + + [Serializable] + public class Settings + { + [Header("Fur Shell Settings")] + + [Tooltip("Enable to render fur shells.\n\nDefault: ON")] + [OverrideDisplayName("Enable?")] + [Revertible] + public bool ShouldRenderFurShell = true; + + [Tooltip("When to render fur shells in the rendering pipeline.\n\nDefault: AfterRenderingOpaques")] + [Revertible] + [OverrideDisplayName("Render Timing")] + public RenderPassEvent renderTiming = RenderPassEvent.AfterRenderingOpaques; + } + + Settings settings; + ProfilingSampler m_ProfilingSampler; + RenderQueueRange renderQueueRange; + + public NiloToonFurShellPass(Settings settings) + { + this.settings = settings; + m_ProfilingSampler = new ProfilingSampler("NiloToonFurShellPass"); + this.renderQueueRange = RenderQueueRange.all; + } + +#if UNITY_6000_0_OR_NEWER + [Obsolete] +#endif + public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) + { + if (!settings.ShouldRenderFurShell) + return; + + CommandBuffer cmd = CommandBufferPool.Get(); + + using (new ProfilingScope(cmd, m_ProfilingSampler)) + { + context.ExecuteCommandBuffer(cmd); + cmd.Clear(); + + DrawingSettings drawingSettings = CreateDrawingSettings( + furShellLightModeShaderTagId, + ref renderingData, + SortingCriteria.CommonTransparent + ); + + FilteringSettings filteringSettings = new FilteringSettings(renderQueueRange); + context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings); + } + + context.ExecuteCommandBuffer(cmd); + cmd.Clear(); + CommandBufferPool.Release(cmd); + } + +#if UNITY_6000_0_OR_NEWER + private class PassData + { + public RendererListHandle rendererListHandle; + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + if (!settings.ShouldRenderFurShell) + return; + + UniversalRenderingData renderingData = frameData.Get(); + UniversalCameraData cameraData = frameData.Get(); + UniversalLightData lightData = frameData.Get(); + UniversalResourceData resourceData = frameData.Get(); + + using (var builder = renderGraph.AddRasterRenderPass("NiloToonFurShellPass", out var passData, m_ProfilingSampler)) + { + var drawingSettings = RenderingUtils.CreateDrawingSettings( + furShellLightModeShaderTagId, + renderingData, + cameraData, + lightData, + SortingCriteria.CommonTransparent + ); + + var filteringSettings = new FilteringSettings(renderQueueRange); + var renderListParams = new RendererListParams(renderingData.cullResults, drawingSettings, filteringSettings); + passData.rendererListHandle = renderGraph.CreateRendererList(renderListParams); + + builder.UseRendererList(passData.rendererListHandle); + + builder.SetRenderAttachment(resourceData.activeColorTexture, 0); + builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture); + + builder.SetRenderFunc((PassData data, RasterGraphContext rgContext) => + { + rgContext.cmd.DrawRendererList(data.rendererListHandle); + }); + } + } +#endif + } +} diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurShellPass.cs.meta b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurShellPass.cs.meta new file mode 100644 index 00000000..4954c697 --- /dev/null +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonFurShellPass.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6edb03bf5b2c68d4dbc8b39fac860935 \ No newline at end of file diff --git a/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonPrepassBufferRTPass.cs b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonPrepassBufferRTPass.cs index 42e8359e..9a0581a5 100644 --- a/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonPrepassBufferRTPass.cs +++ b/Assets/NiloToonURP/Runtime/RendererFeatures/Passes/NiloToonPrepassBufferRTPass.cs @@ -5,12 +5,14 @@ /* _NiloToonPrepassBufferRT is storing the following data --r: face --g: character visible area (for NiloToon Bloom / NiloToon Tonemapping) +-r: face (legacy, kept for compatibility) +-g: character visible area (for NiloToon Bloom / NiloToon Tonemapping) - includes face, body, and fur -b: unused -a: unused * We may draw IsFace,IsSkin,RimArea into this RT in future versions +* Fur mask is now integrated: renders fur shells to G channel +* All character areas (face, body, fur) use G channel for unified character mask */ using System; using UnityEngine; @@ -38,6 +40,10 @@ namespace NiloToon.NiloToonURP // Constants static readonly ShaderTagId shaderTagId = new ShaderTagId("NiloToonPrepassBuffer"); + static readonly ShaderTagId furShellMaskShaderTagId = new ShaderTagId("NiloToonFurShellMask"); + + // Fur mask settings + bool enableFurMask = true; // Constructor(will not call every frame) public NiloToonPrepassBufferRTPass(NiloToonRendererFeatureSettings allSettings) @@ -50,6 +56,14 @@ namespace NiloToon.NiloToonURP base.profilingSampler = new ProfilingSampler(nameof(NiloToonPrepassBufferRTPass)); } + + /// + /// Enable or disable fur mask rendering + /// + public void SetFurMaskEnabled(bool enabled) + { + enableFurMask = enabled; + } // This method is called before executing the render pass. // It can be used to configure render targets and their clear state. Also to create temporary render target textures. @@ -149,16 +163,27 @@ namespace NiloToon.NiloToonURP cmd.Clear(); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // draw all nilotoon character renderer's "NiloToonPrepassBuffer" pass using SRP batching + // 1. Draw all nilotoon character renderer's "NiloToonPrepassBuffer" pass using SRP batching //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - var filterSetting = new FilteringSettings(RenderQueueRange.opaque); //TODO: should include transparent also? + var filterSetting = new FilteringSettings(RenderQueueRange.opaque); //TODO: should include transparent also? var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags; var drawSettings = CreateDrawingSettings(shaderTagId, ref renderingData, sortFlags); drawSettings.perObjectData = PerObjectData.None; context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSetting); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 2. Draw fur shell mask (NiloToonFurShellMask pass) - adds fur area to G and B channels + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (enableFurMask) + { + var furFilterSetting = new FilteringSettings(RenderQueueRange.all); + var furDrawSettings = CreateDrawingSettings(furShellMaskShaderTagId, ref renderingData, SortingCriteria.CommonTransparent); + furDrawSettings.perObjectData = PerObjectData.None; + context.DrawRenderers(renderingData.cullResults, ref furDrawSettings, ref furFilterSetting); + } } - + // must write these line after using{} finished, to ensure profiler and frame debugger display correctness context.ExecuteCommandBuffer(cmd); cmd.Clear(); @@ -187,13 +212,15 @@ namespace NiloToon.NiloToonURP ///////////////////////////////////////////////////////////////////// // RG support ///////////////////////////////////////////////////////////////////// -#if UNITY_6000_0_OR_NEWER +#if UNITY_6000_0_OR_NEWER private class PrepassBufferPassData { public UniversalCameraData cameraData; public TextureHandle colorTarget; public TextureHandle depthTarget; public RendererListHandle rendererList; + public RendererListHandle furRendererList; + public bool enableFurMask; } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameContext) @@ -209,6 +236,7 @@ namespace NiloToon.NiloToonURP using (var builder = renderGraph.AddRasterRenderPass("NiloToon Prepass Buffer", out var passData, base.profilingSampler)) { passData.cameraData = cameraData; + passData.enableFurMask = enableFurMask; // Create render textures for RG - EXACT same descriptors as OnCameraSetup var colorDesc = cameraData.cameraTargetDescriptor; @@ -224,19 +252,28 @@ namespace NiloToon.NiloToonURP passData.colorTarget = UniversalRenderer.CreateRenderGraphTexture(renderGraph, colorDesc, "_NiloToonPrepassBufferColor", false); passData.depthTarget = UniversalRenderer.CreateRenderGraphTexture(renderGraph, depthDesc, "_NiloToonPrepassBufferDepth", false); - // Create renderer list - EXACT same settings as Execute + // Create renderer list for character prepass - EXACT same settings as Execute var renderListDesc = new RendererListDesc(shaderTagId, renderingData.cullResults, cameraData.camera) { - sortingCriteria = cameraData.defaultOpaqueSortFlags, // Match: var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags; - renderQueueRange = RenderQueueRange.opaque, // Match: var filterSetting = new FilteringSettings(RenderQueueRange.opaque); + sortingCriteria = cameraData.defaultOpaqueSortFlags, + renderQueueRange = RenderQueueRange.opaque, }; passData.rendererList = renderGraph.CreateRendererList(renderListDesc); + // Create renderer list for fur shell mask + var furRenderListDesc = new RendererListDesc(furShellMaskShaderTagId, renderingData.cullResults, cameraData.camera) + { + sortingCriteria = SortingCriteria.CommonTransparent, + renderQueueRange = RenderQueueRange.all, + }; + passData.furRendererList = renderGraph.CreateRendererList(furRenderListDesc); + // Set up render targets - Match ConfigureTarget behavior builder.SetRenderAttachment(passData.colorTarget, 0, AccessFlags.Write); builder.SetRenderAttachmentDepth(passData.depthTarget, AccessFlags.Write); builder.UseRendererList(passData.rendererList); + builder.UseRendererList(passData.furRendererList); // Set global textures - Match cmd.SetGlobalTexture behavior builder.SetGlobalTextureAfterPass(passData.colorTarget, Shader.PropertyToID("_NiloToonPrepassBufferTex")); @@ -246,9 +283,15 @@ namespace NiloToon.NiloToonURP { // Match ConfigureClear(ClearFlag.All, Color.black) behavior context.cmd.ClearRenderTarget(RTClearFlags.All, Color.black, 1.0f, 0); - - // Match context.DrawRenderers behavior + + // 1. Draw character prepass context.cmd.DrawRendererList(data.rendererList); + + // 2. Draw fur shell mask (adds fur area to G and B channels) + if (data.enableFurMask) + { + context.cmd.DrawRendererList(data.furRendererList); + } }); } } diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur.shader b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur.shader new file mode 100644 index 00000000..cd79c632 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur.shader @@ -0,0 +1,566 @@ +// NiloToon Character Fur Shader +// Single-material fur shader with Base + Fur Shell rendering +// Requires NiloToonFurRendererFeature to be added to URP Renderer +// +// Usage: +// 1. Add NiloToonFurRendererFeature to your URP Renderer Asset +// 2. Apply this shader to your mesh material (single material slot) + +Shader "Universal Render Pipeline/NiloToon/NiloToon_Character_Fur" +{ + Properties + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Base Color + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Base Color)] + [Space(5)] + [MainTexture] _BaseMap("Base Map", 2D) = "white" {} + [HDR][MainColor] _BaseColor("Base Color", Color) = (1,1,1,1) + _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Normal Map + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Normal Map)] + [Space(5)] + [Toggle(_NORMALMAP)] _UseNormalMap("Enable Normal Map", Float) = 0 + [Normal] _BumpMap("Normal Map", 2D) = "bump" {} + _BumpScale("Normal Scale", Range(0, 2)) = 1.0 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Fur Settings + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Fur Shape)] + [Space(5)] + _FurNoiseMask("Fur Noise Mask", 2D) = "white" {} + _FurMask("Fur Mask (where fur appears)", 2D) = "white" {} + _FurLengthMask("Fur Length Mask", 2D) = "white" {} + _FurVector("Fur Direction (XYZ) + Length (W)", Vector) = (0, 0, 1, 0.02) + [Normal] _FurVectorTex("Fur Direction Map (Normal)", 2D) = "bump" {} + _FurVectorScale("Fur Direction Scale", Range(-10, 10)) = 1.0 + _FurGravity("Fur Gravity", Range(0, 1)) = 0.25 + _FurRandomize("Fur Randomize", Range(0, 1)) = 0.1 + _FurAO("Fur Ambient Occlusion", Range(0, 1)) = 0.5 + + [Header(Shell Layers)] + [Space(5)] + [IntRange] _FurLayerNum("Fur Layer Count", Range(1, 3)) = 2 + _FurRootOffset("Fur Root Offset", Range(-1, 0)) = 0 + + [Header(Fur Rim Light)] + [Space(5)] + [HDR] _FurRimColor("Fur Rim Color", Color) = (1, 1, 1, 1) + _FurRimFresnelPower("Fur Rim Fresnel Power", Range(0.01, 50)) = 3.0 + _FurRimAntiLight("Fur Rim Anti-Light", Range(0, 1)) = 0.5 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Toon Shading (NiloToon Style) + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Cel Shading)] + [Space(5)] + _CelShadeMidPoint("Cel Shade Mid Point", Range(-1, 1)) = 0 + _CelShadeSoftness("Cel Shade Softness", Range(0, 1)) = 0.1 + + [Header(Shadow Color)] + [Space(5)] + [Toggle(_SHADOW_COLOR)] _EnableShadowColor("Enable Shadow Color", Float) = 0 + [HDR] _ShadowColor("Shadow Tint Color", Color) = (1, 1, 1, 1) + _ShadowBrightness("Shadow Brightness", Range(0, 2)) = 1.0 + + [Header(HSV Adjustment in Shadow)] + [Space(5)] + _ShadowHueShift("Shadow Hue Shift", Range(-0.5, 0.5)) = 0 + _ShadowSaturationBoost("Shadow Saturation Boost", Range(0, 2)) = 1.0 + _ShadowValueMultiplier("Shadow Value Multiplier", Range(0, 2)) = 1.0 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // MatCap + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(MatCap Additive)] + [Space(5)] + [Toggle(_MATCAP_ADD)] _UseMatCapAdd("Enable MatCap (Add)", Float) = 0 + _MatCapAddMap("MatCap Add Map", 2D) = "black" {} + [HDR] _MatCapAddColor("MatCap Add Color", Color) = (1, 1, 1, 1) + _MatCapAddIntensity("MatCap Add Intensity", Range(0, 5)) = 1.0 + _MatCapAddMask("MatCap Add Mask", 2D) = "white" {} + + [Header(MatCap Multiply)] + [Space(5)] + [Toggle(_MATCAP_MUL)] _UseMatCapMul("Enable MatCap (Multiply)", Float) = 0 + _MatCapMulMap("MatCap Multiply Map", 2D) = "white" {} + _MatCapMulIntensity("MatCap Multiply Intensity", Range(0, 2)) = 1.0 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Rim Light (General) + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Rim Light)] + [Space(5)] + [Toggle(_RIMLIGHT)] _UseRimLight("Enable Rim Light", Float) = 0 + [HDR] _RimLightColor("Rim Light Color", Color) = (1, 1, 1, 1) + _RimLightPower("Rim Light Power", Range(0.01, 20)) = 5.0 + _RimLightIntensity("Rim Light Intensity", Range(0, 5)) = 1.0 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Emission + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Emission)] + [Space(5)] + [Toggle(_EMISSION)] _UseEmission("Enable Emission", Float) = 0 + _EmissionMap("Emission Map", 2D) = "white" {} + [HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 1) + _EmissionIntensity("Emission Intensity", Range(0, 10)) = 1.0 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Occlusion + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Occlusion)] + [Space(5)] + [Toggle(_OCCLUSIONMAP)] _UseOcclusion("Enable Occlusion Map", Float) = 0 + _OcclusionMap("Occlusion Map", 2D) = "white" {} + _OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1.0 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Outline + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Outline)] + [Space(5)] + _OutlineWidth("Outline Width", Range(0, 10)) = 1.0 + [HDR] _OutlineColor("Outline Color", Color) = (0, 0, 0, 1) + _OutlineWidthMask("Outline Width Mask", 2D) = "white" {} + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Rendering Options + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [Header(Rendering Options)] + [Space(5)] + [Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull Mode", Float) = 2 + [Enum(UnityEngine.Rendering.CullMode)] _FurCull("Fur Cull Mode", Float) = 0 + [Enum(Off, 0, On, 1)] _ZWrite("ZWrite", Float) = 1 + _FurZWrite("Fur ZWrite", Float) = 0 + [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest("ZTest", Float) = 4 + + // Hidden + [HideInInspector] _Surface("__surface", Float) = 0.0 + [HideInInspector] _Blend("__blend", Float) = 0.0 + [HideInInspector] _QueueOffset("Queue offset", Float) = 0.0 + } + + SubShader + { + Tags + { + "RenderType" = "Opaque" + "RenderPipeline" = "UniversalPipeline" + "UniversalMaterialType" = "Lit" + "IgnoreProjector" = "True" + "Queue" = "Geometry" + } + LOD 300 + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 0: ForwardLit - Base Surface (Full NiloToon Style) + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "ForwardLit" + Tags { "LightMode" = "UniversalForward" } + + Cull [_Cull] + ZWrite On + ZTest LEqual + Blend One Zero + + HLSLPROGRAM + #pragma target 4.5 + + // Shader Features + #pragma shader_feature_local _NORMALMAP + #pragma shader_feature_local _SHADOW_COLOR + #pragma shader_feature_local _MATCAP_ADD + #pragma shader_feature_local _MATCAP_MUL + #pragma shader_feature_local _RIMLIGHT + #pragma shader_feature_local _EMISSION + #pragma shader_feature_local _OCCLUSIONMAP + + // URP Keywords + #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN + #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS + #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS + #pragma multi_compile_fragment _ _SHADOWS_SOFT + #pragma multi_compile_fog + + // GPU Instancing + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex vert + #pragma fragment frag + + #define NILOTOON_FUR_BASE_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl" + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 1: Fur Shell Pass (Geometry Shader) - Standard single target + // Rendered by NiloToonFurRendererFeature via "NiloToonFurShell" LightMode + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "FurShell" + Tags { "LightMode" = "NiloToonFurShell" } + + Cull [_FurCull] + ZWrite [_FurZWrite] + ZTest LEqual + Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha + + HLSLPROGRAM + #pragma target 4.5 + #pragma require geometry + + // Shader Features + #pragma shader_feature_local _NORMALMAP + #pragma shader_feature_local _SHADOW_COLOR + #pragma shader_feature_local _MATCAP_ADD + #pragma shader_feature_local _MATCAP_MUL + #pragma shader_feature_local _RIMLIGHT + #pragma shader_feature_local _EMISSION + #pragma shader_feature_local _OCCLUSIONMAP + + // URP Keywords + #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN + #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS + #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS + #pragma multi_compile_fragment _ _SHADOWS_SOFT + #pragma multi_compile_fog + + // GPU Instancing + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex vert_fur + #pragma geometry geom_fur + #pragma fragment frag_fur + + #define NILOTOON_FUR_SHELL_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl" + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 1.5: Fur Shell Mask Pass (Geometry Shader) + // Renders fur shells to mask buffer only (white where fur is rendered) + // Used for fur area detection in post-processing + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "FurShellMask" + Tags { "LightMode" = "NiloToonFurShellMask" } + + Cull [_FurCull] + ZWrite Off + ZTest Always // Always pass - we want to mark ALL fur pixels regardless of depth + Blend One Zero // Just overwrite + + HLSLPROGRAM + #pragma target 4.5 + #pragma require geometry + + // GPU Instancing + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex vert_fur + #pragma geometry geom_fur + #pragma fragment frag_fur_mask + + #define NILOTOON_FUR_SHELL_PASS + #define NILOTOON_FUR_MASK_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl" + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 2: Outline (NiloToon Style) + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "Outline" + Tags { "LightMode" = "NiloToonOutline" } + + Cull Front + ZWrite [_ZWrite] + ZTest [_ZTest] + + HLSLPROGRAM + #pragma target 4.5 + + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + #pragma multi_compile_fog + + #pragma vertex vert_outline + #pragma fragment frag_outline + + #define NILOTOON_FUR_OUTLINE_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + + Varyings vert_outline(Attributes input) + { + Varyings output = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + float outlineWidth = _OutlineWidth * 0.001; + float outlineMask = SAMPLE_TEXTURE2D_LOD(_OutlineWidthMask, sampler_OutlineWidthMask, input.uv, 0).r; + outlineWidth *= outlineMask; + + // FOV-aware outline width + float fovFactor = unity_OrthoParams.w > 0.5 ? 1.0 : (2.0 * abs(UNITY_MATRIX_P[1][1])); + outlineWidth *= fovFactor; + + float3 posOS = input.positionOS.xyz + input.normalOS * outlineWidth; + output.positionCS = TransformObjectToHClip(posOS); + output.uv = input.uv; + output.fogFactor = ComputeFogFactor(output.positionCS.z); + + return output; + } + + half4 frag_outline(Varyings input) : SV_Target + { + half4 color = _OutlineColor; + color.rgb = MixFog(color.rgb, input.fogFactor); + return color; + } + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 3: ShadowCaster + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "ShadowCaster" + Tags { "LightMode" = "ShadowCaster" } + + ZWrite On + ZTest LEqual + ColorMask 0 + Cull [_Cull] + + HLSLPROGRAM + #pragma target 4.5 + + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex ShadowPassVertex + #pragma fragment ShadowPassFragment + + #define NILOTOON_FUR_SHADOW_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + + float3 _LightDirection; + float3 _LightPosition; + + float4 GetShadowPositionHClip(Attributes input) + { + float3 positionWS = TransformObjectToWorld(input.positionOS.xyz); + float3 normalWS = TransformObjectToWorldNormal(input.normalOS); + + #if _CASTING_PUNCTUAL_LIGHT_SHADOW + float3 lightDirectionWS = normalize(_LightPosition - positionWS); + #else + float3 lightDirectionWS = _LightDirection; + #endif + + float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS)); + + #if UNITY_REVERSED_Z + positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE); + #else + positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE); + #endif + + return positionCS; + } + + Varyings ShadowPassVertex(Attributes input) + { + Varyings output = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + + output.uv = input.uv; + output.positionCS = GetShadowPositionHClip(input); + return output; + } + + half4 ShadowPassFragment(Varyings input) : SV_TARGET + { + half alpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a * _BaseColor.a; + clip(alpha - _Cutoff); + return 0; + } + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 4: DepthOnly + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "DepthOnly" + Tags { "LightMode" = "DepthOnly" } + + ZWrite On + ColorMask R + Cull [_Cull] + + HLSLPROGRAM + #pragma target 4.5 + + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex DepthOnlyVertex + #pragma fragment DepthOnlyFragment + + #define NILOTOON_FUR_DEPTH_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + + Varyings DepthOnlyVertex(Attributes input) + { + Varyings output = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + + output.uv = input.uv; + output.positionCS = TransformObjectToHClip(input.positionOS.xyz); + return output; + } + + half DepthOnlyFragment(Varyings input) : SV_TARGET + { + half alpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a * _BaseColor.a; + clip(alpha - _Cutoff); + return input.positionCS.z; + } + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 5: DepthNormals + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "DepthNormals" + Tags { "LightMode" = "DepthNormals" } + + ZWrite On + Cull [_Cull] + + HLSLPROGRAM + #pragma target 4.5 + + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex DepthNormalsVertex + #pragma fragment DepthNormalsFragment + + #define NILOTOON_FUR_DEPTHNORMALS_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + + Varyings DepthNormalsVertex(Attributes input) + { + Varyings output = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + + output.uv = input.uv; + output.positionCS = TransformObjectToHClip(input.positionOS.xyz); + output.normalWS = TransformObjectToWorldNormal(input.normalOS); + return output; + } + + float4 DepthNormalsFragment(Varyings input) : SV_TARGET + { + half alpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a * _BaseColor.a; + clip(alpha - _Cutoff); + return float4(normalize(input.normalWS) * 0.5 + 0.5, 0); + } + ENDHLSL + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pass 6: NiloToonPrepassBuffer (Base mesh only) + // For NiloToon Bloom / Tonemapping character area detection - base mesh + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Pass + { + Name "NiloToonPrepassBuffer" + Tags { "LightMode" = "NiloToonPrepassBuffer" } + + ZWrite On + ZTest LEqual + Cull [_Cull] + + HLSLPROGRAM + #pragma target 4.5 + + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex PrepassBufferVertexBase + #pragma fragment PrepassBufferFragmentBase + + #define NILOTOON_FUR_PREPASSBUFFER_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + + Varyings PrepassBufferVertexBase(Attributes input) + { + Varyings output = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + output.uv = input.uv; + output.positionCS = TransformObjectToHClip(input.positionOS.xyz); + output.furLayer = -1; // Base mesh marker + return output; + } + + float4 PrepassBufferFragmentBase(Varyings input) : SV_TARGET + { + UNITY_SETUP_INSTANCE_ID(input); + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + + // Base mesh alpha clip + half alpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a * _BaseColor.a; + clip(alpha - _Cutoff); + + // Output: character visible area = 1 + return float4(0, 1, 0, 1); + } + ENDHLSL + } + + // Note: Fur shell prepass is handled via MRT in the FurShellMRT pass + // When WriteToPrepassBuffer is enabled, FurShellMRT outputs to both: + // - SV_Target0: Main color buffer + // - SV_Target1: PrepassBuffer (character mask with jagged fur silhouette) + } + + FallBack "Universal Render Pipeline/Lit" + CustomEditor "NiloToonURP.ShaderGUI.NiloToonCharacterFurShaderGUI" +} diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur.shader.meta b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur.shader.meta new file mode 100644 index 00000000..c84b1ca2 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d65d4a470e01cd947a07483a9bf34206 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL.meta b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL.meta new file mode 100644 index 00000000..5b2f6baa --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 721b8d7aa96ce3243ad6e988775df5ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl new file mode 100644 index 00000000..13f235ba --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl @@ -0,0 +1,333 @@ +// NiloToon Character Fur - Fragment Shader +// Full-featured NiloToon style cel shading + Fur specific effects + +#ifndef NILOTOON_CHARACTER_FUR_FRAGMENT_INCLUDED +#define NILOTOON_CHARACTER_FUR_FRAGMENT_INCLUDED + +//------------------------------------------------------------------------------------------------------------------------------ +// Get Normal from Normal Map +//------------------------------------------------------------------------------------------------------------------------------ +half3 GetNormalFromMap(float2 uv, float3 normalWS, float4 tangentWS) +{ + #if defined(_NORMALMAP) + half4 normalMap = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, uv); + half3 normalTS = UnpackNormalWithScale(normalMap, _BumpScale); + + float3 bitangent = cross(normalWS, tangentWS.xyz) * tangentWS.w; + float3x3 TBN = float3x3(tangentWS.xyz, bitangent, normalWS); + + return normalize(mul(normalTS, TBN)); + #else + return normalize(normalWS); + #endif +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Apply All Effects (MatCap, Rim, Emission) +//------------------------------------------------------------------------------------------------------------------------------ +half3 ApplyEffects( + half3 color, + float2 uv, + float3 normalWS, + float3 viewDirWS, + half3 lightColor, + half furLayer +) +{ + // MatCap UV + float2 matCapUV = GetMatCapUV(normalWS, viewDirWS); + + // MatCap Additive + #if defined(_MATCAP_ADD) + half matCapAddMask = SAMPLE_TEXTURE2D(_MatCapAddMask, sampler_MatCapAddMask, uv).r; + half3 matCapAdd = SAMPLE_TEXTURE2D(_MatCapAddMap, sampler_MatCapAddMap, matCapUV).rgb; + matCapAdd *= _MatCapAddColor.rgb * _MatCapAddIntensity * matCapAddMask; + color += matCapAdd; + #endif + + // MatCap Multiply + #if defined(_MATCAP_MUL) + half3 matCapMul = SAMPLE_TEXTURE2D(_MatCapMulMap, sampler_MatCapMulMap, matCapUV).rgb; + color *= lerp(half3(1, 1, 1), matCapMul, _MatCapMulIntensity); + #endif + + // Rim Light (General) - not applied to fur shells to avoid double rim + #if defined(_RIMLIGHT) + if (furLayer < 0) // Only for base pass + { + half NdotV = saturate(dot(normalWS, viewDirWS)); + half rim = pow(1.0 - NdotV, _RimLightPower); + color += rim * _RimLightColor.rgb * _RimLightIntensity * lightColor; + } + #endif + + // Emission + #if defined(_EMISSION) + half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, uv).rgb; + emission *= _EmissionColor.rgb * _EmissionIntensity; + color += emission; + #endif + + return color; +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Base Pass Fragment Shader +//------------------------------------------------------------------------------------------------------------------------------ +#if defined(NILOTOON_FUR_BASE_PASS) +half4 frag(Varyings input) : SV_Target +{ + UNITY_SETUP_INSTANCE_ID(input); + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + + // Sample base texture + half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); + half4 color = baseMap * _BaseColor; + + // Alpha cutoff + clip(color.a - _Cutoff); + + // Get normal (with normal map if enabled) + float3 normalWS = GetNormalFromMap(input.uv, input.normalWS, input.tangentWS); + + // Get view direction + float3 viewDirWS = GetWorldSpaceViewDirSafe(input.positionWS); + + // Get main light with NiloToon override support + Light mainLight = GetMainLightWithNiloToonOverride(); + + // Try to get shadow attenuation if shadow coord is valid + half shadowAttenuation = 1.0; + #if defined(_MAIN_LIGHT_SHADOWS) || defined(_MAIN_LIGHT_SHADOWS_CASCADE) || defined(_MAIN_LIGHT_SHADOWS_SCREEN) + shadowAttenuation = MainLightRealtimeShadow(input.shadowCoord); + #endif + mainLight.shadowAttenuation = shadowAttenuation; + + // Occlusion + half occlusion = 1.0; + #if defined(_OCCLUSIONMAP) + occlusion = lerp(1.0, SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, input.uv).r, _OcclusionStrength); + #endif + + // Apply NiloToon cel shading + color.rgb = ApplyNiloToonCelShading( + color.rgb, + mainLight.color, + mainLight.direction, + normalWS, + viewDirWS, + shadowAttenuation, + occlusion + ); + + // Apply additional effects + color.rgb = ApplyEffects(color.rgb, input.uv, normalWS, viewDirWS, mainLight.color, input.furLayer); + + // Apply fog + color.rgb = MixFog(color.rgb, input.fogFactor); + + return color; +} +#endif + +//------------------------------------------------------------------------------------------------------------------------------ +// Fur Shell Pass Fragment Shader +//------------------------------------------------------------------------------------------------------------------------------ +#if defined(NILOTOON_FUR_SHELL_PASS) + +// MRT output structure for simultaneous color + prepass buffer rendering +struct FurMRTOutput +{ + half4 color : SV_Target0; // Main color buffer + half4 prepass : SV_Target1; // PrepassBuffer (character mask) +}; + +// Internal function to compute fur color (shared between standard and MRT versions) +half4 ComputeFurColor(Varyings input, out half furAlpha) +{ + UNITY_SETUP_INSTANCE_ID(input); + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + + // Sample base texture + half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); + half4 color = baseMap * _BaseColor; + + // Get main light with NiloToon override support + Light mainLight = GetMainLightWithNiloToonOverride(); + half3 lightColor = mainLight.color; + half3 lightDir = mainLight.direction; + + // Sample fur noise mask for alpha (default white = full fur) + float2 furNoiseUV = input.uv * _FurNoiseMask_ST.xy + _FurNoiseMask_ST.zw; + half furNoise = SAMPLE_TEXTURE2D(_FurNoiseMask, sampler_FurNoiseMask, furNoiseUV).r; + + // Sample fur mask (where fur appears, default white = everywhere) + float2 furMaskUV = input.uv * _FurMask_ST.xy + _FurMask_ST.zw; + half furMask = SAMPLE_TEXTURE2D(_FurMask, sampler_FurMask, furMaskUV).r; + + // furLayer: 0 = root/base, 1 = tip + half furLayer = saturate(input.furLayer); + + // Calculate fur alpha using lilToon-style non-linear curve + // This creates natural-looking fur tips instead of obvious hair cards + + // furLayerShift with root offset adjustment (lilToon style) + // _FurRootOffset range is -1 to 0: -1 = hide roots completely, 0 = show all + half furLayerShift = furLayer - furLayer * _FurRootOffset + _FurRootOffset; + half furLayerAbs = abs(furLayerShift); + + // Non-linear alpha curve: creates sharp tip cutoff + // Using cubic falloff for natural-looking fur tips + furAlpha = saturate(furNoise - furLayerShift * furLayerAbs * furLayerAbs * furLayerAbs + 0.25); + + // Apply fur mask + furAlpha *= furMask; + + // Minimum alpha threshold + clip(furAlpha - 0.05); + + // Get normal (with normal map if enabled) + float3 normalWS = GetNormalFromMap(input.uv, input.normalWS, input.tangentWS); + + // Get view direction + float3 viewDirWS = GetWorldSpaceViewDirSafe(input.positionWS); + + // Occlusion + half occlusion = 1.0; + #if defined(_OCCLUSIONMAP) + occlusion = lerp(1.0, SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, input.uv).r, _OcclusionStrength); + #endif + + // Cel shading + half NdotL = dot(normalWS, lightDir); + half halfLambert = NdotL * 0.5 + 0.5; + half celShadeResult = smoothstep( + _CelShadeMidPoint + 0.5 - _CelShadeSoftness, + _CelShadeMidPoint + 0.5 + _CelShadeSoftness, + halfLambert + ); + + celShadeResult *= occlusion; + + #if defined(_SHADOW_COLOR) + // Apply HSV adjustment to shadow + half3 shadowAlbedo = ApplyHSVChange( + color.rgb, + _ShadowHueShift, + _ShadowSaturationBoost, + _ShadowValueMultiplier + ); + shadowAlbedo *= _ShadowColor.rgb * _ShadowBrightness; + color.rgb = lerp(shadowAlbedo, color.rgb, celShadeResult); + #else + color.rgb = lerp(color.rgb * 0.5, color.rgb, celShadeResult); + #endif + + // Apply light color + color.rgb *= lightColor; + + // Apply fur ambient occlusion (lilToon style) + // Uses fwidth to reduce aliasing at layer boundaries + half furAOFactor = _FurAO * saturate(1.0 - fwidth(input.furLayer)); + color.rgb *= furLayer * furAOFactor * 2.0 + 1.0 - furAOFactor; + + // Apply fur rim lighting + half NdotV = abs(dot(normalWS, viewDirWS)); + half rimFresnel = pow(saturate(1.0 - NdotV), _FurRimFresnelPower); + half antiLightFactor = lerp(1.0, 1.0 - Grayscale(lightColor), _FurRimAntiLight); + half3 rimColor = furLayer * rimFresnel * antiLightFactor * _FurRimColor.rgb; + color.rgb += rimColor; + + // Apply additional effects (MatCap, Emission) + // MatCap UV + float2 matCapUV = GetMatCapUV(normalWS, viewDirWS); + + #if defined(_MATCAP_ADD) + half matCapAddMask = SAMPLE_TEXTURE2D(_MatCapAddMask, sampler_MatCapAddMask, input.uv).r; + half3 matCapAdd = SAMPLE_TEXTURE2D(_MatCapAddMap, sampler_MatCapAddMap, matCapUV).rgb; + matCapAdd *= _MatCapAddColor.rgb * _MatCapAddIntensity * matCapAddMask * (1.0 - furLayer * 0.5); + color.rgb += matCapAdd; + #endif + + #if defined(_MATCAP_MUL) + half3 matCapMul = SAMPLE_TEXTURE2D(_MatCapMulMap, sampler_MatCapMulMap, matCapUV).rgb; + color.rgb *= lerp(half3(1, 1, 1), matCapMul, _MatCapMulIntensity * (1.0 - furLayer * 0.5)); + #endif + + #if defined(_EMISSION) + half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, input.uv).rgb; + emission *= _EmissionColor.rgb * _EmissionIntensity * (1.0 - furLayer * 0.5); + color.rgb += emission; + #endif + + // Apply fog + color.rgb = MixFog(color.rgb, input.fogFactor); + + // Final alpha + color.a = furAlpha; + + return color; +} + +// Standard single render target version +half4 frag_fur(Varyings input) : SV_Target +{ + half furAlpha; + return ComputeFurColor(input, furAlpha); +} + +// MRT version: outputs to both color buffer and Fur Mask buffer simultaneously +// This creates the jagged mask effect where only actual rendered fur pixels are marked +FurMRTOutput frag_fur_mrt(Varyings input) +{ + FurMRTOutput output; + + half furAlpha; + output.color = ComputeFurColor(input, furAlpha); + + // Write to Fur Mask Buffer (_NiloToonFurMaskTex): + // Write white (1,1,1,1) where fur pixels are rendered + // This creates a mask that shows exactly where fur is visible + output.prepass = half4(1, 1, 1, 1); + + return output; +} + +// Mask-only version: outputs to PrepassBuffer format +// R: face (not used by fur), G: character area (fur adds here), B: fur area only +// Used for two-pass approach (more compatible than MRT) +half4 frag_fur_mask(Varyings input) : SV_Target +{ + UNITY_SETUP_INSTANCE_ID(input); + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + + // Sample fur noise mask for alpha (default white = full fur) + float2 furNoiseUV = input.uv * _FurNoiseMask_ST.xy + _FurNoiseMask_ST.zw; + half furNoise = SAMPLE_TEXTURE2D(_FurNoiseMask, sampler_FurNoiseMask, furNoiseUV).r; + + // Sample fur mask (where fur appears, default white = everywhere) + float2 furMaskUV = input.uv * _FurMask_ST.xy + _FurMask_ST.zw; + half furMask = SAMPLE_TEXTURE2D(_FurMask, sampler_FurMask, furMaskUV).r; + + // furLayer: 0 = root/base, 1 = tip + half furLayer = saturate(input.furLayer); + + // Calculate fur alpha using lilToon-style non-linear curve + half furLayerShift = furLayer - furLayer * _FurRootOffset + _FurRootOffset; + half furLayerAbs = abs(furLayerShift); + half furAlpha = saturate(furNoise - furLayerShift * furLayerAbs * furLayerAbs * furLayerAbs + 0.25); + + // Apply fur mask + furAlpha *= furMask; + + // Clip pixels that don't pass threshold (same as main fur rendering) + clip(furAlpha - 0.05); + + // Output to PrepassBuffer format: + // G channel = character visible area (unified mask for face, body, and fur) + // All character areas use G channel for consistent masking + return half4(0, 1, 0, 0); +} +#endif + +#endif // NILOTOON_CHARACTER_FUR_FRAGMENT_INCLUDED diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl.meta b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl.meta new file mode 100644 index 00000000..fb43f5c3 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 734309d3fd8d31246bde14f3278d4ee3 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl new file mode 100644 index 00000000..8a960389 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl @@ -0,0 +1,149 @@ +// NiloToon Character Fur - Geometry Shader +// Shell-based fur generation (based on lilToon implementation) + +#ifndef NILOTOON_CHARACTER_FUR_GEOMETRY_INCLUDED +#define NILOTOON_CHARACTER_FUR_GEOMETRY_INCLUDED + +//------------------------------------------------------------------------------------------------------------------------------ +// Barycentric Interpolation Helper +//------------------------------------------------------------------------------------------------------------------------------ +float LerpBary(float a, float b, float c, float3 factor) { return a * factor.x + b * factor.y + c * factor.z; } +float2 LerpBary(float2 a, float2 b, float2 c, float3 factor) { return a * factor.x + b * factor.y + c * factor.z; } +float3 LerpBary(float3 a, float3 b, float3 c, float3 factor) { return a * factor.x + b * factor.y + c * factor.z; } +float4 LerpBary(float4 a, float4 b, float4 c, float3 factor) { return a * factor.x + b * factor.y + c * factor.z; } + +//------------------------------------------------------------------------------------------------------------------------------ +// Append Single Fur Shell (Base + Tip) +//------------------------------------------------------------------------------------------------------------------------------ +void AppendFurShell( + inout TriangleStream outStream, + V2G input[3], + float3 furVectors[3], + float3 factor, + float layerProgress // 0 = base, 1 = outer +) +{ + Varyings output = (Varyings)0; + + UNITY_TRANSFER_INSTANCE_ID(input[0], output); + UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(input[0], output); + + // Interpolate base attributes + output.uv = LerpBary(input[0].uv, input[1].uv, input[2].uv, factor); + output.normalWS = normalize(LerpBary(input[0].normalWS, input[1].normalWS, input[2].normalWS, factor)); + output.tangentWS = LerpBary(input[0].tangentWS, input[1].tangentWS, input[2].tangentWS, factor); + output.tangentWS.xyz = normalize(output.tangentWS.xyz); + output.fogFactor = LerpBary(input[0].fogFactor, input[1].fogFactor, input[2].fogFactor, factor); + + // Base position (furLayer = 0) + float3 basePositionWS = LerpBary(input[0].positionWS, input[1].positionWS, input[2].positionWS, factor); + + output.positionWS = basePositionWS; + output.positionCS = TransformWorldToHClip(basePositionWS); + output.furLayer = 0; + + // Clipping canceller for near plane + #if defined(UNITY_REVERSED_Z) + if(output.positionCS.w < _ProjectionParams.y * 1.01 && output.positionCS.w > 0) + { + output.positionCS.z = output.positionCS.z * 0.0001 + output.positionCS.w * 0.999; + } + #else + if(output.positionCS.w < _ProjectionParams.y * 1.01 && output.positionCS.w > 0) + { + output.positionCS.z = output.positionCS.z * 0.0001 - output.positionCS.w * 0.999; + } + #endif + + outStream.Append(output); + + // Tip position (furLayer = 1) + float3 mixedFurVector = LerpBary(furVectors[0], furVectors[1], furVectors[2], factor); + float3 tipPositionWS = basePositionWS + mixedFurVector; + + output.positionWS = tipPositionWS; + output.positionCS = TransformWorldToHClip(tipPositionWS); + output.furLayer = layerProgress; + + // Clipping canceller for near plane + #if defined(UNITY_REVERSED_Z) + if(output.positionCS.w < _ProjectionParams.y * 1.01 && output.positionCS.w > 0) + { + output.positionCS.z = output.positionCS.z * 0.0001 + output.positionCS.w * 0.999; + } + #else + if(output.positionCS.w < _ProjectionParams.y * 1.01 && output.positionCS.w > 0) + { + output.positionCS.z = output.positionCS.z * 0.0001 - output.positionCS.w * 0.999; + } + #endif + + outStream.Append(output); +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Geometry Shader - Shell Generation +// Based on lilToon's fur geometry shader +//------------------------------------------------------------------------------------------------------------------------------ +[maxvertexcount(40)] +void geom_fur(triangle V2G input[3], inout TriangleStream outStream) +{ + UNITY_SETUP_INSTANCE_ID(input[0]); + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input[0]); + + // Get fur vectors for each vertex + float3 furVectors[3]; + furVectors[0] = input[0].furVector; + furVectors[1] = input[1].furVector; + furVectors[2] = input[2].furVector; + + // Apply procedural randomization using vertex IDs (from lilToon) + uint3 n0 = (input[0].vertexID * 3 + input[1].vertexID * 1 + input[2].vertexID * 1) * uint3(1597334677U, 3812015801U, 2912667907U); + uint3 n1 = (input[0].vertexID * 1 + input[1].vertexID * 3 + input[2].vertexID * 1) * uint3(1597334677U, 3812015801U, 2912667907U); + uint3 n2 = (input[0].vertexID * 1 + input[1].vertexID * 1 + input[2].vertexID * 3) * uint3(1597334677U, 3812015801U, 2912667907U); + + float3 noise0 = normalize(float3(n0) * (2.0 / float(0xffffffffU)) - 1.0); + float3 noise1 = normalize(float3(n1) * (2.0 / float(0xffffffffU)) - 1.0); + float3 noise2 = normalize(float3(n2) * (2.0 / float(0xffffffffU)) - 1.0); + + furVectors[0] += noise0 * _FurVector.w * _FurRandomize; + furVectors[1] += noise1 * _FurVector.w * _FurRandomize; + furVectors[2] += noise2 * _FurVector.w * _FurRandomize; + + // Generate shells based on layer count + // Layer pattern from lilToon: spreads shells across triangle vertices + + if (_FurLayerNum >= 1) + { + // 3 shells at each vertex + AppendFurShell(outStream, input, furVectors, float3(1.0, 0.0, 0.0), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.0, 1.0, 0.0), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.0, 0.0, 1.0), 1.0); + } + + if (_FurLayerNum >= 2) + { + // 3 more shells at edge midpoints + AppendFurShell(outStream, input, furVectors, float3(0.0, 0.5, 0.5), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.5, 0.0, 0.5), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.5, 0.5, 0.0), 1.0); + } + + if (_FurLayerNum >= 3) + { + // 6 more shells for dense fur + AppendFurShell(outStream, input, furVectors, float3(1.0/6.0, 4.0/6.0, 1.0/6.0), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.0, 0.5, 0.5), 0.8); + AppendFurShell(outStream, input, furVectors, float3(1.0/6.0, 1.0/6.0, 4.0/6.0), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.5, 0.0, 0.5), 0.8); + AppendFurShell(outStream, input, furVectors, float3(4.0/6.0, 1.0/6.0, 1.0/6.0), 1.0); + AppendFurShell(outStream, input, furVectors, float3(0.5, 0.5, 0.0), 0.8); + } + + // Final shell at first vertex to close the strip + AppendFurShell(outStream, input, furVectors, float3(1.0, 0.0, 0.0), 1.0); + + outStream.RestartStrip(); +} + +#endif // NILOTOON_CHARACTER_FUR_GEOMETRY_INCLUDED diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl.meta b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl.meta new file mode 100644 index 00000000..9c1c4dc0 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 130ac8fd23814be49854157d7be9194e +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl new file mode 100644 index 00000000..a5da66f0 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl @@ -0,0 +1,440 @@ +// NiloToon Character Fur - Shared Definitions +// Full-featured structs, CBUFFER, and common functions + +#ifndef NILOTOON_CHARACTER_FUR_SHARED_INCLUDED +#define NILOTOON_CHARACTER_FUR_SHARED_INCLUDED + +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" + +//------------------------------------------------------------------------------------------------------------------------------ +// NiloToon Global Light Override Variables (from NiloToonCharacterMainLightOverrider) +//------------------------------------------------------------------------------------------------------------------------------ +half4 _GlobalUserOverriddenFinalMainLightDirWSParam; // xyz: direction, w: 1 if enabled +half4 _GlobalUserOverriddenFinalMainLightColorParam; // rgb: color, w: 1 if enabled + +//------------------------------------------------------------------------------------------------------------------------------ +// Helper function for view direction (compatible with all URP versions) +//------------------------------------------------------------------------------------------------------------------------------ +float3 GetWorldSpaceViewDirSafe(float3 positionWS) +{ + return normalize(_WorldSpaceCameraPos.xyz - positionWS); +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Helper function for shadow coord (compatible with all URP versions) +//------------------------------------------------------------------------------------------------------------------------------ +float4 GetShadowCoordSafe(VertexPositionInputs vertexInput) +{ + #if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT) + return ComputeScreenPos(vertexInput.positionCS); + #else + return TransformWorldToShadowCoord(vertexInput.positionWS); + #endif +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Get Main Light with NiloToon Override Support +//------------------------------------------------------------------------------------------------------------------------------ +Light GetMainLightWithNiloToonOverride() +{ + Light mainLight = GetMainLight(); + + // Apply NiloToon MainLightOverrider if enabled + if (_GlobalUserOverriddenFinalMainLightDirWSParam.w > 0.5) + { + mainLight.direction = _GlobalUserOverriddenFinalMainLightDirWSParam.xyz; + } + if (_GlobalUserOverriddenFinalMainLightColorParam.w > 0.5) + { + mainLight.color = _GlobalUserOverriddenFinalMainLightColorParam.rgb; + } + + return mainLight; +} + +//------------------------------------------------------------------------------------------------------------------------------ +// CBUFFER for SRP Batcher +//------------------------------------------------------------------------------------------------------------------------------ +CBUFFER_START(UnityPerMaterial) + // Base + float4 _BaseMap_ST; + half4 _BaseColor; + half _Cutoff; + + // Normal Map + float4 _BumpMap_ST; + half _BumpScale; + + // Fur Shape + float4 _FurNoiseMask_ST; + float4 _FurMask_ST; + float4 _FurLengthMask_ST; + float4 _FurVector; + float4 _FurVectorTex_ST; + half _FurVectorScale; + half _FurGravity; + half _FurRandomize; + half _FurAO; + half _FurLayerNum; + half _FurRootOffset; + + // Fur Rim + half4 _FurRimColor; + half _FurRimFresnelPower; + half _FurRimAntiLight; + + // Cel Shading + half _CelShadeMidPoint; + half _CelShadeSoftness; + + // Shadow Color + half4 _ShadowColor; + half _ShadowBrightness; + half _ShadowHueShift; + half _ShadowSaturationBoost; + half _ShadowValueMultiplier; + + // MatCap Add + float4 _MatCapAddMap_ST; + half4 _MatCapAddColor; + half _MatCapAddIntensity; + float4 _MatCapAddMask_ST; + + // MatCap Multiply + float4 _MatCapMulMap_ST; + half _MatCapMulIntensity; + + // Rim Light + half4 _RimLightColor; + half _RimLightPower; + half _RimLightIntensity; + + // Emission + float4 _EmissionMap_ST; + half4 _EmissionColor; + half _EmissionIntensity; + + // Occlusion + float4 _OcclusionMap_ST; + half _OcclusionStrength; + + // Outline + half _OutlineWidth; + half4 _OutlineColor; + float4 _OutlineWidthMask_ST; + + // Rendering + half _Cull; + half _FurCull; +CBUFFER_END + +//------------------------------------------------------------------------------------------------------------------------------ +// Texture Declarations (all textures declared unconditionally to avoid compilation issues) +//------------------------------------------------------------------------------------------------------------------------------ +TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); +TEXTURE2D(_BumpMap); SAMPLER(sampler_BumpMap); + +// Fur textures +TEXTURE2D(_FurNoiseMask); SAMPLER(sampler_FurNoiseMask); +TEXTURE2D(_FurMask); SAMPLER(sampler_FurMask); +TEXTURE2D(_FurLengthMask); SAMPLER(sampler_FurLengthMask); +TEXTURE2D(_FurVectorTex); SAMPLER(sampler_FurVectorTex); + +// MatCap +TEXTURE2D(_MatCapAddMap); SAMPLER(sampler_MatCapAddMap); +TEXTURE2D(_MatCapAddMask); SAMPLER(sampler_MatCapAddMask); +TEXTURE2D(_MatCapMulMap); SAMPLER(sampler_MatCapMulMap); + +// Emission +TEXTURE2D(_EmissionMap); SAMPLER(sampler_EmissionMap); + +// Occlusion +TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap); + +// Outline +TEXTURE2D(_OutlineWidthMask); SAMPLER(sampler_OutlineWidthMask); + +//------------------------------------------------------------------------------------------------------------------------------ +// Vertex Input Structure +//------------------------------------------------------------------------------------------------------------------------------ +struct Attributes +{ + float4 positionOS : POSITION; + float3 normalOS : NORMAL; + float4 tangentOS : TANGENT; + float2 uv : TEXCOORD0; + float2 uv2 : TEXCOORD1; + float4 color : COLOR; + uint vertexID : SV_VertexID; + UNITY_VERTEX_INPUT_INSTANCE_ID +}; + +//------------------------------------------------------------------------------------------------------------------------------ +// Vertex to Geometry Structure (for Fur Shell Pass) +//------------------------------------------------------------------------------------------------------------------------------ +struct V2G +{ + float2 uv : TEXCOORD0; + float3 positionWS : TEXCOORD1; + float3 normalWS : TEXCOORD2; + float4 tangentWS : TEXCOORD3; + float3 furVector : TEXCOORD4; + float fogFactor : TEXCOORD5; + uint vertexID : TEXCOORD6; + float4 color : COLOR; + UNITY_VERTEX_INPUT_INSTANCE_ID + UNITY_VERTEX_OUTPUT_STEREO +}; + +//------------------------------------------------------------------------------------------------------------------------------ +// Fragment Input Structure +//------------------------------------------------------------------------------------------------------------------------------ +struct Varyings +{ + float4 positionCS : SV_POSITION; + float2 uv : TEXCOORD0; + float3 positionWS : TEXCOORD1; + float3 normalWS : TEXCOORD2; + float4 tangentWS : TEXCOORD3; + float fogFactor : TEXCOORD4; + float furLayer : TEXCOORD5; // 0 = base, 1 = outer shell + float4 shadowCoord : TEXCOORD6; + UNITY_VERTEX_INPUT_INSTANCE_ID + UNITY_VERTEX_OUTPUT_STEREO +}; + +//------------------------------------------------------------------------------------------------------------------------------ +// Color Space Conversion (RGB <-> HSV) +//------------------------------------------------------------------------------------------------------------------------------ +float3 RGBToHSV(float3 rgb) +{ + float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + float4 p = lerp(float4(rgb.bg, K.wz), float4(rgb.gb, K.xy), step(rgb.b, rgb.g)); + float4 q = lerp(float4(p.xyw, rgb.r), float4(rgb.r, p.yzx), step(p.x, rgb.r)); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +float3 HSVToRGB(float3 hsv) +{ + float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + float3 p = abs(frac(hsv.xxx + K.xyz) * 6.0 - K.www); + return hsv.z * lerp(K.xxx, saturate(p - K.xxx), hsv.y); +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Utility: Grayscale +//------------------------------------------------------------------------------------------------------------------------------ +half Grayscale(half3 color) +{ + return dot(color, half3(0.299, 0.587, 0.114)); +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Normal Map Unpacking +//------------------------------------------------------------------------------------------------------------------------------ +half3 UnpackNormalWithScale(half4 packedNormal, half scale) +{ + #if defined(UNITY_NO_DXT5nm) + half3 normal = packedNormal.xyz * 2.0 - 1.0; + #else + half3 normal; + normal.xy = packedNormal.ag * 2.0 - 1.0; + normal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy))); + #endif + normal.xy *= scale; + return normalize(normal); +} + +//------------------------------------------------------------------------------------------------------------------------------ +// MatCap UV Calculation +//------------------------------------------------------------------------------------------------------------------------------ +float2 GetMatCapUV(float3 normalWS, float3 viewDirWS) +{ + float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, normalWS); + return viewNormal.xy * 0.5 + 0.5; +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Apply HSV Change to Color +//------------------------------------------------------------------------------------------------------------------------------ +half3 ApplyHSVChange(half3 color, half hueOffset, half saturationBoost, half valueMul) +{ + float3 hsv = RGBToHSV(color); + hsv.x = frac(hsv.x + hueOffset); + hsv.y = saturate(hsv.y * saturationBoost); + hsv.z = hsv.z * valueMul; + return HSVToRGB(hsv); +} + +//------------------------------------------------------------------------------------------------------------------------------ +// NiloToon Style Cel Shading (Full Version) +//------------------------------------------------------------------------------------------------------------------------------ +half3 ApplyNiloToonCelShading( + half3 baseColor, + half3 lightColor, + half3 lightDir, + half3 normalWS, + half3 viewDir, + half shadowAttenuation, + half occlusion +) +{ + // Calculate NdotL + half NdotL = dot(normalWS, lightDir); + + // Cel shading with configurable mid point and softness + half halfLambert = NdotL * 0.5 + 0.5; + half celShadeResult = smoothstep( + _CelShadeMidPoint + 0.5 - _CelShadeSoftness, + _CelShadeMidPoint + 0.5 + _CelShadeSoftness, + halfLambert + ); + + // Apply shadow map attenuation + celShadeResult *= shadowAttenuation; + + // Apply occlusion + celShadeResult *= occlusion; + + half3 finalColor = baseColor; + + #if defined(_SHADOW_COLOR) + // Apply HSV adjustment to shadow + half3 shadowAlbedo = ApplyHSVChange( + baseColor, + _ShadowHueShift, + _ShadowSaturationBoost, + _ShadowValueMultiplier + ); + + // Apply shadow tint and brightness + shadowAlbedo *= _ShadowColor.rgb * _ShadowBrightness; + + // Blend between shadow and lit based on cel shade result + finalColor = lerp(shadowAlbedo, baseColor, celShadeResult); + #else + // Simple brightness reduction in shadow + finalColor = lerp(baseColor * 0.5, baseColor, celShadeResult); + #endif + + // Apply light color + finalColor *= lightColor; + + return finalColor; +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Fur Vector Calculation (from lilToon) +//------------------------------------------------------------------------------------------------------------------------------ +float3 CalculateFurVector(float3 normalOS, float4 tangentOS, float2 uv, float4 vertexColor, bool useVertexColor) +{ + // Build TBN matrix + float3 bitangentOS = normalize(cross(normalOS, tangentOS.xyz)) * (tangentOS.w * length(normalOS)); + float3x3 tbnOS = float3x3(tangentOS.xyz, bitangentOS, normalOS); + + // Start with base fur vector + float3 furVector = _FurVector.xyz + float3(0, 0, 0.001); + + // Optional: blend with vertex color + if (useVertexColor) + { + float3 vertexNormal = vertexColor.xyz * 2.0 - 1.0; + furVector = normalize(furVector + vertexNormal); + } + + // Transform to tangent space, then apply fur direction texture + #if defined(NILOTOON_FUR_SHELL_PASS) + float4 furDirTex = SAMPLE_TEXTURE2D_LOD(_FurVectorTex, sampler_FurVectorTex, uv * _FurVectorTex_ST.xy + _FurVectorTex_ST.zw, 0); + float3 furDirFromTex = UnpackNormalWithScale(furDirTex, _FurVectorScale); + furVector = normalize(furVector + furDirFromTex); + #endif + + // Transform to object space direction + furVector = mul(normalize(furVector), tbnOS); + furVector *= _FurVector.w; + + // Apply gravity + float furLength = length(furVector); + furVector.y -= _FurGravity * furLength; + + return furVector; +} + +//------------------------------------------------------------------------------------------------------------------------------ +// Base Pass Vertex Shader +//------------------------------------------------------------------------------------------------------------------------------ +#if defined(NILOTOON_FUR_BASE_PASS) +Varyings vert(Attributes input) +{ + Varyings output = (Varyings)0; + + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + // Transform + VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); + VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); + + output.positionCS = vertexInput.positionCS; + output.positionWS = vertexInput.positionWS; + output.normalWS = normalInput.normalWS; + output.tangentWS = float4(normalInput.tangentWS, input.tangentOS.w); + output.uv = TRANSFORM_TEX(input.uv, _BaseMap); + + // Shadow coord + output.shadowCoord = GetShadowCoordSafe(vertexInput); + + // Fog + output.fogFactor = ComputeFogFactor(vertexInput.positionCS.z); + + // Base pass has no fur layer + output.furLayer = -1.0; + + return output; +} +#endif + +//------------------------------------------------------------------------------------------------------------------------------ +// Fur Pass Vertex Shader (outputs to Geometry Shader) +//------------------------------------------------------------------------------------------------------------------------------ +#if defined(NILOTOON_FUR_SHELL_PASS) +V2G vert_fur(Attributes input) +{ + V2G output = (V2G)0; + + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, output); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + // Transform position and normal to world space + VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); + + output.positionWS = TransformObjectToWorld(input.positionOS.xyz); + output.normalWS = normalInput.normalWS; + output.tangentWS = float4(normalInput.tangentWS, input.tangentOS.w); + output.uv = TRANSFORM_TEX(input.uv, _BaseMap); + output.color = input.color; + output.vertexID = input.vertexID; + + // Calculate fur vector in world space + float3 furVectorOS = CalculateFurVector(input.normalOS, input.tangentOS, input.uv, input.color, false); + output.furVector = TransformObjectToWorldDir(furVectorOS, false); + + // Apply fur length mask + float furLengthMask = SAMPLE_TEXTURE2D_LOD(_FurLengthMask, sampler_FurLengthMask, input.uv * _FurLengthMask_ST.xy + _FurLengthMask_ST.zw, 0).r; + output.furVector *= furLengthMask; + + // Fog + float4 posCS = TransformWorldToHClip(output.positionWS); + output.fogFactor = ComputeFogFactor(posCS.z); + + return output; +} +#endif + +#endif // NILOTOON_CHARACTER_FUR_SHARED_INCLUDED diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl.meta b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl.meta new file mode 100644 index 00000000..f886f458 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 433eaffdfbbdaff41b9794f5adc9878a +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_ShellOnly.shader b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_ShellOnly.shader new file mode 100644 index 00000000..50845aa8 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_ShellOnly.shader @@ -0,0 +1,156 @@ +// NiloToon Character Fur Shell Only Shader +// Use this shader on a second material slot to render fur shells +// The base mesh should use NiloToonCharacter or NiloToonCharacterFur shader + +Shader "Universal Render Pipeline/NiloToon/NiloToon_Character_Fur_ShellOnly" +{ + Properties + { + [Header(Base Color)] + [Space(5)] + [MainTexture] _BaseMap("Base Map", 2D) = "white" {} + [HDR][MainColor] _BaseColor("Base Color", Color) = (1,1,1,1) + _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5 + + [Header(Normal Map)] + [Space(5)] + [Toggle(_NORMALMAP)] _UseNormalMap("Enable Normal Map", Float) = 0 + [Normal] _BumpMap("Normal Map", 2D) = "bump" {} + _BumpScale("Normal Scale", Range(0, 2)) = 1.0 + + [Header(Fur Shape)] + [Space(5)] + _FurNoiseMask("Fur Noise Mask", 2D) = "white" {} + _FurMask("Fur Mask (where fur appears)", 2D) = "white" {} + _FurLengthMask("Fur Length Mask", 2D) = "white" {} + _FurVector("Fur Direction (XYZ) + Length (W)", Vector) = (0, 0, 1, 0.02) + [Normal] _FurVectorTex("Fur Direction Map (Normal)", 2D) = "bump" {} + _FurVectorScale("Fur Direction Scale", Range(-10, 10)) = 1.0 + _FurGravity("Fur Gravity", Range(0, 1)) = 0.25 + _FurRandomize("Fur Randomize", Range(0, 1)) = 0.1 + _FurAO("Fur Ambient Occlusion", Range(0, 1)) = 0.5 + + [Header(Shell Layers)] + [Space(5)] + [IntRange] _FurLayerNum("Fur Layer Count", Range(1, 3)) = 2 + _FurRootOffset("Fur Root Offset", Range(-1, 0)) = 0 + + [Header(Fur Rim Light)] + [Space(5)] + [HDR] _FurRimColor("Fur Rim Color", Color) = (1, 1, 1, 1) + _FurRimFresnelPower("Fur Rim Fresnel Power", Range(0.01, 50)) = 3.0 + _FurRimAntiLight("Fur Rim Anti-Light", Range(0, 1)) = 0.5 + + [Header(Cel Shading)] + [Space(5)] + _CelShadeMidPoint("Cel Shade Mid Point", Range(-1, 1)) = 0 + _CelShadeSoftness("Cel Shade Softness", Range(0, 1)) = 0.1 + + [Header(Shadow Color)] + [Space(5)] + [Toggle(_SHADOW_COLOR)] _EnableShadowColor("Enable Shadow Color", Float) = 0 + [HDR] _ShadowColor("Shadow Tint Color", Color) = (1, 1, 1, 1) + _ShadowBrightness("Shadow Brightness", Range(0, 2)) = 1.0 + _ShadowHueShift("Shadow Hue Shift", Range(-0.5, 0.5)) = 0 + _ShadowSaturationBoost("Shadow Saturation Boost", Range(0, 2)) = 1.0 + _ShadowValueMultiplier("Shadow Value Multiplier", Range(0, 2)) = 1.0 + + [Header(MatCap Additive)] + [Space(5)] + [Toggle(_MATCAP_ADD)] _UseMatCapAdd("Enable MatCap (Add)", Float) = 0 + _MatCapAddMap("MatCap Add Map", 2D) = "black" {} + [HDR] _MatCapAddColor("MatCap Add Color", Color) = (1, 1, 1, 1) + _MatCapAddIntensity("MatCap Add Intensity", Range(0, 5)) = 1.0 + _MatCapAddMask("MatCap Add Mask", 2D) = "white" {} + + [Header(MatCap Multiply)] + [Space(5)] + [Toggle(_MATCAP_MUL)] _UseMatCapMul("Enable MatCap (Multiply)", Float) = 0 + _MatCapMulMap("MatCap Multiply Map", 2D) = "white" {} + _MatCapMulIntensity("MatCap Multiply Intensity", Range(0, 2)) = 1.0 + + [Header(Emission)] + [Space(5)] + [Toggle(_EMISSION)] _UseEmission("Enable Emission", Float) = 0 + _EmissionMap("Emission Map", 2D) = "white" {} + [HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 1) + _EmissionIntensity("Emission Intensity", Range(0, 10)) = 1.0 + + [Header(Occlusion)] + [Space(5)] + [Toggle(_OCCLUSIONMAP)] _UseOcclusion("Enable Occlusion Map", Float) = 0 + _OcclusionMap("Occlusion Map", 2D) = "white" {} + _OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1.0 + + [Header(Outline)] + [Space(5)] + _OutlineWidth("Outline Width", Range(0, 10)) = 0 + [HDR] _OutlineColor("Outline Color", Color) = (0, 0, 0, 1) + _OutlineWidthMask("Outline Width Mask", 2D) = "white" {} + + [Header(Rendering Options)] + [Space(5)] + [Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull Mode", Float) = 2 + [Enum(UnityEngine.Rendering.CullMode)] _FurCull("Fur Cull Mode", Float) = 0 + [Enum(Off, 0, On, 1)] _ZWrite("ZWrite", Float) = 0 + _FurZWrite("Fur ZWrite", Float) = 0 + [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest("ZTest", Float) = 4 + } + + SubShader + { + Tags + { + "RenderType" = "Transparent" + "RenderPipeline" = "UniversalPipeline" + "IgnoreProjector" = "True" + "Queue" = "Transparent-50" + } + LOD 300 + + // Fur Shell Pass + Pass + { + Name "FurShell" + Tags { "LightMode" = "UniversalForward" } + + Cull [_FurCull] + ZWrite [_FurZWrite] + ZTest LEqual + Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha + + HLSLPROGRAM + #pragma target 4.5 + #pragma require geometry + + #pragma shader_feature_local _NORMALMAP + #pragma shader_feature_local _SHADOW_COLOR + #pragma shader_feature_local _MATCAP_ADD + #pragma shader_feature_local _MATCAP_MUL + #pragma shader_feature_local _RIMLIGHT + #pragma shader_feature_local _EMISSION + #pragma shader_feature_local _OCCLUSIONMAP + + #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN + #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS + #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS + #pragma multi_compile_fragment _ _SHADOWS_SOFT + #pragma multi_compile_fog + + #pragma multi_compile_instancing + #pragma instancing_options renderinglayer + + #pragma vertex vert_fur + #pragma geometry geom_fur + #pragma fragment frag_fur + + #define NILOTOON_FUR_SHELL_PASS + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Shared.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Geometry.hlsl" + #include "NiloToonCharacterFur_HLSL/NiloToonCharacterFur_Fragment.hlsl" + ENDHLSL + } + } + + FallBack Off +} diff --git a/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_ShellOnly.shader.meta b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_ShellOnly.shader.meta new file mode 100644 index 00000000..7a5f0d87 --- /dev/null +++ b/Assets/NiloToonURP/Shaders/NiloToonCharacterFur_ShellOnly.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 9b3349963a9dea04da1b88df137b8d82 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: