using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using UnityEditorInternal;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using System.Linq;
using System;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor.ShaderGraph;
namespace UnityEditor.Rendering.HighDefinition
{
///
/// FullScreen custom pass drawer
///
[CustomPassDrawerAttribute(typeof(FullScreenCustomPass))]
class FullScreenCustomPassDrawer : CustomPassDrawer
{
private class Styles
{
public static float defaultLineSpace = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
public static float helpBoxHeight = EditorGUIUtility.singleLineHeight * 2;
public static float indentPadding = 17;
public static GUIContent fullScreenPassMaterial = new GUIContent("FullScreen Material", "FullScreen Material used for the full screen DrawProcedural.");
public static GUIContent materialPassName = new GUIContent("Pass Name", "The shader pass to use for your fullscreen pass.");
public static GUIContent fetchColorBuffer = new GUIContent("Fetch Color Buffer", "Tick this if your effect sample/fetch the camera color buffer");
public static GUIContent newMaterialButton = new GUIContent("New", "Creates a new Shader and Material asset using the fullscreen templates.");
public readonly static string writeAndFetchColorBufferWarning = "Fetching and Writing to the camera color buffer at the same time is not supported on most platforms.";
public readonly static string stencilWriteOverReservedBits = "The Stencil Write Mask of your material overwrites the bits reserved by HDRP. To avoid rendering errors, set the Write Mask to " + (int)(UserStencilUsage.AllUserBits);
public readonly static string stencilHelpInfo = $"Stencil is enabled on the material. To help you configure the stencil operations, use these values for the bits available in HDRP: User Bit 0: {(int)UserStencilUsage.UserBit0} User Bit 1: {(int)UserStencilUsage.UserBit1}";
}
class CreateFullscreenMaterialAction : ProjectWindowCallback.EndNameEditAction
{
public bool createShaderGraphShader; // TODO
public FullScreenCustomPass customPass;
static Regex s_ShaderNameRegex = new(@"(Material$|Mat$)", RegexOptions.IgnoreCase);
public override void Action(int instanceId, string pathName, string resourceFile)
{
string fileName = Path.GetFileNameWithoutExtension(pathName);
string directoryName = Path.GetDirectoryName(pathName);
// Clean up name to create shader file:
var shaderName = s_ShaderNameRegex.Replace(fileName, "") + "Shader";
shaderName += createShaderGraphShader ? "." + ShaderGraphImporter.Extension : ".shader";
string shaderPath = Path.Combine(directoryName, shaderName);
shaderPath = AssetDatabase.GenerateUniqueAssetPath(shaderPath);
pathName = AssetDatabase.GenerateUniqueAssetPath(pathName);
string templateFolder = $"{HDUtils.GetHDRenderPipelinePath()}/Editor/RenderPipeline/CustomPass";
string templatePath = createShaderGraphShader ? $"{templateFolder}/CustomPassFullScreenShader.shadergraph" : $"{templateFolder}/CustomPassFullScreenShader.template";
// Load template code and replace shader name with current file name
string templateCode = File.ReadAllText(templatePath);
templateCode = templateCode.Replace("#SCRIPTNAME#", fileName);
File.WriteAllText(shaderPath, templateCode);
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(shaderPath);
var shader = AssetDatabase.LoadAssetAtPath(shaderPath);
shader.name = Path.GetFileName(pathName);
var material = new Material(shader);
customPass.fullscreenPassMaterial = material;
AssetDatabase.CreateAsset(material, pathName);
ProjectWindowUtil.ShowCreatedAsset(material);
}
}
// Fullscreen pass
SerializedProperty m_FullScreenPassMaterial;
SerializedProperty m_MaterialPassName;
SerializedProperty m_FetchColorBuffer;
SerializedProperty m_TargetColorBuffer;
SerializedProperty m_TargetDepthBuffer;
bool m_ShowStencilWriteWarning = false;
bool m_ShowStencilInfoBox = false;
static readonly float k_NewMaterialButtonWidth = 60;
static readonly string k_DefaultMaterialName = "New FullScreen Material.mat";
CustomPass.TargetBuffer targetColorBuffer => (CustomPass.TargetBuffer)m_TargetColorBuffer.intValue;
CustomPass.TargetBuffer targetDepthBuffer => (CustomPass.TargetBuffer)m_TargetDepthBuffer.intValue;
protected override void Initialize(SerializedProperty customPass)
{
m_FullScreenPassMaterial = customPass.FindPropertyRelative("fullscreenPassMaterial");
m_MaterialPassName = customPass.FindPropertyRelative("materialPassName");
m_FetchColorBuffer = customPass.FindPropertyRelative("fetchColorBuffer");
m_TargetColorBuffer = customPass.FindPropertyRelative("targetColorBuffer");
m_TargetDepthBuffer = customPass.FindPropertyRelative("targetDepthBuffer");
}
protected override void DoPassGUI(SerializedProperty customPass, Rect rect)
{
EditorGUI.PropertyField(rect, m_FetchColorBuffer, Styles.fetchColorBuffer);
rect.y += Styles.defaultLineSpace;
if (m_FetchColorBuffer.boolValue && targetColorBuffer == CustomPass.TargetBuffer.Camera)
{
// We add a warning to prevent fetching and writing to the same render target
Rect helpBoxRect = rect;
helpBoxRect.height = Styles.helpBoxHeight;
EditorGUI.HelpBox(helpBoxRect, Styles.writeAndFetchColorBufferWarning, MessageType.Warning);
rect.y += Styles.helpBoxHeight;
}
Rect materialField = rect;
Rect newMaterialField = rect;
if (m_FullScreenPassMaterial.objectReferenceValue == null)
materialField.xMax -= k_NewMaterialButtonWidth;
newMaterialField.xMin += materialField.width;
EditorGUI.PropertyField(materialField, m_FullScreenPassMaterial, Styles.fullScreenPassMaterial);
rect.y += Styles.defaultLineSpace;
if (m_FullScreenPassMaterial.objectReferenceValue is Material mat)
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUI.BeginProperty(rect, Styles.materialPassName, m_MaterialPassName);
{
EditorGUI.BeginChangeCheck();
int index = mat.FindPass(m_MaterialPassName.stringValue);
if (index == -1)
index = 0; // Select the first pass by default when the previous pass name doesn't exist in the new material.
index = EditorGUI.IntPopup(rect, Styles.materialPassName, index, GetMaterialPassNames(mat), Enumerable.Range(0, mat.passCount).ToArray());
if (EditorGUI.EndChangeCheck())
m_MaterialPassName.stringValue = mat.GetPassName(index);
}
EditorGUI.EndProperty();
rect.y += Styles.defaultLineSpace;
GetStencilInfo(mat, out bool stencilEnabled, out int writeMask);
if (stencilEnabled && targetDepthBuffer != CustomPass.TargetBuffer.Custom)
{
if (!m_ShowStencilInfoBox)
{
m_ShowStencilInfoBox = true;
GUI.changed = true;
}
Rect helpBoxRect = rect;
helpBoxRect.height = Styles.helpBoxHeight;
helpBoxRect.xMin += Styles.indentPadding;
EditorGUI.HelpBox(helpBoxRect, Styles.stencilHelpInfo, MessageType.Info);
rect.y += Styles.helpBoxHeight;
}
else if (m_ShowStencilInfoBox)
{
m_ShowStencilInfoBox = false;
GUI.changed = true; // Workaround to update the internal state of the ReorderableList and update the height of the element.
}
if (DoesWriteMaskContainsReservedBits(writeMask))
{
if (!m_ShowStencilWriteWarning)
{
m_ShowStencilWriteWarning = true;
GUI.changed = true; // Workaround to update the internal state of the ReorderableList and update the height of the element.
}
Rect helpBoxRect = rect;
helpBoxRect.height = Styles.helpBoxHeight;
helpBoxRect.xMin += Styles.indentPadding;
EditorGUI.HelpBox(helpBoxRect, Styles.stencilWriteOverReservedBits, MessageType.Warning);
rect.y += Styles.helpBoxHeight;
}
else if (m_ShowStencilWriteWarning)
{
m_ShowStencilWriteWarning = false;
GUI.changed = true; // Workaround to update the internal state of the ReorderableList and update the height of the element.
}
}
}
else if (m_FullScreenPassMaterial.objectReferenceValue == null)
{
// null material, show the button to create a new material & associated shaders
ShowNewMaterialButton(newMaterialField);
}
}
void ShowNewMaterialButton(Rect buttonRect)
{
// Small padding to separate both fields:
buttonRect.xMin += 2;
if (!EditorGUI.DropdownButton(buttonRect, Styles.newMaterialButton, FocusType.Keyboard))
return;
void CreateMaterial(bool shaderGraph)
{
var materialIcon = AssetPreview.GetMiniTypeThumbnail(typeof(Material));
var action = ScriptableObject.CreateInstance();
action.createShaderGraphShader = shaderGraph;
action.customPass = target as FullScreenCustomPass;
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, action, k_DefaultMaterialName, materialIcon, null);
}
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("ShaderGraph"), false, () => CreateMaterial(true));
menu.AddItem(new GUIContent("Handwritten Shader"), false, () => CreateMaterial(false));
menu.DropDown(buttonRect);
}
bool DoesWriteMaskContainsReservedBits(int writeMask)
{
if (targetDepthBuffer == CustomPass.TargetBuffer.Custom)
return false;
return ((writeMask & (int)~UserStencilUsage.AllUserBits) != 0);
}
void GetStencilInfo(Material material, out bool stencilEnabled, out int writeMaskValue)
{
writeMaskValue = 0;
stencilEnabled = false;
if (material.shader == null)
return;
try
{
// Try to retrieve the serialized information of the stencil in the shader
var serializedShader = new SerializedObject(material.shader);
var parsed = serializedShader.FindProperty("m_ParsedForm");
var subShaders = parsed.FindPropertyRelative("m_SubShaders");
var subShader = subShaders.GetArrayElementAtIndex(0);
var passes = subShader.FindPropertyRelative("m_Passes");
var pass = passes.GetArrayElementAtIndex(0);
var state = pass.FindPropertyRelative("m_State");
var writeMask = state.FindPropertyRelative("stencilWriteMask");
var readMask = state.FindPropertyRelative("stencilWriteMask");
var reference = state.FindPropertyRelative("stencilRef");
var stencilOpFront = state.FindPropertyRelative("stencilOpFront");
var passOp = stencilOpFront.FindPropertyRelative("pass");
var failOp = stencilOpFront.FindPropertyRelative("fail");
var zFailOp = stencilOpFront.FindPropertyRelative("zFail");
var writeMaskFloatValue = writeMask.FindPropertyRelative("val");
var writeMaskPropertyName = writeMask.FindPropertyRelative("name");
bool IsStencilEnabled()
{
bool enabled = false;
enabled |= IsNotDefaultValue(reference, 0);
enabled |= IsNotDefaultValue(passOp, 0);
enabled |= IsNotDefaultValue(failOp, 0);
enabled |= IsNotDefaultValue(zFailOp, 0);
enabled |= IsNotDefaultValue(writeMask, 255);
enabled |= IsNotDefaultValue(readMask, 255);
return enabled;
}
bool IsNotDefaultValue(SerializedProperty prop, float defaultValue)
{
var value = prop.FindPropertyRelative("val");
var propertyName = prop.FindPropertyRelative("name");
if (value.floatValue != defaultValue)
return true;
if (material.HasProperty(propertyName.stringValue))
return true;
return false;
}
// First check if the stencil is enabled in the shader:
// We can do this by checking if there are any non-default values in the stencil state
stencilEnabled = IsStencilEnabled();
if (!stencilEnabled)
return;
if (material.HasProperty(writeMaskPropertyName.stringValue))
writeMaskValue = (int)material.GetFloat(writeMaskPropertyName.stringValue);
else
writeMaskValue = (int)writeMaskFloatValue.floatValue;
}
catch { }
}
protected override float GetPassHeight(SerializedProperty customPass)
{
int lineCount = (m_FullScreenPassMaterial.objectReferenceValue is Material ? 3 : 2);
int height = (int)(Styles.defaultLineSpace * lineCount);
height += (m_FetchColorBuffer.boolValue && targetColorBuffer == CustomPass.TargetBuffer.Camera) ? (int)Styles.helpBoxHeight : 0;
if (m_FullScreenPassMaterial.objectReferenceValue is Material mat)
{
if (targetDepthBuffer == CustomPass.TargetBuffer.Camera)
{
GetStencilInfo(mat, out bool stencilEnabled, out int writeMask);
height += stencilEnabled ? (int)Styles.helpBoxHeight : 0;
height += (DoesWriteMaskContainsReservedBits(writeMask)) ? (int)Styles.helpBoxHeight : 0;
}
}
return height;
}
}
}