// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= CameraController.cpp: Implements controls for a camera with pseudo-physics =============================================================================*/ #include "CameraController.h" #include "EditorModeManager.h" #include "EditorModes.h" /** Constructor */ FEditorCameraController::FEditorCameraController() : Config(), MovementVelocity(0.0f, 0.0f, 0.0f), FOVVelocity(0.0f), RotationVelocityEuler(0.0f, 0.0f, 0.0f), OriginalFOVForRecoil(-1.0f) // -1.0f here means 'initialize me on demand' { } /** * Updates the position and orientation of the camera as well as other state (like velocity.) Should be * called every frame. * * @param UserImpulseData Input data from the user this frame * @param DeltaTime Time interval since last update * @param bAllowRecoilIfNoImpulse True if we should recoil FOV if needed * @param MovementSpeedScale Scales the speed of movement * @param InOutCameraPosition [in, out] Camera position * @param InOutCameraEuler [in, out] Camera orientation * @param InOutCameraFOV [in, out] Camera field of view */ void FEditorCameraController::UpdateSimulation( const FCameraControllerUserImpulseData& UserImpulseData, const float DeltaTime, const bool bAllowRecoilIfNoImpulse, const float MovementSpeedScale, FVector& InOutCameraPosition, FVector& InOutCameraEuler, float& InOutCameraFOV) { bool bAnyUserImpulse = false; // Apply dead zone test to user impulse data // ApplyImpulseDeadZone( UserImpulseData, FinalUserImpulse, bAnyUserImpulse ); if (UserImpulseData.RotateYawVelocityModifier != 0.0f || UserImpulseData.RotatePitchVelocityModifier != 0.0f || UserImpulseData.RotateRollVelocityModifier != 0.0f || UserImpulseData.MoveForwardBackwardImpulse != 0.0f || UserImpulseData.MoveRightLeftImpulse != 0.0f || UserImpulseData.MoveUpDownImpulse != 0.0f || UserImpulseData.ZoomOutInImpulse != 0.0f || UserImpulseData.RotateYawImpulse != 0.0f || UserImpulseData.RotatePitchImpulse != 0.0f || UserImpulseData.RotateRollImpulse != 0.0f) { bAnyUserImpulse = true; } FVector TranslationCameraEuler = InOutCameraEuler; if (Config.bPlanarCamera) { // remove roll TranslationCameraEuler.X = 0; // remove pitch TranslationCameraEuler.Y = 0; } // Movement UpdatePosition(UserImpulseData, DeltaTime, MovementSpeedScale, TranslationCameraEuler, InOutCameraPosition); // Rotation UpdateRotation(UserImpulseData, DeltaTime, InOutCameraEuler); // FOV UpdateFOV(UserImpulseData, DeltaTime, InOutCameraFOV); // Recoil camera FOV if we need to ApplyRecoil(DeltaTime, bAllowRecoilIfNoImpulse, bAnyUserImpulse, InOutCameraFOV); } /**true if this camera currently has rotational velocity*/ bool FEditorCameraController::IsRotating(void) const { if ((RotationVelocityEuler.X != 0.0f) || (RotationVelocityEuler.Y != 0.0f) || (RotationVelocityEuler.Z != 0.0f)) { return true; } return false; } /** * Applies the dead zone setting to the incoming user impulse data * * @param InUserImpulse Input user impulse data * @param OutUserImpulse [out] Output user impulse data with dead zone applied * @param bOutAnyImpulseData [out] True if there was any user impulse this frame */ void FEditorCameraController::ApplyImpulseDeadZone(const FCameraControllerUserImpulseData& InUserImpulse, FCameraControllerUserImpulseData& OutUserImpulse, bool& bOutAnyImpulseData) { FMemory::Memzero(&OutUserImpulse, sizeof(OutUserImpulse)); // Keep track if there is any actual user input. This is so that when all of the flight controls // are released, we can take action (such as resetting the camera FOV back to what it was.) bOutAnyImpulseData = false; if (FMath::Abs(InUserImpulse.MoveRightLeftImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.MoveRightLeftImpulse = InUserImpulse.MoveRightLeftImpulse; bOutAnyImpulseData = true; } if (FMath::Abs(InUserImpulse.MoveForwardBackwardImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.MoveForwardBackwardImpulse = InUserImpulse.MoveForwardBackwardImpulse; bOutAnyImpulseData = true; } if (FMath::Abs(InUserImpulse.MoveUpDownImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.MoveUpDownImpulse = InUserImpulse.MoveUpDownImpulse; bOutAnyImpulseData = true; } if (FMath::Abs(InUserImpulse.RotateYawImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.RotateYawImpulse = InUserImpulse.RotateYawImpulse; bOutAnyImpulseData = true; } if (FMath::Abs(InUserImpulse.RotatePitchImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.RotatePitchImpulse = InUserImpulse.RotatePitchImpulse; bOutAnyImpulseData = true; } if (FMath::Abs(InUserImpulse.RotateRollImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.RotateRollImpulse = InUserImpulse.RotateRollImpulse; bOutAnyImpulseData = true; } if (FMath::Abs(InUserImpulse.ZoomOutInImpulse) >= Config.ImpulseDeadZoneAmount) { OutUserImpulse.ZoomOutInImpulse = InUserImpulse.ZoomOutInImpulse; bOutAnyImpulseData = true; } // No dead zone for these OutUserImpulse.RotateYawVelocityModifier = InUserImpulse.RotateYawVelocityModifier; OutUserImpulse.RotatePitchVelocityModifier = InUserImpulse.RotatePitchVelocityModifier; OutUserImpulse.RotateRollVelocityModifier = InUserImpulse.RotateRollVelocityModifier; if (OutUserImpulse.RotateYawVelocityModifier != 0.0f || OutUserImpulse.RotatePitchVelocityModifier != 0.0f || OutUserImpulse.RotateRollVelocityModifier != 0.0f) { bOutAnyImpulseData = true; } } /** * Updates the camera position. Called every frame by UpdateSimulation. * * @param UserImpulse User impulse data for the current frame * @param DeltaTime Time interval * @param MovementSpeedScale Additional movement accel/speed scale * @param CameraEuler Current camera rotation * @param InOutCameraPosition [in, out] Camera position */ void FEditorCameraController::UpdatePosition(const FCameraControllerUserImpulseData& UserImpulse, const float DeltaTime, const float MovementSpeedScale, const FVector& CameraEuler, FVector& InOutCameraPosition) { // Compute local impulse FVector LocalSpaceImpulse; { // NOTE: Forward/back and right/left impulse are applied in local space, but up/down impulse is // applied in world space. This is because it feels more intuitive to always move straight // up or down with those controls. LocalSpaceImpulse = FVector(UserImpulse.MoveForwardBackwardImpulse, // Local space forward/back UserImpulse.MoveRightLeftImpulse, // Local space right/left 0.0f); // Local space up/down } // Compute world space acceleration FVector WorldSpaceAcceleration; { // Compute camera orientation, then rotate our local space impulse to world space const FQuat CameraOrientation = FQuat::MakeFromEuler(CameraEuler); FVector WorldSpaceImpulse = CameraOrientation.RotateVector(LocalSpaceImpulse); // Accumulate any world space impulse we may have // NOTE: Up/down impulse is applied in world space. See above comments for more info. WorldSpaceImpulse += FVector(0.0f, // World space forward/back 0.0f, // World space right/left UserImpulse.MoveUpDownImpulse); // World space up/down // Cap impulse by normalizing, but only if our magnitude is greater than 1.0 // if( WorldSpaceImpulse.SizeSquared() > 1.0f ) { // WorldSpaceImpulse = WorldSpaceImpulse.UnsafeNormal(); } // Compute world space acceleration WorldSpaceAcceleration = WorldSpaceImpulse * Config.MovementAccelerationRate * MovementSpeedScale; } if (Config.bUsePhysicsBasedMovement) { // Accelerate the movement velocity MovementVelocity += WorldSpaceAcceleration * DeltaTime; // Apply damping { const float DampingFactor = FMath::Clamp(Config.MovementVelocityDampingAmount * DeltaTime, 0.0f, 0.75f); // Decelerate MovementVelocity += -MovementVelocity * DampingFactor; } } else { // No physics, so just use the acceleration as our velocity MovementVelocity = WorldSpaceAcceleration; } // Constrain maximum movement speed if (MovementVelocity.SizeSquared() > FMath::Square(Config.MaximumMovementSpeed * MovementSpeedScale)) { MovementVelocity = MovementVelocity.GetUnsafeNormal() * Config.MaximumMovementSpeed * MovementSpeedScale; } // Clamp velocity to a reasonably small number if (MovementVelocity.SizeSquared() < FMath::Square(KINDA_SMALL_NUMBER)) { MovementVelocity = FVector::ZeroVector; } // Update camera position InOutCameraPosition += MovementVelocity * DeltaTime; } /** * Updates the camera rotation. Called every frame by UpdateSimulation. * * @param UserImpulse User impulse data for this frame * @param DeltaTime Time interval * @param InOutCameraEuler [in, out] Camera rotation */ void FEditorCameraController::UpdateRotation(const FCameraControllerUserImpulseData& UserImpulse, const float DeltaTime, FVector& InOutCameraEuler) { FVector RotateImpulseEuler = FVector(UserImpulse.RotateRollImpulse, UserImpulse.RotatePitchImpulse, UserImpulse.RotateYawImpulse); FVector RotateVelocityModifierEuler = FVector(UserImpulse.RotateRollVelocityModifier, UserImpulse.RotatePitchVelocityModifier, UserImpulse.RotateYawVelocityModifier); // Iterate for each euler axis - yaw, pitch and roll for (int32 CurRotationAxis = 0; CurRotationAxis < 3; ++CurRotationAxis) { // This will serve as both our source and destination rotation value float& RotationVelocity = RotationVelocityEuler[CurRotationAxis]; const float RotationImpulse = RotateImpulseEuler[CurRotationAxis]; const float RotationVelocityModifier = RotateVelocityModifierEuler[CurRotationAxis]; // Compute acceleration float RotationAcceleration = RotationImpulse * Config.RotationAccelerationRate; if (Config.bUsePhysicsBasedRotation || Config.bForceRotationalPhysics) { // Accelerate the rotation velocity RotationVelocity += RotationAcceleration * DeltaTime; // Apply velocity modifier. This is used for mouse-look based camera rotation, where // we don't need to account for DeltaTime, since the value is based on an explicit number // of degrees per cursor pixel moved. RotationVelocity += RotationVelocityModifier; // Apply damping { const float DampingFactor = FMath::Clamp(Config.RotationVelocityDampingAmount * DeltaTime, 0.0f, 0.75f); // Decelerate RotationVelocity += -RotationVelocity * DampingFactor; } } else { // No physics, so just use the acceleration as our velocity RotationVelocity = RotationAcceleration; // Apply velocity modifier. This is used for mouse-look based camera rotation, where // we don't need to account for DeltaTime, since the value is based on an explicit number // of degrees per cursor pixel moved. RotationVelocity += RotationVelocityModifier; } // Constrain maximum rotation speed RotationVelocity = FMath::Clamp(RotationVelocity, -Config.MaximumRotationSpeed, Config.MaximumRotationSpeed); // Clamp velocity to a reasonably small number if (FMath::Abs(RotationVelocity) < KINDA_SMALL_NUMBER) { RotationVelocity = 0.0f; } // Update rotation InOutCameraEuler[CurRotationAxis] += RotationVelocity * DeltaTime; // Constrain final pitch rotation value to configured range if (CurRotationAxis == 1) // 1 == pitch { // Normalize the angle to -180 to 180. float Angle = FMath::Fmod(InOutCameraEuler[CurRotationAxis], 360.0f); if (Angle > 180.f) { Angle -= 360.f; } else if (Angle < -180.f) { Angle += 360.f; } // allow for unlocked pitch constraints while in matinee if (Config.bLockedPitch || !GLevelEditorModeTools().GetActiveMode(FBuiltinEditorModes::EM_InterpEdit)) { // Clamp the angle. InOutCameraEuler[CurRotationAxis] = FMath::Clamp(Angle, Config.MinimumAllowedPitchRotation, Config.MaximumAllowedPitchRotation); } } } } /** * Update the field of view. Called every frame by UpdateSimulation. * * @param UserImpulse User impulse data for this frame * @param DeltaTime Time interval * @param InOutCameraFOV [in, out] Camera field of view */ void FEditorCameraController::UpdateFOV(const FCameraControllerUserImpulseData& UserImpulse, const float DeltaTime, float& InOutCameraFOV) { // Compute acceleration float FOVAcceleration = UserImpulse.ZoomOutInImpulse * Config.FOVAccelerationRate; // Is the user actively changing the FOV? if (FMath::Abs(FOVAcceleration) > KINDA_SMALL_NUMBER) { // If we've never cached a FOV, then go ahead and do that now if (OriginalFOVForRecoil < 0.0f) { OriginalFOVForRecoil = InOutCameraFOV; } } if (Config.bUsePhysicsBasedFOV) { // Accelerate the FOV velocity FOVVelocity += FOVAcceleration * DeltaTime; // Apply damping { const float DampingFactor = FMath::Clamp(Config.FOVVelocityDampingAmount * DeltaTime, 0.0f, 0.75f); // Decelerate FOVVelocity += -FOVVelocity * DampingFactor; } } else { // No physics, so just use the acceleration as our velocity FOVVelocity = FOVAcceleration; } // Constrain maximum FOV speed FOVVelocity = FMath::Clamp(FOVVelocity, -Config.MaximumFOVSpeed, Config.MaximumFOVSpeed); // Clamp velocity to a reasonably small number if (FMath::Abs(FOVVelocity) < KINDA_SMALL_NUMBER) { FOVVelocity = 0.0f; } // Update camera FOV InOutCameraFOV += FOVVelocity * DeltaTime; // Constrain final FOV to configured range InOutCameraFOV = FMath::Clamp(InOutCameraFOV, Config.MinimumAllowedFOV, Config.MaximumAllowedFOV); } /** * Applies FOV recoil (if appropriate). Called every frame by UpdateSimulation. * * @param DeltaTime Time interval * @param bAllowRecoilIfNoImpulse Whether recoil should be allowed if there wasn't any user impulse * @param bAnyUserImpulse True if there was user impulse data this iteration * @param InOutCameraFOV [in, out] Camera field of view */ void FEditorCameraController::ApplyRecoil(const float DeltaTime, const bool bAllowRecoilIfNoImpulse, bool bAnyUserImpulse, float& InOutCameraFOV) { bool bIsRecoilingFOV = false; // Is the FOV 'recoil' feature enabled? If so, we'll smoothly snap the FOV angle back to what // it was before the user started interacting with the camera. if (Config.bEnableFOVRecoil) { // We don't need to recoil if the user hasn't started changing the FOV yet if (OriginalFOVForRecoil >= 0.0f) { // If there isn't any user impulse, then go ahead and recoil the camera FOV if (!bAnyUserImpulse && bAllowRecoilIfNoImpulse) { // Kill any physics-based FOV velocity FOVVelocity = 0.0f; const float FOVDistance = FMath::Abs(InOutCameraFOV - OriginalFOVForRecoil); if (FOVDistance > 0.1f) { // Recoil speed in 'distances' per second const float CameraFOVRecoilSpeedScale = 10.0f; if (InOutCameraFOV < OriginalFOVForRecoil) { InOutCameraFOV += FOVDistance * DeltaTime * CameraFOVRecoilSpeedScale; } else { InOutCameraFOV -= FOVDistance * DeltaTime * CameraFOVRecoilSpeedScale; } // We've tinkered with the FOV, so make sure we don't cache these changes bIsRecoilingFOV = true; } else { // Close enough, so snap it! InOutCameraFOV = OriginalFOVForRecoil; // We're done done manipulating the FOV for now OriginalFOVForRecoil = -1.0f; } } } } }