兩種默認產生噪音的方式
Nosie階段的Component
Component在流水線中主要通過MuteCameraState來處理對State的計算。
對於Noise類型的Component來說,就是在MuteCameraState中,通過將噪音數據應用到State中的PositionCorrection和OrientationCorrection兩個字段上,來提供相機的抖動功能(比如Cinemachine提供的BasicMultiChannelPerlin)。
沒有開始和停止的概念。有Nosie文件的時候就會產生噪音,沒有就停止。
監聽ImpulseManager的Extension
通過對Impulse Sourse發出的震動事件(這個事件十分完善,有位置、半徑、持續時間等參數,模擬一個真實的震動)監聽的處理來產生震動。
噪音類
ISignalSource6D
ISignalSource6D就是Cinemachine提供的用來描述噪音數據的接口,主要提供三個能力:
- 保存噪音的數據。
- 獲取噪音的總時長,用來判斷噪音是否結束。
- 獲取某一時間點的噪音數據。
NoiseSettings
可作為ImpulseDefinition和BasicMultiChannelPerlin的噪音數據使用。
最上面兩行是NoiseSettings在Inspector面板中預覽的參數,分別是預覽的時間長度、圖像高度、是否動畫。
NoiseSetting中對旋轉、位置的每一個軸的震動都分別描述。
每個震動都可以由多個波疊加而成。
每個波由頻率和振幅描述,后面那個Toggle勾選上代表這個波是非隨機波(實際上就是使用Mathf.Cos函數計算),不勾選就是隨機的(Mathf.PerlinNoise函數)。
CinemachineFixedSignal
只能用於ImpulseDefinition的噪聲文件。
這個是可以用在沖擊(Impulse Source)中使用的噪音。只能對位置產生影響。
三個參數分別代表x、y、z軸的噪音曲線。
Tips
- BasicMultiChannelPerlin所產生噪聲在開始生效的時候會通過ReSeed對x、y、z軸初始數據做隨機偏移,導致每次開始震動的時機都不一樣。
- ImpulseListener產生的沖擊是可以選擇是否做隨機偏移的。
存在問題及擴展思路
Cinemachine自帶的兩種產生噪聲的方式比較單一,可能會不滿足復雜的噪音需求。
比如項目組之前已經有一套成熟的通過表格配置來描述一個噪音的方案。我們希望可以直接把這個表格的配置直接用在Cinemachine中怎么辦。
這里提供兩個思路:
- 寫一個可以使用表格數據的Component。
- 通過ImpulseManager和Extension來產生和處理這種表格所描述的噪音。
通過Component產生噪音
這里的例子是實現一個簡單的相機震屏效果,相機的震動是在相機空間內的,和相機當前的世界坐標和旋轉都無關。
首先我們需要一個可以描述表格數據的噪音類
假如我們的噪音在表格中是這么描述的:
延遲開始的時間 | xyz軸的震動強度 | 震動一次的時間 | 震動持續的總時間 |
---|---|---|---|
delay | strength | cycleTime | duration |
我們這個噪音類只是用來對表格中的噪音數據做一次轉換,來供ImpulseManager或Component來使用,並不是用來存儲噪音數據的,所以我們直接繼承ISignalSource6D就可以,不用繼承SignalSourceAsset。
因為功能很簡單,所以就直接貼一下代碼:
public class GameShakeSource : ISignalSource6D
{
public float Delay;
public Vector3 Strength;
public float CycleTime;
public float Duration;
public GameShakeSource(float delay, Vector3 strength, float cycleTime, float duration)
{
Delay = delay;
Strength = strength;
CycleTime = cycleTime;
Duration = duration;
}
//噪音持續的總時間,用於判斷這個噪音是否結束
public float SignalDuration
{
get
{
return Delay + Duration;
}
}
//根據當前噪音經過的時間,獲取噪音產生的位置和旋轉偏移量。
//因為表格中沒有旋轉相關的數據,所以直接返回identity值。
public void GetSignal(float timeSinceSignalStart, out Vector3 pos, out Quaternion rot)
{
if(timeSinceSignalStart <= Delay)
{
pos = Vector3.zero;
}
else
{
float times = timeSinceSignalStart / (CycleTime / 4);
int cycle25Count = Mathf.FloorToInt(times);
float inCycle25Time = times - cycle25Count;
if(cycle25Count % 4 == 0)
{
pos = Vector3.Lerp(Vector3.zero, Strength, inCycle25Time);
}
else if(cycle25Count % 4 == 1)
{
pos = Vector3.Lerp(Strength, Vector3.zero, inCycle25Time);
}
else if (cycle25Count % 4 == 2)
{
pos = Vector3.Lerp(Vector3.zero, -Strength, inCycle25Time);
}
else
{
pos = Vector3.Lerp(-Strength, Vector3.zero, inCycle25Time);
}
}
rot = Quaternion.identity;
}
}
使用這個噪音文件的Component:
public class CinemachineShake : CinemachineComponentBase
{
public ISignalSource6D ShakeSetting;
public override bool IsValid { get { return enabled; } }
public override CinemachineCore.Stage Stage { get { return CinemachineCore.Stage.Noise; } }
private float mNoiseTime;
private Matrix4x4 shakeMatrix = new Matrix4x4();
//VirtualCamera用來在流水線中計算State的接口
public override void MutateCameraState(ref CameraState curState, float deltaTime)
{
if (!IsValid || deltaTime < 0)
return;
if (ShakeSetting == null || mNoiseTime > ShakeSetting.SignalDuration)
return;
mNoiseTime += deltaTime;
ShakeSetting.GetSignal(mNoiseTime, out Vector3 pos, out Quaternion rot);
//因為這里是希望實現的是震屏功能,所以需要將ShakeSetting計算出的相機空間中的偏移量,轉化為世界坐標中的偏移量。
//直接用相機的旋轉生成的矩陣乘一下就可以了
shakeMatrix.SetTRS(Vector3.zero, curState.FinalOrientation, Vector3.one);
//把位置偏移量應用到State上
curState.PositionCorrection += shakeMatrix.MultiplyPoint(-pos);
rot = Quaternion.SlerpUnclamped(Quaternion.identity, rot, -1);
//把旋轉偏移量應用到State上
curState.OrientationCorrection = curState.OrientationCorrection * rot;
}
public void Shake(ISignalSource6D shakeSetting)
{
ShakeSetting = shakeSetting;
mNoiseTime = 0;
}
public void Shake(float delay, Vector3 strength, float cycleTime, float duration)
{
Shake(new GameShakeSource(delay, strength, cycleTime, duration));
}
public void Shake()
{
mNoiseTime = 0;
}
}
使用的時候調這個Component的Shake接口即可。
通過Extension產生噪音
噪音類就直接用上面的那個。
先提供一個新的Chanel用於這個震屏,用來和普通沖擊產生的震動做區分。
寫一個ShakeManager代替ImpulseSource產生Impulse事件,直接生成事件加到ImpulseManager中。
public class ShakeManager
{
public static void Test()
{
AddShake(0, new Vector3(0.3f, 0.3f, 0), 0.2f, 0.1f);
}
public static void AddShake(float delay, Vector3 strength, float cycleTime, float duration)
{
CinemachineImpulseManager.ImpulseEvent e
= CinemachineImpulseManager.Instance.NewImpulseEvent();
e.m_Envelope = new CinemachineImpulseManager.EnvelopeDefinition();
//開始和衰減階段的時間都填0,只留下中間一段時間
e.m_Envelope.m_AttackTime = 0;
e.m_Envelope.m_DecayTime = 0;
e.m_Envelope.m_SustainTime = delay + duration;
e.m_SignalSource = new GameShakeSource(delay, strength, cycleTime, duration);
//產生沖擊的位置和影響半徑,這里填Vector3.zero和float.MaxValue,
//獲取的震動數據的時候從Vector3.zero這個位置獲取就可以獲取全量沒有衰減的數據。
e.m_Position = Vector3.zero;
e.m_Radius = float.MaxValue;
//2就是剛定義的gameShakeChannel
e.m_Channel = 2;
//選Fixed,不希望震動的方向對相機產生額外影響
e.m_DirectionMode = CinemachineImpulseManager.ImpulseEvent.DirectionMode.Fixed;
//衰減方式隨便填,這里用不到
e.m_DissipationMode = CinemachineImpulseManager.ImpulseEvent.DissipationMode.LinearDecay;
//這個也用不到
e.m_DissipationDistance = 0;
CinemachineImpulseManager.Instance.AddImpulseEvent(e);
}
}
寫一個處理這類震動數據的Extension。
public class CinemachineShakeListener : CinemachineExtension
{
[Tooltip("Impulse events on channels not included in the mask will be ignored.")]
[CinemachineImpulseChannelProperty]
public int m_ChannelMask = 1;
[Tooltip("Gain to apply to the Impulse signal. 1 is normal strength. Setting this to 0 completely mutes the signal.")]
public float m_Gain = 1;
[Tooltip("Enable this to perform distance calculation in 2D (ignore Z)")]
public bool m_Use2DDistance = false;
private Matrix4x4 shakeMatrix = new Matrix4x4();
//VirtualCamera用來在流水線中計算State的接口
protected override void PostPipelineStageCallback(
CinemachineVirtualCameraBase vcam,
CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
{
//由於這個接口在么個階段后都會調用,所以要加這個判斷。
//保證只在Aim結束后指調用一次
if (stage == CinemachineCore.Stage.Aim)
{
Vector3 impulsePos = Vector3.zero;
Quaternion impulseRot = Quaternion.identity;
//直接調ImpulseManager的接口獲取gameShakeChannel產生的震動數據,
//位置填zero,保證噪音不會衰減
if (CinemachineImpulseManager.Instance.GetImpulseAt(
Vector3.zero, m_Use2DDistance, m_ChannelMask, out impulsePos, out impulseRot))
{
//轉換到世界坐標
shakeMatrix.SetTRS(Vector3.zero, state.FinalOrientation, Vector3.one);
//增加強度參數的影響后,應用到當前State上
state.PositionCorrection += shakeMatrix.MultiplyPoint(impulsePos * -m_Gain);
impulseRot = Quaternion.SlerpUnclamped(Quaternion.identity, impulseRot, -m_Gain);
state.OrientationCorrection = state.OrientationCorrection * impulseRot;
}
}
}
}
其他方案
也可以選擇不通過將自己組裝的ImpulseEvent傳給ImpulseManager來產生震動。
直接單獨寫一個Manager來專門管理這一類震動。通過Extension直接從這個Manager中獲取震動數據。就可以避免ImpulseManager中的一些比如范圍判斷、強度衰減等無效計算。
效果
小結
Cinemachine中的噪音的核心思路其實就是在相機的基本位置旋轉(也就是流水線中的Aim階段之后)確定后,為相機添加一個額外的偏移量(OrientationCorrection,PositionCorrection參數)。
不管是通過Compoent、Extension或者其他什么奇妙的操作來添加這個偏移量都可以。