// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEditor.Rendering.BuiltIn; using UnityEditor.Rendering.BuiltIn.ShaderGraph; using UnityEditor.ShaderGraph; using UnityEngine; using UnityEngine.Rendering; #if UNITY_2022_3_OR_NEWER using UnityEngine.UIElements; #else using UnityEngine.UIElements; using UnityEditor.UIElements; #endif using UnityBuiltInLitSubTarget = UnityEditor.Rendering.BuiltIn.ShaderGraph.BuiltInLitSubTarget; namespace WaveHarmonic.Crest.Editor.ShaderGraph { sealed class MaterialModificationProcessor : AssetModificationProcessor { static void OnWillCreateAsset(string asset) { if (!asset.ToLowerInvariant().EndsWith(".mat")) { return; } MaterialPostProcessor.s_CreatedAssets.Add(asset); } } sealed class MaterialPostProcessor : AssetPostprocessor { public override int GetPostprocessOrder() { return 1; } internal static readonly List s_CreatedAssets = new(); static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (var asset in importedAssets) { // We only care about materials if (!asset.EndsWith(".mat", System.StringComparison.InvariantCultureIgnoreCase)) { continue; } // Load the material and look for it's BuiltIn ShaderID. // We only care about versioning materials using a known BuiltIn ShaderID. // This skips any materials that only target other render pipelines, are user shaders, // or are shaders we don't care to version var material = (Material)AssetDatabase.LoadAssetAtPath(asset, typeof(Material)); var shaderID = ShaderUtils.GetShaderID(material.shader); if (shaderID == ShaderUtils.ShaderID.Unknown) { continue; } if (material.shader == null || material.shader.name != "Crest/Water") { continue; } // Look for the BuiltIn AssetVersion AssetVersion assetVersion = null; var allAssets = AssetDatabase.LoadAllAssetsAtPath(asset); foreach (var subAsset in allAssets) { if (subAsset is AssetVersion sub) { assetVersion = sub; } } if (!assetVersion) { if (s_CreatedAssets.Contains(asset)) { s_CreatedAssets.Remove(asset); CustomBuiltInLitGUI.UpdateMaterial(material); } } } } } class CustomBuiltInLitGUI : BuiltInLitGUI { MaterialEditor _MaterialEditor; MaterialProperty[] _Properties; static readonly GUIContent s_WorkflowModeText = EditorGUIUtility.TrTextContent ( "Workflow Mode", "Select a workflow that fits your textures. Choose between Metallic or Specular." ); static readonly GUIContent s_TransparentReceiveShadowsText = EditorGUIUtility.TrTextContent ( "Receives Shadows", "When enabled, other GameObjects can cast shadows onto this GameObject." ); public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) { _MaterialEditor = materialEditor; _Properties = properties; base.OnGUI(materialEditor, properties); } public override void ValidateMaterial(Material material) { base.ValidateMaterial(material); UpdateMaterial(material); } public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader) { base.AssignNewShaderToMaterial(material, oldShader, newShader); UpdateMaterial(material); } protected override void DrawSurfaceOptions(Material material) { var materialEditor = _MaterialEditor; var properties = _Properties; var workflowProperty = FindProperty(Property.SpecularWorkflowMode(), properties, false); if (workflowProperty != null) { DoPopup(s_WorkflowModeText, materialEditor, workflowProperty, System.Enum.GetNames(typeof(WorkflowMode))); } base.DrawSurfaceOptions(material); var surfaceTypeProp = FindProperty(Property.Surface(), properties, false); if (surfaceTypeProp != null && (SurfaceType)surfaceTypeProp.floatValue == SurfaceType.Transparent) { var trsProperty = FindProperty(BuiltInLitSubTarget.s_TransparentReceiveShadowsProperty, properties, false); DrawFloatToggleProperty(s_TransparentReceiveShadowsText, trsProperty); } } // Should be called by ShaderGraphMaterialsUpdater, but we will never upgrade. public static new void UpdateMaterial(Material material) { if (material.HasProperty(Property.SpecularWorkflowMode())) { var workflow = (WorkflowMode)material.GetFloat(Property.SpecularWorkflowMode()); CoreUtils.SetKeyword(material, BuiltInLitSubTarget.LitDefines.s_SpecularSetup.referenceName, workflow == WorkflowMode.Specular); } if (material.HasProperty(BuiltInLitSubTarget.s_TransparentReceiveShadowsProperty)) { var receive = material.GetFloat(BuiltInLitSubTarget.s_TransparentReceiveShadowsProperty) == 1f; CoreUtils.SetKeyword(material, BuiltInLitSubTarget.LitDefines.s_TransparentReceivesShadows.referenceName, receive); } } } sealed class BuiltInLitSubTarget : BuiltInSubTarget { const string k_ShaderPath = "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Utility/Legacy"; const string k_TemplatePath = "Packages/com.waveharmonic.crest/Editor/Shaders/Templates"; readonly UnityBuiltInLitSubTarget _BuiltInLitSubTarget; #pragma warning disable IDE0032, IDE1006 [SerializeField] WorkflowMode m_WorkflowMode = WorkflowMode.Metallic; [SerializeField] NormalDropOffSpace m_NormalDropOffSpace = NormalDropOffSpace.Tangent; [SerializeField] bool m_TransparentReceiveShadows = true; #pragma warning restore IDE0032, IDE1006 public static readonly string s_TransparentReceiveShadowsProperty = "_BUILTIN_TransparentReceiveShadows"; public BuiltInLitSubTarget() { _BuiltInLitSubTarget = new(); displayName = _BuiltInLitSubTarget.displayName; } protected override ShaderUtils.ShaderID shaderID => ShaderUtils.ShaderID.SG_Lit; public override bool IsActive() => true; WorkflowMode WorkflowMode { get => m_WorkflowMode; set => m_WorkflowMode = value; } NormalDropOffSpace NormalDropOffSpace { get => m_NormalDropOffSpace; set => m_NormalDropOffSpace = value; } bool TransparentReceiveShadows { get => m_TransparentReceiveShadows; set => m_TransparentReceiveShadows = value; } #if UNITY_2022_3_OR_NEWER static FieldInfo s_CustomEditorForRenderPipelines; static FieldInfo CustomEditorForRenderPipelines => s_CustomEditorForRenderPipelines ??= typeof(TargetSetupContext).GetField("customEditorForRenderPipelines", BindingFlags.NonPublic | BindingFlags.Instance); #endif public override void Setup(ref TargetSetupContext context) { _BuiltInLitSubTarget.target = target; _BuiltInLitSubTarget.normalDropOffSpace = NormalDropOffSpace; _BuiltInLitSubTarget.Setup(ref context); // Caused a crash: !context.HasCustomEditorForRenderPipeline(null) if (string.IsNullOrEmpty(target.customEditorGUI)) { #if UNITY_2022_3_OR_NEWER var editors = (List)CustomEditorForRenderPipelines.GetValue(context); if (editors.Count > 0) { editors.RemoveAt(editors.Count - 1); } context.AddCustomEditorForRenderPipeline(typeof(CustomBuiltInLitGUI).FullName, ""); #else if (context.customEditorForRenderPipelines.Count > 0) { context.customEditorForRenderPipelines.RemoveAt(context.customEditorForRenderPipelines.Count - 1); } context.customEditorForRenderPipelines.Add((typeof(CustomBuiltInLitGUI).FullName, "")); #endif } context.subShaders.RemoveAt(0); context.AddSubShader(SubShaders.Lit(this)); } public override void ProcessPreviewMaterial(Material material) { _BuiltInLitSubTarget.target = target; _BuiltInLitSubTarget.normalDropOffSpace = NormalDropOffSpace; _BuiltInLitSubTarget.ProcessPreviewMaterial(material); CustomBuiltInLitGUI.UpdateMaterial(material); } public override void GetFields(ref TargetFieldContext context) { _BuiltInLitSubTarget.target = target; _BuiltInLitSubTarget.normalDropOffSpace = NormalDropOffSpace; _BuiltInLitSubTarget.GetFields(ref context); // Do not use this, as we handle this properly. context.AddField(BuiltInFields.SpecularSetup, false); } public override void GetActiveBlocks(ref TargetActiveBlockContext context) { _BuiltInLitSubTarget.target = target; _BuiltInLitSubTarget.normalDropOffSpace = NormalDropOffSpace; _BuiltInLitSubTarget.GetActiveBlocks(ref context); context.activeBlocks.Remove(BlockFields.SurfaceDescription.Metallic); var insertion = context.activeBlocks.FindIndex(x => x == BlockFields.SurfaceDescription.Occlusion) + 1; if ((WorkflowMode == WorkflowMode.Specular) || target.allowMaterialOverride) { context.activeBlocks.Insert(insertion, BlockFields.SurfaceDescription.Specular); } if ((WorkflowMode == WorkflowMode.Metallic) || target.allowMaterialOverride) { context.activeBlocks.Insert(insertion, BlockFields.SurfaceDescription.Metallic); } } public override void CollectShaderProperties(PropertyCollector collector, GenerationMode generationMode) { if (target.allowMaterialOverride) { collector.AddFloatProperty(Property.SpecularWorkflowMode(), (float)WorkflowMode); } _BuiltInLitSubTarget.target = target; _BuiltInLitSubTarget.normalDropOffSpace = NormalDropOffSpace; _BuiltInLitSubTarget.CollectShaderProperties(collector, generationMode); if (target.allowMaterialOverride) { collector.AddFloatProperty(s_TransparentReceiveShadowsProperty, TransparentReceiveShadows ? 1f : 0f); } // LEqual collector.AddFloatProperty(SubShaders.k_ShadowCasterZTest, 4, UnityEditor.ShaderGraph.Internal.HLSLDeclaration.UnityPerMaterial); } public override void GetPropertiesGUI(ref TargetPropertyGUIContext context, System.Action onChange, System.Action registerUndo) { target.AddDefaultMaterialOverrideGUI(ref context, onChange, registerUndo); context.AddProperty("Workflow", new EnumField(WorkflowMode.Metallic) { value = WorkflowMode }, (evt) => { if (Equals(WorkflowMode, evt.newValue)) return; registerUndo("Change Workflow"); WorkflowMode = (WorkflowMode)evt.newValue; onChange(); }); target.GetDefaultSurfacePropertiesGUI(ref context, onChange, registerUndo); context.AddProperty("Transparent Receives Shadows", new Toggle() { value = TransparentReceiveShadows }, (evt) => { if (Equals(TransparentReceiveShadows, evt.newValue)) return; registerUndo("Change Transparent Receives Shadows"); TransparentReceiveShadows = evt.newValue; onChange(); }); context.AddProperty("Fragment Normal Space", new EnumField(NormalDropOffSpace.Tangent) { value = NormalDropOffSpace }, (evt) => { if (Equals(NormalDropOffSpace, evt.newValue)) return; registerUndo("Change Fragment Normal Space"); NormalDropOffSpace = (NormalDropOffSpace)evt.newValue; _BuiltInLitSubTarget.normalDropOffSpace = NormalDropOffSpace; onChange(); }); } static class SubShaders { static readonly string s_ShaderPathDefines = $"{k_ShaderPath}/Defines.hlsl"; static readonly string s_ShaderPathBuilding = $"{k_ShaderPath}/LegacyBuilding.hlsl"; // SetShaderPassEnabled on ShadowCaster pass does not work for BIRP. We set ZTest // to Never which is the best we can do. We are still incurring the draw call cost. // This is an issue because of the way we trigger motion vectors, but is a bug with // Unity and should be reported. internal const string k_ShadowCasterZTest = "_Crest_BUILTIN_ShadowCasterZTest"; internal static System.Type s_SubShadersType; internal static System.Type SubShadersType => s_SubShadersType ??= typeof(UnityBuiltInLitSubTarget).GetNestedType("SubShaders", BindingFlags.Static | BindingFlags.NonPublic); internal static MethodInfo s_LitMethod; internal static MethodInfo LitMethod => s_LitMethod ??= SubShadersType.GetMethod("Lit", BindingFlags.Static | BindingFlags.Public); static void PatchIncludes(ref PassDescriptor result) { var includes = new IncludeCollection(); includes.Add(s_ShaderPathDefines, IncludeLocation.Pregraph); includes.Add("Packages/com.unity.shadergraph/Editor/Generation/Targets/BuiltIn/Editor/ShaderGraph/Includes/ShaderPass.hlsl", IncludeLocation.Pregraph); foreach (var include in result.includes) { includes.AddInternal(include.guid, include.path, include.location, include.fieldConditions); } result.includes = includes; } static void PatchSpecularIncludes(ref PassDescriptor result, string file) { var ic = new IncludeCollection(); foreach (var include in result.includes) { if (include.path.EndsWith(file)) { ic.Add(s_ShaderPathBuilding, include.location); ic.AddInternal(include.guid, include.path, include.location, include.fieldConditions); } else { ic.AddInternal(include.guid, include.path, include.location, include.fieldConditions); } } result.includes = ic; } static readonly Dictionary s_Mappings = new() { { "SHADERPASS_FORWARD", "PBRForwardPass.hlsl" }, { "SHADERPASS_FORWARD_ADD", "PBRForwardAddPass.hlsl" }, { "SHADERPASS_DEFERRED", "PBRDeferredPass.hlsl" }, }; static readonly string[] s_SkipVariants = new string[] { "LIGHTMAP_ON", "LIGHTMAP_SHADOW_MIXING", "DIRLIGHTMAP_COMBINED", "DYNAMICLIGHTMAP_ON", "SHADOWS_SHADOWMASK", }; public static SubShaderDescriptor Lit(BuiltInLitSubTarget subtarget) { var target = subtarget.target; var ssd = (SubShaderDescriptor)LitMethod.Invoke(null, new object[] { target, target.renderType, target.renderQueue }); PassCollection passes = new(); foreach (var item in ssd.passes) { // Many artifacts in U6 if our Write Depth enabled. // Caused by _SURFACE_TYPE_TRANSPARENT in m_ValidKeywords. if (item.descriptor.referenceName == "SceneSelectionPass") { continue; } var result = item.descriptor; var keywords = new KeywordCollection(); foreach (var keyword in result.keywords) { // All others are either duplicate or unused. if (!keyword.descriptor.referenceName.StartsWith("_BUILTIN_")) { continue; } keywords.Add(keyword.descriptor, keyword.fieldConditions); } result.keywords = keywords; switch (item.descriptor.referenceName) { case "SHADERPASS_FORWARD": case "SHADERPASS_FORWARD_ADD": case "SHADERPASS_DEFERRED": AddWorkflowModeControlToPass(ref result, target, subtarget.WorkflowMode); PatchSpecularIncludes(ref result, s_Mappings[item.descriptor.referenceName]); var pragmas = new PragmaCollection(); foreach (var pragma in result.pragmas) { // For UAVs (RWStructuredBuffer). if (pragma.descriptor.value.StartsWithNoAlloc("target")) { pragmas.Add(Pragma.Target(ShaderModel.Target45)); continue; } if (pragma.descriptor.value.StartsWithNoAlloc("vertex")) { pragmas.Add(Pragma.SkipVariants(s_SkipVariants)); } pragmas.Add(pragma.descriptor, pragma.fieldConditions); } result.pragmas = pragmas; goto default; default: PatchIncludes(ref result); break; } switch (item.descriptor.referenceName) { case "SHADERPASS_FORWARD": case "SHADERPASS_FORWARD_ADD": AddReceivesShadowsControlToPass(ref result, target, subtarget.TransparentReceiveShadows); break; case "SHADERPASS_SHADOWCASTER": var states = new RenderStateCollection(); foreach (var state in result.renderStates) { if (state.descriptor.type == RenderStateType.ZTest) { states.Add(RenderState.ZTest($"[{k_ShadowCasterZTest}]")); continue; } states.Add(state.descriptor, state.fieldConditions); } result.renderStates = states; break; } // Add missing cull render state. if (item.descriptor.referenceName == "SHADERPASS_FORWARD_ADD") { CoreRenderStates.AddUberSwitchedCull(target, result.renderStates); } // Inject MV before DO pass. if (item.descriptor.referenceName == "SHADERPASS_DEPTHONLY") { var mv = LitPasses.MotionVectors(target); PatchIncludes(ref mv); passes.Add(mv); } // Fix XR SPI. if (result.requiredFields != null) { var found = false; foreach (var collection in result.requiredFields) { if (collection.field == StructFields.Attributes.instanceID) { found = true; break; } } if (!found) { result.requiredFields.Add(StructFields.Attributes.instanceID); } } passes.Add(result); } ssd.passes = passes; return ssd; } static void AddWorkflowModeControlToPass(ref PassDescriptor pass, BuiltInTarget target, WorkflowMode workflowMode) { if (target.allowMaterialOverride) { pass.keywords.Add(LitDefines.s_SpecularSetup); } else if (workflowMode == WorkflowMode.Specular) { pass.defines.Add(LitDefines.s_SpecularSetup, 1); } } static void AddReceivesShadowsControlToPass(ref PassDescriptor pass, BuiltInTarget target, bool receives) { if (target.allowMaterialOverride) { pass.keywords.Add(LitDefines.s_TransparentReceivesShadows); pass.keywords.Add(LitDefines.s_ShadowsSingleCascade); pass.keywords.Add(LitDefines.s_ShadowsSplitSpheres); pass.keywords.Add(LitDefines.s_ShadowsSoft); } else if (receives) { pass.defines.Add(LitDefines.s_TransparentReceivesShadows, 1); pass.keywords.Add(LitDefines.s_ShadowsSingleCascade); pass.keywords.Add(LitDefines.s_ShadowsSplitSpheres); pass.keywords.Add(LitDefines.s_ShadowsSoft); } } } static class LitPasses { static readonly string s_ShaderPathMotionVectorCommon = $"{k_ShaderPath}/MotionVectorCommon.hlsl"; static readonly string s_ShaderPathMotionVectorPass = $"{k_ShaderPath}/MotionVectorPass.hlsl"; public static RenderStateDescriptor UberSwitchedCullRenderState(BuiltInTarget target) { if (target.allowMaterialOverride) { return RenderState.Cull(CoreRenderStates.Uniforms.cullMode); } else { return RenderState.Cull(CoreRenderStates.RenderFaceToCull(target.renderFace)); } } public static PassDescriptor MotionVectors(BuiltInTarget target) { var result = new PassDescriptor() { // Definition displayName = "BuiltIn MotionVectors", referenceName = "SHADERPASS_MOTION_VECTORS", lightMode = "MotionVectors", useInPreview = false, // Template passTemplatePath = BuiltInTarget.kTemplatePath, sharedTemplateDirectories = BuiltInTarget.kSharedTemplateDirectories.Union ( new string[] { k_TemplatePath, "Packages/com.unity.shadergraph/Editor/Generation/Targets/BuiltIn/Editor/ShaderGraph" } ).ToArray(), // Port Mask validVertexBlocks = new BlockFieldDescriptor[] { BlockFields.VertexDescription.Position, }, validPixelBlocks = CoreBlockMasks.FragmentAlphaOnly, // Fields structs = CoreStructCollections.Default, requiredFields = new() { // Needed for XR, but not sure if correct. StructFields.Attributes.instanceID, }, fieldDependencies = CoreFieldDependencies.Default, // Conditional State renderStates = new() { { RenderState.ZTest(ZTest.LEqual) }, { RenderState.ZWrite(ZWrite.On) }, { UberSwitchedCullRenderState(target) }, // MVs write to the depth buffer causing z-fighting. Luckily, the depth texture has // already been updated, and will not be updated before water renders. { RenderState.ColorMask("ColorMask RG\nOffset 1, 1") }, }, pragmas = new() { { Pragma.Target(ShaderModel.Target35) }, // NOTE: SM 2.0 only GL { Pragma.MultiCompileInstancing }, { Pragma.Vertex("vert") }, { Pragma.Fragment("frag") }, }, defines = new() { CoreDefines.BuiltInTargetAPI }, keywords = new(), includes = new() { // Pre-graph { CoreIncludes.CorePregraph }, { CoreIncludes.ShaderGraphPregraph }, // Post-graph { s_ShaderPathMotionVectorCommon, IncludeLocation.Postgraph }, { CoreIncludes.CorePostgraph }, { s_ShaderPathMotionVectorPass, IncludeLocation.Postgraph }, }, // Custom Interpolator Support customInterpolators = CoreCustomInterpDescriptors.Common, }; // Only support time for now. result.defines.Add(LitDefines.s_AutomaticTimeBasedMotionVectors, 1); CorePasses.AddAlphaClipControlToPass(ref result, target); return result; } } internal static class LitDefines { public static readonly KeywordDescriptor s_AutomaticTimeBasedMotionVectors = new() { displayName = "Automatic Time-Based Motion Vectors", referenceName = "AUTOMATIC_TIME_BASED_MOTION_VECTORS", type = KeywordType.Boolean, definition = KeywordDefinition.Predefined, scope = KeywordScope.Local, stages = KeywordShaderStage.Vertex, }; public static readonly KeywordDescriptor s_SpecularSetup = new() { displayName = "Specular Setup", referenceName = "_BUILTIN_SPECULAR_SETUP", type = KeywordType.Boolean, definition = KeywordDefinition.ShaderFeature, scope = KeywordScope.Local, stages = KeywordShaderStage.Fragment }; public static readonly KeywordDescriptor s_TransparentReceivesShadows = new() { displayName = "Transparent Receives Shadows", referenceName = "_BUILTIN_TRANSPARENT_RECEIVES_SHADOWS", type = KeywordType.Boolean, definition = KeywordDefinition.ShaderFeature, scope = KeywordScope.Local, stages = KeywordShaderStage.Fragment }; public static readonly KeywordDescriptor s_ShadowsSingleCascade = new() { displayName = "Single Cascade Shadows", referenceName = "SHADOWS_SINGLE_CASCADE", type = KeywordType.Boolean, definition = KeywordDefinition.MultiCompile, scope = KeywordScope.Global, stages = KeywordShaderStage.All, }; public static readonly KeywordDescriptor s_ShadowsSoft = new() { displayName = "Soft Shadows", referenceName = "SHADOWS_SOFT", type = KeywordType.Boolean, definition = KeywordDefinition.MultiCompile, scope = KeywordScope.Global, stages = KeywordShaderStage.All, }; public static readonly KeywordDescriptor s_ShadowsSplitSpheres = new() { displayName = "Stable Fit Shadows", referenceName = "SHADOWS_SPLIT_SPHERES", type = KeywordType.Boolean, definition = KeywordDefinition.MultiCompile, scope = KeywordScope.Global, stages = KeywordShaderStage.All, }; } } }