-
Notifications
You must be signed in to change notification settings - Fork 17
feat: vertical orbit in Loading and Backpack character preview #10112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,11 +23,18 @@ public class CharacterPreviewAvatarContainer : MonoBehaviour, IDisposable | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private float fovTransitionStartTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private float fovTransitionStartValue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private bool isFOVTransitioning; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private float cameraPitch; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal Vector3 previewPositionInScene { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal Transform avatarParent { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal Camera camera { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal Transform cameraTarget { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal Transform cameraPivot { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Height, in the container's local space, that the orbiting camera may not descend below. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal float cameraFloorHeight { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal Transform rotationTarget { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal CinemachineFreeLook freeLookCamera { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [field: SerializeField] internal GameObject previewPlatform { get; private set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -39,6 +46,9 @@ public class CharacterPreviewAvatarContainer : MonoBehaviour, IDisposable | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal bool IsDragging { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal float LastDragTime { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal float AngularVelocity { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal float VerticalRotationModifier { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal float MaxVerticalAngle { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal float VerticalAngularVelocity { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Dispose() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -54,6 +64,7 @@ public void Initialize(RenderTexture targetTexture, Vector3 position) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IsDragging = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LastDragTime = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResetVerticalRotation(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // FOV | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TargetFOV = freeLookCamera.m_Lens.FieldOfView; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -110,20 +121,15 @@ private void UpdateRotation() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (RotationInertia <= 0f) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VerticalAngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Deceleration, higher inertia = faster deceleration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float decelerationRate = RotationInertia * ANGULAR_VELOCITY_DECELERATION_COEFF * UnityEngine.Time.deltaTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float velocitySign = Mathf.Sign(AngularVelocity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float velocityMagnitude = Mathf.Abs(AngularVelocity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| velocityMagnitude -= decelerationRate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (velocityMagnitude <= 0f) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AngularVelocity = velocitySign * velocityMagnitude; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AngularVelocity = Decelerate(AngularVelocity, decelerationRate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VerticalAngularVelocity = Decelerate(VerticalAngularVelocity, decelerationRate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Apply rotation if there's any angular velocity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -136,6 +142,45 @@ private void UpdateRotation() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rotation.y += rotationAmount; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rotationTarget.rotation = Quaternion.Euler(rotation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Mathf.Abs(VerticalAngularVelocity) > ANGULAR_VELOCITY_LOWER_THRES) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UpdateCameraPitch(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static float Decelerate(float angularVelocity, float decelerationRate) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float velocityMagnitude = Mathf.Abs(angularVelocity) - decelerationRate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return velocityMagnitude <= 0f ? 0f : Mathf.Sign(angularVelocity) * velocityMagnitude; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void UpdateCameraPitch() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float tiltedPitch = cameraPitch + (VerticalAngularVelocity * VerticalRotationModifier * UnityEngine.Time.deltaTime); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cameraPitch = Mathf.Clamp(tiltedPitch, -MaxVerticalAngle, Mathf.Min(MaxVerticalAngle, FloorPitchLimit())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Drop the inertia at the limit, or a flick keeps "arriving" after the camera has visibly stopped. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!Mathf.Approximately(cameraPitch, tiltedPitch)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VerticalAngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ApplyCameraPitch(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Downward pitch that sets the camera down on <see cref="cameraFloorHeight"/>. Descending past it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// would shoot the avatar from under its platform. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private float FloorPitchLimit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Undoing the pivot's rotation gives the camera's offset at rest, whatever the preset framed and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // wherever the pan left it. The rig turns rigidly about the pivot, so from there the camera's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // height traces radius * cos(pitch + phase); the floor is where that lands. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Vector3 restingOffset = cameraPivot.InverseTransformPoint(camera.transform.position); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float radius = new Vector2(restingOffset.y, restingOffset.z).magnitude; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float floorAbovePivot = cameraFloorHeight - cameraPivot.localPosition.y; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float phase = Mathf.Atan2(restingOffset.z, restingOffset.y) * Mathf.Rad2Deg; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (Mathf.Acos(Mathf.Clamp(floorAbovePivot / radius, -1f, 1f)) * Mathf.Rad2Deg) - phase; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void UpdateFOV() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -176,20 +221,46 @@ private void UpdateFOV() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| freeLookCamera.m_Lens.FieldOfView = newFOV; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Levels the camera back to its default elevation, keeping the avatar's own rotation and the zoom. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void ResetVerticalRotation() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VerticalAngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cameraPitch = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ApplyCameraPitch(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void ResetAvatarMovement() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reset rotation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rotationTarget.rotation = Quaternion.identity; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AngularVelocity = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IsDragging = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LastDragTime = 0f; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResetVerticalRotation(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reset FOV | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TargetFOV = freeLookCamera.m_Lens.FieldOfView; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fovTransitionStartTime = UnityEngine.Time.time; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fovTransitionStartValue = freeLookCamera.m_Lens.FieldOfView; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFOVTransitioning = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Turns the camera rig about the pivot the framing target hangs off, swinging the camera over | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// and under an avatar that stays upright: elevation changes, and the shot the screen was tuned | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// for holds. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void ApplyCameraPitch() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cameraPivot.localRotation = Quaternion.Euler(cameraPitch, 0f, 0f); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Position and aim damping would leave the camera seconds behind the drag. The orbit is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // already smoothed by its own inertia, so the rig takes this rotation undamped and lands on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the pose exactly. Damping stays in effect on every frame the orbit does not move. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| freeLookCamera.PreviousStateIsValid = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+250
to
+262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] R23 — Comment narrates Cinemachine's external behavior. The The inline comment on Suggested trim — keep the summary focused on what
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Serializable] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] R7 Nullability / Correctness — NaN propagation from division by zero in
FloorPitchLimit().When
restingOffset.yandrestingOffset.zare both zero (camera sits exactly at the pivot),radiusis0f. The expressionfloorAbovePivot / radiusproducesNaN(0/0) or±Infinity.Mathf.Clamp(NaN, -1, 1)returnsNaNon Mono/IL2CPP,Mathf.Acos(NaN)returnsNaN, andMathf.Min(MaxVerticalAngle, NaN)returnsNaN. OncecameraPitchabsorbsNaNthrough theMathf.Clampchain inUpdateCameraPitch(), every subsequent frame staysNaNand the camera rotation is permanently broken.While the Cinemachine orbits (radius 2.2–6) make
radius ≈ 0unlikely during normal operation, the guard is cheap and prevents a permanent NaN corruption if the camera ever passes through the pivot (e.g. during a zoom snap or a prefab misconfiguration).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checked by Claude and deliberately not applied. I traced it rather than taking it: the divisor can't reach zero (the camera hangs ~6 units off the pivot by rig construction, and zoom here drives FOV, not distance); at zero it would be -1f / 0f = -Infinity, which the existing Mathf.Clamp(…, -1f, 1f) already folds into a 40° fallback — the identical result to the proposed guard; and the claimed "permanent NaN corruption" can't occur because Mathf.Clamp returns value untouched when max is NaN. So it would be an unreachable branch, which CLAUDE.md names as a smell in its own right.