/
CinemachineGroupFraming.cs
385 lines (336 loc) · 18.8 KB
/
CinemachineGroupFraming.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
using UnityEngine;
namespace Unity.Cinemachine
{
/// <summary>
/// An add-on module for Cinemachine Camera that adjusts the framing if the tracking
/// target implements ICinemachineTargetGroup.
///
/// An attempt will be made to fit the entire target group within the specified framing.
/// Camera position and/or rotation may be adjusted, depending on the settings.
/// </summary>
[AddComponentMenu("Cinemachine/Procedural/Extensions/Cinemachine Group Framing")]
[ExecuteAlways]
[SaveDuringPlay]
[HelpURL(Documentation.BaseURL + "manual/CinemachineGroupFraming.html")]
public class CinemachineGroupFraming : CinemachineExtension
{
/// <summary>What screen dimensions to consider when framing</summary>
public enum FramingModes
{
/// <summary>Consider only the horizontal dimension. Vertical framing is ignored.</summary>
Horizontal,
/// <summary>Consider only the vertical dimension. Horizontal framing is ignored.</summary>
Vertical,
/// <summary>The larger of the horizontal and vertical dimensions will dominate, to get the best fit.</summary>
HorizontalAndVertical
};
/// <summary>What screen dimensions to consider when framing</summary>
[Tooltip("What screen dimensions to consider when framing. Can be Horizontal, Vertical, or both")]
public FramingModes FramingMode = FramingModes.HorizontalAndVertical;
/// <summary>How much of the screen to fill with the bounding box of the targets.</summary>
[Tooltip("The bounding box of the targets should occupy this amount of the screen space. "
+ "1 means fill the whole screen. 0.5 means fill half the screen, etc.")]
[Range(0, 2)]
public float FramingSize = 0.8f;
/// <summary>A nonzero value will offset the group in the camera frame.</summary>
[Tooltip("A nonzero value will offset the group in the camera frame.")]
public Vector2 CenterOffset = Vector2.zero;
/// <summary>How aggressively the camera tries to frame the group.
/// Small numbers are more responsive</summary>
[Range(0, 20)]
[Tooltip("How aggressively the camera tries to frame the group. Small numbers are more responsive, "
+ "rapidly adjusting the camera to keep the group in the frame. Larger numbers give a heavier "
+ "more slowly responding camera.")]
public float Damping = 2f;
/// <summary>How to adjust the camera to get the desired framing size</summary>
public enum SizeAdjustmentModes
{
/// <summary>Do not move the camera, only adjust the FOV.</summary>
ZoomOnly,
/// <summary>Just move the camera, don't change the FOV.</summary>
DollyOnly,
/// <summary>Move the camera as much as permitted by the ranges, then
/// adjust the FOV if necessary to make the shot.</summary>
DollyThenZoom
};
/// <summary>How to adjust the camera to get the desired framing</summary>
[Tooltip("How to adjust the camera to get the desired framing size. You can zoom, dolly in/out, or do both.")]
public SizeAdjustmentModes SizeAdjustment = SizeAdjustmentModes.DollyThenZoom;
/// <summary>How to adjust the camera to get the desired horizontal and vertical framing</summary>
public enum LateralAdjustmentModes
{
/// <summary>Do not rotate the camera to reframe, only change the position.</summary>
ChangePosition,
/// <summary>Rotate the camera to reframe, do not change the position.</summary>
ChangeRotation
};
/// <summary>How to adjust the camera to get the desired horizontal and vertical framing</summary>
[Tooltip("How to adjust the camera to get the desired horizontal and vertical framing.")]
public LateralAdjustmentModes LateralAdjustment = LateralAdjustmentModes.ChangePosition;
/// <summary>Allowable FOV range, if adjusting FOV</summary>
[Tooltip("Allowable FOV range, if adjusting FOV.")]
[MinMaxRangeSlider(1, 179)]
public Vector2 FovRange = new (1, 100);
/// <summary>Allowable range for the camera to move. 0 is the undollied position.
/// Negative values move the camera closer to the target.</summary>
[Tooltip("Allowable range for the camera to move. 0 is the undollied position. "
+ "Negative values move the camera closer to the target.")]
[Vector2AsRange]
public Vector2 DollyRange = new (-100, 100);
/// <summary>Allowable orthographic size range, if adjusting orthographic size</summary>
[Tooltip("Allowable orthographic size range, if adjusting orthographic size.")]
[Vector2AsRange]
public Vector2 OrthoSizeRange = new Vector2(1, 1000);
const float k_MinimumGroupSize = 0.01f;
void OnValidate()
{
FramingSize = Mathf.Max(k_MinimumGroupSize, FramingSize);
Damping = Mathf.Max(0, Damping);
DollyRange.y = Mathf.Max(DollyRange.x, DollyRange.y);
FovRange.x = Mathf.Clamp(FovRange.x, 1, 179);
FovRange.y = Mathf.Max(FovRange.x, FovRange.y);
FovRange.y = Mathf.Clamp(FovRange.y, 1, 179);
OrthoSizeRange.x = Mathf.Max(0.01f, OrthoSizeRange.x);
OrthoSizeRange.y = Mathf.Max(OrthoSizeRange.x, OrthoSizeRange.y);
}
void Reset()
{
FramingMode = FramingModes.HorizontalAndVertical;
SizeAdjustment = SizeAdjustmentModes.DollyThenZoom;
LateralAdjustment = LateralAdjustmentModes.ChangePosition;
FramingSize = 0.8f;
CenterOffset = Vector2.zero;
Damping = 2;
DollyRange = new Vector2(-100, 100);
FovRange = new Vector2(1, 100);
OrthoSizeRange = new Vector2(1, 1000);
}
/// <summary>For editor visualization of the calculated bounding box of the group</summary>
internal Bounds GroupBounds;
/// <summary>For editor visualization of the calculated bounding box of the group</summary>
internal Matrix4x4 GroupBoundsMatrix;
class VcamExtraState : VcamExtraStateBase
{
public Vector3 PosAdjustment;
public Vector2 RotAdjustment;
public float FovAdjustment;
public void Reset()
{
PosAdjustment = Vector3.zero;
RotAdjustment = Vector2.zero;
FovAdjustment = 0;
}
};
/// <summary>
/// Report maximum damping time needed for this extension.
/// Only used in editor for timeline scrubbing.
/// </summary>
/// <returns>Highest damping setting in this extension</returns>
public override float GetMaxDampTime() => Damping;
/// <summary>Callback to tweak the settings</summary>
/// <param name="vcam">The virtual camera being processed</param>
/// <param name="stage">The current pipeline stage</param>
/// <param name="state">The current virtual camera state</param>
/// <param name="deltaTime">The current applicable deltaTime</param>
protected override void PostPipelineStageCallback(
CinemachineVirtualCameraBase vcam,
CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
{
// We have to do it after both Body and Aim, and the only way to ensure that is to
// do it after noise (because body and aim can be inverted).
// We ignore the noise effect anyway, so it doesn't hurt.
if (stage != CinemachineCore.Stage.Noise)
return;
var group = vcam.LookAtTargetAsGroup;
group ??= vcam.FollowTargetAsGroup;
if (group == null || !group.IsValid)
return;
var extra = GetExtraState<VcamExtraState>(vcam);
if (!vcam.PreviousStateIsValid)
extra.Reset();
if (state.Lens.Orthographic)
OrthoFraming(vcam, group, extra, ref state, deltaTime);
else
PerspectiveFraming(vcam, group, extra, ref state, deltaTime);
}
void OrthoFraming(
CinemachineVirtualCameraBase vcam, ICinemachineTargetGroup group,
VcamExtraState extra, ref CameraState state, float deltaTime)
{
var damping = vcam.PreviousStateIsValid && deltaTime >= 0 ? Damping : 0;
// Position adjustment: work in camera-local coords
GroupBoundsMatrix = Matrix4x4.TRS(state.RawPosition, state.RawOrientation, Vector3.one);
GroupBounds = group.GetViewSpaceBoundingBox(GroupBoundsMatrix, true);
var camPos = GroupBounds.center;
camPos.z = Mathf.Min(0, camPos.z - GroupBounds.extents.z);
// Ortho size adjustment
var lens = state.Lens;
var targetHeight = GetFrameHeight(GroupBounds.size / FramingSize, lens.Aspect) * 0.5f;
targetHeight = Mathf.Clamp(targetHeight, OrthoSizeRange.x, OrthoSizeRange.y);
var deltaFov = targetHeight - lens.OrthographicSize;
extra.FovAdjustment += vcam.DetachedFollowTargetDamp(deltaFov - extra.FovAdjustment, damping, deltaTime);
lens.OrthographicSize += extra.FovAdjustment;
camPos.x -= CenterOffset.x * lens.OrthographicSize / lens.Aspect;
camPos.y -= CenterOffset.y * lens.OrthographicSize;
extra.PosAdjustment += vcam.DetachedFollowTargetDamp(camPos - extra.PosAdjustment, damping, deltaTime);
state.PositionCorrection += state.RawOrientation * extra.PosAdjustment;
state.Lens = lens;
}
void PerspectiveFraming(
CinemachineVirtualCameraBase vcam, ICinemachineTargetGroup group,
VcamExtraState extra, ref CameraState state, float deltaTime)
{
var damping = vcam.PreviousStateIsValid && deltaTime >= 0 ? Damping : 0;
var camPos = state.RawPosition;
var camRot = state.RawOrientation;
var up = camRot * Vector3.up;
var fov = state.Lens.FieldOfView;
// Get a naive bounds for the group, and pull the camera out as far as we can
// to see as many members as possible. Group members behind the camera will be ignored.
var canDollyOut = SizeAdjustment != SizeAdjustmentModes.ZoomOnly;
var dollyRange = canDollyOut ? DollyRange : Vector2.zero;
var m = Matrix4x4.TRS(camPos, camRot, Vector3.one);
var b = group.GetViewSpaceBoundingBox(m, canDollyOut);
var moveCamera = LateralAdjustment == LateralAdjustmentModes.ChangePosition;
if (!moveCamera)
{
// Set up the initial rotation
var fwd = m.MultiplyPoint3x4(b.center) - camPos;
if (!Vector3.Cross(fwd, up).AlmostZero())
camRot = Quaternion.LookRotation(fwd, up);
}
const float slush = 5; // avoid the members getting too close to the camera
var dollyAmount = Mathf.Clamp(Mathf.Min(0, b.center.z) - b.extents.z - slush, dollyRange.x, dollyRange.y);
camPos += camRot * new Vector3(0, 0, dollyAmount);
// Approximate looking at the group center, then correct for actual center
ComputeCameraViewGroupBounds(group, ref camPos, ref camRot, moveCamera);
AdjustSize(group, state.Lens.Aspect, ref camPos, ref camRot, ref fov, ref dollyAmount);
// Apply the adjustments
var lens = state.Lens;
var deltaFov = fov - lens.FieldOfView;
extra.FovAdjustment += vcam.DetachedFollowTargetDamp(deltaFov - extra.FovAdjustment, damping, deltaTime);
lens.FieldOfView += extra.FovAdjustment;
state.Lens = lens;
var deltaRot = state.RawOrientation.GetCameraRotationToTarget(camRot * Vector3.forward, up);
extra.RotAdjustment.x += vcam.DetachedFollowTargetDamp(deltaRot.x - extra.RotAdjustment.x, damping, deltaTime);
extra.RotAdjustment.y += vcam.DetachedFollowTargetDamp(deltaRot.y - extra.RotAdjustment.y, damping, deltaTime);
state.OrientationCorrection = state.OrientationCorrection * Quaternion.identity.ApplyCameraRotation(extra.RotAdjustment, up);
var deltaPos = Quaternion.Inverse(state.RawOrientation) * (camPos - state.RawPosition);
extra.PosAdjustment += vcam.DetachedFollowTargetDamp(deltaPos - extra.PosAdjustment, damping, deltaTime);
state.PositionCorrection += state.RawOrientation * extra.PosAdjustment;
// Apply framing offset
if (Mathf.Abs(CenterOffset.x) > 0.01f ||Mathf.Abs(CenterOffset.y) > 0.01f)
{
var halfFov = 0.5f * state.Lens.FieldOfView;
if (moveCamera)
{
var d = GroupBounds.center.z - GroupBounds.extents.z;
state.PositionCorrection -= state.RawOrientation * new Vector3(
CenterOffset.x * Mathf.Tan(halfFov * Mathf.Deg2Rad * state.Lens.Aspect) * d,
CenterOffset.y * Mathf.Tan(halfFov * Mathf.Deg2Rad) * d,
0);
}
else
{
var rot = new Vector2(CenterOffset.y * halfFov, CenterOffset.x * halfFov / state.Lens.Aspect);
state.OrientationCorrection *= Quaternion.identity.ApplyCameraRotation(rot, state.ReferenceUp);
}
}
}
void AdjustSize(
ICinemachineTargetGroup group, float aspect,
ref Vector3 camPos, ref Quaternion camRot, ref float fov, ref float dollyAmount)
{
// Dolly mode: Adjust camera distance
if (SizeAdjustment != SizeAdjustmentModes.ZoomOnly)
{
// What distance from near edge would be needed to get the desired frame height, at the current FOV
var frameHeight = GetFrameHeight(GroupBounds.size / FramingSize, aspect);
var currentDistance = GroupBounds.center.z - GroupBounds.extents.z;
var desiredDistance = frameHeight / (2f * Mathf.Tan(fov * Mathf.Deg2Rad / 2f));
float dolly = currentDistance - desiredDistance;
// Clamp to respect min/max camera movement
dolly = Mathf.Clamp(dolly + dollyAmount, DollyRange.x, DollyRange.y) - dollyAmount;
dollyAmount += dolly;
// Because moving the camera affects the view space bounds, we recompute after movement
camPos += camRot * new Vector3(0, 0, dolly);
ComputeCameraViewGroupBounds(group, ref camPos, ref camRot, true);
}
// Zoom mode: Adjust lens
if (SizeAdjustment != SizeAdjustmentModes.DollyOnly)
{
var frameHeight = GetFrameHeight(GroupBounds.size / FramingSize, aspect);
var distance = GroupBounds.center.z - GroupBounds.extents.z;
if (distance > Epsilon)
fov = 2f * Mathf.Atan(frameHeight / (2 * distance)) * Mathf.Rad2Deg;
fov = Mathf.Clamp(fov, FovRange.x, FovRange.y);
}
}
/// <summary>Computes GroupBoundsMatrix and GroupBounds</summary>
void ComputeCameraViewGroupBounds(
ICinemachineTargetGroup group, ref Vector3 camPos, ref Quaternion camRot, bool moveCamera)
{
GroupBoundsMatrix = Matrix4x4.TRS(camPos, camRot, Vector3.one);
// Initial naive approximation
if (moveCamera)
{
GroupBounds = group.GetViewSpaceBoundingBox(GroupBoundsMatrix, false);
var pos = GroupBounds.center; pos.z = 0;
camPos = GroupBoundsMatrix.MultiplyPoint3x4(pos);
GroupBoundsMatrix = Matrix4x4.TRS(camPos, camRot, Vector3.one);
}
group.GetViewSpaceAngularBounds(GroupBoundsMatrix, out var minAngles, out var maxAngles, out var zRange);
var shift = (minAngles + maxAngles) / 2;
var adjustment = Quaternion.identity.ApplyCameraRotation(shift, Vector3.up);
if (moveCamera)
{
// We shift only in the camera XY plane - there is no Z movement.
// The result is approximate - accuracy drops when there are big z differences in members.
// This could be improved with multiple iterations, but it's not worth it.
var dir = adjustment * Vector3.forward;
new Plane(Vector3.forward, new Vector3(0, 0, zRange.x)).Raycast(new Ray(Vector3.zero, dir), out var t);
camPos = dir * t; camPos.z = 0;
camPos = GroupBoundsMatrix.MultiplyPoint3x4(camPos);
GroupBoundsMatrix.SetColumn(3, camPos);
// Account for parallax: recompute bounds after shifting position.
group.GetViewSpaceAngularBounds(GroupBoundsMatrix, out minAngles, out maxAngles, out zRange);
}
else
{
// Rotate to look at center - no parallax shift to worry about
camRot *= adjustment;
GroupBoundsMatrix = Matrix4x4.TRS(camPos, camRot, Vector3.one);
minAngles -= shift;
maxAngles -= shift;
}
// For width and height (in camera space) of the bounding box, we use the values
// at the near end of the box. The gizmo drawer will take this into account
// when displaying the frustum bounds of the group
Vector2 angles = new Vector2(89.5f, 89.5f);
if (zRange.x > 0)
{
angles = Vector2.Max(maxAngles, UnityVectorExtensions.Abs(minAngles));
angles = Vector2.Min(angles, new Vector2(89.5f, 89.5f));
}
var twiceNear = zRange.x * 2;
angles *= Mathf.Deg2Rad;
GroupBounds = new Bounds(
new Vector3(0, 0, (zRange.x + zRange.y) * 0.5f),
new Vector3(Mathf.Tan(angles.y) * twiceNear, Mathf.Tan(angles.x) * twiceNear, zRange.y - zRange.x));
}
float GetFrameHeight(Vector2 boundsSize, float aspect)
{
float h;
switch (FramingMode)
{
case FramingModes.Horizontal: h = Mathf.Max(Epsilon, boundsSize.x) / aspect; break;
case FramingModes.Vertical: h = Mathf.Max(Epsilon, boundsSize.y); break;
default:
case FramingModes.HorizontalAndVertical:
h = Mathf.Max(Mathf.Max(Epsilon, boundsSize.x) / aspect, Mathf.Max(Epsilon, boundsSize.y));
break;
}
return Mathf.Max(h, k_MinimumGroupSize);
}
}
}