// EMERGENT GAME TECHNOLOGIES PROPRIETARY INFORMATION // // This software is supplied under the terms of a license agreement or // nondisclosure agreement with Emergent Game Technologies and may not // be copied or disclosed except in accordance with the terms of that // agreement. // // Copyright (c) 1996-2007 Emergent Game Technologies. // All Rights Reserved. // // Emergent Game Technologies, Chapel Hill, North Carolina 27517 // http://www.emergent.net #include "stdafx.h" #include #include #if defined(WIN32) || defined(_XENON) #include #endif #include "SceneApp.h" #include "CameraController.h" #include "GamepadMappings.h" #include "SceneAppAccum.h" //--------------------------------------------------------------------------- CameraController::CameraData::CameraData() : m_spCameraEntity(NULL), m_fCamDist(0.0f), m_fCamHeight(0.0f), m_fCamPitch(0.0f), m_fHeading(0.0f), m_fFrameYaw(0.0f), m_fFramePitch(0.0f), m_fForward(0.0f), m_fStrafe(0.0f), m_fVertical(0.0f), m_fMaxCameraRaise(1000.0f), m_fMaxCameraDrop(1000.0f), m_fWalkVelocity(150.0f), m_fRunVelocity(350.0f), m_kTrans(NiPoint3::ZERO), m_bInvert(false), m_eMoveState(CameraController::CameraData::LANDING) { } //--------------------------------------------------------------------------- CameraController::CameraController(NiScene* pkEntityScene) : m_fLastUpdateTime(0.0f) { m_kRotationProperty = NiFixedString("Rotation"); m_kTranslationProperty = NiFixedString("Translation"); // It is very easy to mistakenly misname the entity, try variations. NiEntityInterface* pkEntity = NULL; if (!pkEntity) pkEntity = pkEntityScene->GetEntityByName("SceneAppCameraController"); if (!pkEntity) pkEntity = pkEntityScene->GetEntityByName("SceneAppCameraController 01"); if (!pkEntity) pkEntity = pkEntityScene->GetEntityByName("SceneApp"); if (!pkEntity) pkEntity = pkEntityScene->GetEntityByName("SceneApp 01"); // Look for the "Start Camera" if (pkEntity) { NiEntityInterface* pkCameraEntity = NULL; if (pkEntity->GetPropertyData("Start Camera", pkCameraEntity)) { NiObject* pkObject; if (pkCameraEntity->GetPropertyData("Scene Root Pointer", pkObject)) { if (NiIsKindOf(NiCamera, pkObject)) { m_kCameraDefaults.m_spCameraEntity = pkCameraEntity; } } } pkEntity->GetPropertyData("Max Camera Raise", m_kCameraDefaults.m_fMaxCameraRaise); pkEntity->GetPropertyData("Max Camera Drop", m_kCameraDefaults.m_fMaxCameraDrop); pkEntity->GetPropertyData("Walk Velocity", m_kCameraDefaults.m_fWalkVelocity); pkEntity->GetPropertyData("Run Velocity", m_kCameraDefaults.m_fRunVelocity); pkEntity->GetPropertyData("Invert Pitch", m_kCameraDefaults.m_bInvert); } // If no start camera could be found, then look for first camera // component. if (m_kCameraDefaults.m_spCameraEntity == NULL) { m_kCameraDefaults.m_spCameraEntity = FindFirstCamera(pkEntityScene); if (!m_kCameraDefaults.m_spCameraEntity) { return; } } // Find all cameras for cycling FindAllSceneCameras(pkEntityScene, m_kSceneCameras); NiMatrix3 kCameraRot; NIVERIFY(m_kCameraDefaults.m_spCameraEntity->GetPropertyData( m_kRotationProperty, kCameraRot)); //We want to preserve the "look at vector" //Gamebryo camera looks down the positive X axis, //so the 0th col is the lookat NiPoint3 kLookAtVector; kCameraRot.GetCol(0, kLookAtVector); NiPoint3 kHeadingVector = NiPoint3(kLookAtVector[0], kLookAtVector[1], 0); kHeadingVector.Unitize(); //The heading vector is the 'look at' vector projected into the x-y plane //so simple trig can get the heading m_kCameraDefaults.m_fHeading = atan2(-kHeadingVector[0], -kHeadingVector[1]); //the pitch is the angle between the look at vector, and the x-y plane //we can find this through the dot product, and the z component of the //'look at' vector for the sign m_kCameraDefaults.m_fCamPitch = acos(kHeadingVector.Dot( kLookAtVector)) * (kLookAtVector[2] >= 0 ? -1 : 1); // SetInitial translation NIVERIFY(m_kCameraDefaults.m_spCameraEntity->GetPropertyData( m_kTranslationProperty, m_kCameraDefaults.m_kTrans)); // Initialize the pick controlelr m_kPickController.Initialize(pkEntityScene); // Get height from camera m_kCameraDefaults.m_fCamHeight = GetInitialHeight(m_kCameraDefaults.m_kTrans); m_kCameraDefaults.m_kTrans.z -= m_kCamera.m_fCamHeight; // Set the base camera values Reset(); // Set aspect ratio of cameras. { Ni2DBuffer* pkDefaultBackBuffer = NiRenderer::GetRenderer()->GetDefaultBackBuffer(); NIASSERT(pkDefaultBackBuffer); float fAspectRatio = (float) pkDefaultBackBuffer->GetWidth() / pkDefaultBackBuffer->GetHeight(); unsigned int uiSceneCameras = m_kSceneCameras.GetSize(); for (unsigned int ui = 0; ui < uiSceneCameras; ui++) { NiEntityInterface* pkEnt = m_kSceneCameras.GetAt(ui); NIVERIFY(pkEnt->SetPropertyData("Aspect Ratio", fAspectRatio)); } } } //--------------------------------------------------------------------------- CameraController::~CameraController() { } //--------------------------------------------------------------------------- NiMatrix3 MakeRotationMatrix( const float fHeading, const float fPitch) { NiMatrix3 kR; kR.MakeZRotation(fHeading); NiMatrix3 kV; NiMatrix3 kRotX, kRotZ; kRotX.MakeXRotation(-NI_HALF_PI); kRotZ.MakeZRotation(NI_HALF_PI); kV = kRotZ * kRotX; NiMatrix3 kRY; kRY.MakeXRotation(-fPitch); kV = kR * kRY * kV; return kV; } //--------------------------------------------------------------------------- void CameraController::Reset() { m_kCamera = m_kCameraDefaults; } //--------------------------------------------------------------------------- void CameraController::UpdateAllCameras(float fTime, NiEntityErrorInterface* pkErrorInterface, NiExternalAssetManager* pkAssetManager) { // Mouselook update code NiInputMouse* pkMouse = NiApplication::ms_pkApplication-> GetInputSystem()->GetMouse(); NiInputKeyboard* pkKeyboard = NiApplication::ms_pkApplication-> GetInputSystem()->GetKeyboard(); float fDelta; // Limit fDelta, when frame rate is very low we don't want large jumps { fDelta = fTime - m_fLastUpdateTime; m_fLastUpdateTime = fTime; const float fWorstCaseFPS = 15.f; fDelta = NiClamp(fDelta, 0.0f, 1/fWorstCaseFPS); } int iX = 0; int iY = 0; float fVelocity = m_kCamera.m_fWalkVelocity; float fVelocityXDelta = fVelocity * fDelta; float fHeadingDelta = 0.0f; float fPitchDelta = 0.0f; m_kCamera.m_fStrafe = 0.0f; m_kCamera.m_fForward = 0.0f; m_kCamera.m_fVertical = 0.0f; for (unsigned int uiPort = 0; uiPort < NiInputSystem::MAX_GAMEPADS; uiPort++) { NiInputGamePad* pkGamePad = NiApplication::ms_pkApplication-> GetInputSystem()->GetGamePad(uiPort); if (!pkGamePad) continue; if (pkGamePad->ButtonWasPressed(SCENEAPP_RESET_BUTTON)) Reset(); // Axis values of the right thumb stick control pitch and yaw. iX = pkGamePad->GetAxisValue(SCENEAPP_RSTICK_HORZ); iY = pkGamePad->GetAxisValue(SCENEAPP_RSTICK_VERT); if (m_kCamera.m_bInvert) iY = -iY; // Compute the change to the heading of the character static float fSpeed = 0.025f; fHeadingDelta = fSpeed * fDelta * (float)(iX); fPitchDelta = fSpeed * fDelta * (float)(iY); float fVertPercent = pkGamePad->GetAxisValue(SCENEAPP_LSTICK_VERT)*0.005f; float fHorzPercent = pkGamePad->GetAxisValue(SCENEAPP_LSTICK_HORZ)*0.005f; if (fVertPercent || fHorzPercent) { // GamePad is always in run mode fVelocity = m_kCamera.m_fRunVelocity; fVelocityXDelta = fVelocity * fDelta; m_kCamera.m_fForward = -fVelocityXDelta * fVertPercent; m_kCamera.m_fStrafe = fVelocityXDelta * fHorzPercent; } } if (pkMouse) { // If the gamepad read values, then don't use the mouse if (!iX && !iY) { int iZ; NIVERIFY(pkMouse->GetPositionDelta(iX, iY, iZ)); if (m_kCamera.m_bInvert) iY = -iY; // Compute the change to the heading of the character, based on the // horizontal mouse motion - // we must factor in the size of the screen in pixels to avoid the // sensitivity changing with screen resolution unsigned int uiAppWidth = NiApplication::ms_pkApplication-> GetAppWindow()->GetWidth(); if (uiAppWidth > 0) { fHeadingDelta = NI_PI * 0.5f * (float)(iX) / (float)uiAppWidth; } unsigned int uiAppHeight = NiApplication::ms_pkApplication-> GetAppWindow()->GetHeight(); if (uiAppHeight > 0) { fPitchDelta = NI_PI * 0.375f * (float)(iY) / (float)uiAppHeight; } } } if (pkKeyboard) { if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_R) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_R)) { Reset(); } if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_LSHIFT) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_LSHIFT) || pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_RSHIFT) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_RSHIFT)) { // Running: Recalc. fVelocity = m_kCamera.m_fRunVelocity; fVelocityXDelta = fVelocity * fDelta; } if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_LEFT) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_LEFT) || pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_A) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_A)) { m_kCamera.m_fStrafe = -fVelocityXDelta; } else if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_RIGHT) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_RIGHT) || pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_D) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_D)) { m_kCamera.m_fStrafe = fVelocityXDelta; } if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_DOWN) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_DOWN) || pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_S) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_S)) { m_kCamera.m_fForward = -fVelocityXDelta; } else if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_UP) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_UP) || pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_W) || pkKeyboard->KeyIsDown(NiInputKeyboard::KEY_W)) { m_kCamera.m_fForward = fVelocityXDelta; } if (pkKeyboard->KeyWasPressed(NiInputKeyboard::KEY_F3)) { m_kPickController.ToggleDisplayWalkables(); } } // Compute the change to the heading of the character, based on the // horizontal mouse motion - we must factor in the size of the screen // in pixels to avoid the sensitivity changing with screen resolution if (fHeadingDelta) { m_kCamera.m_fHeading += fHeadingDelta; m_kCamera.m_fHeading = NiFmod(m_kCamera.m_fHeading, NI_TWO_PI); } // Avoid making the frame heading go nuts if, for any reason, the // framerate is very high. Otherwise (normal framerate), the frame // heading (which is used by external classes to render effects such // as head turning) is based on the change in heading induced by the // controller this frame if (fDelta > 0.005f && fHeadingDelta) m_kCamera.m_fFrameYaw = -fHeadingDelta / (NI_PI * fDelta); else m_kCamera.m_fFrameYaw = 0.0f; if (fPitchDelta) m_kCamera.m_fCamPitch += fPitchDelta; // Avoid making the frame pitch go nuts if the framerate is very high // for any reason. Otherwise (normal framerate), the frame pitch // (which is used by external classes to render effects such as head // tilting) is based on the change in pitch induced by the controller // this frame if (fDelta > 0.005f) m_kCamera.m_fFramePitch = -fPitchDelta / (NI_PI * fDelta); else m_kCamera.m_fFramePitch = 0.0f; float fPitchMax = NI_PI * 0.4f; // Compute an upward-looking pitch angle clamp. Upward-looking clamp is // adjusted to be more restrictive as the camera zooms away. This is // adjusted to be <45deg, but quickly falls off to ensure that the camera // never goes below the ground height _at_the_character_ (may be // different than the height at the camera). We also check the camera // distance to be positive, so that the division in the asin doesn't give // NaN. If the distance could cause asin to NaN then we return the fixed // 45 degree limit float fPitchMin = (m_kCamera.m_fCamDist > 1e-5f) ? -NiASin(m_kCamera.m_fCamHeight / (m_kCamera.m_fCamHeight + m_kCamera.m_fCamDist)) : -fPitchMax; // clamp the camera pitch angle // Downward-looking clamp is constant: 45deg if (m_kCamera.m_fCamPitch < fPitchMin) m_kCamera.m_fCamPitch = fPitchMin; else if (m_kCamera.m_fCamPitch > fPitchMax) m_kCamera.m_fCamPitch = fPitchMax; // Check if camera can be updated to new location based on walkables. bool bSuccess = false; if (m_kCamera.m_eMoveState != CameraData::FLYING && m_kPickController.HasWalkables()) { NiPoint3 kNewPosition; bool bSuccess = TryPick(m_kCamera, m_kPickController, kNewPosition); if (bSuccess) // Success, the step is OK { // If Landing: Complete landing and start walking if (m_kCamera.m_eMoveState == CameraData::LANDING && IsHeightInWalkingRange(kNewPosition.z, m_kCamera)) { m_kCamera.m_eMoveState = CameraData::WALKING; } m_kCamera.m_kTrans.x = kNewPosition.x; m_kCamera.m_kTrans.y = kNewPosition.y; // z is smoothly updated below: // This smooths the camera out when dropping levels... { float fDiff = kNewPosition.z - m_kCamera.m_kTrans.z; float fPercent = fDelta * 10.0f; fPercent = NiMin(1.0f, fPercent); m_kCamera.m_kTrans.z += fDiff * fPercent; m_kCamera.m_fLastWorldHeight = kNewPosition.z; } } } // If we have not validated the location with walkables, // and we're not in a walking mode, // then fly if (!bSuccess && m_kCamera.m_eMoveState != CameraData::WALKING) { NiPoint3 kHeading = CameraController::CalculateNewHeading(m_kCamera, 0, 1); m_kCamera.m_kTrans += kHeading; } UpdateCameraTransforms(); m_kCamera.m_spCameraEntity->Update(NULL, fTime, pkErrorInterface, pkAssetManager); } //--------------------------------------------------------------------------- bool CameraController::TryPick( const CameraController::CameraData &kCamera, PickController &kPickController, NiPoint3 &kNewPosition, const int iTry) { // Recursive function that attempts to return a valid position to move to. // // kCamera and kPickController are used to form the picking samples against // the walkable polygons. // // If the function returns true, // kNewPosition is the best valid movement location. // // The initial call to the function should specify 0 for iTry. // // If a pick fails, a series of additional picks are tried following a // teardrop pattern. // The samples alternate between the right and left side, // and decreasing in distance. // // When walking into walls, or corners, this allows a sliding action. const int iTriesTotal = 30; // Max picks to attempt if (iTry >= iTriesTotal) return false; // fAngle & fMagnitude are used to modify how a new pick sample is found. // fAngle offsets the "forward" direction by that angle // fMagnitude shortens the magnitude of the heading vector float fAngle = 0; float fMagnitude = 1; { const float fTryNormalized = iTry / (float) iTriesTotal; const bool bOddTry = (iTry % 2) > 0; const float fMagnitudeStart = 1.0f; const float fMagnitudeFinish = 0.0f; fMagnitude = NiLerp(fTryNormalized, fMagnitudeStart, fMagnitudeFinish); const float fAngleStart = 0.0f; const float fAngleFinish = NI_PI / 2; fAngle = NiLerp(fTryNormalized, fAngleStart, fAngleFinish); if (bOddTry) fAngle *= -1; } NiPoint3 kHeading = CameraController::CalculateNewHeading(kCamera, fAngle, fMagnitude); kNewPosition = kCamera.m_kTrans + kHeading; NiPoint3 kPickPoint = kNewPosition + NiPoint3(0.0f, 0.0f, NiMax(kCamera.m_fCamHeight, kCamera.m_fMaxCameraRaise)); float fHeight; bool bSuccess = kPickController.GetHeight(kPickPoint, fHeight); if (bSuccess) { // Is this a valid pick to transition to? if (kCamera.m_eMoveState == CameraData::LANDING || ( kCamera.m_eMoveState == CameraData::WALKING && IsHeightInWalkingRange(fHeight, kCamera) ) ) { kNewPosition.z = fHeight; return true; } } // Failed, try again: return TryPick(kCamera, kPickController, kNewPosition, iTry + 1); } //--------------------------------------------------------------------------- NiPoint3 CameraController::CalculateNewHeading( const CameraController::CameraData &kCamera, const float fOffsetAngleRadians, const float fMagnitude ) { // Returns a vector (kHeading) that can be used to // move the current camera position. // // The vector is modified by inputs: // fOffsetAngleRadians // The direction considered forward is rotated by this angle // // fMagnitude // The magnitude of the resultant vector is multiplied by this value NiPoint3 kHeading = NiPoint3::ZERO; NiMatrix3 kR; // If walking, update heading to ignore pitch, only horizontal movement if (kCamera.m_eMoveState == CameraData::WALKING) { kR = MakeRotationMatrix( kCamera.m_fHeading + fOffsetAngleRadians, 0); } else { kR = MakeRotationMatrix( kCamera.m_fHeading + fOffsetAngleRadians, kCamera.m_fCamPitch); } if (kCamera.m_fStrafe) { NiPoint3 kStrafe = NiPoint3::ZERO; kR.GetCol(2, kStrafe); kStrafe *= kCamera.m_fStrafe; kHeading = kStrafe; } if (kCamera.m_fForward) { NiPoint3 kForward = NiPoint3::ZERO; kR.GetCol(0, kForward); kForward *= kCamera.m_fForward; kHeading += kForward; } if (kCamera.m_fVertical) { NiPoint3 kVertical = NiPoint3::ZERO; kR.GetCol(1, kVertical); kVertical *= kCamera.m_fVertical; kHeading += kVertical; } kHeading *= fMagnitude; return kHeading; } //--------------------------------------------------------------------------- NiEntityInterface* CameraController::GetEntity() const { return m_kCamera.m_spCameraEntity; } //--------------------------------------------------------------------------- float CameraController::GetInitialHeight(NiPoint3& kWorldTrans) { if (!m_kPickController.HasWalkables()) { return 0; } float fHeight; if (!m_kPickController.GetHeight(kWorldTrans, fHeight)) { return 0; } else { return (kWorldTrans.z - fHeight); } } //--------------------------------------------------------------------------- void CameraController::UpdateCameraTransforms() { // The method used to position the camera is refered to as "camera on a // stick". In other words, the camera is assumed to be attached to a // long stick that comes out of the back of the character. The length of // the stick and the angle of elevation of the stick as it comes out of // a fixed point on the back of the character may be adjusted. As a // result, while the heading is implicit from the fact that the camera is // a child of the character's root node, we must add in the rotation of // the camera in pitch, and must also append that rotation to the "stick" // translation - thus, the stick rotation is applied to the camera's // local transforms, and the rotation is also manually applied to the // stick's translation. NiMatrix3 kV = MakeRotationMatrix( m_kCamera.m_fHeading, m_kCamera.m_fCamPitch); NIVERIFY(m_kCamera.m_spCameraEntity->SetPropertyData( m_kRotationProperty, kV)); NIVERIFY(m_kCamera.m_spCameraEntity->SetPropertyData( m_kTranslationProperty, m_kCamera.m_kTrans + NiPoint3(0.0f, 0.0f, m_kCamera.m_fCamHeight))); } //--------------------------------------------------------------------------- NiEntityInterface* CameraController::FindFirstCamera(NiScene* pkEntityScene) { unsigned int uiEntities = pkEntityScene->GetEntityCount(); unsigned int uiIndex; for(uiIndex=0; uiIndex < uiEntities; uiIndex++) { NiEntityInterface* pkEntity = pkEntityScene->GetEntityAt(uiIndex); NiObject* pkObject; if (!pkEntity->GetPropertyData("Scene Root Pointer", pkObject)) continue; if (!NiIsKindOf(NiCamera, pkObject)) continue; return pkEntity; } return NULL; } //--------------------------------------------------------------------------- void CameraController::FindAllSceneCameras( NiScene* pkEntityScene, NiTObjectArray& kCameras) { kCameras.RemoveAll(); unsigned int uiEntities = pkEntityScene->GetEntityCount(); unsigned int uiIndex; for(uiIndex=0; uiIndex < uiEntities; uiIndex++) { NiEntityInterface* pkEntity = pkEntityScene->GetEntityAt(uiIndex); NiObject* pkObject; if (!pkEntity->GetPropertyData("Scene Root Pointer", pkObject)) continue; if (NiIsKindOf(NiCamera, pkObject)) kCameras.Add(pkEntity); } } //--------------------------------------------------------------------------- void CameraController::GetAllCameras( NiTPrimitiveArray& kCameras) const { kCameras.RemoveAll(); unsigned int uiSize = m_kSceneCameras.GetSize(); for (unsigned int ui = 0; ui < uiSize; ui++) { NiEntityInterface* pkEnt = m_kSceneCameras.GetAt(ui); NiObject* pkObject; if (pkEnt && pkEnt->GetPropertyData("Scene Root Pointer", pkObject)) { if (NiIsKindOf(NiCamera, pkObject)) kCameras.Add((NiCamera*)pkObject); } } } //---------------------------------------------------------------------------