using System.Collections.Generic; using System.Linq; using UnityEditor.SceneManagement; using UnityEditorInternal; using UnityEngine; using UnityEngine.Rendering.Universal; namespace UnityEditor.Rendering.Universal { using Styles = UniversalRenderPipelineCameraUI.Styles; [CustomEditorForRenderPipeline(typeof(Camera), typeof(UniversalRenderPipelineAsset))] [CanEditMultipleObjects] class UniversalRenderPipelineCameraEditor : CameraEditor { ReorderableList m_LayerList; public Camera camera => target as Camera; static Camera selectedCameraInStack; List validCameras = new List(); List m_TypeErrorCameras = new List(); List m_NotSupportedOverlayCameras = new List(); List m_IncompatibleCameras = new List(); List<(Camera, UniversalRenderPipelineSerializedCamera)> m_OutputWarningCameras = new(); UniversalRenderPipelineSerializedCamera m_SerializedCamera; public new void OnEnable() { base.OnEnable(); settings.OnEnable(); selectedCameraInStack = null; m_SerializedCamera = new UniversalRenderPipelineSerializedCamera(serializedObject, settings); validCameras.Clear(); m_TypeErrorCameras.Clear(); m_NotSupportedOverlayCameras.Clear(); m_IncompatibleCameras.Clear(); m_OutputWarningCameras.Clear(); UpdateCameras(); } void UpdateCameras() { m_SerializedCamera.Refresh(); m_LayerList = new ReorderableList(m_SerializedCamera.serializedObject, m_SerializedCamera.cameras, true, true, true, true) { drawHeaderCallback = rect => EditorGUI.LabelField(rect, Styles.cameras), drawElementCallback = DrawElementCallback, onSelectCallback = SelectElement, onRemoveCallback = RemoveCamera, onCanRemoveCallback = CanRemoveCamera, onAddDropdownCallback = AddCameraToCameraList }; } bool CanRemoveCamera(ReorderableList list) => m_SerializedCamera.numCameras > 0; void RemoveCamera(ReorderableList list) { // As multi selection is disabled, selectedIndices will only return 1 element, remove that element from the list if (list.selectedIndices.Any()) { m_SerializedCamera.cameras.DeleteArrayElementAtIndex(list.selectedIndices.First()); } else { // Nothing selected, remove the last item on the list ReorderableList.defaultBehaviours.DoRemoveButton(list); } // Force update the list as removed camera could been there m_TypeErrorCameras.Clear(); m_NotSupportedOverlayCameras.Clear(); m_IncompatibleCameras.Clear(); m_OutputWarningCameras.Clear(); } void SelectElement(ReorderableList list) { var element = m_SerializedCamera.cameras.GetArrayElementAtIndex(list.index); selectedCameraInStack = element.objectReferenceValue as Camera; if (Event.current.clickCount == 2) { Selection.activeObject = selectedCameraInStack; } EditorGUIUtility.PingObject(selectedCameraInStack); } void DrawElementCallback(Rect rect, int index, bool isActive, bool isFocused) { rect.height = EditorGUIUtility.singleLineHeight; rect.y += 1; (Camera camera, UniversalRenderPipelineSerializedCamera serializedCamera) overlayCamera = m_SerializedCamera[index]; Camera cam = overlayCamera.camera; if (cam != null) { var baseAdditionalData = camera.GetUniversalAdditionalCameraData(); bool outputWarning = false; // Checking if the Base Camera and the overlay cameras are of the same type. // If not, we report an error. var overlayAdditionalData = cam.GetUniversalAdditionalCameraData(); var type = overlayAdditionalData.renderType; GUIContent errorContent = EditorGUIUtility.TrTextContent(type.GetName()); ; var renderer = overlayAdditionalData.scriptableRenderer; if (baseAdditionalData.scriptableRenderer.GetType() != renderer.GetType()) { if (!m_IncompatibleCameras.Contains(cam)) { m_IncompatibleCameras.Add(cam); } errorContent = EditorGUIUtility.TrTextContent("", $"Only cameras with compatible renderer types can be stacked. " + $"The camera: {cam.name} are using the renderer {renderer.GetType().Name}, " + $"but the base camera: {camera.name} are using {baseAdditionalData.scriptableRenderer.GetType().Name}. Will skip rendering", CoreEditorStyles.iconFail); } else if (m_IncompatibleCameras.Contains(cam)) { m_IncompatibleCameras.Remove(cam); } // Check if the renderer on the camera we are checking does indeed support overlay camera // This can fail due to changing the renderer in the UI to a renderer that does not support overlay cameras // The UI will not stop you from changing the renderer sadly so this will have to tell the user that the // entry in the stack now is invalid. else if (!renderer.SupportsCameraStackingType(CameraRenderType.Overlay)) { if (!m_NotSupportedOverlayCameras.Contains(cam)) { m_NotSupportedOverlayCameras.Add(cam); } errorContent = EditorGUIUtility.TrTextContent("", $"The camera: {cam.name} is using a renderer of type {renderer.GetType().Name} which does not support Overlay cameras in it's current state.", CoreEditorStyles.iconFail); } else if (m_NotSupportedOverlayCameras.Contains(cam)) { m_NotSupportedOverlayCameras.Remove(cam); } else if (type != CameraRenderType.Overlay) { if (!m_TypeErrorCameras.Contains(cam)) { m_TypeErrorCameras.Add(cam); } errorContent = EditorGUIUtility.TrTextContent(type.GetName(), $"Stack can only contain Overlay cameras. The camera: {cam.name} " + $"has a type {type} that is not supported. Will skip rendering.", CoreEditorStyles.iconFail); } else if (m_TypeErrorCameras.Contains(cam)) { m_TypeErrorCameras.Remove(cam); } if (IsStackCameraOutputDirty(cam, overlayCamera.serializedCamera)) { outputWarning = true; if (!m_OutputWarningCameras.Exists(c => c.Item1 == cam)) { m_OutputWarningCameras.Add((cam, overlayCamera.serializedCamera)); } } else { m_OutputWarningCameras.RemoveAll(c => c.Item1 == cam); } GUIContent nameContent = outputWarning ? EditorGUIUtility.TrTextContent(cam.name, "Output properties do not match base camera", CoreEditorStyles.iconWarn) : EditorGUIUtility.TrTextContent(cam.name); EditorGUI.BeginProperty(rect, GUIContent.none, m_SerializedCamera.cameras.GetArrayElementAtIndex(index)); var labelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth -= 20f; using (var iconSizeScope = new EditorGUIUtility.IconSizeScope(new Vector2(rect.height, rect.height))) { EditorGUI.LabelField(rect, nameContent, errorContent); } // Printing if Post Processing is on or not. var isPostActive = cam.GetUniversalAdditionalCameraData().renderPostProcessing; if (isPostActive) { Rect selectRect = new Rect(rect.width - 20, rect.y, 50, EditorGUIUtility.singleLineHeight); EditorGUI.LabelField(selectRect, "PP"); } EditorGUI.EndProperty(); EditorGUIUtility.labelWidth = labelWidth; } else { camera.GetUniversalAdditionalCameraData().UpdateCameraStack(); // Need to clean out the errorCamera list here. m_TypeErrorCameras.Clear(); m_OutputWarningCameras.Clear(); } } // Modified version of StageHandle.FindComponentsOfType() // This version more closely represents unity object referencing restrictions. // I added these restrictions: // - Can not reference scene object outside scene // - Can not reference cross scenes // - Can reference child objects if it is prefab Camera[] FindCamerasToReference(GameObject gameObject) { var scene = gameObject.scene; var inScene = !EditorUtility.IsPersistent(camera) || scene.IsValid(); var inPreviewScene = EditorSceneManager.IsPreviewScene(scene) && scene.IsValid(); var inCurrentScene = !EditorUtility.IsPersistent(camera) && scene.IsValid(); Camera[] cameras = Resources.FindObjectsOfTypeAll(); List result = new List(); if (!inScene) { foreach (var camera in cameras) { if (camera.transform.IsChildOf(gameObject.transform)) result.Add(camera); } } else if (inPreviewScene) { foreach (var camera in cameras) { if (camera.gameObject.scene == scene) result.Add(camera); } } else if (inCurrentScene) { foreach (var camera in cameras) { if (!EditorUtility.IsPersistent(camera) && !EditorSceneManager.IsPreviewScene(camera.gameObject.scene) && camera.gameObject.scene == scene) result.Add(camera); } } return result.ToArray(); } void AddCameraToCameraList(Rect rect, ReorderableList list) { // Need to do clear the list here otherwise the menu just fills up with more and more entries validCameras.Clear(); // Need to get the base renderer here first var renderer = camera.GetUniversalAdditionalCameraData().scriptableRenderer.GetType(); var allCameras = FindCamerasToReference(camera.gameObject); foreach (var camera in allCameras) { var component = camera.GetUniversalAdditionalCameraData(); if (component != null) { if (component.renderType == CameraRenderType.Overlay && component.scriptableRenderer.GetType() == renderer) { validCameras.Add(camera); } } } var names = new GUIContent[validCameras.Count]; for (int i = 0; i < validCameras.Count; ++i) { names[i] = new GUIContent((i + 1) + " " + validCameras[i].name); } if (!validCameras.Any()) { names = new GUIContent[1]; names[0] = new GUIContent("No Overlay Cameras exist."); } EditorUtility.DisplayCustomMenu(rect, names, -1, AddCameraToCameraListMenuSelected, null); } void AddCameraToCameraListMenuSelected(object userData, string[] options, int selected) { if (!validCameras.Any()) return; m_SerializedCamera.cameras.InsertArrayElementAtIndex(m_SerializedCamera.numCameras); m_SerializedCamera.cameras.GetArrayElementAtIndex(m_SerializedCamera.numCameras - 1).objectReferenceValue = validCameras[selected]; m_SerializedCamera.serializedAdditionalDataObject.ApplyModifiedProperties(); m_SerializedCamera.Refresh(); (Camera camera, UniversalRenderPipelineSerializedCamera serializedCamera) overlayCamera = m_SerializedCamera[m_SerializedCamera.numCameras - 1]; UpdateStackCameraOutput(overlayCamera.camera, overlayCamera.serializedCamera); } public new void OnDisable() { base.OnDisable(); } // IsPreset is an internal API - lets reuse the usable part of this function // 93 is a "magic number" and does not represent a combination of other flags here internal static bool IsPresetEditor(UnityEditor.Editor editor) { return (int)((editor.target as Component).gameObject.hideFlags) == 93; } public override void OnInspectorGUI() { var rpAsset = UniversalRenderPipeline.asset; if (rpAsset == null) { base.OnInspectorGUI(); return; } m_SerializedCamera.Update(); if (IsPresetEditor(this)) { UniversalRenderPipelineCameraUI.PresetInspector.Draw(m_SerializedCamera, this); } else { UniversalRenderPipelineCameraUI.Inspector.Draw(m_SerializedCamera, this); } m_SerializedCamera.Apply(); } private void UpdateStackCameraToOverlay() { var additionalCameraData = selectedCameraInStack.GetUniversalAdditionalCameraData(); if (additionalCameraData == null) return; if (additionalCameraData.renderType == CameraRenderType.Base) { Undo.RecordObject(additionalCameraData, Styles.inspectorOverlayCameraText); additionalCameraData.renderType = CameraRenderType.Overlay; EditorUtility.SetDirty(additionalCameraData); } } private void UpdateStackCameraOutput(Camera cam, UniversalRenderPipelineSerializedCamera serializedCamera) { if ((CameraRenderType)serializedCamera.cameraType.intValue == CameraRenderType.Base) return; serializedCamera.Update(); Undo.RecordObject(camera, Styles.inspectorOverlayCameraText); bool isChanged = false; // Force same render texture RenderTexture targetTexture = settings.targetTexture.objectReferenceValue as RenderTexture; if (cam.targetTexture != targetTexture) { cam.targetTexture = targetTexture; isChanged = true; } // Force same hdr bool allowHDR = settings.HDR.boolValue; if (cam.allowHDR != allowHDR) { cam.allowHDR = allowHDR; isChanged = true; } // Force same mssa bool allowMSSA = settings.allowMSAA.boolValue; if (cam.allowMSAA != allowMSSA) { cam.allowMSAA = allowMSSA; isChanged = true; } // Force same viewport rect Rect rect = settings.normalizedViewPortRect.rectValue; if (cam.rect != rect) { cam.rect = settings.normalizedViewPortRect.rectValue; isChanged = true; } // Force same dynamic resolution bool allowDynamicResolution = settings.allowDynamicResolution.boolValue; if (serializedCamera.allowDynamicResolution.boolValue != allowDynamicResolution) { cam.allowDynamicResolution = allowDynamicResolution; isChanged = true; } // Force same target display int targetDisplay = settings.targetDisplay.intValue; if (cam.targetDisplay != targetDisplay) { cam.targetDisplay = targetDisplay; isChanged = true; } #if ENABLE_VR && ENABLE_XR_MODULE // Force same target display int selectedValue = !m_SerializedCamera.allowXRRendering.boolValue ? 0 : 1; int overlayCameraSelectedValue = !serializedCamera.allowXRRendering.boolValue ? 0 : 1; if (overlayCameraSelectedValue != selectedValue) { serializedCamera.allowXRRendering.boolValue = selectedValue == 1; isChanged = true; } #endif if (isChanged) { EditorUtility.SetDirty(cam); serializedCamera.Apply(); } } private bool IsStackCameraOutputDirty(Camera cam, UniversalRenderPipelineSerializedCamera serializedCamera) { serializedCamera.Update(); // Force same render texture RenderTexture targetTexture = settings.targetTexture.objectReferenceValue as RenderTexture; if (cam.targetTexture != targetTexture) return true; // Force same hdr bool allowHDR = settings.HDR.boolValue; if (cam.allowHDR != allowHDR) return true; // Force same mssa bool allowMSSA = settings.allowMSAA.boolValue; if (cam.allowMSAA != allowMSSA) return true; // Force same viewport rect Rect rect = settings.normalizedViewPortRect.rectValue; if (cam.rect != rect) return true; // Force same dynamic resolution bool allowDynamicResolution = settings.allowDynamicResolution.boolValue; if (serializedCamera.allowDynamicResolution.boolValue != allowDynamicResolution) return true; // Force same target display int targetDisplay = settings.targetDisplay.intValue; if (cam.targetDisplay != targetDisplay) return true; #if ENABLE_VR && ENABLE_XR_MODULE // Force same target display int selectedValue = !m_SerializedCamera.allowXRRendering.boolValue ? 0 : 1; int overlayCameraSelectedValue = !serializedCamera.allowXRRendering.boolValue ? 0 : 1; if (overlayCameraSelectedValue != selectedValue) return true; #endif return false; } internal void DrawStackSettings() { if (m_SerializedCamera.cameras.hasMultipleDifferentValues) { EditorGUILayout.HelpBox("Cannot multi edit stack of multiple cameras.", MessageType.Info); EditorGUILayout.EndFoldoutHeaderGroup(); return; } bool cameraStackingAvailable = m_SerializedCamera .camerasAdditionalData .All(c => c.scriptableRenderer?.SupportsCameraStackingType(CameraRenderType.Base) ?? false); if (!cameraStackingAvailable) { EditorGUILayout.HelpBox("The renderer used by this camera doesn't support camera stacking. Only Base camera will render.", MessageType.Warning); return; } EditorGUILayout.Space(); m_LayerList.DoLayoutList(); m_SerializedCamera.Apply(); EditorGUI.indentLevel--; bool oldRichTextSupport = EditorStyles.helpBox.richText; EditorStyles.helpBox.richText = true; if (selectedCameraInStack != null) { if (m_IncompatibleCameras.Any()) { if (m_IncompatibleCameras.Contains(selectedCameraInStack)) { var message = "This camera does not use the same type of renderer as the Base camera."; EditorGUILayout.HelpBox(message, MessageType.Error); } } if (m_NotSupportedOverlayCameras.Any()) { if (m_NotSupportedOverlayCameras.Contains(selectedCameraInStack)) { var message = "This camera uses a renderer which does not support Overlays in it's current state."; EditorGUILayout.HelpBox(message, MessageType.Error); } } if (m_TypeErrorCameras.Any()) { if (m_TypeErrorCameras.Contains(selectedCameraInStack)) { var message = "The type of this Camera must be Overlay render type."; CoreEditorUtils.DrawFixMeBox(message, MessageType.Error, UpdateStackCameraToOverlay); } } if (m_OutputWarningCameras.Any()) { var camIndex = m_OutputWarningCameras.FindIndex(c => c.Item1 == selectedCameraInStack); if (camIndex != -1) { var message = "The output properties of this Camera do not match the output properties."; if ((CameraRenderType)m_OutputWarningCameras[camIndex].Item2.cameraType.intValue == CameraRenderType.Base) { EditorGUILayout.HelpBox(message, MessageType.Warning); } else { CoreEditorUtils.DrawFixMeBox(message, MessageType.Warning, () => UpdateStackCameraOutput(m_OutputWarningCameras[camIndex].Item1, m_OutputWarningCameras[camIndex].Item2)); } } } } EditorStyles.helpBox.richText = oldRichTextSupport; EditorGUI.indentLevel++; EditorGUILayout.Space(); } } }