using UnityEngine; using UnityEngine.UI; using System.IO; public class BgmParticleEmitter : MonoBehaviour { [Header("Audio")] public AudioSource source; public FFTWindow fftWindow = FFTWindow.BlackmanHarris; [Header("Rendering")] public Material particleMaterial; public Texture2D particleTexture; public string particleTextureRelativePath = "artworks/UI_UI/新版主界面 (2)/矩形 2-1.png"; public string fallbackTextureRelativePath = "artworks/UI_UI/新版主界面 (2)/矩形 2-1.png"; public bool useTextureAlphaOnly = true; [Header("Emission")] public int spectrumSize = 256; public float minRate = 2f; public float maxRate = 30f; public float minSpeed = 25f; public float maxSpeed = 80f; public float minSize = 4f; public float maxSize = 8f; public float energyScale = 50f; public float startLifetime = 34.2f; [Header("Rotation Drift")] public Vector2 startRotationRange = new Vector2(0f, 360f); public Vector2 angularVelocityRange = new Vector2(3f, 12f); [Header("Spawn Area")] public float spawnMargin = 40f; public float spawnHeight = 10f; private ParticleSystem mainPs; private float[] spectrum; private RectTransform canvasRect; private static Material defaultParticleMat; private static Texture2D circleTex; private Material runtimeMat; private Vector2 lastCanvasSize; private bool hasLastCanvasSize; private float lastSpawnMargin; private float lastSpawnHeight; private bool spawnAreaInitialized; private void Awake() { BuildIfNeeded(); } private void OnEnable() { BuildIfNeeded(); } private void BuildIfNeeded() { if (mainPs == null) mainPs = GetComponent(); if (mainPs == null) { mainPs = gameObject.AddComponent(); } if (particleTexture == null) particleTexture = LoadTextureFromFile(); #if UNITY_EDITOR if (particleTexture == null) particleTexture = LoadTextureFromAsset(); #endif if (particleTexture != null && useTextureAlphaOnly) particleTexture = ConvertTextureToWhiteAlpha(particleTexture); if (particleTexture == null) particleTexture = GetCircleTexture(); ConfigureSystem(mainPs, Color.white, 10); var shadow = transform.Find("Shadow"); if (shadow != null) { if (Application.isPlaying) Destroy(shadow.gameObject); else DestroyImmediate(shadow.gameObject); } if (canvasRect == null) { var canvas = GetComponentInParent(); if (canvas != null) canvasRect = canvas.GetComponent(); } if (spectrum == null || spectrum.Length != spectrumSize) spectrum = new float[spectrumSize]; } private void Update() { UpdateSpawnArea(); if (!Application.isPlaying) return; if (source == null) { var mgr = BgmPlaybackManager.Instance; if (mgr != null) source = mgr.audioSource; } if (source == null || source.clip == null) return; if (spectrum == null || spectrum.Length != spectrumSize) spectrum = new float[spectrumSize]; source.GetSpectrumData(spectrum, 0, fftWindow); float energy = 0f; int start = 2; int end = Mathf.Min(64, spectrumSize - 1); for (int i = start; i <= end; i++) energy += spectrum[i]; energy /= Mathf.Max(1, end - start + 1); float level = Mathf.Clamp01(energy * Mathf.Max(1f, energyScale)); float rate = Mathf.Lerp(minRate, maxRate, level); float speed = Mathf.Lerp(minSpeed, maxSpeed, level); float size = Mathf.Lerp(minSize, maxSize, level); ApplyRuntimeParams(mainPs, rate, speed, size); } private void ConfigureSystem(ParticleSystem ps, Color color, int order) { var main = ps.main; main.loop = true; main.playOnAwake = true; main.startLifetime = startLifetime; main.startSpeed = minSpeed; main.startSize = minSize; main.startColor = color; main.simulationSpace = ParticleSystemSimulationSpace.Local; main.scalingMode = ParticleSystemScalingMode.Hierarchy; main.maxParticles = 2000; main.startRotation3D = true; main.startRotationX = new ParticleSystem.MinMaxCurve(startRotationRange.x * Mathf.Deg2Rad, startRotationRange.y * Mathf.Deg2Rad); main.startRotationY = new ParticleSystem.MinMaxCurve(startRotationRange.x * Mathf.Deg2Rad, startRotationRange.y * Mathf.Deg2Rad); main.startRotationZ = new ParticleSystem.MinMaxCurve(startRotationRange.x * Mathf.Deg2Rad, startRotationRange.y * Mathf.Deg2Rad); var emission = ps.emission; emission.rateOverTime = minRate; var shape = ps.shape; shape.enabled = true; shape.shapeType = ParticleSystemShapeType.Box; shape.scale = new Vector3(200f, spawnHeight, 0f); var vel = ps.velocityOverLifetime; vel.enabled = true; vel.space = ParticleSystemSimulationSpace.Local; vel.x = new ParticleSystem.MinMaxCurve(-5f, 5f); vel.y = new ParticleSystem.MinMaxCurve(-minSpeed, -minSpeed * 1.2f); vel.z = new ParticleSystem.MinMaxCurve(0f, 0f); var renderer = ps.GetComponent(); renderer.sortingOrder = order; renderer.renderMode = ParticleSystemRenderMode.Billboard; var mat = GetParticleMaterial(); renderer.material = mat; renderer.sharedMaterial = mat; var rot = ps.rotationOverLifetime; rot.enabled = true; rot.separateAxes = true; rot.x = new ParticleSystem.MinMaxCurve(angularVelocityRange.x * Mathf.Deg2Rad, angularVelocityRange.y * Mathf.Deg2Rad); rot.y = new ParticleSystem.MinMaxCurve(angularVelocityRange.x * Mathf.Deg2Rad, angularVelocityRange.y * Mathf.Deg2Rad); rot.z = new ParticleSystem.MinMaxCurve(angularVelocityRange.x * Mathf.Deg2Rad, angularVelocityRange.y * Mathf.Deg2Rad); } private void ApplyRuntimeParams(ParticleSystem ps, float rate, float speed, float size) { if (ps == null) return; var main = ps.main; main.startSpeed = speed; main.startSize = size; main.startLifetime = startLifetime; var emission = ps.emission; emission.rateOverTime = rate; var vel = ps.velocityOverLifetime; vel.x = new ParticleSystem.MinMaxCurve(-5f, 5f); vel.y = new ParticleSystem.MinMaxCurve(-speed, -speed * 1.2f); vel.z = new ParticleSystem.MinMaxCurve(0f, 0f); } private void UpdateSpawnArea() { var canvas = GetComponentInParent(); if (canvas != null) { var rt = canvas.GetComponent(); if (canvasRect == null || canvasRect != rt) canvasRect = rt; } Vector2 size = canvasRect != null ? canvasRect.rect.size : new Vector2(Screen.width, Screen.height); bool sizeChanged = !hasLastCanvasSize || size != lastCanvasSize; bool marginChanged = !spawnAreaInitialized || !Mathf.Approximately(lastSpawnMargin, spawnMargin); bool heightChanged = !spawnAreaInitialized || !Mathf.Approximately(lastSpawnHeight, spawnHeight); if (!sizeChanged && !marginChanged && !heightChanged) return; hasLastCanvasSize = true; lastCanvasSize = size; lastSpawnMargin = spawnMargin; lastSpawnHeight = spawnHeight; spawnAreaInitialized = true; var shape = mainPs.shape; shape.scale = new Vector3(size.x, spawnHeight, 0f); var rect = transform as RectTransform; if (rect != null) { if (rect.anchorMin != new Vector2(0.5f, 1f)) rect.anchorMin = new Vector2(0.5f, 1f); if (rect.anchorMax != new Vector2(0.5f, 1f)) rect.anchorMax = new Vector2(0.5f, 1f); if (rect.pivot != new Vector2(0.5f, 0.5f)) rect.pivot = new Vector2(0.5f, 0.5f); if (rect.anchoredPosition != new Vector2(0f, spawnMargin)) rect.anchoredPosition = new Vector2(0f, spawnMargin); if (rect.localScale != Vector3.one) rect.localScale = Vector3.one; } else { var targetPos = new Vector3(0f, size.y * 0.5f + spawnMargin, 0f); if (transform.localPosition != targetPos) transform.localPosition = targetPos; if (transform.localScale != Vector3.one) transform.localScale = Vector3.one; } } public void ForceLayoutUpdate() { UpdateSpawnArea(); } private static Material GetDefaultParticleMaterial() { if (defaultParticleMat == null) { defaultParticleMat = Resources.GetBuiltinResource("Default-Particle.mat"); if (defaultParticleMat == null || defaultParticleMat.shader == null || !defaultParticleMat.shader.isSupported) { Shader shader = Shader.Find("Particles/Standard Unlit"); if (shader == null) shader = Shader.Find("Universal Render Pipeline/Particles/Unlit"); if (shader == null) shader = Shader.Find("Particles/Standard"); if (shader == null) shader = Shader.Find("Unlit/Texture"); if (shader != null) defaultParticleMat = new Material(shader); } if (defaultParticleMat != null && defaultParticleMat.mainTexture == null) defaultParticleMat.mainTexture = GetCircleTexture(); } return defaultParticleMat; } private Material GetParticleMaterial() { Material baseMat = GetSafeParticleMaterial(); if (baseMat == null) baseMat = CreateFallbackMaterial(); if (baseMat == null) baseMat = GetDefaultParticleMaterial(); if (baseMat == null) baseMat = CreateFallbackMaterial(); if (baseMat != null && baseMat.mainTexture == null && particleTexture == null) baseMat = CreateFallbackMaterial(); if (runtimeMat == null || runtimeMat.shader != baseMat.shader) runtimeMat = new Material(baseMat); if (particleTexture != null) runtimeMat.mainTexture = particleTexture; else if (runtimeMat.mainTexture == null) runtimeMat.mainTexture = GetCircleTexture(); if (runtimeMat.HasProperty("_Color")) runtimeMat.SetColor("_Color", Color.white); if (runtimeMat.HasProperty("_TintColor")) runtimeMat.SetColor("_TintColor", Color.white); if (runtimeMat.HasProperty("_BaseColor")) runtimeMat.SetColor("_BaseColor", Color.white); if (runtimeMat.HasProperty("_MainTex")) runtimeMat.SetTexture("_MainTex", runtimeMat.mainTexture); if (runtimeMat.HasProperty("_BaseMap")) runtimeMat.SetTexture("_BaseMap", runtimeMat.mainTexture); return runtimeMat; } private Material CreateFallbackMaterial() { Shader shader = Shader.Find("Universal Render Pipeline/Particles/Unlit"); if (shader == null) shader = Shader.Find("Particles/Standard Unlit"); if (shader == null) shader = Shader.Find("Particles/Standard"); if (shader == null) shader = Shader.Find("Unlit/Transparent"); if (shader == null) shader = Shader.Find("Unlit/Texture"); if (shader == null) return null; return new Material(shader); } private Material GetSafeParticleMaterial() { if (particleMaterial == null) return null; Shader shader = particleMaterial.shader; if (shader == null) return null; string name = shader.name; if (string.IsNullOrEmpty(name)) return null; string lower = name.ToLowerInvariant(); if (lower.Contains("particle")) return particleMaterial; return null; } private static Texture2D GetCircleTexture() { if (circleTex != null) return circleTex; const int size = 32; circleTex = new Texture2D(size, size, TextureFormat.ARGB32, false); circleTex.wrapMode = TextureWrapMode.Clamp; circleTex.filterMode = FilterMode.Bilinear; Vector2 center = new Vector2((size - 1) * 0.5f, (size - 1) * 0.5f); float radius = size * 0.5f * 0.9f; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { float dist = Vector2.Distance(new Vector2(x, y), center); float t = Mathf.InverseLerp(radius * 0.9f, radius, dist); float alpha = Mathf.Clamp01(1f - t); circleTex.SetPixel(x, y, new Color(1f, 1f, 1f, alpha)); } } circleTex.Apply(); return circleTex; } private Texture2D LoadTextureFromFile() { string relPath = particleTextureRelativePath; if (string.IsNullOrEmpty(relPath)) relPath = fallbackTextureRelativePath; if (string.IsNullOrEmpty(relPath)) return null; string path = Path.Combine(Application.dataPath, relPath); if (!File.Exists(path) && !string.IsNullOrEmpty(fallbackTextureRelativePath)) { string fallback = Path.Combine(Application.dataPath, fallbackTextureRelativePath); if (File.Exists(fallback)) { particleTextureRelativePath = fallbackTextureRelativePath; path = fallback; } } if (!File.Exists(path)) return null; var data = File.ReadAllBytes(path); var tex = new Texture2D(2, 2, TextureFormat.ARGB32, false); if (tex.LoadImage(data)) return tex; return null; } private static Texture2D ConvertTextureToWhiteAlpha(Texture2D src) { if (src == null) return null; try { var pixels = src.GetPixels32(); if (pixels == null || pixels.Length == 0) return src; var tex = new Texture2D(src.width, src.height, TextureFormat.ARGB32, false); for (int i = 0; i < pixels.Length; i++) { byte a = pixels[i].a; pixels[i] = new Color32(255, 255, 255, a); } tex.SetPixels32(pixels); tex.Apply(); tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; return tex; } catch { return src; } } #if UNITY_EDITOR private Texture2D LoadTextureFromAsset() { string relPath = particleTextureRelativePath; if (string.IsNullOrEmpty(relPath)) relPath = fallbackTextureRelativePath; if (string.IsNullOrEmpty(relPath)) return null; string assetPath = "Assets/" + relPath.TrimStart('/', '\\'); return UnityEditor.AssetDatabase.LoadAssetAtPath(assetPath); } #endif #if UNITY_EDITOR private void OnValidate() { if (particleTexture == null) { const string path = "Assets/artworks/UI_UI/新版主界面 (2)/矩形 2-1.png"; particleTexture = UnityEditor.AssetDatabase.LoadAssetAtPath(path); } if (runtimeMat != null && particleTexture != null) runtimeMat.mainTexture = particleTexture; } #endif }