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