Cinemachine中噪音的应用
两种默认产生噪音的方式
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或者其他什么奇妙的操作来添加这个偏移量都可以。