// 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.MoveWorldUpDownImpulse != 0.0f || UserImpulseData.MoveLocalUpDownImpulse != 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 ); } void FEditorCameraController::ResetVelocity() { MovementVelocity = FVector::ZeroVector; FOVVelocity = 0.f; RotationVelocityEuler = FVector::ZeroVector; } /**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.MoveWorldUpDownImpulse ) >= Config.ImpulseDeadZoneAmount ) { OutUserImpulse.MoveWorldUpDownImpulse = InUserImpulse.MoveWorldUpDownImpulse; bOutAnyImpulseData = true; } if( FMath::Abs( InUserImpulse.MoveLocalUpDownImpulse ) >= Config.ImpulseDeadZoneAmount ) { OutUserImpulse.MoveLocalUpDownImpulse = InUserImpulse.MoveLocalUpDownImpulse; 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 UserImpulse.MoveLocalUpDownImpulse ); // 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.MoveWorldUpDownImpulse ); // 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 FVector::FReal& RotationVelocity = RotationVelocityEuler[ CurRotationAxis ]; const FVector::FReal RotationImpulse = RotateImpulseEuler[CurRotationAxis]; const FVector::FReal RotationVelocityModifier = RotateVelocityModifierEuler[ CurRotationAxis ]; // Compute acceleration FVector::FReal 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. FVector::FReal Angle = FMath::Fmod(InOutCameraEuler[ CurRotationAxis ], 360.0); if (Angle > 180.) { Angle -= 360.; } else if (Angle < -180.) { Angle += 360.; } if (Config.bLockedPitch) { // Clamp the angle. InOutCameraEuler[ CurRotationAxis ] = FMath::Clamp( Angle, (FVector::FReal)Config.MinimumAllowedPitchRotation, (FVector::FReal)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; } } } } }