321 lines
12 KiB
C#
321 lines
12 KiB
C#
using UnityEngine;
|
||
using UnityEngine.Serialization;
|
||
|
||
namespace AibisDream.MiniGame.HuoShan
|
||
{
|
||
/// <summary>
|
||
/// 笑话发生器 · 像素噪点云效果 — 逐像素写 Texture2D,量化色阶呈现粗颗粒像素风
|
||
/// 由 KnobController 通过 SetIntensity / TriggerPulse 驱动
|
||
/// 表现:满强度时噪点云活跃跳动(笑声节奏),随强度降低逐步被压制变稀变暗,
|
||
/// 但反抗脉冲会不断把云重新炸亮 —— 读作"抑制在进行,但没能成功"
|
||
/// </summary>
|
||
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);
|
||
|
||
/// <summary>当前显示强度(含脉冲)</summary>
|
||
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
|
||
|
||
/// <summary>设置基础强度(0=熄灭, 1=最亮, 可超过 1)</summary>
|
||
public void SetIntensity(float value)
|
||
{
|
||
_intensity = Mathf.Max(0f, value);
|
||
}
|
||
|
||
/// <summary>触发一次反抗脉冲爆亮</summary>
|
||
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<SpriteRenderer>();
|
||
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<SpriteRenderer>();
|
||
|
||
_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;
|
||
}
|
||
|
||
/// <summary>成组尖峰包络:"哈-哈-哈"式快攻击、弹跳衰减的脉冲串</summary>
|
||
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
|
||
}
|
||
}
|