Skip to content
Permalink
Browse files

[3d] export all frames from QGIS 3d animations as images (#9244)

[feature] [3d] export all frames from QGIS 3d animations as images #21300
  • Loading branch information
PeterPetrik committed Feb 27, 2019
1 parent 36ca201 commit 209e9144f87654ac6dc11dadfc539d387d571ece
@@ -4,6 +4,7 @@
SET(QGIS_3D_SRCS
qgsaabb.cpp
qgsabstract3dengine.cpp
qgs3danimationsettings.cpp
qgs3dmapscene.cpp
qgs3dmapsettings.cpp
qgs3dutils.cpp
@@ -90,6 +91,7 @@ QT5_ADD_RESOURCES(QGIS_3D_RCC_SRCS shaders.qrc)
SET(QGIS_3D_HDRS
qgsaabb.h
qgsabstract3dengine.h
qgs3danimationsettings.h
qgs3dmapscene.h
qgs3dmapsettings.h
qgs3dtypes.h
@@ -18,6 +18,8 @@
#include <QEasingCurve>
#include <QDomDocument>

Qgs3DAnimationSettings::Qgs3DAnimationSettings() = default;

float Qgs3DAnimationSettings::duration() const
{
return mKeyframes.isEmpty() ? 0 : mKeyframes.constLast().time;
@@ -17,6 +17,7 @@
#define QGS3DANIMATIONSETTINGS_H

#include "qgsvector3d.h"
#include "qgis_3d.h"

#include <QEasingCurve>
#include <QVector>
@@ -26,13 +27,16 @@ class QDomElement;
class QgsReadWriteContext;

/**
* \ingroup 3d
* Class that holds information about animation in 3D view. The animation is defined
* as a series of keyframes
* \since QGIS 3.8
*/
class Qgs3DAnimationSettings
class _3D_EXPORT Qgs3DAnimationSettings
{
public:
Qgs3DAnimationSettings() = default;
//! ctor
Qgs3DAnimationSettings();

//! keyframe definition
struct Keyframe
@@ -23,18 +23,22 @@
#include "qgsabstractgeometry.h"
#include "qgsvectorlayer.h"
#include "qgsexpressioncontextutils.h"
#include "qgsfeedback.h"
#include "qgsexpression.h"
#include "qgsexpressionutils.h"
#include "qgsoffscreen3dengine.h"

#include "qgs3dmapscene.h"
#include "qgsabstract3dengine.h"
#include "qgsterraingenerator.h"
#include "qgscameracontroller.h"

#include "qgsline3dsymbol.h"
#include "qgspoint3dsymbol.h"
#include "qgspolygon3dsymbol.h"

#include <Qt3DExtras/QPhongMaterial>


QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene )
{
QImage resImage;
@@ -76,6 +80,93 @@ QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene
return resImage;
}

bool Qgs3DUtils::exportAnimation( const Qgs3DAnimationSettings &animationSettings,
const Qgs3DMapSettings &mapSettings,
int framesPerSecond,
const QString &outputDirectory,
const QString &fileNameTemplate,
const QSize &outputSize,
QString &error,
QgsFeedback *feedback
)
{
QgsOffscreen3DEngine engine;
engine.setSize( outputSize );
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
engine.setRootEntity( scene );

if ( animationSettings.keyFrames().size() < 2 )
{
error = QObject::tr( "Unable to export 3D animation. Add at least 2 keyframes" );
return false;
}

const float duration = animationSettings.duration(); //in seconds
if ( duration <= 0 )
{
error = QObject::tr( "Unable to export 3D animation (invalid duration)." );
return false;
}

float time = 0;
int frameNo = 0;
int totalFrames = static_cast<int>( duration * framesPerSecond );

if ( fileNameTemplate.isEmpty() )
{
error = QObject::tr( "Filename template is empty" );
return false;
}

int numberOfDigits = fileNameTemplate.count( QLatin1Char( '#' ) );
if ( numberOfDigits < 0 )
{
error = QObject::tr( "Wrong filename template format (must contain #)" );
return false;
}
const QString token( numberOfDigits, QLatin1Char( '#' ) );
if ( !fileNameTemplate.contains( token ) )
{
error = QObject::tr( "Filename template must contain all # placeholders in one continuous group." );
return false;
}

while ( time <= duration )
{

if ( feedback )
{
if ( feedback->isCanceled() )
{
error = QObject::tr( "Export canceled" );
return false;
}
feedback->setProgress( frameNo / static_cast<double>( totalFrames ) * 100 );
}
++frameNo;

Qgs3DAnimationSettings::Keyframe kf = animationSettings.interpolate( time );
scene->cameraController()->setLookingAtPoint( kf.point, kf.dist, kf.pitch, kf.yaw );

QString fileName( fileNameTemplate );
const QString frameNoPaddedLeft( QStringLiteral( "%1" ).arg( frameNo, numberOfDigits, 10, QChar( '0' ) ) ); // e.g. 0001
fileName.replace( token, frameNoPaddedLeft );
const QString path = QDir( outputDirectory ).filePath( fileName );

// It would initially return empty rendered image.
// Capturing the initial image and throwing it away fixes that.
// Hopefully we will find a better fix in the future.
Qgs3DUtils::captureSceneImage( engine, scene );
QImage img = Qgs3DUtils::captureSceneImage( engine, scene );

img.save( path );

time += 1.0f / static_cast<float>( framesPerSecond );
}

return true;
}


int Qgs3DUtils::maxZoomLevel( double tile0width, double tileResolution, double maxError )
{
@@ -20,6 +20,7 @@

class QgsLineString;
class QgsPolygon;
class QgsFeedback;

class QgsAbstract3DEngine;
class QgsAbstract3DSymbol;
@@ -31,6 +32,7 @@ namespace Qt3DExtras
}

#include "qgs3dmapsettings.h"
#include "qgs3danimationsettings.h"
#include "qgs3dtypes.h"
#include "qgsaabb.h"

@@ -54,6 +56,34 @@ class _3D_EXPORT Qgs3DUtils
*/
static QImage captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene );

/**
* Captures 3D animation frames to the selected folder
*
* \param animationSettings Settings for keyframes and camera
* \param mapSettings 3d map settings
* \param framesPerSecond number of frames per second to export
* \param outputDirectory output directory where to export frames
* \param fileNameTemplate template for exporting the frames.
* Must be in format prefix####.format, where number of
* # represents how many 0 should be left-padded to the frame number
* e.g. my###.jpg will create frames my001.jpg, my002.jpg, etc
* \param outputSize size of the frame in pixels
* \param error error string in case of failure
* \param feedback optional feedback object used to cancel export or report progress
* \return whether export succeeded. In case of failure, see error argument
*
* \since QGIS 3.8
*/
static bool exportAnimation( const Qgs3DAnimationSettings &animationSettings,
const Qgs3DMapSettings &mapSettings,
int framesPerSecond,
const QString &outputDirectory,
const QString &fileNameTemplate,
const QSize &outputSize,
QString &error,
QgsFeedback *feedback = nullptr
);

/**
* Calculates the highest needed zoom level for tiles in quad-tree given width of the base tile (zoom level 0)
* in map units, resolution of the tile (e.g. tile's texture width) and desired maximum error in map units.
@@ -0,0 +1,110 @@
/***************************************************************************
qgs3danimationexportdialog.cpp
------------------------------
Date : February 2019
Copyright : (C) 2019 by Peter Petrik
Email : zilolv at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgs3danimationexportdialog.h"
#include "qgsproject.h"
#include "qgsgui.h"
#include "qgssettings.h"
#include "qgsoffscreen3dengine.h"
#include "qgs3danimationsettings.h"
#include "qgs3dmapsettings.h"
#include "qgs3dmapscene.h"
#include "qgs3dutils.h"
#include "qgscameracontroller.h"
#include "qgsspinbox.h"

#include <QtGlobal>

Qgs3DAnimationExportDialog::Qgs3DAnimationExportDialog(): QDialog( nullptr )
{
setupUi( this );
QgsSettings settings;

const QString templateText = settings.value( QStringLiteral( "Export3DAnimation/fileNameTemplate" ),
QStringLiteral( "%1####.jpg" ).arg( QgsProject::instance()->baseName() )
, QgsSettings::App ).toString();
mTemplateLineEdit->setText( templateText );
QRegExp rx( QStringLiteral( "\\w+#+\\.{1}\\w+" ) ); //e.g. anyprefix#####.png
QValidator *validator = new QRegExpValidator( rx, this );
mTemplateLineEdit->setValidator( validator );

connect( mTemplateLineEdit, &QLineEdit::textChanged, this, [ = ]
{
QgsSettings settings;
settings.setValue( QStringLiteral( "Export3DAnimation/fileNameTemplate" ), mTemplateLineEdit->text() );
} );

mOutputDirFileWidget->setStorageMode( QgsFileWidget::GetDirectory );
mOutputDirFileWidget->setDialogTitle( tr( "Select directory for 3D animation frames" ) );
mOutputDirFileWidget->lineEdit()->setShowClearButton( false );
mOutputDirFileWidget->setDefaultRoot( settings.value( QStringLiteral( "Export3DAnimation/lastDir" ), QString(), QgsSettings::App ).toString() );
mOutputDirFileWidget->setFilePath( settings.value( QStringLiteral( "Export3DAnimation/lastDir" ), QString(), QgsSettings::App ).toString() );

connect( mOutputDirFileWidget, &QgsFileWidget::fileChanged, this, [ = ]
{
QgsSettings settings;
settings.setValue( QStringLiteral( "Export3DAnimation/lastDir" ), mOutputDirFileWidget->filePath(), QgsSettings::App );
} );

mFpsSpinBox->setValue( settings.value( QStringLiteral( "Export3DAnimation/fps" ), 30 ).toInt() );
connect( mFpsSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QgsSpinBox::valueChanged ), this, [ = ]
{
QgsSettings settings;
settings.setValue( QStringLiteral( "Export3DAnimation/fps" ), mFpsSpinBox->value() );
} );

mWidthSpinBox->setValue( settings.value( QStringLiteral( "Export3DAnimation/width" ), 800 ).toInt() );
connect( mWidthSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QgsSpinBox::valueChanged ), this, [ = ]
{
QgsSettings settings;
settings.setValue( QStringLiteral( "Export3DAnimation/width" ), mWidthSpinBox->value() );
} );

mHeightSpinBox->setValue( settings.value( QStringLiteral( "Export3DAnimation/height" ), 600 ).toInt() );
connect( mHeightSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QgsSpinBox::valueChanged ), this, [ = ]
{
QgsSettings settings;
settings.setValue( QStringLiteral( "Export3DAnimation/height" ), mHeightSpinBox->value() );
} );

QgsGui::enableAutoGeometryRestore( this );
}

QString Qgs3DAnimationExportDialog::outputDirectory() const
{
const QString dir = mOutputDirFileWidget->filePath();
return dir;
}

QString Qgs3DAnimationExportDialog::fileNameExpression() const
{
const QString name = mTemplateLineEdit->text();
return name;
}

Qgs3DAnimationExportDialog::~Qgs3DAnimationExportDialog() = default;

int Qgs3DAnimationExportDialog::fps() const
{
const int fps = mFpsSpinBox->value();
return fps;
}

QSize Qgs3DAnimationExportDialog::frameSize() const
{
const int width = mWidthSpinBox->value();
const int height = mHeightSpinBox->value();
return QSize( width, height );
}
@@ -0,0 +1,51 @@
/***************************************************************************
qgs3danimationexportdialog.h
----------------------------
Date : February 2019
Copyright : (C) 2019 by Peter Petrik
Email : zilolv at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#ifndef QGS3DANIMATIONEXPORTDIALOG_H
#define QGS3DANIMATIONEXPORTDIALOG_H

#include <QWidget>
#include <memory>
#include <QSize>

#include "qgs3dmapsettings.h"
#include "qgs3danimationsettings.h"

#include "ui_animationexport3ddialog.h"

/**
* Dialog for settings for 3D animation export
*/
class Qgs3DAnimationExportDialog : public QDialog, private Ui::AnimationExport3DDialog
{
Q_OBJECT
public:
explicit Qgs3DAnimationExportDialog();
~Qgs3DAnimationExportDialog() override;

//! Returns output directory for frames
QString outputDirectory( ) const;

//! Returns filename template for frames
QString fileNameExpression( ) const;

//! Returns frames per second
int fps() const;

//! Returns size of frame in pixels
QSize frameSize() const;
};

#endif // QGS3DANIMATIONEXPORTDIALOG_H

0 comments on commit 209e914

Please sign in to comment.
You can’t perform that action at this time.