PrimeWorldEditor/src/Editor/CharacterEditor/CCharacterEditor.cpp

448 lines
14 KiB
C++

#include "CCharacterEditor.h"
#include "ui_CCharacterEditor.h"
#include "Editor/UICommon.h"
#include <Common/Macros.h>
#include <Common/Math/MathUtil.h>
#include <QFileDialog>
#include <QMessageBox>
#include <QTreeView>
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())
, mpSelectedBone(nullptr)
, mBindPose(false)
, mPlayAnim(true)
, mLoopAnim(true)
, mAnimTime(0.f)
, mPlaybackSpeed(1.f)
{
ui->setupUi(this);
REPLACE_WINDOWTITLE_APPVARS;
mpCharNode = new CCharacterNode(mpScene, -1);
ui->Viewport->SetNode(mpCharNode);
CCamera& rCamera = ui->Viewport->Camera();
rCamera.SetMoveSpeed(0.5f);
rCamera.SetPitch(-0.3f);
rCamera.SetMoveMode(ECameraMoveMode::Orbit);
// 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(uint32)), this, SLOT(OnViewportHoverBoneChanged(uint32)));
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<int> 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;
delete mpScene;
delete mpCharNode;
}
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 (uint32 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 (uint32 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(EShowFlag::ObjectGeometry, Visible);
}
void CCharacterEditor::ToggleSkeletonVisible(bool Visible)
{
ui->Viewport->SetShowFlag(EShowFlag::Skeletons, 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 ? ECameraMoveMode::Orbit : ECameraMoveMode::Free);
}
void CCharacterEditor::RefreshViewport()
{
ui->Viewport->ProcessInput();
ui->Viewport->Render();
}
void CCharacterEditor::OnViewportHoverBoneChanged(uint32 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()
{
uint32 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((uint32) CharIndex);
}
void CCharacterEditor::SetActiveAnimation(int AnimIndex)
{
mCurrentAnim = AnimIndex;
mpCharNode->SetActiveAnim((uint32) 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()
{
uint32 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();
uint32 NumKeys = 1, CurKey = 0;
if (pAnim)
{
NumKeys = pAnim->NumKeys();
CurKey = Math::Min<uint32>((uint32) (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.svg" : ":/icons/Play_24px.svg");
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.svg");
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;
}