Files
Test-Multiplayer/Assets/Scripts/Editor/SetupMainMenuScene.cs
T

373 lines
17 KiB
C#

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<Camera>();
}
// 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<UniversalAdditionalCameraData>();
if (camData == null) camData = cam.gameObject.AddComponent<UniversalAdditionalCameraData>();
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<Light>();
GameObject go;
if (light == null)
{
go = new GameObject("Directional Light", typeof(Light));
light = go.GetComponent<Light>();
}
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<MenuLightAnimator>() == null)
go.AddComponent<MenuLightAnimator>();
}
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<TriangleWavePanel>();
// 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<MenuFadeIn>();
if (existingFadeIn == null)
existingFadeIn = existingImage.gameObject.AddComponent<MenuFadeIn>();
existingFadeIn.delay = 2.5f;
existingFadeIn.duration = 3.5f;
existingFadeIn.enabled = true;
var existingGroup = existingImage.GetComponent<CanvasGroup>();
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>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
// Ensure fade canvas draws on top of other UI canvases
canvas.overrideSorting = true;
canvas.sortingOrder = 1000;
var scaler = canvasGo.GetComponent<CanvasScaler>();
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>();
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<MenuFadeIn>();
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<RectTransform>();
// 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<Image>();
btnImage.color = new Color(0.08f, 0.08f, 0.08f, 0.85f);
var button = buttonGo.GetComponent<UnityEngine.UI.Button>();
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<UnityEngine.UI.Text>();
txt.text = "Toggle Fade";
txt.alignment = TextAnchor.MiddleCenter;
txt.color = Color.white;
txt.fontSize = 14;
var txtRect = textGo.GetComponent<RectTransform>();
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<ProceduralEditor.DebugFadeToggleBridge>();
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<VolumeProfile>(PostProcessProfilePath);
if (profile == null)
{
if (!AssetDatabase.IsValidFolder(SettingsFolder))
AssetDatabase.CreateFolder("Assets", "Settings");
profile = ScriptableObject.CreateInstance<VolumeProfile>();
AssetDatabase.CreateAsset(profile, PostProcessProfilePath);
}
if (!profile.TryGet(out DepthOfField dof))
dof = profile.Add<DepthOfField>();
// "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>();
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<Material> configure)
{
var existing = AssetDatabase.LoadAssetAtPath<Material>(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;
}
}
}