using Procedural; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.UI; namespace ProceduralEditor { public static class SetupMainMenuScene { const string ScenePath = "Assets/Scenes/Menu.unity"; const string MaterialsFolder = "Assets/Materials"; const string FaceMaterialPath = MaterialsFolder + "/LowPolyTriangleFace.mat"; const string SettingsFolder = "Assets/Settings"; const string PostProcessProfilePath = SettingsFolder + "/MenuPostProcessProfile.asset"; // Slightly deeper background and a wider field of view for a more open framing static readonly Color BackgroundColor = new Color(0.92f, 0.93f, 0.95f); static readonly Vector3 PanelCenter = new Vector3(0f, 0f, 10f); [MenuItem("Tools/Scene/Setup Main Menu Scene")] public static void Setup() { if (EditorApplication.isPlaying) { Debug.LogError("Setup Main Menu Scene: exit Play Mode first — EditorSceneManager.OpenScene() can't run during Play Mode."); return; } var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); var faceMaterial = GetOrCreateMaterial(FaceMaterialPath, "Universal Render Pipeline/Lit", mat => { mat.SetColor("_BaseColor", new Color(0.08f, 0.09f, 0.12f)); if (mat.HasProperty("_Smoothness")) mat.SetFloat("_Smoothness", 0.3f); if (mat.HasProperty("_Metallic")) mat.SetFloat("_Metallic", 0.05f); if (mat.HasProperty("_Cull")) mat.SetFloat("_Cull", 0f); }); SetupQuality(); SetupCamera(); SetupLight(); SetupPanel(faceMaterial); SetupFog(); SetupPostProcessVolume(); RemoveLegacyLogo(); SetupFadeCanvas(); EditorSceneManager.MarkSceneDirty(scene); Debug.Log("Main Menu scene set up. Review the Game view framing, then save the scene."); } static void SetupCamera() { var cam = Camera.main; if (cam == null) { var go = new GameObject("Main Camera", typeof(Camera), typeof(AudioListener)); go.tag = "MainCamera"; cam = go.GetComponent(); } // Position/rotation/FOV captured from the live scene after manual framing in the // Editor (approved there first, then copied back here) rather than computed — the // rotation includes a deliberate manual roll (Z) that a LookAt-based rotation // wouldn't produce, and the narrow FOV is a much tighter, more telephoto framing // than the original 60. cam.transform.position = new Vector3(-0.86f, 0.07f, 5.88f); cam.transform.rotation = Quaternion.Euler(-29.064f, -21.054f, -11.311f); // Increased FOV to give a more open, less telephoto composition cam.fieldOfView = 35f; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = BackgroundColor; cam.nearClipPlane = 0.3f; cam.farClipPlane = 60f; // URP silently disables TAA when MSAA is active (they're mutually exclusive) — the // pipeline asset has MSAA on for other scenes (see SetupQuality), so it's turned off // per-camera here rather than touching that shared, project-wide asset. cam.allowMSAA = false; // URP has no "Post Process Layer" component (that's Built-in RP's Post-Processing // Stack v2) — post-processing is a per-camera flag on UniversalAdditionalCameraData // plus the antialiasing mode below; that combination is the URP equivalent. // // Switched from SMAA to TAA (Temporal Anti-Aliasing) for higher quality — SMAA was // already maxed at "High" quality with no further headroom, while TAA accumulates // sub-pixel samples across frames and handles the specular sparkle far better. // Tradeoff: TAA can ghost/smear on continuously moving edges, and this panel never // stops moving (wave + tilt + sway), so varianceClampScale is pulled down from URP's // default (0.9 -> 0.65) to bias toward less ghosting, with contrast-adaptive // sharpening turned on to counter the softening TAA otherwise introduces. Watch for // trailing/streaking behind fast-moving edges — if it shows up, SMAA is the fallback. var camData = cam.GetComponent(); if (camData == null) camData = cam.gameObject.AddComponent(); camData.renderPostProcessing = true; camData.antialiasing = AntialiasingMode.TemporalAntiAliasing; ref var taa = ref camData.taaSettings; taa.quality = TemporalAAQuality.High; taa.varianceClampScale = 0.65f; taa.contrastAdaptiveSharpening = 0.6f; } static void SetupLight() { Light light = Object.FindFirstObjectByType(); GameObject go; if (light == null) { go = new GameObject("Directional Light", typeof(Light)); light = go.GetComponent(); } else { go = light.gameObject; } light.type = LightType.Directional; go.transform.position = new Vector3(0f, 3f, 0f); // The panel's visible faces have world normal +Z, so the light needs a strong // -Z travel direction (yaw ~180) to actually illuminate them head-on instead of // grazing the wall edge-on the way the old floor-tuned angle did. go.transform.rotation = Quaternion.Euler(35f, 180f, 0f); light.color = new Color(1f, 0.96f, 0.86f); light.intensity = 1.9f; RenderSettings.ambientMode = AmbientMode.Flat; RenderSettings.ambientLight = new Color(0.22f, 0.24f, 0.28f); if (go.GetComponent() == null) go.AddComponent(); } static void SetupPanel(Material faceMaterial) { var existing = GameObject.Find("Triangle Wave Panel"); if (existing != null) Object.DestroyImmediate(existing); var go = new GameObject("Triangle Wave Panel"); go.transform.position = PanelCenter; // Rotate the panel from its default horizontal (floor, XZ) orientation to vertical // (XY, facing the camera along Z). Local Y (the wave/thickness axis) becomes world Z, // so tiles ripple toward/away from the camera instead of bobbing up/down. go.transform.rotation = Quaternion.Euler(90f, 0f, 0f); var panel = go.AddComponent(); // Slightly larger than the minimum frame-filling size, since the camera now views the // panel off-axis (see SetupCamera) rather than dead-on, which eats into the margin. panel.columns = 46; panel.rows = 30; panel.triangleSize = 1f; // Values below captured from the live scene after manual tweaking in the Inspector // (approved there first, then copied back here). panel.gapScale = 0.973f; panel.cornerRadius = 0f; panel.cornerSegments = 3; panel.thickness = 0.15f; panel.waveAmplitude = 0.45f; panel.waveFrequency = 0.18f; panel.waveSpeed = 0.8f; panel.tiltAmount = 1.4f; panel.swayAmplitude = 10f; panel.swaySpeed = 0.2f; panel.showBacker = false; panel.faceMaterial = faceMaterial; panel.backerMaterial = faceMaterial; // AddComponent already triggered one Rebuild() using class defaults before the fields // above were set (see TriangleWavePanel.RebuildNow's doc comment) — force a real one // now that the panel is fully configured. panel.RebuildNow(); } static void SetupFog() { // A vertical, camera-facing wall has no receding horizon to hide, so fog stays off; // the grid is sized so its edges fall outside the frustum instead. RenderSettings.fog = false; } static void RemoveLegacyLogo() { var legacyLogo = GameObject.Find("NetCode Logo"); if (legacyLogo != null) Object.DestroyImmediate(legacyLogo); } static void SetupFadeCanvas() { var existing = GameObject.Find("Fade Canvas"); if (existing != null) { existing.SetActive(true); existing.transform.localScale = Vector3.one; var existingImage = existing.transform.Find("Fade Image"); if (existingImage != null) { existingImage.gameObject.SetActive(true); var existingFadeIn = existingImage.GetComponent(); if (existingFadeIn == null) existingFadeIn = existingImage.gameObject.AddComponent(); existingFadeIn.delay = 2.5f; existingFadeIn.duration = 3.5f; existingFadeIn.enabled = true; var existingGroup = existingImage.GetComponent(); if (existingGroup != null) { existingGroup.alpha = 1f; existingGroup.blocksRaycasts = true; existingGroup.interactable = true; } return; } } var canvasGo = new GameObject("Fade Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); canvasGo.transform.localScale = Vector3.one; var canvas = canvasGo.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; // Ensure fade canvas draws on top of other UI canvases canvas.overrideSorting = true; canvas.sortingOrder = 1000; var scaler = canvasGo.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); var imageGo = new GameObject("Fade Image", typeof(Image), typeof(CanvasGroup)); imageGo.transform.SetParent(canvasGo.transform, false); var image = imageGo.GetComponent(); image.color = new Color(0.03f, 0.03f, 0.04f, 1f); var rect = image.rectTransform; rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero; var fadeIn = imageGo.AddComponent(); fadeIn.delay = 2.5f; fadeIn.duration = 3.5f; // Make sure the image initially is fully opaque so the menu starts covered by the backdrop var initColor = image.color; initColor.a = 1f; image.color = initColor; // DEBUG: add a small top-right button to toggle fade (full opacity <-> transparent) var buttonGo = new GameObject("Debug Toggle Fade Button", typeof(RectTransform), typeof(UnityEngine.UI.Button), typeof(Image)); buttonGo.transform.SetParent(canvasGo.transform, false); var btnRect = buttonGo.GetComponent(); // anchor to top-right btnRect.anchorMin = new Vector2(1f, 1f); btnRect.anchorMax = new Vector2(1f, 1f); btnRect.pivot = new Vector2(1f, 1f); btnRect.sizeDelta = new Vector2(140f, 32f); btnRect.anchoredPosition = new Vector2(-10f, -10f); var btnImage = buttonGo.GetComponent(); btnImage.color = new Color(0.08f, 0.08f, 0.08f, 0.85f); var button = buttonGo.GetComponent(); var colors = button.colors; colors.normalColor = new Color(0.12f, 0.12f, 0.12f, 0.9f); colors.highlightedColor = new Color(0.18f, 0.18f, 0.18f, 0.95f); colors.pressedColor = new Color(0.06f, 0.06f, 0.06f, 0.9f); button.colors = colors; // simple text label (legacy UI Text so no TMP dependency required) var textGo = new GameObject("Text", typeof(RectTransform), typeof(UnityEngine.UI.Text)); textGo.transform.SetParent(buttonGo.transform, false); var txt = textGo.GetComponent(); txt.text = "Toggle Fade"; txt.alignment = TextAnchor.MiddleCenter; txt.color = Color.white; txt.fontSize = 14; var txtRect = textGo.GetComponent(); txtRect.anchorMin = Vector2.zero; txtRect.anchorMax = Vector2.one; txtRect.offsetMin = Vector2.zero; txtRect.offsetMax = Vector2.zero; // add runtime helper (component created in new file DebugFadeToggle.cs) var debugToggle = buttonGo.AddComponent(); debugToggle.targetImage = image; debugToggle.duration = fadeIn.duration; } static void SetupPostProcessVolume() { var existing = GameObject.Find("Post Process Volume"); if (existing != null) Object.DestroyImmediate(existing); var profile = AssetDatabase.LoadAssetAtPath(PostProcessProfilePath); if (profile == null) { if (!AssetDatabase.IsValidFolder(SettingsFolder)) AssetDatabase.CreateFolder("Assets", "Settings"); profile = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(profile, PostProcessProfilePath); } if (!profile.TryGet(out DepthOfField dof)) dof = profile.Add(); // "Blur" here is depth-of-field rather than a flat uniform blur: at this close a // camera distance (see SetupCamera, Z=7.5 vs panel at Z=10) the panel's own width // creates a big apparent depth range across the frame purely from perspective, so // gaussianStart/End are tuned to keep roughly the center sharp and let the // corners/edges soften — a graduated blur rather than blurring everything equally. dof.active = true; dof.mode.overrideState = true; dof.mode.value = DepthOfFieldMode.Gaussian; dof.gaussianStart.overrideState = true; dof.gaussianStart.value = 2f; dof.gaussianEnd.overrideState = true; dof.gaussianEnd.value = 9f; dof.gaussianMaxRadius.overrideState = true; dof.gaussianMaxRadius.value = 1f; dof.highQualitySampling.overrideState = true; dof.highQualitySampling.value = true; EditorUtility.SetDirty(profile); AssetDatabase.SaveAssets(); var go = new GameObject("Post Process Volume", typeof(Volume)); var volume = go.GetComponent(); volume.isGlobal = true; volume.priority = 0f; volume.weight = 1f; volume.sharedProfile = profile; } static void SetupQuality() { // Sets MSAA on the shared pipeline asset for scenes/cameras that still use SMAA/FXAA // (which do benefit from stacking with MSAA). This menu's own camera now uses TAA // instead, which requires MSAA off, so it opts itself out via cam.allowMSAA in // SetupCamera() rather than leaving this project-wide asset at 1x for everyone. if (GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset urpAsset) { urpAsset.msaaSampleCount = 8; EditorUtility.SetDirty(urpAsset); AssetDatabase.SaveAssets(); } } static Material GetOrCreateMaterial(string path, string shaderName, System.Action configure) { var existing = AssetDatabase.LoadAssetAtPath(path); if (existing != null) return existing; if (!AssetDatabase.IsValidFolder(MaterialsFolder)) AssetDatabase.CreateFolder("Assets", "Materials"); var shader = Shader.Find(shaderName); if (shader == null) { Debug.LogError($"SetupMainMenuScene: could not find shader '{shaderName}'."); return null; } var mat = new Material(shader); configure(mat); AssetDatabase.CreateAsset(mat, path); AssetDatabase.SaveAssets(); return mat; } } }