1
2 //
3 // This source file is part of appleseed.
4 // Visit https://appleseedhq.net/ for additional information and resources.
5 //
6 // This software is released under the MIT license.
7 //
8 // Copyright (c) 2010-2013 Francois Beaune, Jupiter Jazz Limited
9 // Copyright (c) 2014-2018 Francois Beaune, The appleseedhq Organization
10 //
11 // Permission is hereby granted, free of charge, to any person obtaining a copy
12 // of this software and associated documentation files (the "Software"), to deal
13 // in the Software without restriction, including without limitation the rights
14 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 // copies of the Software, and to permit persons to whom the Software is
16 // furnished to do so, subject to the following conditions:
17 //
18 // The above copyright notice and this permission notice shall be included in
19 // all copies or substantial portions of the Software.
20 //
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 // THE SOFTWARE.
28 //
29
30 // animatecamera headers.
31 #include "animationpath.h"
32 #include "commandlinehandler.h"
33
34 // appleseed.shared headers.
35 #include "application/application.h"
36 #include "application/superlogger.h"
37
38 // appleseed.renderer headers.
39 #include "renderer/api/camera.h"
40 #include "renderer/api/project.h"
41 #include "renderer/api/scene.h"
42 #include "renderer/api/utility.h"
43
44 // appleseed.foundation headers.
45 #include "foundation/math/aabb.h"
46 #include "foundation/math/matrix.h"
47 #include "foundation/math/scalar.h"
48 #include "foundation/math/transform.h"
49 #include "foundation/math/vector.h"
50 #include "foundation/platform/types.h"
51 #include "foundation/utility/autoreleaseptr.h"
52 #include "foundation/utility/containers/dictionary.h"
53 #include "foundation/utility/log.h"
54 #include "foundation/utility/string.h"
55
56 // Boost headers.
57 #include "boost/filesystem/path.hpp"
58
59 // Standard headers.
60 #include <cmath>
61 #include <cstddef>
62 #include <cstdlib>
63 #include <iomanip>
64 #include <map>
65 #include <memory>
66 #include <sstream>
67 #include <string>
68 #include <utility>
69 #include <vector>
70
71 using namespace appleseed::animatecamera;
72 using namespace appleseed::shared;
73 using namespace foundation;
74 using namespace renderer;
75 using namespace std;
76 namespace bf = boost::filesystem;
77
78 namespace
79 {
80 //
81 // Command line parameters are globally accessible within this file.
82 //
83
84 CommandLineHandler g_cl;
85
86
87 //
88 // Define a strict weak ordering on foundation::Transform<> objects.
89 //
90
91 template <typename T>
92 struct TransformComparer
93 {
operator ()__anon02dc10d70111::TransformComparer94 bool operator()(const Transform<T>& lhs, const Transform<T>& rhs) const
95 {
96 typedef typename Transform<T>::MatrixType MatrixType;
97
98 const MatrixType& lhs_mat = lhs.get_local_to_parent();
99 const MatrixType& rhs_mat = rhs.get_local_to_parent();
100
101 const T Eps = make_eps<T>(1.0e-6f, 1.0e-12);
102
103 for (size_t i = 0; i < MatrixType::Components; ++i)
104 {
105 const T delta = lhs_mat[i] - rhs_mat[i];
106
107 if (delta < -Eps)
108 return true;
109
110 if (delta > Eps)
111 return false;
112 }
113
114 return false;
115 }
116 };
117
118
119 //
120 // Define an ordering on pairs of transforms.
121 //
122
123 template <typename T>
124 struct TransformPairComparer
125 {
126 typedef pair<Transform<T>, Transform<T>> TransformPair;
127
operator ()__anon02dc10d70111::TransformPairComparer128 bool operator()(const TransformPair& lhs, const TransformPair& rhs) const
129 {
130 TransformComparer<T> transform_comparer;
131
132 return
133 transform_comparer(lhs.first, rhs.first) ? true :
134 transform_comparer(rhs.first, lhs.first) ? false :
135 transform_comparer(lhs.second, rhs.second);
136 }
137 };
138
139
140 //
141 // The base class for animation generators.
142 //
143
144 class AnimationGenerator
145 {
146 public:
AnimationGenerator(const string & base_output_filename,Logger & logger)147 AnimationGenerator(
148 const string& base_output_filename,
149 Logger& logger)
150 : m_base_output_filename(base_output_filename)
151 , m_logger(logger)
152 {
153 }
154
~AnimationGenerator()155 virtual ~AnimationGenerator() {}
156
generate()157 void generate()
158 {
159 const vector<size_t> frames = do_generate();
160
161 LOG_INFO(
162 m_logger,
163 "generating render script%s...",
164 g_cl.m_part_count.value() > 1 ? "s" : "");
165
166 #ifdef _WIN32
167 generate_windows_render_script(frames);
168 #else
169 LOG_WARNING(m_logger, "render script generation is not supported on this platform.");
170 #endif
171 }
172
173 protected:
174 const string m_base_output_filename;
175 Logger& m_logger;
176
177 virtual vector<size_t> do_generate() = 0;
178
make_numbered_filename(const string & filename,const size_t number,const size_t digits=4)179 static string make_numbered_filename(
180 const string& filename,
181 const size_t number,
182 const size_t digits = 4)
183 {
184 const bf::path path(filename);
185
186 stringstream sstr;
187 sstr << path.stem().string();
188 sstr << '.';
189 sstr << setw(digits) << setfill('0') << number;
190 sstr << path.extension().string();
191
192 return sstr.str();
193 }
194
load_master_project()195 auto_release_ptr<Project> load_master_project()
196 {
197 // Construct the schema file path.
198 const bf::path schema_filepath =
199 bf::path(Application::get_root_path())
200 / "schemas"
201 / "project.xsd";
202
203 // Read the master project file.
204 const char* project_filepath = g_cl.m_filenames.values()[0].c_str();
205 ProjectFileReader reader;
206 auto_release_ptr<Project> project(
207 reader.read(
208 project_filepath,
209 schema_filepath.string().c_str()));
210
211 // Bail out if the master project file couldn't be read.
212 if (project.get() == nullptr)
213 LOG_FATAL(m_logger, "failed to load master project file %s", project_filepath);
214
215 return project;
216 }
217
218 private:
generate_windows_render_script(const vector<size_t> & frames) const219 void generate_windows_render_script(const vector<size_t>& frames) const
220 {
221 const size_t part_count = g_cl.m_part_count.value();
222 const size_t frames_per_part =
223 static_cast<size_t>(ceil(static_cast<double>(frames.size()) / part_count));
224
225 for (size_t part = 1, frame_begin = 0; frame_begin < frames.size(); ++part)
226 {
227 const size_t frame_end = min(frame_begin + frames_per_part, frames.size());
228
229 generate_windows_render_script(
230 frames,
231 part,
232 frame_begin,
233 frame_end);
234
235 frame_begin = frame_end;
236 }
237 }
238
generate_windows_render_script(const vector<size_t> & frames,const size_t part,const size_t frame_begin,const size_t frame_end) const239 void generate_windows_render_script(
240 const vector<size_t>& frames,
241 const size_t part,
242 const size_t frame_begin,
243 const size_t frame_end) const
244 {
245 const string RenderScriptBaseFileName = "render.bat";
246
247 const string script_filename =
248 frame_begin == 0 && frame_end == frames.size()
249 ? RenderScriptBaseFileName
250 : make_numbered_filename(RenderScriptBaseFileName, part);
251
252 FILE* script_file = fopen(script_filename.c_str(), "wt");
253
254 if (script_file == nullptr)
255 LOG_FATAL(m_logger, "could not write to %s.", script_filename.c_str());
256
257 fprintf(
258 script_file,
259 "%s",
260 "@echo off\n"
261 "\n"
262 "set cmd_options=/WAIT /BELOWNORMAL /MIN\n"
263 "set bin=\"%1\"\n"
264 "set output_path=\"%2\"\n"
265 "set options=%*\n"
266 "\n"
267 "if %output_path% == \"\" (\n"
268 " set output_path=frames\n"
269 ")\n"
270 "\n"
271 "if %bin% == \"\" (\n"
272 " echo Usage: %0 ^<path-to-appleseed-cli^>\n"
273 " goto :end\n"
274 ")\n"
275 "\n"
276 "if not exist %bin% (\n"
277 " echo Could not find %bin%, exiting.\n"
278 " goto :end\n"
279 ")\n"
280 "\n"
281 "if not exist %output_path% (\n"
282 " mkdir %output_path%\n"
283 ")\n"
284 "\n");
285
286 const string output_format = g_cl.m_output_format.value();
287
288 for (size_t i = frame_begin; i < frame_end; ++i)
289 {
290 const size_t current_frame = i + 1;
291 const size_t actual_frame = frames[i];
292
293 const string project_filename = make_numbered_filename(m_base_output_filename + ".appleseed", current_frame);
294 const string image_filename = make_numbered_filename(m_base_output_filename + "." + output_format, current_frame);
295 const string image_filepath = "%output_path%\\" + image_filename;
296
297 fprintf(script_file, "if exist \"%s\" (\n", image_filepath.c_str());
298 fprintf(script_file, " echo Skipping %s because it was already rendered...\n", project_filename.c_str());
299 fprintf(script_file, ") else (\n");
300
301 if (actual_frame == current_frame)
302 {
303 fprintf(script_file, " echo Rendering %s to %s...\n", project_filename.c_str(), image_filepath.c_str());
304 fprintf(
305 script_file,
306 " start \"Rendering %s to %s...\" %%cmd_options%% %%bin%% %s --output \"%s\" --noise-seed " FMT_SIZE_T " %%options%%\n",
307 project_filename.c_str(),
308 image_filepath.c_str(),
309 project_filename.c_str(),
310 image_filepath.c_str(),
311 current_frame);
312 }
313 else
314 {
315 const string source_image_filename = make_numbered_filename(m_base_output_filename + "." + output_format, actual_frame);
316 const string source_image_filepath = "%output_path%\\" + source_image_filename;
317
318 fprintf(
319 script_file,
320 " echo Copying %s to %s...\n",
321 source_image_filepath.c_str(),
322 image_filepath.c_str());
323 fprintf(
324 script_file,
325 " copy \"%s\" \"%s\" >nul\n",
326 source_image_filepath.c_str(),
327 image_filepath.c_str());
328 }
329
330 fprintf(script_file, ")\n\n");
331 }
332
333 fprintf(
334 script_file,
335 "echo.\n"
336 "echo Rendering complete.\n"
337 "echo.\n"
338 "\n"
339 ":end\n");
340
341 fclose(script_file);
342 }
343 };
344
345
346 //
347 // Generate an animation using a custom camera path.
348 //
349
350 class PathAnimationGenerator
351 : public AnimationGenerator
352 {
353 public:
PathAnimationGenerator(const string & base_output_filename,Logger & logger)354 PathAnimationGenerator(
355 const string& base_output_filename,
356 Logger& logger)
357 : AnimationGenerator(base_output_filename, logger)
358 {
359 }
360
361 private:
do_generate()362 vector<size_t> do_generate() override
363 {
364 typedef pair<Transformd, Transformd> TransformPair;
365 typedef map<TransformPair, size_t, TransformPairComparer<double>> TransformMap;
366
367 vector<size_t> frames;
368
369 // Load the animation path file from disk.
370 AnimationPath animation_path(m_logger);
371 animation_path.load(
372 g_cl.m_animation_path.value().c_str(),
373 g_cl.m_3dsmax_mode.is_set() ? AnimationPath::Autodesk3dsMax : AnimationPath::Default);
374
375 // No frame to render.
376 if (animation_path.size() == 0)
377 return frames;
378
379 const size_t frame_count =
380 animation_path.size() > 1
381 ? animation_path.size() - 1
382 : 1;
383
384 // Load the master project from disk.
385 auto_release_ptr<Project> project(load_master_project());
386
387 const float motion_blur_amount = g_cl.m_motion_blur.value();
388 TransformMap transform_map;
389
390 for (size_t i = 0; i < frame_count; ++i)
391 {
392 const size_t frame = i + 1;
393
394 // Don't render twice the same frame.
395 if (i + 1 < animation_path.size())
396 {
397 const TransformPair transform_pair(animation_path[i], animation_path[i + 1]);
398 const TransformMap::const_iterator transform_map_key = transform_map.find(transform_pair);
399
400 if (transform_map_key != transform_map.end())
401 {
402 LOG_INFO(
403 m_logger, "frame " FMT_SIZE_T " is the same as frame " FMT_SIZE_T ".",
404 frame,
405 transform_map_key->second);
406 frames.push_back(transform_map_key->second);
407 continue;
408 }
409
410 transform_map.insert(make_pair(transform_pair, frame));
411 }
412
413 // Set the camera's transform sequence.
414 Camera* camera = project->get_uncached_active_camera();
415 camera->transform_sequence().clear();
416 camera->transform_sequence().set_transform(0.0f, animation_path[i]);
417 if (i + 1 < animation_path.size())
418 camera->transform_sequence().set_transform(1.0f, animation_path[i + 1]);
419
420 // Set the shutter close times.
421 camera->get_parameters().insert("shutter_close_begin_time", motion_blur_amount);
422 camera->get_parameters().insert("shutter_close_end_time", motion_blur_amount);
423
424 // Write the project file for this frame.
425 const string new_path = make_numbered_filename(m_base_output_filename + ".appleseed", frame);
426 ProjectFileWriter::write(
427 project.ref(),
428 new_path.c_str(),
429 i == 0 ? ProjectFileWriter::Defaults : ProjectFileWriter::OmitWritingGeometryFiles);
430 project->set_path(new_path.c_str());
431
432 frames.push_back(frame);
433 }
434
435 assert(frames.size() == frame_count);
436
437 return frames;
438 }
439 };
440
441
442 //
443 // Generate a turntable animation.
444 //
445
446 class TurntableAnimationGenerator
447 : public AnimationGenerator
448 {
449 public:
TurntableAnimationGenerator(const string & base_output_filename,Logger & logger)450 TurntableAnimationGenerator(
451 const string& base_output_filename,
452 Logger& logger)
453 : AnimationGenerator(base_output_filename, logger)
454 {
455 }
456
457 private:
do_generate()458 vector<size_t> do_generate() override
459 {
460 vector<size_t> frames;
461
462 // Retrieve the command line parameter values.
463 const int frame_count = g_cl.m_frame_count.value();
464 const Vector3d center_offset(
465 g_cl.m_camera_target.values()[0],
466 g_cl.m_camera_target.values()[1],
467 g_cl.m_camera_target.values()[2]);
468 const double normalized_distance = g_cl.m_camera_distance.value();
469 const double normalized_elevation = g_cl.m_camera_elevation.value();
470 const float motion_blur_amount = g_cl.m_motion_blur.value();
471
472 if (frame_count < 1)
473 LOG_FATAL(m_logger, "the frame count must be greater than or equal to 1.");
474
475 // Load the master project from disk.
476 auto_release_ptr<Project> project(load_master_project());
477
478 // Retrieve the scene's bounding box.
479 const AABB3d scene_bbox(project->get_scene()->compute_bbox());
480 const Vector3d extent = scene_bbox.extent();
481 const double max_radius = 0.5 * max(extent.x, extent.z);
482 const double max_height = 0.5 * extent.y;
483
484 // Precompute some stuff.
485 const Vector3d Up(0.0, 1.0, 0.0);
486 const Vector3d center = scene_bbox.center() + center_offset;
487 const double distance = max_radius * normalized_distance;
488 const double elevation = max_height * normalized_elevation;
489
490 // Compute the transform of the camera at the last frame.
491 const double angle = -1.0 / frame_count * TwoPi<double>();
492 const Vector3d position(distance * cos(angle), elevation, distance * sin(angle));
493 Transformd previous_transform(
494 Transformd::from_local_to_parent(
495 Matrix4d::make_lookat(position, center, Up)));
496
497 for (int i = 0; i < frame_count; ++i)
498 {
499 // Compute the transform of the camera at this frame.
500 const double angle = (i * TwoPi<double>()) / frame_count;
501 const Vector3d position(distance * cos(angle), elevation, distance * sin(angle));
502 const Transformd new_transform(
503 Transformd::from_local_to_parent(
504 Matrix4d::make_lookat(position, center, Up)));
505
506 // Set the camera's transform sequence.
507 Camera* camera = project->get_uncached_active_camera();
508 camera->transform_sequence().clear();
509 camera->transform_sequence().set_transform(0.0f, previous_transform);
510 camera->transform_sequence().set_transform(1.0f, new_transform);
511 previous_transform = new_transform;
512
513 // Set the shutter close times.
514 camera->get_parameters().insert("shutter_close_begin_time", motion_blur_amount);
515 camera->get_parameters().insert("shutter_close_end_time", motion_blur_amount);
516
517 // Write the project file for this frame.
518 const size_t frame = static_cast<size_t>(i + 1);
519 const string new_path = make_numbered_filename(m_base_output_filename + ".appleseed", frame);
520 ProjectFileWriter::write(
521 project.ref(),
522 new_path.c_str(),
523 i == 0 ? ProjectFileWriter::Defaults : ProjectFileWriter::OmitWritingGeometryFiles);
524 project->set_path(new_path.c_str());
525
526 frames.push_back(frame);
527 }
528
529 assert(frames.size() == static_cast<size_t>(frame_count));
530
531 return frames;
532 }
533 };
534 }
535
536
537 //
538 // Entry point of animatecamera.
539 //
540
main(int argc,char * argv[])541 int main(int argc, char* argv[])
542 {
543 // Construct the logger that will be used throughout the program.
544 SuperLogger logger;
545
546 // Make sure this build can run on this host.
547 Application::check_compatibility_with_host(logger);
548
549 // Make sure appleseed is correctly installed.
550 Application::check_installation(logger);
551
552 // Parse the command line.
553 g_cl.parse(argc, argv, logger);
554
555 // Load an apply settings from the settings file.
556 Dictionary settings;
557 Application::load_settings("appleseed.tools.xml", settings, logger);
558 logger.configure_from_settings(settings);
559
560 // Apply command line arguments.
561 g_cl.apply(logger);
562
563 // Configure the renderer's global logger.
564 // Must be done after settings have been loaded and the command line
565 // has been parsed, because these two operations may replace the log
566 // target of the global logger.
567 global_logger().initialize_from(logger);
568
569 const string base_output_filename =
570 bf::path(g_cl.m_filenames.values()[1]).stem().string();
571
572 unique_ptr<AnimationGenerator> generator;
573
574 if (g_cl.m_animation_path.is_set())
575 generator.reset(new PathAnimationGenerator(base_output_filename, logger));
576 else generator.reset(new TurntableAnimationGenerator(base_output_filename, logger));
577
578 generator->generate();
579
580 return 0;
581 }
582