#include "CCharacterEditor.h" #include "ui_CCharacterEditor.h" #include "Editor/UICommon.h" #include #include #include #include #include const CVector3f CCharacterEditor::skDefaultOrbitTarget = CVector3f(0,0,1); const float CCharacterEditor::skDefaultOrbitDistance = 4.f; CCharacterEditor::CCharacterEditor(CAnimSet *pSet, QWidget *parent) : IEditor(parent) , ui(new Ui::CCharacterEditor) , mpScene(new CScene()) , mpCharNode(new CCharacterNode(mpScene, -1)) , mpSelectedBone(nullptr) , mBindPose(false) , mPlayAnim(true) , mLoopAnim(true) , mAnimTime(0.f) , mPlaybackSpeed(1.f) { ui->setupUi(this); REPLACE_WINDOWTITLE_APPVARS; ui->Viewport->SetNode(mpCharNode); CCamera& rCamera = ui->Viewport->Camera(); rCamera.SetMoveSpeed(0.5f); rCamera.SetPitch(-0.3f); rCamera.SetMoveMode(eOrbitCamera); // Init UI ui->ToolBar->addSeparator(); mpCharComboBox = new QComboBox(this); mpCharComboBox->setMinimumWidth(175); ui->ToolBar->addWidget(mpCharComboBox); mpAnimComboBox = new QComboBox(this); mpAnimComboBox->setMinimumWidth(175); ui->ToolBar->addWidget(mpAnimComboBox); connect(ui->Viewport, SIGNAL(HoverBoneChanged(u32)), this, SLOT(OnViewportHoverBoneChanged(u32))); connect(ui->Viewport, SIGNAL(ViewportClick(QMouseEvent*)), this, SLOT(OnViewportClick())); connect(ui->ActionShowGrid, SIGNAL(toggled(bool)), this, SLOT(ToggleGrid(bool))); connect(ui->ActionShowMesh, SIGNAL(toggled(bool)), this, SLOT(ToggleMeshVisible(bool))); connect(ui->ActionShowSkeleton, SIGNAL(toggled(bool)), this, SLOT(ToggleSkeletonVisible(bool))); connect(ui->ActionBindPose, SIGNAL(toggled(bool)), this, SLOT(ToggleBindPose(bool))); connect(ui->ActionOrbit, SIGNAL(toggled(bool)), this, SLOT(ToggleOrbit(bool))); connect(ui->ActionPlay, SIGNAL(triggered()), this, SLOT(TogglePlay())); connect(ui->ActionLoop, SIGNAL(toggled(bool)), this, SLOT(ToggleLoop(bool))); connect(ui->ActionRewind, SIGNAL(triggered()), this, SLOT(Rewind())); connect(ui->ActionFastForward, SIGNAL(triggered()), this, SLOT(FastForward())); connect(ui->ActionPrevAnim, SIGNAL(triggered()), this, SLOT(PrevAnim())); connect(ui->ActionNextAnim, SIGNAL(triggered()), this, SLOT(NextAnim())); connect(mpCharComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(SetActiveCharacterIndex(int))); connect(mpAnimComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(SetActiveAnimation(int))); connect(ui->AnimSlider, SIGNAL(valueChanged(int)), this, SLOT(SetAnimTime(int))); connect(ui->PlayPauseButton, SIGNAL(pressed()), this, SLOT(TogglePlay())); connect(ui->LoopButton, SIGNAL(toggled(bool)), this, SLOT(ToggleLoop(bool))); connect(ui->RewindButton, SIGNAL(pressed()), this, SLOT(Rewind())); connect(ui->FastForwardButton, SIGNAL(pressed()), this, SLOT(FastForward())); connect(ui->AnimSpeedSpinBox, SIGNAL(valueChanged(double)), this, SLOT(AnimSpeedSpinBoxChanged(double))); // Init skeleton tree view ui->SkeletonHierarchyTreeView->setModel(&mSkeletonModel); QList SplitterSizes; SplitterSizes << width() * 0.2 << width() * 0.8; ui->splitter->setSizes(SplitterSizes); connect(ui->SkeletonHierarchyTreeView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(OnSkeletonTreeSelectionChanged(QModelIndex))); SetActiveAnimSet(pSet); } CCharacterEditor::~CCharacterEditor() { delete ui; } void CCharacterEditor::EditorTick(float DeltaTime) { UpdateAnimTime(DeltaTime); UpdateCameraOrbit(); } void CCharacterEditor::UpdateAnimTime(float DeltaTime) { CAnimation *pAnim = CurrentAnimation(); if (pAnim && mPlayAnim && !mBindPose && !ui->AnimSlider->isSliderDown()) { mAnimTime += DeltaTime * mPlaybackSpeed; CAnimation *pAnim = CurrentAnimation(); float AnimLength = (pAnim ? pAnim->Duration() : 0.f); if (mAnimTime > AnimLength) { if (mLoopAnim) { mAnimTime = fmodf(mAnimTime, AnimLength); } else { mAnimTime = AnimLength; TogglePlay(); } } if (mAnimTime < 0.f) { if (mLoopAnim) { mAnimTime = AnimLength + fmodf(mAnimTime, AnimLength); } else { mAnimTime = 0.f; TogglePlay(); } } SetAnimTime(mAnimTime); } } void CCharacterEditor::UpdateCameraOrbit() { CSkeleton *pSkel = CurrentSkeleton(); if (!pSkel) { // Center around character if we have one, otherwise fall back to default orbit. if (mpSet) ui->Viewport->Camera().SetOrbitTarget(mpCharNode->CenterPoint()); else ui->Viewport->Camera().SetOrbit(skDefaultOrbitTarget, skDefaultOrbitDistance); } else { // If we have a selected bone, orbit around that. if (mpSelectedBone) ui->Viewport->Camera().SetOrbitTarget(mpCharNode->BonePosition(mpSelectedBone->ID())); // Otherwise, try to find Skeleton_Root. Barring that, we can orbit the root bone. else { CBone *pRoot = pSkel->RootBone(); CBone *pSkelRoot = (pRoot ? pRoot->ChildByIndex(0) : pRoot); CVector3f OrbitTarget = (pSkelRoot ? mpCharNode->BonePosition(pSkelRoot->ID()) : mpCharNode->CenterPoint()); ui->Viewport->Camera().SetOrbitTarget(OrbitTarget); } } } CSkeleton* CCharacterEditor::CurrentSkeleton() const { return mpSet ? mpSet->Character(mCurrentChar)->pSkeleton : nullptr; } CAnimation* CCharacterEditor::CurrentAnimation() const { return mpSet ? mpSet->FindAnimationAsset(mCurrentAnim) : nullptr; } void CCharacterEditor::SetActiveAnimSet(CAnimSet *pSet) { mpSet = pSet; mpCharNode->SetCharSet(mpSet); SET_WINDOWTITLE_APPVARS("%APP_FULL_NAME% - Character Editor: " + TO_QSTRING(mpSet->Source())); // Clear selected bone ui->SkeletonHierarchyTreeView->selectionModel()->clear(); SetSelectedBone(nullptr); // Set up character combo box mpCharComboBox->blockSignals(true); mpCharComboBox->clear(); for (u32 iChar = 0; iChar < mpSet->NumCharacters(); iChar++) mpCharComboBox->addItem( TO_QSTRING(mpSet->Character(iChar)->Name) ); SetActiveCharacterIndex(0); mpCharComboBox->blockSignals(false); // Set up anim combo box mpAnimComboBox->blockSignals(true); mpAnimComboBox->clear(); for (u32 iAnim = 0; iAnim < mpSet->NumAnimations(); iAnim++) mpAnimComboBox->addItem( TO_QSTRING(mpSet->Animation(iAnim)->Name) ); SetActiveAnimation(0); mpAnimComboBox->blockSignals(false); // Set up skeleton tree view CSkeleton *pSkel = mpSet->Character(mCurrentChar)->pSkeleton; mSkeletonModel.SetSkeleton(pSkel); ui->SkeletonHierarchyTreeView->expandAll(); ui->SkeletonHierarchyTreeView->resizeColumnToContents(0); // Select first child bone of root (which should be Skeleton_Root) to line up the camera for orbiting. QModelIndex RootIndex = mSkeletonModel.index(0, 0, QModelIndex()); ui->SkeletonHierarchyTreeView->selectionModel()->setCurrentIndex( mSkeletonModel.index(0, 0, RootIndex), QItemSelectionModel::ClearAndSelect ); // Run CCamera::SetOrbit to reset orbit distance. ui->Viewport->Camera().SetOrbit(mpCharNode->AABox()); } void CCharacterEditor::SetSelectedBone(CBone *pBone) { if (pBone != mpSelectedBone) { if (mpSelectedBone) mpSelectedBone->SetSelected(false); mpSelectedBone = pBone; if (mpSelectedBone) mpSelectedBone->SetSelected(true); } } CCharacterEditorViewport* CCharacterEditor::Viewport() const { return ui->Viewport; } // ************ PUBLIC SLOTS ************ void CCharacterEditor::ToggleGrid(bool Enable) { ui->Viewport->SetGridEnabled(Enable); } void CCharacterEditor::ToggleMeshVisible(bool Visible) { // eShowObjectGeometry isn't the best fit, but close enough...? ui->Viewport->SetShowFlag(eShowObjectGeometry, Visible); } void CCharacterEditor::ToggleSkeletonVisible(bool Visible) { ui->Viewport->SetShowFlag(eShowSkeletons, Visible); } void CCharacterEditor::ToggleBindPose(bool Enable) { mpCharNode->SetAnimated(!Enable); if (sender() != ui->ActionBindPose) { ui->ActionBindPose->blockSignals(true); ui->ActionBindPose->setChecked(Enable); ui->ActionBindPose->blockSignals(false); } if (Enable && mPlayAnim) TogglePlay(); ui->AnimSlider->setEnabled(!Enable); mBindPose = Enable; } void CCharacterEditor::ToggleOrbit(bool Enable) { ui->Viewport->Camera().SetMoveMode(Enable ? eOrbitCamera : eFreeCamera); } void CCharacterEditor::RefreshViewport() { ui->Viewport->ProcessInput(); ui->Viewport->Render(); } void CCharacterEditor::OnViewportHoverBoneChanged(u32 BoneID) { if (BoneID == 0xFFFFFFFF) ui->StatusBar->clearMessage(); else ui->StatusBar->showMessage(QString("Bone %1: %2").arg(BoneID).arg( TO_QSTRING(mpSet->Character(mCurrentChar)->pSkeleton->BoneByID(BoneID)->Name()) )); } void CCharacterEditor::OnViewportClick() { u32 HoverBoneID = ui->Viewport->HoverBoneID(); CSkeleton *pSkel = (mpSet ? mpSet->Character(mCurrentChar)->pSkeleton : nullptr); CBone *pBone = (pSkel ? pSkel->BoneByID(HoverBoneID) : nullptr); if (!pBone || !pBone->IsSelected()) { if (pBone) { QModelIndex NewBoneIndex = mSkeletonModel.IndexForBone(pBone); ui->SkeletonHierarchyTreeView->selectionModel()->setCurrentIndex(NewBoneIndex, QItemSelectionModel::ClearAndSelect); } else ui->SkeletonHierarchyTreeView->selectionModel()->clear(); SetSelectedBone(pBone); } } void CCharacterEditor::OnSkeletonTreeSelectionChanged(const QModelIndex& rkIndex) { CBone *pBone = mSkeletonModel.BoneForIndex(rkIndex); SetSelectedBone(pBone); } void CCharacterEditor::SetActiveCharacterIndex(int CharIndex) { mCurrentChar = CharIndex; mpCharNode->SetActiveChar((u32) CharIndex); } void CCharacterEditor::SetActiveAnimation(int AnimIndex) { mCurrentAnim = AnimIndex; mpCharNode->SetActiveAnim((u32) AnimIndex); ui->AnimSlider->blockSignals(true); ui->AnimSlider->setMaximum((int) (CurrentAnimation() ? CurrentAnimation()->Duration() * 1000 : 0)); ui->AnimSlider->blockSignals(false); mpAnimComboBox->blockSignals(true); mpAnimComboBox->setCurrentIndex(AnimIndex); mpAnimComboBox->blockSignals(false); SetAnimTime(0.f); } void CCharacterEditor::PrevAnim() { if (mCurrentAnim > 0) SetActiveAnimation(mCurrentAnim - 1); } void CCharacterEditor::NextAnim() { u32 MaxAnim = (mpSet ? mpSet->NumAnimations() - 1 : 0); if (mCurrentAnim < MaxAnim) SetActiveAnimation(mCurrentAnim + 1); } void CCharacterEditor::SetAnimTime(int Time) { float FloatTime = Time / 1000.f; SetAnimTime(FloatTime); } void CCharacterEditor::SetAnimTime(float Time) { if (mBindPose) Time = 0.f; mAnimTime = Time; if (ui->AnimSlider != sender() || mBindPose) { int IntTime = (int) (Time * 1000); ui->AnimSlider->setValue(IntTime); } mpCharNode->SetAnimTime(Time); CAnimation *pAnim = CurrentAnimation(); u32 NumKeys = 1, CurKey = 0; if (pAnim) { NumKeys = pAnim->NumKeys(); CurKey = Math::Min((u32) (Time / pAnim->TickInterval()) + 1, NumKeys - 1); } ui->FrameLabel->setText(QString("Frame %1 / %2 (%3s/%4s)").arg(CurKey).arg(NumKeys - 1).arg(mAnimTime, 0, 'f', 3).arg(pAnim ? pAnim->Duration() : 0.f, 0, 'f', 3)); } void CCharacterEditor::TogglePlay() { if (mBindPose) ToggleBindPose(false); mPlayAnim = !mPlayAnim; QString NewText = (mPlayAnim ? "Pause" : "Play"); ui->PlayPauseButton->setToolTip(NewText); ui->ActionPlay->setText(NewText); QIcon PlayPauseIcon = QIcon(mPlayAnim ? ":/icons/Pause_24px.png" : ":/icons/Play_24px.png"); ui->PlayPauseButton->setIcon(PlayPauseIcon); if (ui->ActionPlay != sender()) { ui->ActionPlay->blockSignals(true); ui->ActionPlay->setChecked(mPlayAnim); ui->ActionPlay->blockSignals(false); } CAnimation *pAnim = CurrentAnimation(); if (pAnim && mPlayAnim) { if (mPlaybackSpeed < 0.f && mAnimTime == 0.f) SetAnimTime(pAnim->Duration()); if (mPlaybackSpeed >= 0.f && mAnimTime == pAnim->Duration()) SetAnimTime(0.f); } } void CCharacterEditor::ToggleLoop(bool Loop) { mLoopAnim = Loop; QString NewText = (Loop ? "Disable Loop" : "Loop"); ui->LoopButton->setToolTip(NewText); ui->ActionLoop->setText(NewText); QIcon ActionIcon = QIcon(Loop ? ":/icons/DontLoop_24px" : ":/icons/Loop_24px.png"); ui->ActionLoop->setIcon(ActionIcon); if (sender() != ui->LoopButton) { ui->LoopButton->blockSignals(true); ui->LoopButton->setChecked(Loop); ui->LoopButton->blockSignals(false); } if (sender() != ui->ActionLoop) { ui->LoopButton->blockSignals(true); ui->ActionLoop->setChecked(Loop); ui->LoopButton->blockSignals(false); } } void CCharacterEditor::Rewind() { SetAnimTime(0.f); } void CCharacterEditor::FastForward() { CAnimation *pAnim = CurrentAnimation(); if (pAnim && !mBindPose) SetAnimTime(pAnim->Duration()); } void CCharacterEditor::AnimSpeedSpinBoxChanged(double NewVal) { mPlaybackSpeed = (float) NewVal; }