Files
bansonic_beta_main/Assets/scripts/Audio/BgmParticleEmitter.cs
T

431 lines
15 KiB
C#

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<ParticleSystem>();
if (mainPs == null)
{
mainPs = gameObject.AddComponent<ParticleSystem>();
}
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<Canvas>();
if (canvas != null)
canvasRect = canvas.GetComponent<RectTransform>();
}
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<ParticleSystemRenderer>();
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<Canvas>();
if (canvas != null)
{
var rt = canvas.GetComponent<RectTransform>();
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<Material>("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<Texture2D>(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<Texture2D>(path);
}
if (runtimeMat != null && particleTexture != null)
runtimeMat.mainTexture = particleTexture;
}
#endif
}