using UnityEngine; using UnityEngine.Serialization; namespace AibisDream.MiniGame.HuoShan { /// /// 笑话发生器 · 像素噪点云效果 — 逐像素写 Texture2D,量化色阶呈现粗颗粒像素风 /// 由 KnobController 通过 SetIntensity / TriggerPulse 驱动 /// 表现:满强度时噪点云活跃跳动(笑声节奏),随强度降低逐步被压制变稀变暗, /// 但反抗脉冲会不断把云重新炸亮 —— 读作"抑制在进行,但没能成功" /// public class GlowTubeEffect : MonoBehaviour { [Header("区域")] [Tooltip("效果区域参考(取其 SpriteRenderer bounds),不填则用自身")] [SerializeField] private SpriteRenderer areaReference; [Header("像素网格")] [Tooltip("横向像素格数(纵向按区域纵横比自动推算)")] [SerializeField] private int pixelColumns = 72; [Tooltip("渲染用材质(Unlit,不受 2D 灯光影响),不填则回退到 areaReference 的材质")] [FormerlySerializedAs("particleMaterial")] [SerializeField] private Material pixelMaterial; [Header("Sorting")] [CustomSortingLayer] [SerializeField] private int sortingLayerId; [SerializeField] private int sortingOrder = 55; [Header("颜色(量化四档)")] [SerializeField] private Color lowColor = new Color(0.31f, 0.08f, 0.16f, 1f); [SerializeField] private Color midColor = new Color(1f, 0.47f, 0.31f, 1f); [SerializeField] private Color highColor = new Color(1f, 0.78f, 0.67f, 1f); [Header("笑声节奏")] [Tooltip("笑声脉冲串的基础频率(次/秒),强度越高笑得越快")] [SerializeField] private float laughRate = 2.2f; [Tooltip("笑声包络对亮度/密度的影响强度")] [SerializeField] private float laughAmplitude = 0.6f; [Header("噪声流动")] [SerializeField] private float noiseScale = 3f; [SerializeField] private float noiseSpeed = 1.2f; [Header("闪烁")] [SerializeField] private float flickerRate = 8f; [SerializeField] private float flickerAmplitude = 0.15f; [Header("量化阈值(值→档位,制造啪嗒式整块亮灭)")] [SerializeField] private float thresholdLow = 0.16f; [SerializeField] private float thresholdMid = 0.42f; [SerializeField] private float thresholdHigh = 0.72f; private SpriteRenderer _sr; private Texture2D _tex; private Sprite _sprite; private Color32[] _pixels; private Material _matInstance; private int _texWidth; private int _texHeight; private float _intensity = 1f; private float _resistancePulse; private float _nextPulseTime; private float _time; private float _seedX; private float _seedY; private float _laughSeed; private Vector2 _areaHalfSize = new Vector2(0.5f, 0.25f); /// 当前显示强度(含脉冲) public float DisplayIntensity { get; private set; } #region Lifecycle private void Awake() { _seedX = Random.Range(0f, 1000f); _seedY = Random.Range(0f, 1000f); _laughSeed = Random.Range(0f, 1000f); CacheAreaBounds(); EnsurePixelRenderer(); } private void OnDestroy() { if (_matInstance != null) Destroy(_matInstance); if (_tex != null) Destroy(_tex); if (_sprite != null) Destroy(_sprite); } private void Update() { float dt = Time.deltaTime; _time += dt; UpdateResistancePulse(dt); RenderPixels(); } #endregion #region Public API /// 设置基础强度(0=熄灭, 1=最亮, 可超过 1) public void SetIntensity(float value) { _intensity = Mathf.Max(0f, value); } /// 触发一次反抗脉冲爆亮 public void TriggerPulse(float strength = 0.5f) { _resistancePulse = Mathf.Max(_resistancePulse, Mathf.Clamp01(strength)); } #endregion #region Setup private void CacheAreaBounds() { var reference = areaReference != null ? areaReference : GetComponent(); if (reference != null) { var b = reference.bounds; _areaHalfSize = new Vector2(Mathf.Max(0.01f, b.extents.x), Mathf.Max(0.01f, b.extents.y)); } } private void EnsurePixelRenderer() { var go = new GameObject("GlowTubePixels"); go.transform.SetParent(transform); go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; _sr = go.AddComponent(); _texWidth = Mathf.Max(4, pixelColumns); float aspect = _areaHalfSize.y / Mathf.Max(0.001f, _areaHalfSize.x); _texHeight = Mathf.Max(2, Mathf.RoundToInt(_texWidth * aspect)); _tex = new Texture2D(_texWidth, _texHeight, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; _pixels = new Color32[_texWidth * _texHeight]; float ppu = _texWidth / (_areaHalfSize.x * 2f); _sprite = Sprite.Create(_tex, new Rect(0, 0, _texWidth, _texHeight), new Vector2(0.5f, 0.5f), ppu); _sr.sprite = _sprite; var mat = pixelMaterial != null ? new Material(pixelMaterial) : (areaReference != null && areaReference.sharedMaterial != null ? new Material(areaReference.sharedMaterial) : null); if (mat != null) { _matInstance = mat; _sr.sharedMaterial = mat; } int lid = SortingLayer.IsValid(sortingLayerId) ? sortingLayerId : SortingLayer.layers[0].id; _sr.sortingLayerID = lid; _sr.sortingOrder = sortingOrder; } #endregion #region Per-frame Update private void UpdateResistancePulse(float dt) { if (_intensity < 0.5f && _intensity > 0.03f) { float suppression = 1f - _intensity; if (_time > _nextPulseTime) { _resistancePulse = Mathf.Max(_resistancePulse, suppression * (0.25f + Random.value * 0.5f)); _nextPulseTime = _time + 0.2f + Random.value * (2.5f - suppression * 2f); } } _resistancePulse *= Mathf.Exp(-dt * 9f); if (_resistancePulse < 0.008f) _resistancePulse = 0f; } /// 成组尖峰包络:"哈-哈-哈"式快攻击、弹跳衰减的脉冲串 private float GetLaughEnvelope(float activity) { float rate = laughRate * (0.4f + activity * 1.1f); float phase = _time * rate + _laughSeed; float cycle = phase - Mathf.Floor(phase); // 每个周期内是一串递减的尖峰(哈-哈-哈),而不是单个正弦波 float burst = 0f; for (int i = 0; i < 3; i++) { float spikeCenter = i * 0.28f; float d = cycle - spikeCenter; float spike = Mathf.Exp(-d * d * 220f) * (1f - i * 0.28f); burst += Mathf.Max(0f, spike); } return Mathf.Clamp01(burst) * activity; } private void RenderPixels() { if (_tex == null) return; float di = Mathf.Clamp(_intensity + _resistancePulse, 0f, 1.5f); DisplayIntensity = di; // 活动度:强度越高越活跃,且用陡峭曲线让压制过程变化明显 float activity = Mathf.Pow(Mathf.Clamp01(_intensity), 1.8f); float laugh = GetLaughEnvelope(activity); float baseFlicker = 1f + Mathf.Sin(_time * flickerRate) * flickerAmplitude * di; float laughBoost = 1f + laugh * laughAmplitude; float totalBoost = baseFlicker * laughBoost; // 点亮密度随活动度收缩:功率越低,能被点亮的格子越少(云变稀);满功率时基本全亮 float densityGate = 0.3f + activity * 0.75f + _resistancePulse * 0.6f; int w = _texWidth; int h = _texHeight; if (di < 0.01f) { var clear = new Color32(0, 0, 0, 0); for (int i = 0; i < _pixels.Length; i++) _pixels[i] = clear; _tex.SetPixels32(_pixels); _tex.Apply(false); return; } float nt = _time * noiseSpeed; for (int py = 0; py < h; py++) { float cy = (py / (float)(h - 1) - 0.5f) * 2f; for (int px = 0; px < w; px++) { int idx = py * w + px; float cx = (px / (float)(w - 1) - 0.5f) * 2f; // 超椭圆遮罩:只裁掉四角,让噪点云基本铺满整个管体 float cx2 = cx * cx; float cy2 = cy * cy; float super = cx2 * cx2 * cx2 + cy2 * cy2 * cy2; if (super > 1f) { _pixels[idx] = new Color32(0, 0, 0, 0); continue; } float edgeFade = 1f - super * super; float nx = px * (noiseScale / w) + _seedX; float ny = py * (noiseScale / w) + _seedY; float n = Mathf.PerlinNoise(nx + nt, ny - nt * 0.6f); // 每格固定 hash 抖动,避免整片同步开合,形成噪点云颗粒感 float hash = Frac(Mathf.Sin((px * 12.9898f + py * 78.233f) + _seedX) * 43758.5453f); // 基线抬高:满强度时整管基本填满,靠色阶档位区分明暗 float val = (0.35f + n * 0.75f) * edgeFade * totalBoost * di; val *= Mathf.Lerp(0.7f, 1.15f, hash); bool lit = hash < densityGate; if (!lit) { _pixels[idx] = new Color32(0, 0, 0, 0); continue; } Color32 c; if (val < thresholdLow) { _pixels[idx] = new Color32(0, 0, 0, 0); continue; } else if (val < thresholdMid) { c = lowColor; } else if (val < thresholdHigh) { c = midColor; } else { c = highColor; } if (_resistancePulse > 0.05f && hash > 1f - _resistancePulse) { c = highColor; } _pixels[idx] = c; } } _tex.SetPixels32(_pixels); _tex.Apply(false); } private static float Frac(float v) => v - Mathf.Floor(v); #endregion } }