431 lines
15 KiB
C#
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
|
|
}
|
|
|