You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Editor/TrackView/SequenceBatchRenderDialog.cpp

1654 lines
58 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
// Description : A dialog for batch-rendering sequences
#include "EditorDefs.h"
#include "SequenceBatchRenderDialog.h"
#include <AzCore/Component/ComponentApplication.h>
#include <AzFramework/Windowing/WindowBus.h>
// Qt
#include <QAction>
#include <QFileDialog>
#include <QMessageBox>
#include <QStringListModel>
#include <QtConcurrent>
// CryCommon
#include <CryCommon/Maestro/Types/AnimNodeType.h>
// Editor
#include "MainWindow.h"
#include "CustomResolutionDlg.h"
#include "ViewPane.h"
#include "GameEngine.h"
#include "Include/ICommandManager.h"
#include "CryEdit.h"
#include "Viewport.h"
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
#include <TrackView/ui_SequenceBatchRenderDialog.h>
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
namespace
{
const int g_useActiveViewportResolution = -1; // reserved value to indicate the use of the active viewport resolution
int resolutions[][2] = {
{1280, 720},
{1920, 1080},
{1998, 1080},
{2048, 858},
{2560, 1440},
{3840, 2160},
{ g_useActiveViewportResolution, g_useActiveViewportResolution } // active viewport res must be the last element of the resolution array
};
// cached current active viewport resolution
int activeViewportWidth;
int activeViewportHeight;
struct SFPSPair
{
int fps;
const char* fpsDesc;
} fpsOptions[] = {
{24, "Film(24)"}, {25, "PAL(25)"}, {30, "NTSC(30)"},
{48, "Show(48)"}, {50, "PAL Field(50)"}, {60, "NTSC Field(60)"}
};
// currently supported file extensions
const char* imageFormatExtensions[] = {"dds", "ppm"};
const char defaultPresetFilename[] = "defaultBatchRender.preset";
const char customResFormat[] = "Custom(%1 x %2)...";
const int kBatchRenderFileVersion = 2; // This version number should be incremented every time available options like the list of formats,
// the list of buffers change.
// get the actual render width to use (substitutes active viewport width if needed)
int getResWidth(int renderItemWidth)
{
return (renderItemWidth == g_useActiveViewportResolution) ? activeViewportWidth : renderItemWidth;
}
// get the actual render height to use (substitutes active viewport height if needed)
int getResHeight(int renderItemHeight)
{
return (renderItemHeight == g_useActiveViewportResolution) ? activeViewportHeight : renderItemHeight;
}
}
static void UpdateAtomOutputFrameCaptureView(TrackView::AtomOutputFrameCapture& atomOutputFrameCapture, const int width, const int height)
{
const AZ::EntityId activeCameraEntityId = TrackView::ActiveCameraEntityId();
atomOutputFrameCapture.UpdateView(
TrackView::TransformFromEntityId(activeCameraEntityId),
TrackView::ProjectionFromCameraEntityId(activeCameraEntityId, static_cast<float>(width), static_cast<float>(height)));
}
CSequenceBatchRenderDialog::CSequenceBatchRenderDialog(float fps, QWidget* pParent /* = nullptr */)
: QDialog(pParent)
, m_fpsForTimeToFrameConversion(fps)
, m_customResH(0)
, m_customResW(0)
, m_customFPS(0)
, m_bFFMPEGCommandAvailable(false)
, m_ui(new Ui::SequenceBatchRenderDialog)
, m_renderListModel(new QStringListModel(this))
, CV_TrackViewRenderOutputCapturing(0)
, m_prefixValidator(new CPrefixValidator(this))
{
m_ui->setupUi(this);
setFixedSize(size());
m_ui->m_renderList->setModel(m_renderListModel);
OnInitDialog();
connect(&m_renderTimer, &QTimer::timeout, this, &CSequenceBatchRenderDialog::OnKickIdleTimout);
m_renderTimer.setInterval(0);
m_renderTimer.setSingleShot(true);
REGISTER_CVAR3("TrackViewRenderOutputCapturing", CV_TrackViewRenderOutputCapturing, 0, VF_NULL, "Set to 1 when Track View is actively capturing render output.");
}
CSequenceBatchRenderDialog::~CSequenceBatchRenderDialog()
= default;
void CSequenceBatchRenderDialog::reject()
{
if (m_renderContext.IsInRendering())
{
OnCancelRender();
}
else
{
QDialog::reject();
}
}
void CSequenceBatchRenderDialog::OnInitDialog()
{
QAction* browseAction = m_ui->m_destinationEdit->addAction(style()->standardPixmap(QStyle::SP_DirOpenIcon), QLineEdit::TrailingPosition);
connect(browseAction, &QAction::triggered, this, [=]()
{
const QString dir = QFileDialog::getExistingDirectory(this);
if (!dir.isEmpty())
{
m_ui->m_destinationEdit->setText(dir);
}
});
void(QComboBox::* activated)(int) = &QComboBox::activated;
void(QSpinBox::* editingFinished)() = &QSpinBox::editingFinished;
connect(m_ui->BATCH_RENDER_ADD_SEQ, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnAddRenderItem);
connect(m_ui->BATCH_RENDER_REMOVE_SEQ, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnRemoveRenderItem);
connect(m_ui->BATCH_RENDER_CLEAR_SEQ, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnClearRenderItems);
connect(m_ui->m_updateBtn, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnUpdateRenderItem);
connect(m_ui->BATCH_RENDER_LOAD_PRESET, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnLoadPreset);
connect(m_ui->BATCH_RENDER_SAVE_PRESET, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnSavePreset);
connect(m_ui->BATCH_RENDER_LOAD_BATCH, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnLoadBatch);
connect(m_ui->BATCH_RENDER_SAVE_BATCH, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnSaveBatch);
connect(m_ui->m_pGoBtn, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnGo);
connect(m_ui->CANCEL, &QPushButton::clicked, this, &CSequenceBatchRenderDialog::OnDone);
connect(m_ui->m_sequenceCombo, activated, this, &CSequenceBatchRenderDialog::OnSequenceSelected);
connect(m_ui->m_fpsCombo->lineEdit(), &QLineEdit::textEdited, this, &CSequenceBatchRenderDialog::OnFPSEditChange);
connect(m_ui->m_fpsCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &CSequenceBatchRenderDialog::OnFPSChange);
connect(m_ui->m_renderList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &CSequenceBatchRenderDialog::OnRenderItemSelChange);
connect(m_ui->m_resolutionCombo, activated, this, &CSequenceBatchRenderDialog::OnResolutionSelected);
connect(m_ui->m_startFrame, editingFinished, this, &CSequenceBatchRenderDialog::OnStartFrameChange);
connect(m_ui->m_endFrame, editingFinished, this, &CSequenceBatchRenderDialog::OnEndFrameChange);
connect(m_ui->m_imageFormatCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &CSequenceBatchRenderDialog::OnImageFormatChange);
const int bigEnoughNumber = 1000000;
m_ui->m_startFrame->setRange(0, bigEnoughNumber);
m_ui->m_endFrame->setRange(0, bigEnoughNumber);
// Fill the sequence combo box.
bool activeSequenceWasSet = false;
for (int k = 0; k < GetIEditor()->GetMovieSystem()->GetNumSequences(); ++k)
{
IAnimSequence* pSequence = GetIEditor()->GetMovieSystem()->GetSequence(k);
m_ui->m_sequenceCombo->addItem(pSequence->GetName());
if (pSequence->IsActivated())
{
m_ui->m_sequenceCombo->setCurrentIndex(k);
activeSequenceWasSet = true;
}
}
if (!activeSequenceWasSet)
{
m_ui->m_sequenceCombo->setCurrentIndex(0);
}
m_ui->m_fpsCombo->setEditable(true);
// Fill the shot combos and the default frame range.
OnSequenceSelected();
// Fill the resolution combo box.
for (int i = 0; i < arraysize(resolutions); ++i)
{
if (resolutions[i][0] == g_useActiveViewportResolution && resolutions[i][1] == g_useActiveViewportResolution)
{
m_ui->m_resolutionCombo->addItem(tr("Active View Resolution"));
stashActiveViewportResolution(); // render dialog is modal, so we can stash the viewport res on init
}
else
{
m_ui->m_resolutionCombo->addItem(tr("%1 x %2").arg(resolutions[i][0]).arg(resolutions[i][1]));
}
}
m_ui->m_resolutionCombo->addItem(tr("Custom..."));
m_ui->m_resolutionCombo->setCurrentIndex(0);
// Fill the FPS combo box.
for (int i = 0; i < AZStd::size(fpsOptions); ++i)
{
m_ui->m_fpsCombo->addItem(fpsOptions[i].fpsDesc);
}
m_ui->m_fpsCombo->setCurrentIndex(0);
// Fill the image format combo box.
for (int i = 0; i < AZStd::size(imageFormatExtensions); ++i)
{
m_ui->m_imageFormatCombo->addItem(imageFormatExtensions[i]);
}
m_ui->m_imageFormatCombo->setCurrentIndex(0);
m_ui->BATCH_RENDER_FILE_PREFIX->setText("Frame");
m_ui->BATCH_RENDER_FILE_PREFIX->setValidator(m_prefixValidator.data());
m_ui->m_progressStatusMsg->setText("Not running");
m_ui->BATCH_RENDER_REMOVE_SEQ->setEnabled(false);
m_ui->m_pGoBtn->setEnabled(false);
m_ui->m_pGoBtn->setIcon(QPixmap(":/Trackview/clapperboard_ready.png"));
m_ui->m_progressBar->setRange(0, 100);
m_ui->BATCH_RENDER_FRAME_IN_FPS->setText(tr("In %1 FPS").arg(static_cast<int>(m_fpsForTimeToFrameConversion)));
m_bFFMPEGCommandAvailable = GetIEditor()->GetICommandManager()->IsRegistered("plugin", "ffmpeg_encode");
m_ffmpegPluginStatusMsg = m_bFFMPEGCommandAvailable ?
QString("") :
tr("FFMPEG plug-in isn't found(creating a video isn't supported).");
m_ui->BATCH_RENDER_PRESS_ESC_TO_CANCEL->setText(m_ffmpegPluginStatusMsg);
// Disable the create video checkbox if the ffmpeg command is not available
if (!m_bFFMPEGCommandAvailable)
{
m_ui->m_createVideoCheckBox->setChecked(false);
m_ui->m_createVideoCheckBox->setEnabled(false);
}
// Load previously saved options, if any.
QString defaultPresetPath = Path::GetUserSandboxFolder();
defaultPresetPath += defaultPresetFilename;
if (CFileUtil::FileExists(defaultPresetPath))
{
LoadOutputOptions(defaultPresetPath);
}
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnRenderItemSelChange()
{
// Enable/disable the 'remove'/'update' button properly.
bool bNoSelection = !m_ui->m_renderList->selectionModel()->hasSelection();
m_ui->BATCH_RENDER_REMOVE_SEQ->setEnabled(bNoSelection ? false : true);
CheckForEnableUpdateButton();
if (bNoSelection)
{
return;
}
// Apply the settings of the selected one to the dialog.
const SRenderItem& item = m_renderItems[m_ui->m_renderList->currentIndex().row()];
// sequence
for (int i = 0; i < m_ui->m_sequenceCombo->count(); ++i)
{
const QString sequenceName = m_ui->m_sequenceCombo->itemText(i);
if (sequenceName == item.pSequence->GetName())
{
m_ui->m_sequenceCombo->setCurrentIndex(i);
OnSequenceSelected();
break;
}
}
// director
for (int i = 0; i < m_ui->m_shotCombo->count(); ++i)
{
const QString directorName = m_ui->m_shotCombo->itemText(i);
if (directorName == item.pDirectorNode->GetName())
{
m_ui->m_shotCombo->setCurrentIndex(i);
break;
}
}
// frame range
m_ui->m_startFrame->setValue(static_cast<int>(item.frameRange.start * m_fpsForTimeToFrameConversion));
m_ui->m_endFrame->setValue(static_cast<int>(item.frameRange.end * m_fpsForTimeToFrameConversion));
// folder
m_ui->m_destinationEdit->setText(item.folder);
// fps
bool bFound = false;
for (int i = 0; i < arraysize(fpsOptions); ++i)
{
if (item.fps == fpsOptions[i].fps)
{
m_ui->m_fpsCombo->setCurrentIndex(i);
bFound = true;
break;
}
}
if (bFound == false)
{
m_customFPS = item.fps;
m_ui->m_fpsCombo->setCurrentText(QString::number(item.fps));
}
// prefix
m_ui->BATCH_RENDER_FILE_PREFIX->setText(item.prefix);
m_ui->m_disableDebugInfoCheckBox->setChecked(item.disableDebugInfo);
// create_video
if (m_bFFMPEGCommandAvailable)
{
m_ui->m_createVideoCheckBox->setChecked(item.bCreateVideo);
}
// resolution
bFound = false;
for (int i = 0; i < arraysize(resolutions); ++i)
{
if (item.resW == resolutions[i][0] && item.resH == resolutions[i][1])
{
m_ui->m_resolutionCombo->setCurrentIndex(i);
bFound = true;
break;
}
}
if (bFound == false)
{
int indexOfCustomRes = arraysize(resolutions);
const QString resText = QString::fromLatin1(customResFormat).arg(item.resW).arg(item.resH);
m_customResW = item.resW;
m_customResH = item.resH;
m_ui->m_resolutionCombo->removeItem(indexOfCustomRes);
m_ui->m_resolutionCombo->addItem(resText);
m_ui->m_resolutionCombo->setCurrentIndex(indexOfCustomRes);
}
// cvars
QString cvarsText;
for (size_t i = 0; i < item.cvars.size(); ++i)
{
cvarsText += item.cvars[static_cast<int>(i)];
cvarsText += "\r\n";
}
m_ui->m_cvarsEdit->setPlainText(cvarsText);
}
void CSequenceBatchRenderDialog::CheckForEnableUpdateButton()
{
bool enable = false;
// Enable the Update button if any ui elements are changed
// from the currently selected render item.
if (m_ui->m_renderList->selectionModel()->hasSelection())
{
SRenderItem item;
if (SetUpNewRenderItem(item))
{
int index = m_ui->m_renderList->currentIndex().row();
assert(index >= 0 && index < m_renderItems.size());
enable = !(m_renderItems[index] == item);
}
}
m_ui->m_updateBtn->setEnabled(enable);
}
void CSequenceBatchRenderDialog::OnAddRenderItem()
{
// If there is no director node, it cannot be added.
if (m_ui->m_shotCombo->count() == 0)
{
QMessageBox::critical(this, tr("Cannot add"), tr("No director available!"));
return;
}
// Set up a new render item.
SRenderItem item;
if (SetUpNewRenderItem(item) == false)
{
return;
}
// Check a duplication before adding.
for (size_t i = 0; i < m_renderItems.size(); ++i)
{
if (m_renderItems[i] == item)
{
QMessageBox::critical(this, tr("Cannot add"), tr("The same item already exists"));
return;
}
}
AddItem(item);
}
void CSequenceBatchRenderDialog::OnRemoveRenderItem()
{
int index = m_ui->m_renderList->currentIndex().row();
assert(index != CB_ERR);
m_ui->m_renderList->model()->removeRow(index);
m_renderItems.erase(m_renderItems.begin() + index);
if (m_renderItems.empty())
{
m_ui->BATCH_RENDER_REMOVE_SEQ->setEnabled(false);
m_ui->m_pGoBtn->setEnabled(false);
}
else
{
m_ui->m_renderList->setCurrentIndex(m_ui->m_renderList->model()->index(0, 0));
OnRenderItemSelChange();
}
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnClearRenderItems()
{
m_ui->m_renderList->model()->removeRows(0, m_ui->m_renderList->model()->rowCount());
m_renderItems.clear();
m_ui->BATCH_RENDER_REMOVE_SEQ->setEnabled(false);
m_ui->m_pGoBtn->setEnabled(false);
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnUpdateRenderItem()
{
int index = m_ui->m_renderList->currentIndex().row();
assert(index != -1);
// Set up a new render item.
SRenderItem item;
SetUpNewRenderItem(item);
// Check a duplication before updating.
for (size_t i = 0; i < m_renderItems.size(); ++i)
{
if (m_renderItems[i] == item)
{
QMessageBox::critical(this, tr("Cannot update"), tr("The same item already exists!"));
return;
}
}
// Update the item.
m_renderItems[index] = item;
// Update the list box, too.
m_ui->m_renderList->model()->setData(m_ui->m_renderList->model()->index(index, 0), GetCaptureItemString(item));
m_ui->m_updateBtn->setEnabled(false);
}
void CSequenceBatchRenderDialog::OnLoadPreset()
{
QString loadPath;
if (CFileUtil::SelectFile("Preset Files (*.preset)", Path::GetUserSandboxFolder(), loadPath))
{
if (LoadOutputOptions(loadPath) == false)
{
QMessageBox::critical(this, tr("Cannot load"), tr("The file version is different!"));
}
}
}
void CSequenceBatchRenderDialog::OnSavePreset()
{
QString savePath;
if (CFileUtil::SelectSaveFile("Preset Files (*.preset)", "preset", Path::GetUserSandboxFolder(), savePath))
{
SaveOutputOptions(savePath);
}
}
void CSequenceBatchRenderDialog::stashActiveViewportResolution()
{
// stash active resolution in global vars
activeViewportWidth = resolutions[0][0];
activeViewportHeight = resolutions[0][1];
CViewport* activeViewport = GetIEditor()->GetActiveView();
if (activeViewport)
{
activeViewport->GetDimensions(&activeViewportWidth, &activeViewportHeight);
}
}
void CSequenceBatchRenderDialog::OnGo()
{
if (m_renderContext.IsInRendering())
{
OnCancelRender();
}
else
{
// Start a new batch.
m_ui->m_pGoBtn->setText("Cancel");
m_ui->m_pGoBtn->setIcon(QPixmap(":/Trackview/clapperboard_cancel.png"));
// Inform the movie system that it soon will be in a batch-rendering mode.
GetIEditor()->GetMovieSystem()->EnableBatchRenderMode(true);
// Initialize the context.
InitializeContext();
// Trigger the first item.
OnMovieEvent(IMovieListener::eMovieEvent_Stopped, nullptr);
}
}
void CSequenceBatchRenderDialog::OnMovieEvent(IMovieListener::EMovieEvent event, IAnimSequence* pSequence)
{
if (event == IMovieListener::eMovieEvent_Stopped
|| event == IMovieListener::eMovieEvent_Aborted)
{
// Finalize the current one, if any.
if (pSequence)
{
EnterCaptureState(CaptureState::End);
m_renderContext.endingSequence = pSequence;
m_renderContext.canceled = (event == IMovieListener::eMovieEvent_Aborted);
}
else
{
// This is odd, but this is the condition that starts the first item capturing
// when the user presses the start button.
if (m_renderItems.size() > 0)
{
// Setup and trigger the first time
m_renderContext.spentTime = 0.0f;
m_renderContext.currentItemIndex = 0;
CaptureItemStart();
}
}
}
}
void CSequenceBatchRenderDialog::OnDone()
{
if (m_renderContext.IsInRendering())
{
OnCancelRender();
}
else
{
// Save options when closed.
QString defaultPresetPath = Path::GetUserSandboxFolder();
defaultPresetPath += defaultPresetFilename;
SaveOutputOptions(defaultPresetPath);
reject();
}
}
void CSequenceBatchRenderDialog::OnSequenceSelected()
{
// Get the selected sequence.
const QString seqName = m_ui->m_sequenceCombo->currentText();
IAnimSequence* pSequence = GetIEditor()->GetMovieSystem()->FindLegacySequenceByName(seqName.toUtf8().data());
// Adjust the frame range.
float sFrame = pSequence->GetTimeRange().start * m_fpsForTimeToFrameConversion;
float eFrame = pSequence->GetTimeRange().end * m_fpsForTimeToFrameConversion;
m_ui->m_startFrame->setRange(0, static_cast<int>(eFrame));
m_ui->m_endFrame->setRange(0, static_cast<int>(eFrame));
// Set the default start/end frames properly.
m_ui->m_startFrame->setValue(static_cast<int>(sFrame));
m_ui->m_endFrame->setValue(static_cast<int>(eFrame));
m_ui->m_shotCombo->clear();
// Fill the shot combo box with the names of director nodes.
for (int i = 0; i < pSequence->GetNodeCount(); ++i)
{
if (pSequence->GetNode(i)->GetType() == AnimNodeType::Director)
{
m_ui->m_shotCombo->addItem(pSequence->GetNode(i)->GetName());
}
}
m_ui->m_shotCombo->setCurrentIndex(0);
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnFPSEditChange()
{
const QString fpsText = m_ui->m_fpsCombo->currentText();
bool ok;
const int fps = fpsText.toInt(&ok);
bool bInvalidInput = !ok || fps <= 0;
if (bInvalidInput)
{
m_ui->m_fpsCombo->setCurrentIndex(0);
}
else
{
m_customFPS = fps;
}
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnFPSChange(int itemIndex)
{
m_customFPS = fpsOptions[itemIndex].fps;
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnImageFormatChange()
{
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnResolutionSelected()
{
int indexOfCustomRes = arraysize(resolutions);
if (m_ui->m_resolutionCombo->currentIndex() == indexOfCustomRes)
{
int defaultW;
int defaultH;
const QString currentCustomResText = m_ui->m_resolutionCombo->currentText();
GetResolutionFromCustomResText(currentCustomResText.toStdString().c_str(), defaultW, defaultH);
CCustomResolutionDlg resDlg(defaultW, defaultH, this);
if (resDlg.exec() == QDialog::Accepted)
{
const int maxRes = 8192;
m_customResW = min(resDlg.GetWidth(), maxRes);
m_customResH = min(resDlg.GetHeight(), maxRes);
const QString resText = QString(customResFormat).arg(m_customResW).arg(m_customResH);
m_ui->m_resolutionCombo->setItemText(indexOfCustomRes, resText);
m_ui->m_resolutionCombo->setCurrentIndex(indexOfCustomRes);
}
else
{
m_ui->m_resolutionCombo->setCurrentIndex(0);
}
}
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::SaveOutputOptions(const QString& pathname) const
{
XmlNodeRef batchRenderOptionsNode = XmlHelpers::CreateXmlNode("batchrenderoptions");
batchRenderOptionsNode->setAttr("version", kBatchRenderFileVersion);
// Resolution
XmlNodeRef resolutionNode = batchRenderOptionsNode->newChild("resolution");
resolutionNode->setAttr("cursel", m_ui->m_resolutionCombo->currentIndex());
if (m_ui->m_resolutionCombo->currentIndex() == arraysize(resolutions))
{
const QString resText = m_ui->m_resolutionCombo->currentText();
resolutionNode->setContent(resText.toUtf8().data());
}
// FPS
XmlNodeRef fpsNode = batchRenderOptionsNode->newChild("fps");
fpsNode->setAttr("cursel", m_ui->m_fpsCombo->currentIndex());
const QString fpsText = m_ui->m_fpsCombo->currentText();
if (m_ui->m_fpsCombo->currentIndex() == -1 || m_ui->m_fpsCombo->findText(fpsText) == -1)
{
fpsNode->setContent(fpsText.toUtf8().data());
}
// Capture options (format, buffer, prefix, create_video)
XmlNodeRef imageNode = batchRenderOptionsNode->newChild("image");
imageNode->setAttr("format", m_ui->m_imageFormatCombo->currentIndex() % arraysize(imageFormatExtensions));
const QString prefix = m_ui->BATCH_RENDER_FILE_PREFIX->text();
imageNode->setAttr("prefix", prefix.toUtf8().data());
bool disableDebugInfo = m_ui->m_disableDebugInfoCheckBox->isChecked();
imageNode->setAttr("disabledebuginfo", disableDebugInfo);
bool bCreateVideoOn = m_ui->m_createVideoCheckBox->isChecked();
imageNode->setAttr("createvideo", bCreateVideoOn);
// Custom configs
XmlNodeRef cvarsNode = batchRenderOptionsNode->newChild("cvars");
const QStringList lines = m_ui->m_cvarsEdit->toPlainText().split(QStringLiteral("\n"));
for (const QString& line : lines)
{
cvarsNode->newChild("cvar")->setContent(line.toUtf8().data());
}
// Destination
XmlNodeRef destinationNode = batchRenderOptionsNode->newChild("destination");
const QString destinationText = m_ui->m_destinationEdit->text();
destinationNode->setContent(destinationText.toUtf8().data());
batchRenderOptionsNode->saveToFile(pathname.toUtf8().data());
}
bool CSequenceBatchRenderDialog::GetResolutionFromCustomResText(const char* customResText, int& retCustomWidth, int& retCustomHeight) const
{
// initialize to first resolution preset as default if the sscanf below doesn't scan values successfully
retCustomWidth = resolutions[0][0];
retCustomHeight = resolutions[0][1];
bool scanSuccess = false;
int scannedWidth = retCustomWidth; // initialize with default fall-back values - they'll be overwritten in the case of a succesful sscanf below.
int scannedHeight = retCustomHeight;
scanSuccess = (azsscanf(customResText, "Custom(%d x %d)...", &scannedWidth, &scannedHeight) == 2);
if (scanSuccess)
{
retCustomWidth = scannedWidth;
retCustomHeight = scannedHeight;
}
return scanSuccess;
}
bool CSequenceBatchRenderDialog::LoadOutputOptions(const QString& pathname)
{
XmlNodeRef batchRenderOptionsNode = XmlHelpers::LoadXmlFromFile(pathname.toStdString().c_str());
if (batchRenderOptionsNode == nullptr)
{
return true;
}
int version = 0;
batchRenderOptionsNode->getAttr("version", version);
if (version != kBatchRenderFileVersion)
{
return false;
}
// Resolution
XmlNodeRef resolutionNode = batchRenderOptionsNode->findChild("resolution");
if (resolutionNode)
{
int curSel = CB_ERR;
resolutionNode->getAttr("cursel", curSel);
if (curSel == arraysize(resolutions))
{
const QString customResText = resolutionNode->getContent();
m_ui->m_resolutionCombo->setItemText(curSel, customResText);
GetResolutionFromCustomResText(customResText.toStdString().c_str(), m_customResW, m_customResH);
}
m_ui->m_resolutionCombo->setCurrentIndex(curSel);
}
// FPS
XmlNodeRef fpsNode = batchRenderOptionsNode->findChild("fps");
if (fpsNode)
{
int curSel = -1;
fpsNode->getAttr("cursel", curSel);
if (curSel == -1)
{
m_ui->m_fpsCombo->setCurrentIndex(-1);
m_ui->m_fpsCombo->setCurrentText(fpsNode->getContent());
m_customFPS = QString::fromLatin1(fpsNode->getContent()).toInt();
}
else
{
m_ui->m_fpsCombo->setCurrentIndex(curSel);
}
}
// Capture options (format, buffer, prefix, create_video)
XmlNodeRef imageNode = batchRenderOptionsNode->findChild("image");
if (imageNode)
{
int curSel = CB_ERR;
imageNode->getAttr("format", curSel);
m_ui->m_imageFormatCombo->setCurrentIndex(curSel);
curSel = CB_ERR;
m_ui->BATCH_RENDER_FILE_PREFIX->setText(imageNode->getAttr("prefix"));
bool disableDebugInfo = false;
imageNode->getAttr("disabledebuginfo", disableDebugInfo);
m_ui->m_disableDebugInfoCheckBox->setChecked(disableDebugInfo);
if (m_bFFMPEGCommandAvailable)
{
bool bCreateVideoOn = false;
imageNode->getAttr("createvideo", bCreateVideoOn);
m_ui->m_createVideoCheckBox->setChecked(bCreateVideoOn);
}
}
// Custom configs
XmlNodeRef cvarsNode = batchRenderOptionsNode->findChild("cvars");
if (cvarsNode)
{
QString cvarsText;
for (int i = 0; i < cvarsNode->getChildCount(); ++i)
{
cvarsText += cvarsNode->getChild(i)->getContent();
if (i < cvarsNode->getChildCount() - 1)
{
cvarsText += QStringLiteral("\r\n");
}
}
m_ui->m_cvarsEdit->setPlainText(cvarsText);
}
// Destination
XmlNodeRef destinationNode = batchRenderOptionsNode->findChild("destination");
if (destinationNode)
{
m_ui->m_destinationEdit->setText(destinationNode->getContent());
}
return true;
}
void CSequenceBatchRenderDialog::OnStartFrameChange()
{
if (m_ui->m_startFrame->value() >= m_ui->m_endFrame->value())
{
m_ui->m_endFrame->setValue(m_ui->m_startFrame->value() + 1);
}
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::OnEndFrameChange()
{
if (m_ui->m_startFrame->value() >= m_ui->m_endFrame->value())
{
m_ui->m_startFrame->setValue(m_ui->m_endFrame->value() - 1);
}
CheckForEnableUpdateButton();
}
void CSequenceBatchRenderDialog::InitializeContext()
{
m_renderContext.currentItemIndex = 0;
m_renderContext.spentTime = 0;
m_renderContext.expectedTotalTime = 0;
for (size_t i = 0; i < m_renderItems.size(); ++i)
{
Range rng = m_renderItems[i].frameRange;
m_renderContext.expectedTotalTime += rng.end - rng.start;
}
m_renderContext.captureOptions.once = false;
m_ui->BATCH_RENDER_PRESS_ESC_TO_CANCEL->setText(tr("Press ESC to cancel"));
}
void CSequenceBatchRenderDialog::CaptureItemStart()
{
// Disable most of the UI in group chunks.
// (Leave the start/cancel button and feedback elements).
m_ui->BATCH_RENDER_LIST_GROUP_BOX->setEnabled(false);
m_ui->BATCH_RENDER_INPUT_GROUP_BOX->setEnabled(false);
m_ui->BATCH_RENDER_OUTPUT_GROUP_BOX->setEnabled(false);
m_renderContext.canceled = false;
CV_TrackViewRenderOutputCapturing = 1;
SRenderItem renderItem = m_renderItems[m_renderContext.currentItemIndex];
IAnimSequence* nextSequence = renderItem.pSequence;
// Initialize the next one for the batch rendering.
// Set the active shot.
m_renderContext.pActiveDirectorBU = nextSequence->GetActiveDirector();
nextSequence->SetActiveDirector(renderItem.pDirectorNode);
// Back up flags and range of the sequence.
m_renderContext.flagBU = nextSequence->GetFlags();
m_renderContext.rangeBU = nextSequence->GetTimeRange();
// Change flags and range of the sequence so that it automatically starts
// once the game mode kicks in with the specified range.
nextSequence->SetFlags(m_renderContext.flagBU | IAnimSequence::eSeqFlags_PlayOnReset);
m_renderContext.captureOptions.timeStep = 1.0f / renderItem.fps;
Range newRange = renderItem.frameRange;
newRange.start -= m_renderContext.captureOptions.timeStep;
renderItem.pSequence->SetTimeRange(newRange);
// Set up the custom config cvars for this item.
for (size_t i = 0; i < renderItem.cvars.size(); ++i)
{
GetIEditor()->GetSystem()->GetIConsole()->ExecuteString(renderItem.cvars[static_cast<int>(i)].toUtf8().data());
}
// Set specific capture options for this item.
m_renderContext.captureOptions.prefix = renderItem.prefix.toUtf8().data();
Range rng = nextSequence->GetTimeRange();
m_renderContext.captureOptions.duration = rng.end - rng.start;
QString folder = renderItem.folder;
QString itemText = m_ui->m_renderList->model()->index(m_renderContext.currentItemIndex, 0).data().toString();
itemText.replace('/', '-'); // A full sequence name can have slash characters which aren't suitable for a file name.
folder += "/";
folder += itemText;
// If this is a relative path, prepend the @products@ folder to match where the Renderer is going
// to dump the frame buffer image captures.
if (AzFramework::StringFunc::Path::IsRelative(folder.toUtf8().data()))
{
AZStd::string absolutePath;
AZStd::string assetsRoot = AZ::IO::FileIOBase::GetInstance()->GetAlias("@products@");
AzFramework::StringFunc::Path::Join(assetsRoot.c_str(), folder.toUtf8().data(), absolutePath);
folder = absolutePath.c_str();
}
QString finalFolder = folder;
int i = 2;
while (QFileInfo::exists(finalFolder))
{
finalFolder = folder;
const QString suffix = QString::fromLatin1("_v%1").arg(i);
finalFolder += suffix;
++i;
}
// create a new folder before writing any files
QDir().mkdir(finalFolder);
m_renderContext.captureOptions.folder = finalFolder.toUtf8().data();
// Change the resolution.
const int renderWidth = getResWidth(renderItem.resW);
const int renderHeight = getResHeight(renderItem.resH);
ICVar* pCVarCustomResWidth = gEnv->pConsole->GetCVar("r_CustomResWidth");
ICVar* pCVarCustomResHeight = gEnv->pConsole->GetCVar("r_CustomResHeight");
if (pCVarCustomResWidth && pCVarCustomResHeight)
{
// If available, use the custom resolution cvars.
m_renderContext.cvarCustomResWidthBU = pCVarCustomResWidth->GetIVal();
m_renderContext.cvarCustomResHeightBU = pCVarCustomResHeight->GetIVal();
pCVarCustomResWidth->Set(renderWidth);
pCVarCustomResHeight->Set(renderHeight);
}
else
{
// Otherwise, try to adjust the viewport resolution accordingly.
if (CLayoutViewPane* viewPane = MainWindow::instance()->GetActiveView())
{
viewPane->ResizeViewport(renderWidth, renderHeight);
}
}
// turn off debug info if requested
ICVar* cvarDebugInfo = gEnv->pConsole->GetCVar("r_DisplayInfo");
if (cvarDebugInfo)
{
// cache the current value to restore during OnCaptureItemEnd()
m_renderContext.cvarDisplayInfoBU = cvarDebugInfo->GetIVal();
if (renderItem.disableDebugInfo && cvarDebugInfo->GetIVal())
{
const int DISPLAY_INFO_OFF = 0;
cvarDebugInfo->Set(DISPLAY_INFO_OFF);
}
}
// create a new atom pipeline to capture the frames of the current sequence
m_atomOutputFrameCapture.CreatePipeline(
*TrackView::SceneFromGameEntityContext(), "TrackViewSequencePipeline", renderItem.resW, renderItem.resH);
UpdateAtomOutputFrameCaptureView(m_atomOutputFrameCapture, renderItem.resW, renderItem.resH);
GetIEditor()->GetMovieSystem()->EnableFixedStepForCapture(m_renderContext.captureOptions.timeStep);
// The capturing doesn't actually start here. It just flags the warming-up and
// once it's done, then the capturing really begins.
// The warming-up is necessary to settle down some post-fxs after the resolution change.
EnterCaptureState(CaptureState::WarmingUpAfterResChange);
m_renderTimer.start();
}
void CSequenceBatchRenderDialog::OnUpdateWarmingUpAfterResChange()
{
UpdateSpinnerProgressMessage("Warming up");
// Spend 30 frames warming up after frame buffer resolution change
if (m_renderContext.framesSpentInCurrentPhase++ >= 30)
{
// We will handle the idle tick manually now because calling Game Update directly.
SetEnableEditorIdleProcessing(false);
GetIEditor()->SetInGameMode(true);
EnterCaptureState(CaptureState::EnteringGameMode);
}
}
void CSequenceBatchRenderDialog::OnUpdateEnteringGameMode()
{
UpdateSpinnerProgressMessage("Entering game mode");
GetIEditor()->GetGameEngine()->Update();
// Pause the movie player on the first frame
if (m_renderContext.framesSpentInCurrentPhase++ == 0)
{
GetIEditor()->GetMovieSystem()->Pause();
}
// Spend 30 frames warming up after changing to game mode.
else if (m_renderContext.framesSpentInCurrentPhase++ > 30)
{
EnterCaptureState(CaptureState::BeginPlayingSequence);
}
}
void CSequenceBatchRenderDialog::OnUpdateBeginPlayingSequence()
{
UpdateSpinnerProgressMessage("Begin Playing Sequence");
SRenderItem renderItem = m_renderItems[m_renderContext.currentItemIndex];
GetIEditor()->GetMovieSystem()->AddMovieListener(renderItem.pSequence, this);
GetIEditor()->GetMovieSystem()->Resume();
// Set the time range for this render, back it up 1 frame so the capture will start
// exactly on the first frame.
Range newRange = renderItem.frameRange;
newRange.start -= m_renderContext.captureOptions.timeStep;
renderItem.pSequence->SetTimeRange(newRange);
// Start the sequence playing
GetIEditor()->GetMovieSystem()->SetPlayingTime(renderItem.pSequence, newRange.start);
EnterCaptureState(CaptureState::Capturing);
}
void CSequenceBatchRenderDialog::OnUpdateCapturing()
{
// Make sure we are still in game mode if we are capturing, so we can never
// get soft locked if we somehow leave game mode without this module knowing about it.
if (!GetIEditor()->IsInGameMode())
{
m_renderContext.endingSequence = m_renderItems[m_renderContext.currentItemIndex].pSequence;
m_renderContext.canceled = true;
EnterCaptureState(CaptureState::End);
return;
}
// Progress bar
IAnimSequence* pCurSeq = m_renderItems[m_renderContext.currentItemIndex].pSequence;
Range rng = pCurSeq->GetTimeRange();
float elapsedTime = GetIEditor()->GetMovieSystem()->GetPlayingTime(pCurSeq) - rng.start;
int percentage
= int(100.0f * (m_renderContext.spentTime + elapsedTime) / m_renderContext.expectedTotalTime);
m_ui->m_progressBar->setValue(percentage);
// Progress message
const QString itemText = m_ui->m_renderList->model()->index(m_renderContext.currentItemIndex, 0).data().toString();
const QString msg = tr("Rendering '%1'...(%2%)").arg(itemText).arg(static_cast<int>(100.0f * elapsedTime / (rng.end - rng.start)));
UpdateSpinnerProgressMessage(msg.toLatin1().data());
m_renderContext.framesSpentInCurrentPhase++;
}
void CSequenceBatchRenderDialog::OnUpdateEnd(IAnimSequence* sequence)
{
GetIEditor()->GetMovieSystem()->DisableFixedStepForCapture();
// Important: End batch render mode BEFORE leaving Game Mode.
// Otherwise track view will set the active camera based on the directors in the current sequence while leaving game mode
GetIEditor()->GetMovieSystem()->EnableBatchRenderMode(false);
GetIEditor()->GetMovieSystem()->RemoveMovieListener(sequence, this);
GetIEditor()->SetInGameMode(false);
GetIEditor()->GetGameEngine()->Update(); // Update is needed because SetInGameMode() queues game mode, Update() executes it.
ICVar* pCVarCustomResWidth = gEnv->pConsole->GetCVar("r_CustomResWidth");
ICVar* pCVarCustomResHeight = gEnv->pConsole->GetCVar("r_CustomResHeight");
if (pCVarCustomResWidth && pCVarCustomResHeight)
{
// Restore the custom resolution cvars.
pCVarCustomResWidth->Set(m_renderContext.cvarCustomResWidthBU);
pCVarCustomResHeight->Set(m_renderContext.cvarCustomResHeightBU);
}
// Restore display debug info
ICVar* cvarDebugInfo = gEnv->pConsole->GetCVar("r_DisplayInfo");
if (cvarDebugInfo)
{
cvarDebugInfo->Set(m_renderContext.cvarDisplayInfoBU);
}
// Restore flags, range and the active director of the sequence.
sequence->SetFlags(m_renderContext.flagBU);
sequence->SetTimeRange(m_renderContext.rangeBU);
sequence->SetActiveDirector(m_renderContext.pActiveDirectorBU);
const auto imageFormat = m_ui->m_imageFormatCombo->currentText();
SRenderItem renderItem = m_renderItems[m_renderContext.currentItemIndex];
if (m_bFFMPEGCommandAvailable && renderItem.bCreateVideo)
{
// Create a video using the ffmpeg plug-in from captured images.
m_renderContext.processingFFMPEG = true;
AZStd::string outputFolder = m_renderContext.captureOptions.folder;
auto future = QtConcurrent::run(
[renderItem, outputFolder, imageFormat]
{
AZStd::string outputFile;
AzFramework::StringFunc::Path::Join(outputFolder.c_str(), renderItem.prefix.toUtf8().data(), outputFile);
QString inputFile = outputFile.c_str();
outputFile += ".mp4";
// Use a placeholder for the input file, will expand it with replace.
QString inputFileDefine = "__input_file__";
QString command = QStringLiteral("plugin.ffmpeg_encode '%1' '%2' '%3' %4 %5 '-vf crop=%6:%7:0:0'")
.arg(inputFileDefine).arg(outputFile.c_str()).arg("mpeg4")
.arg(10240).arg(renderItem.fps).arg(getResWidth(renderItem.resW)).arg(getResHeight(renderItem.resH));
// Create the input file string, leave the %06d unexpanded for the mpeg tool.
inputFile += "%06d.";
inputFile += imageFormat;
// Replace the input file
command = command.replace(inputFileDefine, inputFile);
// Run the command
GetIEditor()->ExecuteCommand(command);
});
// Use a watcher to set a flag when the mpeg processing is complete.
connect(&m_renderContext.processingFFMPEGWatcher, &QFutureWatcher<void>::finished, this, [this]()
{
m_renderContext.processingFFMPEG = false;
});
m_renderContext.processingFFMPEGWatcher.setFuture(future);
EnterCaptureState(CaptureState::FFMPEGProcessing);
}
else
{
EnterCaptureState(CaptureState::Finalize);
}
}
void CSequenceBatchRenderDialog::OnUpdateFFMPEGProcessing()
{
UpdateSpinnerProgressMessage("FFMPEG processing");
if (!m_renderContext.processingFFMPEG)
{
EnterCaptureState(CaptureState::Finalize);
}
}
void CSequenceBatchRenderDialog::OnUpdateFinalize()
{
SetEnableEditorIdleProcessing(true);
m_renderTimer.stop();
// Turn disabled UI elements back on
m_ui->BATCH_RENDER_LIST_GROUP_BOX->setEnabled(true);
m_ui->BATCH_RENDER_INPUT_GROUP_BOX->setEnabled(true);
m_ui->BATCH_RENDER_OUTPUT_GROUP_BOX->setEnabled(true);
m_renderContext.frameNumber = 0;
m_renderContext.capturingFrame = false;
m_atomOutputFrameCapture.DestroyPipeline(*TrackView::SceneFromGameEntityContext());
// Check to see if there is more items to process
bool done = m_renderContext.currentItemIndex == m_renderItems.size() - 1;
if (done)
{
// Update end the batch message
if (m_renderContext.canceled)
{
m_ui->m_progressBar->setValue(0);
m_ui->m_progressStatusMsg->setText(tr("Rendering canceled"));
}
else
{
m_ui->m_progressBar->setValue(100);
m_ui->m_progressStatusMsg->setText(tr("Rendering finished"));
}
m_ui->m_pGoBtn->setText(tr("Start"));
m_ui->m_pGoBtn->setIcon(QPixmap(":/Trackview/clapperboard_ready.png"));
m_renderContext.currentItemIndex = -1;
m_ui->BATCH_RENDER_PRESS_ESC_TO_CANCEL->setText(m_ffmpegPluginStatusMsg);
CV_TrackViewRenderOutputCapturing = 0;
EnterCaptureState(CaptureState::Idle);
}
else
{
// Update the context.
m_renderContext.spentTime += m_renderContext.captureOptions.duration;
++m_renderContext.currentItemIndex;
// Trigger the next item.
CaptureItemStart();
}
}
void CSequenceBatchRenderDialog::OnKickIdleTimout()
{
OnKickIdle();
if (m_renderContext.IsInRendering())
{
m_renderTimer.start();
}
else
{
// All done with our custom OnKickIdle, restore editor idle.
SetEnableEditorIdleProcessing(true);
}
//When we disable the editor idle processing. system tick is no longer invoked.
//so we call it here to ensure rendering + other systems are updated.
if (!m_editorIdleProcessingEnabled)
{
AZ::ComponentApplication* componentApplication = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(componentApplication, &AZ::ComponentApplicationRequests::GetApplication);
if (componentApplication)
{
componentApplication->TickSystem();
}
}
}
void CSequenceBatchRenderDialog::OnKickIdle()
{
if (m_renderContext.captureState == CaptureState::WarmingUpAfterResChange)
{
OnUpdateWarmingUpAfterResChange();
}
else if (m_renderContext.captureState == CaptureState::EnteringGameMode)
{
OnUpdateEnteringGameMode();
}
else if (m_renderContext.captureState == CaptureState::BeginPlayingSequence)
{
OnUpdateBeginPlayingSequence();
}
else if (m_renderContext.captureState == CaptureState::Capturing)
{
OnUpdateCapturing();
}
else if (m_renderContext.captureState == CaptureState::End)
{
OnUpdateEnd(m_renderContext.endingSequence);
m_renderContext.endingSequence = nullptr;
}
else if (m_renderContext.captureState == CaptureState::FFMPEGProcessing)
{
OnUpdateFFMPEGProcessing();
}
else if (m_renderContext.captureState == CaptureState::Finalize)
{
OnUpdateFinalize();
}
else
{
if (!m_renderContext.IsInRendering())
{
CheckForEnableUpdateButton();
}
}
if (GetIEditor()->IsInGameMode())
{
// note: the internal state may change by calling GetGameEngine()->Update() so
// we must not cache this value
const auto capturing = [this]
{
return m_renderContext.captureState == CaptureState::Capturing
// this lags behind by one frame since we are capturing the back buffer,
// so don't bother enabling the capture on the first frame.
&& m_renderContext.framesSpentInCurrentPhase != 0;
};
// if we're currently trying to capture and aren't waiting for a frame
// capture to complete, it's possible to start the next capture
const auto canBeginFrameCapture = [capturing, this]
{
return capturing() && !m_renderContext.capturingFrame;
};
if (canBeginFrameCapture())
{
// update the time so the frame number can be calculated in StartCapture()
IAnimSequence* sequence = m_renderItems[m_renderContext.currentItemIndex].pSequence;
m_renderContext.captureOptions.time = GetIEditor()->GetMovieSystem()->GetPlayingTime(sequence);
GetIEditor()->GetMovieSystem()->StartCapture(m_renderContext.captureOptions, ++m_renderContext.frameNumber);
GetIEditor()->GetMovieSystem()->ControlCapture();
}
// if we're not capturing or we're not currently waiting for the current frame to finish
// being captured, it's safe to move to the next step of the main update
if (!capturing() || !m_renderContext.capturingFrame)
{
const auto& renderItem = m_renderItems[m_renderContext.currentItemIndex];
// update the view given the current camera transform and projection
UpdateAtomOutputFrameCaptureView(m_atomOutputFrameCapture, renderItem.resW, renderItem.resH);
GetIEditor()->GetGameEngine()->Update(); // step update (original frame capture)
}
if (canBeginFrameCapture())
{
const AZStd::string fileName = AZStd::string::format("Frame_%06d", m_renderContext.frameNumber);
AZStd::string filePath;
AzFramework::StringFunc::Path::Join(
m_renderContext.captureOptions.folder.c_str(), fileName.c_str(), filePath, /*caseInsensitive=*/true,
/*normalize=*/false);
// track view callback after each frame is captured
const auto captureFinishedCallback = [this]() {
m_renderContext.capturingFrame = false;
GetIEditor()->GetMovieSystem()->EndCapture();
GetIEditor()->GetMovieSystem()->ControlCapture();
};
const auto imageFormatExtension = m_ui->m_imageFormatCombo->currentText();
// readback result callback (how the image should be captured)
// currently only .dds and .ppm
const auto readbackCallback = [filePath,
imageFormatExtension](const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult) {
const auto imageFormatExtensionUtf8 = imageFormatExtension.toUtf8();
const auto imageFormatExtensionCstr = imageFormatExtensionUtf8.constData();
const AZStd::string fileName = AZStd::string::format("%s.%s", filePath.c_str(), imageFormatExtensionCstr);
if (AZ::StringFunc::Equal(imageFormatExtensionCstr, "dds"))
{
if (const AZ::Render::FrameCaptureOutputResult result = AZ::Render::DdsFrameCaptureOutput(fileName, readbackResult);
result.m_errorMessage.has_value())
{
AZ_Printf("TrackView", "Dds frame capture failed: %s", result.m_errorMessage.value().c_str());
}
}
else if (AZ::StringFunc::Equal(imageFormatExtensionCstr, "ppm"))
{
if (const AZ::Render::FrameCaptureOutputResult result = AZ::Render::PpmFrameCaptureOutput(fileName, readbackResult);
result.m_errorMessage.has_value())
{
AZ_Printf("TrackView", "Ppm frame capture failed: %s", result.m_errorMessage.value().c_str());
}
}
else
{
AZ_Printf("TrackView", "Image format .%s not supported", imageFormatExtensionCstr);
}
};
m_renderContext.capturingFrame = m_atomOutputFrameCapture.BeginCapture(readbackCallback, captureFinishedCallback);
}
}
else
{
// Post events, this will cause an Update tick.
qApp->sendPostedEvents();
}
}
void CSequenceBatchRenderDialog::OnCancelRender()
{
if (m_renderContext.captureState == CaptureState::Capturing)
{
// In the capturing state, abort the sequence, OnMovieEvent with an abort will fire and cause
// the transition to CaptureState::End.
GetIEditor()->GetMovieSystem()->AbortSequence(m_renderItems[m_renderContext.currentItemIndex].pSequence);
}
else if (m_renderContext.captureState == CaptureState::EnteringGameMode)
{
// In the EnteringGameMode state, the movie sequences hasn't started yet, so we can't count on an
// OnMovieEvent event to end the capture early. So transition into the End state manually.
m_renderContext.endingSequence = m_renderItems[m_renderContext.currentItemIndex].pSequence;
m_renderContext.canceled = true;
EnterCaptureState(CaptureState::End);
}
}
void CSequenceBatchRenderDialog::OnLoadBatch()
{
QString loadPath;
if (CFileUtil::SelectFile("Render Batch Files (*.batch)",
Path::GetUserSandboxFolder(), loadPath))
{
XmlNodeRef batchRenderListNode = XmlHelpers::LoadXmlFromFile(loadPath.toStdString().c_str());
if (batchRenderListNode == nullptr)
{
return;
}
int version = 0;
batchRenderListNode->getAttr("version", version);
if (version != kBatchRenderFileVersion)
{
QMessageBox::critical(this, tr("Cannot load"), tr("The file version is different!"));
return;
}
OnClearRenderItems();
for (int i = 0; i < batchRenderListNode->getChildCount(); ++i)
{
// Get an item.
SRenderItem item;
XmlNodeRef itemNode = batchRenderListNode->getChild(i);
// sequence
const QString seqName = itemNode->getAttr("sequence");
item.pSequence = GetIEditor()->GetMovieSystem()->FindLegacySequenceByName(seqName.toUtf8().data());
if (item.pSequence == nullptr)
{
QMessageBox::warning(this, tr("Sequence not found"), tr("A sequence of '%1' not found! This'll be skipped.").arg(seqName));
continue;
}
// director node
const QString directorName = itemNode->getAttr("director");
for (int k = 0; k < item.pSequence->GetNodeCount(); ++k)
{
IAnimNode* pNode = item.pSequence->GetNode(k);
if (pNode->GetType() == AnimNodeType::Director && directorName == pNode->GetName())
{
item.pDirectorNode = pNode;
break;
}
}
if (item.pDirectorNode == nullptr)
{
QMessageBox::warning(this, tr("Director node not found"), tr("A director node of '%1' not found in the sequence of '%2'! This'll be skipped.").arg(directorName).arg(seqName));
continue;
}
// frame range
itemNode->getAttr("startframe", item.frameRange.start);
itemNode->getAttr("endframe", item.frameRange.end);
// resolution
itemNode->getAttr("width", item.resW);
itemNode->getAttr("height", item.resH);
// fps
itemNode->getAttr("fps", item.fps);
// prefix
item.prefix = itemNode->getAttr("prefix");
// create_video
itemNode->getAttr("createvideo", item.bCreateVideo);
// folder
item.folder = itemNode->getAttr("folder");
// cvars
for (int k = 0; k < itemNode->getChildCount(); ++k)
{
const QString cvar = itemNode->getChild(k)->getContent();
item.cvars.push_back(cvar);
}
AddItem(item);
}
}
}
void CSequenceBatchRenderDialog::OnSaveBatch()
{
QString savePath;
if (CFileUtil::SelectSaveFile("Render Batch Files (*.batch)", "batch",
Path::GetUserSandboxFolder(), savePath))
{
XmlNodeRef batchRenderListNode = XmlHelpers::CreateXmlNode("batchrenderlist");
batchRenderListNode->setAttr("version", kBatchRenderFileVersion);
for (size_t i = 0; i < m_renderItems.size(); ++i)
{
const SRenderItem& item = m_renderItems[i];
XmlNodeRef itemNode = batchRenderListNode->newChild("item");
// sequence
itemNode->setAttr("sequence", item.pSequence->GetName());
// director node
itemNode->setAttr("director", item.pDirectorNode->GetName());
// frame range
itemNode->setAttr("startframe", item.frameRange.start);
itemNode->setAttr("endframe", item.frameRange.end);
// resolution
itemNode->setAttr("width", item.resW);
itemNode->setAttr("height", item.resH);
// fps
itemNode->setAttr("fps", item.fps);
// prefix
itemNode->setAttr("prefix", item.prefix.toUtf8().data());
// create_video
itemNode->setAttr("createvideo", item.bCreateVideo);
// folder
itemNode->setAttr("folder", item.folder.toUtf8().data());
// cvars
for (size_t k = 0; k < item.cvars.size(); ++k)
{
itemNode->newChild("cvar")->setContent(item.cvars[static_cast<int>(k)].toUtf8().data());
}
}
XmlHelpers::SaveXmlNode(GetIEditor()->GetFileUtil(), batchRenderListNode, savePath.toStdString().c_str());
}
}
bool CSequenceBatchRenderDialog::SetUpNewRenderItem(SRenderItem& item)
{
const QString seqName = m_ui->m_sequenceCombo->currentText();
const QString shotName = m_ui->m_shotCombo->currentText();
// folder
item.folder = m_ui->m_destinationEdit->text();
if (item.folder.isEmpty())
{
QMessageBox::critical(this, tr("Cannot add"), tr("The output folder should be specified!"));
return false;
}
// sequence
item.pSequence = GetIEditor()->GetMovieSystem()->FindLegacySequenceByName(seqName.toUtf8().data());
assert(item.pSequence);
// director
for (int i = 0; i < item.pSequence->GetNodeCount(); ++i)
{
IAnimNode* pNode = item.pSequence->GetNode(i);
if (pNode->GetType() == AnimNodeType::Director && shotName == pNode->GetName())
{
item.pDirectorNode = pNode;
break;
}
}
if (item.pDirectorNode == nullptr)
{
return false;
}
// frame range
item.frameRange = Range(m_ui->m_startFrame->value() / m_fpsForTimeToFrameConversion,
m_ui->m_endFrame->value() / m_fpsForTimeToFrameConversion);
// fps
if (m_ui->m_fpsCombo->currentIndex() == -1 || m_ui->m_fpsCombo->currentText() != fpsOptions[m_ui->m_fpsCombo->currentIndex()].fpsDesc)
{
item.fps = m_customFPS;
}
else
{
item.fps = fpsOptions[m_ui->m_fpsCombo->currentIndex()].fps;
}
// prefix
item.prefix = m_ui->BATCH_RENDER_FILE_PREFIX->text();
// disable debug info
item.disableDebugInfo = m_ui->m_disableDebugInfoCheckBox->isChecked();
// create_video
item.bCreateVideo = m_ui->m_createVideoCheckBox->isChecked();
// resolution
int curResSel = m_ui->m_resolutionCombo->currentIndex();
if (curResSel < arraysize(resolutions))
{
item.resW = resolutions[curResSel][0];
item.resH = resolutions[curResSel][1];
}
else
{
item.resW = m_customResW;
item.resH = m_customResH;
}
// cvars
const int kCVarNameMaxSize = 256;
char buf[kCVarNameMaxSize];
buf[kCVarNameMaxSize - 1] = 0;
const QStringList lines = m_ui->m_cvarsEdit->toPlainText().split('\n');
for (const QString& line : lines)
{
if (!line.isEmpty())
{
item.cvars.push_back(line);
}
}
return true;
}
void CSequenceBatchRenderDialog::AddItem(const SRenderItem& item)
{
// Add the item.
m_renderItems.push_back(item);
// Add it to the list box, too.
m_renderListModel->setStringList(m_renderListModel->stringList() << GetCaptureItemString(item));
m_ui->m_pGoBtn->setEnabled(true);
}
QString CSequenceBatchRenderDialog::GetCaptureItemString(const SRenderItem& item) const
{
return QString::fromLatin1("%1_%2_%3-%4(%5x%6,%7)%8").arg(item.pSequence->GetName())
.arg(item.pDirectorNode->GetName())
.arg(int(item.frameRange.start * m_fpsForTimeToFrameConversion))
.arg(int(item.frameRange.end * m_fpsForTimeToFrameConversion))
.arg(getResWidth(item.resW)).arg(getResHeight(item.resH)).arg(item.fps)
.arg(item.bCreateVideo ? "[v]" : "");
}
void CSequenceBatchRenderDialog::UpdateSpinnerProgressMessage(const char* description)
{
static int count = 0;
const char* rotatingCursor[] = { "|", "/", "-", "\\" };
const QString msg = tr("%1 %2").arg(description).arg(rotatingCursor[(count++) % arraysize(rotatingCursor)]);
m_ui->m_progressStatusMsg->setText(msg);
GetIEditor()->Notify(eNotify_OnIdleUpdate);
}
void CSequenceBatchRenderDialog::EnterCaptureState(CaptureState captureState)
{
m_renderContext.captureState = captureState;
m_renderContext.framesSpentInCurrentPhase = 0;
}
void CSequenceBatchRenderDialog::SetEnableEditorIdleProcessing(bool enabled)
{
if (enabled && !m_editorIdleProcessingEnabled)
{
EditorIdleProcessingBus::Broadcast(&EditorIdleProcessing::EnableIdleProcessing);
m_editorIdleProcessingEnabled = true;
}
if (!enabled && m_editorIdleProcessingEnabled)
{
EditorIdleProcessingBus::Broadcast(&EditorIdleProcessing::DisableIdleProcessing);
m_editorIdleProcessingEnabled = false;
}
}