Skip to content

Commit 209e914

Browse files
authored
[3d] export all frames from QGIS 3d animations as images (#9244)
[feature] [3d] export all frames from QGIS 3d animations as images #21300
1 parent 36ca201 commit 209e914

15 files changed

+617
-8
lines changed

src/3d/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
SET(QGIS_3D_SRCS
55
qgsaabb.cpp
66
qgsabstract3dengine.cpp
7+
qgs3danimationsettings.cpp
78
qgs3dmapscene.cpp
89
qgs3dmapsettings.cpp
910
qgs3dutils.cpp
@@ -90,6 +91,7 @@ QT5_ADD_RESOURCES(QGIS_3D_RCC_SRCS shaders.qrc)
9091
SET(QGIS_3D_HDRS
9192
qgsaabb.h
9293
qgsabstract3dengine.h
94+
qgs3danimationsettings.h
9395
qgs3dmapscene.h
9496
qgs3dmapsettings.h
9597
qgs3dtypes.h

src/app/3d/qgs3danimationsettings.cpp renamed to src/3d/qgs3danimationsettings.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include <QEasingCurve>
1919
#include <QDomDocument>
2020

21+
Qgs3DAnimationSettings::Qgs3DAnimationSettings() = default;
22+
2123
float Qgs3DAnimationSettings::duration() const
2224
{
2325
return mKeyframes.isEmpty() ? 0 : mKeyframes.constLast().time;

src/app/3d/qgs3danimationsettings.h renamed to src/3d/qgs3danimationsettings.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#define QGS3DANIMATIONSETTINGS_H
1818

1919
#include "qgsvector3d.h"
20+
#include "qgis_3d.h"
2021

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

2829
/**
30+
* \ingroup 3d
2931
* Class that holds information about animation in 3D view. The animation is defined
3032
* as a series of keyframes
33+
* \since QGIS 3.8
3134
*/
32-
class Qgs3DAnimationSettings
35+
class _3D_EXPORT Qgs3DAnimationSettings
3336
{
3437
public:
35-
Qgs3DAnimationSettings() = default;
38+
//! ctor
39+
Qgs3DAnimationSettings();
3640

3741
//! keyframe definition
3842
struct Keyframe

src/3d/qgs3dutils.cpp

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,22 @@
2323
#include "qgsabstractgeometry.h"
2424
#include "qgsvectorlayer.h"
2525
#include "qgsexpressioncontextutils.h"
26+
#include "qgsfeedback.h"
27+
#include "qgsexpression.h"
28+
#include "qgsexpressionutils.h"
29+
#include "qgsoffscreen3dengine.h"
2630

2731
#include "qgs3dmapscene.h"
2832
#include "qgsabstract3dengine.h"
2933
#include "qgsterraingenerator.h"
34+
#include "qgscameracontroller.h"
3035

3136
#include "qgsline3dsymbol.h"
3237
#include "qgspoint3dsymbol.h"
3338
#include "qgspolygon3dsymbol.h"
3439

3540
#include <Qt3DExtras/QPhongMaterial>
3641

37-
3842
QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene )
3943
{
4044
QImage resImage;
@@ -76,6 +80,93 @@ QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene
7680
return resImage;
7781
}
7882

83+
bool Qgs3DUtils::exportAnimation( const Qgs3DAnimationSettings &animationSettings,
84+
const Qgs3DMapSettings &mapSettings,
85+
int framesPerSecond,
86+
const QString &outputDirectory,
87+
const QString &fileNameTemplate,
88+
const QSize &outputSize,
89+
QString &error,
90+
QgsFeedback *feedback
91+
)
92+
{
93+
QgsOffscreen3DEngine engine;
94+
engine.setSize( outputSize );
95+
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
96+
engine.setRootEntity( scene );
97+
98+
if ( animationSettings.keyFrames().size() < 2 )
99+
{
100+
error = QObject::tr( "Unable to export 3D animation. Add at least 2 keyframes" );
101+
return false;
102+
}
103+
104+
const float duration = animationSettings.duration(); //in seconds
105+
if ( duration <= 0 )
106+
{
107+
error = QObject::tr( "Unable to export 3D animation (invalid duration)." );
108+
return false;
109+
}
110+
111+
float time = 0;
112+
int frameNo = 0;
113+
int totalFrames = static_cast<int>( duration * framesPerSecond );
114+
115+
if ( fileNameTemplate.isEmpty() )
116+
{
117+
error = QObject::tr( "Filename template is empty" );
118+
return false;
119+
}
120+
121+
int numberOfDigits = fileNameTemplate.count( QLatin1Char( '#' ) );
122+
if ( numberOfDigits < 0 )
123+
{
124+
error = QObject::tr( "Wrong filename template format (must contain #)" );
125+
return false;
126+
}
127+
const QString token( numberOfDigits, QLatin1Char( '#' ) );
128+
if ( !fileNameTemplate.contains( token ) )
129+
{
130+
error = QObject::tr( "Filename template must contain all # placeholders in one continuous group." );
131+
return false;
132+
}
133+
134+
while ( time <= duration )
135+
{
136+
137+
if ( feedback )
138+
{
139+
if ( feedback->isCanceled() )
140+
{
141+
error = QObject::tr( "Export canceled" );
142+
return false;
143+
}
144+
feedback->setProgress( frameNo / static_cast<double>( totalFrames ) * 100 );
145+
}
146+
++frameNo;
147+
148+
Qgs3DAnimationSettings::Keyframe kf = animationSettings.interpolate( time );
149+
scene->cameraController()->setLookingAtPoint( kf.point, kf.dist, kf.pitch, kf.yaw );
150+
151+
QString fileName( fileNameTemplate );
152+
const QString frameNoPaddedLeft( QStringLiteral( "%1" ).arg( frameNo, numberOfDigits, 10, QChar( '0' ) ) ); // e.g. 0001
153+
fileName.replace( token, frameNoPaddedLeft );
154+
const QString path = QDir( outputDirectory ).filePath( fileName );
155+
156+
// It would initially return empty rendered image.
157+
// Capturing the initial image and throwing it away fixes that.
158+
// Hopefully we will find a better fix in the future.
159+
Qgs3DUtils::captureSceneImage( engine, scene );
160+
QImage img = Qgs3DUtils::captureSceneImage( engine, scene );
161+
162+
img.save( path );
163+
164+
time += 1.0f / static_cast<float>( framesPerSecond );
165+
}
166+
167+
return true;
168+
}
169+
79170

80171
int Qgs3DUtils::maxZoomLevel( double tile0width, double tileResolution, double maxError )
81172
{

src/3d/qgs3dutils.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
class QgsLineString;
2222
class QgsPolygon;
23+
class QgsFeedback;
2324

2425
class QgsAbstract3DEngine;
2526
class QgsAbstract3DSymbol;
@@ -31,6 +32,7 @@ namespace Qt3DExtras
3132
}
3233

3334
#include "qgs3dmapsettings.h"
35+
#include "qgs3danimationsettings.h"
3436
#include "qgs3dtypes.h"
3537
#include "qgsaabb.h"
3638

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

59+
/**
60+
* Captures 3D animation frames to the selected folder
61+
*
62+
* \param animationSettings Settings for keyframes and camera
63+
* \param mapSettings 3d map settings
64+
* \param framesPerSecond number of frames per second to export
65+
* \param outputDirectory output directory where to export frames
66+
* \param fileNameTemplate template for exporting the frames.
67+
* Must be in format prefix####.format, where number of
68+
* # represents how many 0 should be left-padded to the frame number
69+
* e.g. my###.jpg will create frames my001.jpg, my002.jpg, etc
70+
* \param outputSize size of the frame in pixels
71+
* \param error error string in case of failure
72+
* \param feedback optional feedback object used to cancel export or report progress
73+
* \return whether export succeeded. In case of failure, see error argument
74+
*
75+
* \since QGIS 3.8
76+
*/
77+
static bool exportAnimation( const Qgs3DAnimationSettings &animationSettings,
78+
const Qgs3DMapSettings &mapSettings,
79+
int framesPerSecond,
80+
const QString &outputDirectory,
81+
const QString &fileNameTemplate,
82+
const QSize &outputSize,
83+
QString &error,
84+
QgsFeedback *feedback = nullptr
85+
);
86+
5787
/**
5888
* Calculates the highest needed zoom level for tiles in quad-tree given width of the base tile (zoom level 0)
5989
* in map units, resolution of the tile (e.g. tile's texture width) and desired maximum error in map units.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/***************************************************************************
2+
qgs3danimationexportdialog.cpp
3+
------------------------------
4+
Date : February 2019
5+
Copyright : (C) 2019 by Peter Petrik
6+
Email : zilolv at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgs3danimationexportdialog.h"
17+
#include "qgsproject.h"
18+
#include "qgsgui.h"
19+
#include "qgssettings.h"
20+
#include "qgsoffscreen3dengine.h"
21+
#include "qgs3danimationsettings.h"
22+
#include "qgs3dmapsettings.h"
23+
#include "qgs3dmapscene.h"
24+
#include "qgs3dutils.h"
25+
#include "qgscameracontroller.h"
26+
#include "qgsspinbox.h"
27+
28+
#include <QtGlobal>
29+
30+
Qgs3DAnimationExportDialog::Qgs3DAnimationExportDialog(): QDialog( nullptr )
31+
{
32+
setupUi( this );
33+
QgsSettings settings;
34+
35+
const QString templateText = settings.value( QStringLiteral( "Export3DAnimation/fileNameTemplate" ),
36+
QStringLiteral( "%1####.jpg" ).arg( QgsProject::instance()->baseName() )
37+
, QgsSettings::App ).toString();
38+
mTemplateLineEdit->setText( templateText );
39+
QRegExp rx( QStringLiteral( "\\w+#+\\.{1}\\w+" ) ); //e.g. anyprefix#####.png
40+
QValidator *validator = new QRegExpValidator( rx, this );
41+
mTemplateLineEdit->setValidator( validator );
42+
43+
connect( mTemplateLineEdit, &QLineEdit::textChanged, this, [ = ]
44+
{
45+
QgsSettings settings;
46+
settings.setValue( QStringLiteral( "Export3DAnimation/fileNameTemplate" ), mTemplateLineEdit->text() );
47+
} );
48+
49+
mOutputDirFileWidget->setStorageMode( QgsFileWidget::GetDirectory );
50+
mOutputDirFileWidget->setDialogTitle( tr( "Select directory for 3D animation frames" ) );
51+
mOutputDirFileWidget->lineEdit()->setShowClearButton( false );
52+
mOutputDirFileWidget->setDefaultRoot( settings.value( QStringLiteral( "Export3DAnimation/lastDir" ), QString(), QgsSettings::App ).toString() );
53+
mOutputDirFileWidget->setFilePath( settings.value( QStringLiteral( "Export3DAnimation/lastDir" ), QString(), QgsSettings::App ).toString() );
54+
55+
connect( mOutputDirFileWidget, &QgsFileWidget::fileChanged, this, [ = ]
56+
{
57+
QgsSettings settings;
58+
settings.setValue( QStringLiteral( "Export3DAnimation/lastDir" ), mOutputDirFileWidget->filePath(), QgsSettings::App );
59+
} );
60+
61+
mFpsSpinBox->setValue( settings.value( QStringLiteral( "Export3DAnimation/fps" ), 30 ).toInt() );
62+
connect( mFpsSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QgsSpinBox::valueChanged ), this, [ = ]
63+
{
64+
QgsSettings settings;
65+
settings.setValue( QStringLiteral( "Export3DAnimation/fps" ), mFpsSpinBox->value() );
66+
} );
67+
68+
mWidthSpinBox->setValue( settings.value( QStringLiteral( "Export3DAnimation/width" ), 800 ).toInt() );
69+
connect( mWidthSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QgsSpinBox::valueChanged ), this, [ = ]
70+
{
71+
QgsSettings settings;
72+
settings.setValue( QStringLiteral( "Export3DAnimation/width" ), mWidthSpinBox->value() );
73+
} );
74+
75+
mHeightSpinBox->setValue( settings.value( QStringLiteral( "Export3DAnimation/height" ), 600 ).toInt() );
76+
connect( mHeightSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QgsSpinBox::valueChanged ), this, [ = ]
77+
{
78+
QgsSettings settings;
79+
settings.setValue( QStringLiteral( "Export3DAnimation/height" ), mHeightSpinBox->value() );
80+
} );
81+
82+
QgsGui::enableAutoGeometryRestore( this );
83+
}
84+
85+
QString Qgs3DAnimationExportDialog::outputDirectory() const
86+
{
87+
const QString dir = mOutputDirFileWidget->filePath();
88+
return dir;
89+
}
90+
91+
QString Qgs3DAnimationExportDialog::fileNameExpression() const
92+
{
93+
const QString name = mTemplateLineEdit->text();
94+
return name;
95+
}
96+
97+
Qgs3DAnimationExportDialog::~Qgs3DAnimationExportDialog() = default;
98+
99+
int Qgs3DAnimationExportDialog::fps() const
100+
{
101+
const int fps = mFpsSpinBox->value();
102+
return fps;
103+
}
104+
105+
QSize Qgs3DAnimationExportDialog::frameSize() const
106+
{
107+
const int width = mWidthSpinBox->value();
108+
const int height = mHeightSpinBox->value();
109+
return QSize( width, height );
110+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/***************************************************************************
2+
qgs3danimationexportdialog.h
3+
----------------------------
4+
Date : February 2019
5+
Copyright : (C) 2019 by Peter Petrik
6+
Email : zilolv at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGS3DANIMATIONEXPORTDIALOG_H
17+
#define QGS3DANIMATIONEXPORTDIALOG_H
18+
19+
#include <QWidget>
20+
#include <memory>
21+
#include <QSize>
22+
23+
#include "qgs3dmapsettings.h"
24+
#include "qgs3danimationsettings.h"
25+
26+
#include "ui_animationexport3ddialog.h"
27+
28+
/**
29+
* Dialog for settings for 3D animation export
30+
*/
31+
class Qgs3DAnimationExportDialog : public QDialog, private Ui::AnimationExport3DDialog
32+
{
33+
Q_OBJECT
34+
public:
35+
explicit Qgs3DAnimationExportDialog();
36+
~Qgs3DAnimationExportDialog() override;
37+
38+
//! Returns output directory for frames
39+
QString outputDirectory( ) const;
40+
41+
//! Returns filename template for frames
42+
QString fileNameExpression( ) const;
43+
44+
//! Returns frames per second
45+
int fps() const;
46+
47+
//! Returns size of frame in pixels
48+
QSize frameSize() const;
49+
};
50+
51+
#endif // QGS3DANIMATIONEXPORTDIALOG_H

0 commit comments

Comments
 (0)