Skip to content

Commit 2dbe547

Browse files
authored
Add support for saving multiple frames when using animation (#2785)
Add support for saving multiple frames when using animation with --output and frame template filename
1 parent 57849e0 commit 2dbe547

File tree

5 files changed

+159
-29
lines changed

5 files changed

+159
-29
lines changed

application/F3DStarter.cxx

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include <algorithm>
4444
#include <atomic>
4545
#include <cassert>
46+
#include <cmath>
4647
#include <csignal>
4748
#include <filesystem>
4849
#include <fstream>
@@ -276,6 +277,34 @@ class F3DStarter::F3DInternals
276277
image.setMetadata("camera", cameraMetadata.str());
277278
}
278279

280+
/**
281+
* Save image to file or stdout.
282+
* Returns true on success, false on failure (error already logged).
283+
*/
284+
bool saveImage(f3d::image& img, const fs::path& outputPath, bool toStdout)
285+
{
286+
if (toStdout)
287+
{
288+
const auto buffer = img.saveBuffer();
289+
std::copy(buffer.begin(), buffer.end(), std::ostreambuf_iterator(std::cout));
290+
f3d::log::debug("Output image saved to stdout");
291+
}
292+
else
293+
{
294+
try
295+
{
296+
img.save(outputPath);
297+
}
298+
catch (const f3d::image::write_exception& ex)
299+
{
300+
f3d::log::error("Could not write output: ", ex.what());
301+
return false;
302+
}
303+
f3d::log::debug("Output image saved to ", outputPath);
304+
}
305+
return true;
306+
}
307+
279308
/**
280309
* Substitute the following variables in a filename template:
281310
* - `{app}`: application name (ie. `F3D`)
@@ -289,12 +318,16 @@ class F3DStarter::F3DInternals
289318
* - `{n}`: auto-incremented number to make filename unique (up to 1000000)
290319
* - `{n:2}`, `{n:3}`, ...: zero-padded auto-incremented number to make filename unique
291320
* (up to 1000000)
321+
* - `{frame}`: current animation frame number (when outputting multiple frames)
322+
* - `{frame:4}`, `{frame:5}`, ...: zero-padded animation frame number
292323
*/
293-
fs::path applyFilenameTemplate(const fs::path& templatePath)
324+
fs::path applyFilenameTemplate(
325+
const fs::path& templatePath, std::optional<int> frame = std::nullopt)
294326
{
295327
constexpr size_t maxNumberingAttempts = 1000000;
296328
const std::regex numberingRe("(n:?(.*))");
297329
const std::regex dateRe("date:?(.*)");
330+
const std::regex frameRe("(frame:?(.*))");
298331

299332
/* Return a file related string depending on the currently loaded files, or the empty string if
300333
* a single file is loaded */
@@ -373,6 +406,29 @@ class F3DStarter::F3DInternals
373406
}
374407
return formatted;
375408
}
409+
else if (std::regex_match(var, frameRe))
410+
{
411+
if (!frame.has_value())
412+
{
413+
f3d::log::warn("{frame} variable can only be used when outputting animation frames");
414+
throw f3d::utils::string_template::lookup_error(var);
415+
}
416+
std::stringstream formattedFrame;
417+
const std::string fmt = std::regex_replace(var, frameRe, "$2");
418+
try
419+
{
420+
formattedFrame << std::setfill('0') << std::setw(std::stoi(fmt)) << frame.value();
421+
}
422+
catch (std::invalid_argument&)
423+
{
424+
if (!fmt.empty())
425+
{
426+
f3d::log::warn("ignoring invalid frame format for \"", var, "\"");
427+
}
428+
formattedFrame << std::setw(0) << frame.value();
429+
}
430+
return formattedFrame.str();
431+
}
376432
throw f3d::utils::string_template::lookup_error(var);
377433
};
378434

@@ -710,7 +766,7 @@ class F3DStarter::F3DInternals
710766
}
711767
else
712768
{
713-
T localOption;
769+
T localOption{};
714770
this->ParseOption(appOptions, name, localOption);
715771
option = localOption;
716772
}
@@ -1250,8 +1306,13 @@ int F3DStarter::Start(int argc, char** argv)
12501306
f3d::utils::getEnv("CTEST_F3D_NO_DATA_FORCE_RENDER");
12511307

12521308
fs::path reference = f3d::utils::collapsePath(this->Internals->AppOptions.Reference);
1253-
fs::path output = this->Internals->applyFilenameTemplate(
1254-
f3d::utils::collapsePath(this->Internals->AppOptions.Output));
1309+
// Check if output template contains {frame} - if so, defer template application
1310+
const std::string& outputTemplate = this->Internals->AppOptions.Output;
1311+
const bool hasFrameTemplate =
1312+
f3d::utils::string_template(outputTemplate).hasVariable(std::regex("frame:?.*"));
1313+
fs::path output = hasFrameTemplate
1314+
? fs::path{}
1315+
: this->Internals->applyFilenameTemplate(f3d::utils::collapsePath(outputTemplate));
12551316

12561317
// Render and compare with file if needed
12571318
if (!reference.empty())
@@ -1337,36 +1398,67 @@ int F3DStarter::Start(int argc, char** argv)
13371398
}
13381399
}
13391400
// Render to file if needed
1340-
else if (!output.empty())
1401+
else if (!this->Internals->AppOptions.Output.empty())
13411402
{
13421403
if (this->Internals->LoadedFiles.empty() && !noDataForceRender.has_value())
13431404
{
13441405
f3d::log::error("No files loaded, no rendering performed");
13451406
return EXIT_FAILURE;
13461407
}
13471408

1348-
f3d::image img = window.renderToImage(this->Internals->AppOptions.NoBackground);
1349-
this->Internals->addOutputImageMetadata(img);
1350-
1351-
if (renderToStdout)
1409+
if (hasFrameTemplate)
13521410
{
1353-
const auto buffer = img.saveBuffer();
1354-
std::copy(buffer.begin(), buffer.end(), std::ostreambuf_iterator(std::cout));
1355-
f3d::log::debug("Output image saved to stdout");
1411+
f3d::scene& animScene = this->Internals->Engine->getScene();
1412+
const auto [minTime, maxTime] = animScene.animationTimeRange();
1413+
1414+
const double startTime = this->Internals->AppOptions.AnimationTime.value_or(minTime);
1415+
const double endTime = maxTime;
1416+
const double duration = endTime - startTime;
1417+
const int count = duration > 0
1418+
? static_cast<int>(std::ceil(duration * this->Internals->AppOptions.FrameRate)) + 1
1419+
: 1;
1420+
1421+
if (count == 1)
1422+
{
1423+
f3d::log::warn("No animation available or animation has zero duration, outputting single "
1424+
"frame");
1425+
}
1426+
1427+
const double timeStep = 1.0 / this->Internals->AppOptions.FrameRate;
1428+
1429+
f3d::log::info(
1430+
"Saving ", count, " animation frame(s) from time ", startTime, " to ", endTime);
1431+
1432+
for (int frame = 0; frame < count; ++frame)
1433+
{
1434+
const double currentTime = startTime + frame * timeStep;
1435+
animScene.loadAnimationTime(currentTime);
1436+
1437+
const fs::path frameOutput = renderToStdout
1438+
? fs::path{}
1439+
: this->Internals->applyFilenameTemplate(
1440+
f3d::utils::collapsePath(this->Internals->AppOptions.Output), frame);
1441+
1442+
f3d::image img = window.renderToImage(this->Internals->AppOptions.NoBackground);
1443+
this->Internals->addOutputImageMetadata(img);
1444+
1445+
if (!this->Internals->saveImage(img, frameOutput, renderToStdout))
1446+
{
1447+
return EXIT_FAILURE;
1448+
}
1449+
}
1450+
1451+
f3d::log::info("Saved ", count, " animation frame(s)");
13561452
}
13571453
else
13581454
{
1359-
try
1360-
{
1361-
img.save(output);
1362-
}
1363-
catch (const f3d::image::write_exception& ex)
1455+
f3d::image img = window.renderToImage(this->Internals->AppOptions.NoBackground);
1456+
this->Internals->addOutputImageMetadata(img);
1457+
1458+
if (!this->Internals->saveImage(img, output, renderToStdout))
13641459
{
1365-
f3d::log::error("Could not write output: ", ex.what());
13661460
return EXIT_FAILURE;
13671461
}
1368-
1369-
f3d::log::debug("Output image saved to ", output);
13701462
}
13711463

13721464
if (this->Internals->FilesGroups.size() > 1)

application/testing/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,15 @@ f3d_test(NAME TestOutputOutput DATA cow.vtp ARGS --reference=${CMAKE_BINARY_DIR}
13101310
f3d_test(NAME TestUnsupportedInputOutput DATA unsupportedFile.dummy REGEXP "No files loaded, no rendering performed" NO_BASELINE)
13111311
f3d_test(NAME TestOutputNoBackground DATA cow.vtp ARGS --no-background NO_BASELINE)
13121312

1313+
# Multi-frame output tests
1314+
f3d_test(NAME TestOutputFrameCount DATA BoxAnimated.gltf ARGS --output=${CMAKE_BINARY_DIR}/Testing/Temporary/TestOutputFrameCount_{frame:4}.png --frame-rate=0.25 REGEXP "Saved 2 animation frame" NO_BASELINE NO_OUTPUT)
1315+
f3d_test(NAME TestOutputFrameCountFrame0 DATA BoxAnimated.gltf ARGS --reference=${CMAKE_BINARY_DIR}/Testing/Temporary/TestOutputFrameCount_0000.png --animation-time=0 DEPENDS TestOutputFrameCount NO_BASELINE)
1316+
f3d_test(NAME TestOutputFrameCountFrame1 DATA BoxAnimated.gltf ARGS --reference=${CMAKE_BINARY_DIR}/Testing/Temporary/TestOutputFrameCount_0001.png --animation-time=3.70833 DEPENDS TestOutputFrameCount NO_BASELINE)
1317+
f3d_test(NAME TestOutputFrameCountNoAnimation DATA cow.vtp ARGS --output=${CMAKE_BINARY_DIR}/Testing/Temporary/static_{frame:4}.png REGEXP "No animation available" NO_BASELINE NO_OUTPUT)
1318+
f3d_test(NAME TestOutputFrameCountInvalidFormat DATA BoxAnimated.gltf ARGS --output=${CMAKE_BINARY_DIR}/Testing/Temporary/invalid_{frame:abc}.png --frame-rate=0.25 REGEXP "ignoring invalid frame format" NO_BASELINE NO_OUTPUT)
1319+
f3d_test(NAME TestOutputFrameCountStartTime DATA BoxAnimated.gltf ARGS --output=${CMAKE_BINARY_DIR}/Testing/Temporary/TestOutputFrameCountStartTime_{frame:4}.png --frame-rate=0.3 --animation-time=2.0 REGEXP "Saving 2 animation frame" NO_BASELINE NO_OUTPUT)
1320+
f3d_test(NAME TestScreenshotFrameVariable DATA cow.vtp ARGS --screenshot-filename=${CMAKE_BINARY_DIR}/Testing/Temporary/screenshot_{frame}.png SCRIPT TestCommandScriptScreenshotFrame.txt REGEXP "{frame} variable can only be used when outputting animation frames" NO_BASELINE)
1321+
13131322
# Basic record and play test
13141323
f3d_test(NAME TestInteractionRecord DATA cow.vtp ARGS --interaction-test-record=${CMAKE_BINARY_DIR}/Testing/Temporary/TestInteractionRecord.log NO_BASELINE)
13151324
f3d_test(NAME TestInteractionPlay DATA cow.vtp ARGS --interaction-test-play=${CMAKE_BINARY_DIR}/Testing/Temporary/TestInteractionRecord.log DEPENDS TestInteractionRecord NO_BASELINE)
@@ -1687,6 +1696,8 @@ if(NOT WIN32)
16871696
f3d_test(NAME TestInputTooLong ARGS --input=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE)
16881697
f3d_test(NAME TestReferenceTooLong DATA suzanne.ply ARGS --output=file.png --reference=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
16891698
f3d_test(NAME TestOutputTooLong DATA suzanne.ply ARGS --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
1699+
f3d_test(NAME TestOutputFrameCountTooLong DATA BoxAnimated.gltf ARGS --output=${_f3d_test_invalid_folder}/frame_{frame}.png --frame-rate=0.25 REGEXP "Could not write output" NO_BASELINE NO_OUTPUT)
1700+
f3d_test(NAME TestOutputFrameCountNoAnimationTooLong DATA cow.vtp ARGS --output=${_f3d_test_invalid_folder}/frame_{frame}.png REGEXP "Could not write output" NO_BASELINE NO_OUTPUT)
16901701
f3d_test(NAME TestOutputWithReferenceTooLong DATA suzanne.ply ARGS --reference=file.png --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
16911702
f3d_test(NAME TestOutputWithExistingReferenceTooLong DATA suzanne.ply ARGS --reference=${F3D_SOURCE_DIR}/testing/data/world.png --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
16921703

doc/user/03-OPTIONS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The input file or files to read, can also be provided as a positional argument.
1010

1111
### `--output=<png file>` (_string_)
1212

13-
Instead of showing a render view and render into it, _render directly into a png file_. When used with --ref option, only outputs on failure. If `-` is specified instead of a filename, the PNG file is streamed to the stdout. Can use [template variables](#filename-templating).
13+
Instead of showing a render view and render into it, _render directly into a png file_. When used with --ref option, only outputs on failure. If `-` is specified instead of a filename, the PNG file is streamed to the stdout. Can use [template variables](#filename-templating). When using the `{frame}` variable, multiple animation frames are exported (see [Exporting animation frames](05-ANIMATIONS.md#exporting-animation-frames)).
1414

1515
### `--no-background` (_bool_, default: `false`)
1616

@@ -569,6 +569,8 @@ The destination filename used by `--output` or to save screenshots using `--scre
569569
- `{date:format}`: current date as per C++'s `std::put_time` format
570570
- `{n}`: auto-incremented number to make filename unique (up to 1000000)
571571
- `{n:2}`, `{n:3}`, ...: zero-padded auto-incremented number to make filename unique (up to 1000000)
572+
- `{frame}`: frame number when outputting animation frames (see [Animations](05-ANIMATIONS.md))
573+
- `{frame:4}`, `{frame:5}`, ...: zero-padded frame number when outputting animation frames
572574
- variable names can be escaped by doubling the braces (eg. use `{{model}}.png` to output `{model}.png` without the model name being substituted)
573575

574576
For example the screenshot filename is configured as `{app}/{model}_{n}.png` by default, meaning that, assuming the model `hello.glb` is being viewed,

doc/user/05-ANIMATIONS.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Animations
1+
#Animations
22

33
F3D is able to play animations for any files which contain them.
44
Play them either interactively or by selecting a specific time to display.
@@ -29,13 +29,37 @@ Note: A blue bar runs along the bottom of screen to indicate the current time in
2929

3030
F3D animation behavior can be fully controlled from the command line using the following options.
3131

32-
| Options | Default | Description |
33-
| ---------------------------- | ------------------- | ----------------------------------------------- |
34-
| \-\-animation\-indices | | Select the animations to play. |
35-
| \-\-animation\-indices=-1 | | Play all animations at once (only if supported) |
36-
| \-\-animation\-speed\-factor | Time Unit = Seconds | Adjust time unit. |
37-
| \-\-animation\-frame\-rate | 60 FPS | Adjust animation frame rate. |
38-
| \-\-animation\-time | | Load a specific time value on start. |
32+
| Options | Default | Description |
33+
| ---------------------------- | ------------------- | ---------------------------------------------------- |
34+
| \-\-animation\-indices | | Select the animations to play. |
35+
| \-\-animation\-indices=-1 | | Play all animations at once (only if supported) |
36+
| \-\-animation\-speed\-factor | Time Unit = Seconds | Adjust time unit. |
37+
| \-\-frame\-rate | 60 FPS | Adjust animation (and others components) frame rate. |
38+
| \-\-animation\-time | | Load a specific time value on start. |
39+
40+
## Exporting animation frames
41+
42+
F3D can export multiple frames from an animation to image files. To do this, include `{frame}` in the output filename template:
43+
44+
```bash
45+
f3d example.file --output=frame_{frame:04}.png
46+
```
47+
48+
This will save frames as `frame_0000.png`, `frame_0001.png`, etc.
49+
50+
The number of frames is determined by `--frame-rate`
51+
52+
```bash
53+
f3d example.file --output=frame_{frame}.png --frame-rate=30
54+
```
55+
56+
Use `--animation-time` to start exporting from a specific time instead of the beginning:
57+
58+
```bash
59+
f3d example.file --output=frame_{frame}.png --frame-rate=10 --animation-time=1.5
60+
```
61+
62+
See [Filename templating](03-OPTIONS.md#filename-templating) for more template variables.
3963

4064
## Animation Interactions
4165

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
take_screenshot

0 commit comments

Comments
 (0)