Skip to content

Commit

Permalink
Merge pull request godotengine#62122 from reduz/implement-movie-writer
Browse files Browse the repository at this point in the history
Implement a Movie Maker mode
  • Loading branch information
akien-mga authored Jun 21, 2022
2 parents b5f20a4 + 5786516 commit 40c360b
Show file tree
Hide file tree
Showing 38 changed files with 2,427 additions and 30 deletions.
1 change: 0 additions & 1 deletion core/config/project_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
#include "core/os/keyboard.h"
#include "core/variant/variant_parser.h"
#include "core/version.h"

#include "modules/modules_enabled.gen.h" // For mono.

const String ProjectSettings::PROJECT_DATA_DIR_NAME_SUFFIX = "godot";
Expand Down
1 change: 1 addition & 0 deletions core/core_constants.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ void register_global_constants() {
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_OBJECT_TOO_BIG);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_NODE_PATH_VALID_TYPES);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_SAVE_FILE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_GLOBAL_SAVE_FILE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_OBJECTID);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_POINTER);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ARRAY_TYPE);
Expand Down
20 changes: 20 additions & 0 deletions core/io/image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ const char *Image::format_names[Image::FORMAT_MAX] = {
};

SavePNGFunc Image::save_png_func = nullptr;
SaveJPGFunc Image::save_jpg_func = nullptr;
SaveEXRFunc Image::save_exr_func = nullptr;

SavePNGBufferFunc Image::save_png_buffer_func = nullptr;
SaveJPGBufferFunc Image::save_jpg_buffer_func = nullptr;

void Image::_put_pixelb(int p_x, int p_y, uint32_t p_pixel_size, uint8_t *p_data, const uint8_t *p_pixel) {
uint32_t ofs = (p_y * width + p_x) * p_pixel_size;
Expand Down Expand Up @@ -2286,6 +2288,14 @@ Error Image::save_png(const String &p_path) const {
return save_png_func(p_path, Ref<Image>((Image *)this));
}

Error Image::save_jpg(const String &p_path, float p_quality) const {
if (save_jpg_func == nullptr) {
return ERR_UNAVAILABLE;
}

return save_jpg_func(p_path, Ref<Image>((Image *)this), p_quality);
}

Vector<uint8_t> Image::save_png_to_buffer() const {
if (save_png_buffer_func == nullptr) {
return Vector<uint8_t>();
Expand All @@ -2294,6 +2304,14 @@ Vector<uint8_t> Image::save_png_to_buffer() const {
return save_png_buffer_func(Ref<Image>((Image *)this));
}

Vector<uint8_t> Image::save_jpg_to_buffer(float p_quality) const {
if (save_jpg_buffer_func == nullptr) {
return Vector<uint8_t>();
}

return save_jpg_buffer_func(Ref<Image>((Image *)this), p_quality);
}

Error Image::save_exr(const String &p_path, bool p_grayscale) const {
if (save_exr_func == nullptr) {
return ERR_UNAVAILABLE;
Expand Down Expand Up @@ -3138,6 +3156,8 @@ void Image::_bind_methods() {
ClassDB::bind_method(D_METHOD("load", "path"), &Image::load);
ClassDB::bind_method(D_METHOD("save_png", "path"), &Image::save_png);
ClassDB::bind_method(D_METHOD("save_png_to_buffer"), &Image::save_png_to_buffer);
ClassDB::bind_method(D_METHOD("save_jpg", "path", "quality"), &Image::save_jpg, DEFVAL(0.75));
ClassDB::bind_method(D_METHOD("save_jpg_to_buffer", "quality"), &Image::save_jpg_to_buffer, DEFVAL(0.75));
ClassDB::bind_method(D_METHOD("save_exr", "path", "grayscale"), &Image::save_exr, DEFVAL(false));

ClassDB::bind_method(D_METHOD("detect_alpha"), &Image::detect_alpha);
Expand Down
6 changes: 6 additions & 0 deletions core/io/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class Image;

typedef Error (*SavePNGFunc)(const String &p_path, const Ref<Image> &p_img);
typedef Vector<uint8_t> (*SavePNGBufferFunc)(const Ref<Image> &p_img);
typedef Error (*SaveJPGFunc)(const String &p_path, const Ref<Image> &p_img, float p_quality);
typedef Vector<uint8_t> (*SaveJPGBufferFunc)(const Ref<Image> &p_img, float p_quality);
typedef Ref<Image> (*ImageMemLoadFunc)(const uint8_t *p_png, int p_size);

typedef Error (*SaveEXRFunc)(const String &p_path, const Ref<Image> &p_img, bool p_grayscale);
Expand All @@ -54,8 +56,10 @@ class Image : public Resource {

public:
static SavePNGFunc save_png_func;
static SaveJPGFunc save_jpg_func;
static SaveEXRFunc save_exr_func;
static SavePNGBufferFunc save_png_buffer_func;
static SaveJPGBufferFunc save_jpg_buffer_func;

enum {
MAX_WIDTH = (1 << 24), // force a limit somehow
Expand Down Expand Up @@ -281,7 +285,9 @@ class Image : public Resource {

Error load(const String &p_path);
Error save_png(const String &p_path) const;
Error save_jpg(const String &p_path, float p_quality = 0.75) const;
Vector<uint8_t> save_png_to_buffer() const;
Vector<uint8_t> save_jpg_to_buffer(float p_quality = 0.75) const;
Error save_exr(const String &p_path, bool p_grayscale) const;

void create_empty(int p_width, int p_height, bool p_use_mipmaps, Format p_format) {
Expand Down
1 change: 1 addition & 0 deletions core/object/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ enum PropertyHint {
PROPERTY_HINT_OBJECT_TOO_BIG, ///< object is too big to send
PROPERTY_HINT_NODE_PATH_VALID_TYPES,
PROPERTY_HINT_SAVE_FILE, ///< a file path must be passed, hint_text (optionally) is a filter "*.png,*.wav,*.doc,". This opens a save dialog
PROPERTY_HINT_GLOBAL_SAVE_FILE, ///< a file path must be passed, hint_text (optionally) is a filter "*.png,*.wav,*.doc,". This opens a save dialog
PROPERTY_HINT_INT_IS_OBJECTID,
PROPERTY_HINT_ARRAY_TYPE,
PROPERTY_HINT_INT_IS_POINTER,
Expand Down
4 changes: 4 additions & 0 deletions core/os/os.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ bool OS::has_feature(const String &p_feature) {
return true;
}

if (p_feature == "movie") {
return _writing_movie;
}

#ifdef DEBUG_ENABLED
if (p_feature == "debug") {
return true;
Expand Down
1 change: 1 addition & 0 deletions core/os/os.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class OS {
bool _allow_layered = false;
bool _stdout_enabled = true;
bool _stderr_enabled = true;
bool _writing_movie = false;

CompositeLogger *_logger = nullptr;

Expand Down
14 changes: 8 additions & 6 deletions doc/classes/@GlobalScope.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2569,19 +2569,21 @@
</constant>
<constant name="PROPERTY_HINT_SAVE_FILE" value="38" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_INT_IS_OBJECTID" value="39" enum="PropertyHint">
<constant name="PROPERTY_HINT_GLOBAL_SAVE_FILE" value="39" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_INT_IS_POINTER" value="41" enum="PropertyHint">
<constant name="PROPERTY_HINT_INT_IS_OBJECTID" value="40" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_ARRAY_TYPE" value="40" enum="PropertyHint">
<constant name="PROPERTY_HINT_INT_IS_POINTER" value="42" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_LOCALE_ID" value="42" enum="PropertyHint">
<constant name="PROPERTY_HINT_ARRAY_TYPE" value="41" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_LOCALE_ID" value="43" enum="PropertyHint">
Hints that a string property is a locale code. Editing it will show a locale dialog for picking language and country.
</constant>
<constant name="PROPERTY_HINT_LOCALIZABLE_STRING" value="43" enum="PropertyHint">
<constant name="PROPERTY_HINT_LOCALIZABLE_STRING" value="44" enum="PropertyHint">
Hints that a dictionary property is string translation map. Dictionary keys are locale codes and, values are translated strings.
</constant>
<constant name="PROPERTY_HINT_MAX" value="44" enum="PropertyHint">
<constant name="PROPERTY_HINT_MAX" value="45" enum="PropertyHint">
</constant>
<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags">
</constant>
Expand Down
13 changes: 13 additions & 0 deletions doc/classes/Image.xml
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,19 @@
[b]Note:[/b] The TinyEXR module is disabled in non-editor builds, which means [method save_exr] will return [constant ERR_UNAVAILABLE] when it is called from an exported project.
</description>
</method>
<method name="save_jpg" qualifiers="const">
<return type="int" enum="Error" />
<argument index="0" name="path" type="String" />
<argument index="1" name="quality" type="float" default="0.75" />
<description>
</description>
</method>
<method name="save_jpg_to_buffer" qualifiers="const">
<return type="PackedByteArray" />
<argument index="0" name="quality" type="float" default="0.75" />
<description>
</description>
</method>
<method name="save_png" qualifiers="const">
<return type="int" enum="Error" />
<argument index="0" name="path" type="String" />
Expand Down
53 changes: 53 additions & 0 deletions doc/classes/MovieWriter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="MovieWriter" inherits="Object" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
<methods>
<method name="_get_audio_mix_rate" qualifiers="virtual const">
<return type="int" />
<description>
</description>
</method>
<method name="_get_audio_speaker_mode" qualifiers="virtual const">
<return type="int" enum="AudioServer.SpeakerMode" />
<description>
</description>
</method>
<method name="_handles_file" qualifiers="virtual const">
<return type="bool" />
<argument index="0" name="path" type="String" />
<description>
</description>
</method>
<method name="_write_begin" qualifiers="virtual">
<return type="int" enum="Error" />
<argument index="0" name="movie_size" type="Vector2i" />
<argument index="1" name="fps" type="int" />
<argument index="2" name="base_path" type="String" />
<description>
</description>
</method>
<method name="_write_end" qualifiers="virtual">
<return type="void" />
<description>
</description>
</method>
<method name="_write_frame" qualifiers="virtual">
<return type="int" enum="Error" />
<argument index="0" name="frame_image" type="Image" />
<argument index="1" name="audio_frame_block" type="const void*" />
<description>
</description>
</method>
<method name="add_writer" qualifiers="static">
<return type="void" />
<argument index="0" name="writer" type="MovieWriter" />
<description>
</description>
</method>
</methods>
</class>
12 changes: 12 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,18 @@
See [enum DisplayServer.VSyncMode] for possible values and how they affect the behavior of your application.
Depending on the platform and used renderer, the engine will fall back to [code]Enabled[/code], if the desired mode is not supported.
</member>
<member name="editor/movie_writer/disable_vsync" type="bool" setter="" getter="" default="false">
</member>
<member name="editor/movie_writer/fps" type="int" setter="" getter="" default="60">
</member>
<member name="editor/movie_writer/mix_rate_hz" type="int" setter="" getter="" default="48000">
</member>
<member name="editor/movie_writer/mjpeg_quality" type="float" setter="" getter="" default="0.75">
</member>
<member name="editor/movie_writer/movie_file" type="String" setter="" getter="" default="&quot;&quot;">
</member>
<member name="editor/movie_writer/speaker_mode" type="int" setter="" getter="" default="0">
</member>
<member name="editor/node_naming/name_casing" type="int" setter="" getter="" default="0">
When creating node names automatically, set the type of casing in this project. This is mostly an editor setting.
</member>
Expand Down
44 changes: 43 additions & 1 deletion editor/editor_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2341,6 +2341,20 @@ void EditorNode::_run(bool p_current, const String &p_custom) {
return;
}

String write_movie_file;
if (write_movie_button->is_pressed()) {
if (p_current && get_tree()->get_edited_scene_root() && get_tree()->get_edited_scene_root()->has_meta("movie_file")) {
// If the scene file has a movie_file metadata set, use this as file. Quick workaround if you want to have multiple scenes that write to multiple movies.
write_movie_file = get_tree()->get_edited_scene_root()->get_meta("movie_file");
} else {
write_movie_file = GLOBAL_GET("editor/movie_writer/movie_file");
}
if (write_movie_file == String()) {
show_accept(TTR("Movie Maker mode is enabled, but no movie file path has been specified.\nA default movie file path can be specified in the project settings under the 'Editor/Movie Writer' category.\nAlternatively, for running single scenes, a 'movie_path' metadata can be added to the root node,\nspecifying the path to a movie file that will be used when recording that scene."), TTR("OK"));
return;
}
}

play_button->set_pressed(false);
play_button->set_icon(gui_base->get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
play_scene_button->set_pressed(false);
Expand Down Expand Up @@ -2404,7 +2418,7 @@ void EditorNode::_run(bool p_current, const String &p_custom) {
}

EditorDebuggerNode::get_singleton()->start();
Error error = editor_run.run(run_filename);
Error error = editor_run.run(run_filename, write_movie_file);
if (error != OK) {
EditorDebuggerNode::get_singleton()->stop();
show_accept(TTR("Could not start subprocess(es)!"), TTR("OK"));
Expand Down Expand Up @@ -2787,6 +2801,9 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) {
case RUN_SETTINGS: {
project_settings_editor->popup_project_settings();
} break;
case RUN_WRITE_MOVIE: {
_update_write_movie_icon();
} break;
case FILE_INSTALL_ANDROID_SOURCE: {
if (p_confirmed) {
export_template_manager->install_android_template();
Expand Down Expand Up @@ -4948,6 +4965,14 @@ String EditorNode::get_run_playing_scene() const {
return run_filename;
}

void EditorNode::_update_write_movie_icon() {
if (write_movie_button->is_pressed()) {
write_movie_button->set_icon(gui_base->get_theme_icon(SNAME("MainMovieWriteEnabled"), SNAME("EditorIcons")));
} else {
write_movie_button->set_icon(gui_base->get_theme_icon(SNAME("MainMovieWrite"), SNAME("EditorIcons")));
}
}

void EditorNode::_immediate_dialog_confirmed() {
immediate_dialog_confirmed = true;
}
Expand Down Expand Up @@ -6703,6 +6728,23 @@ EditorNode::EditorNode() {
ED_SHORTCUT_OVERRIDE("editor/play_custom_scene", "macos", KeyModifierMask::CMD | KeyModifierMask::SHIFT | Key::R);
play_custom_scene_button->set_shortcut(ED_GET_SHORTCUT("editor/play_custom_scene"));

write_movie_button = memnew(Button);
write_movie_button->set_flat(true);
write_movie_button->set_toggle_mode(true);
play_hb->add_child(write_movie_button);
write_movie_button->set_pressed(false);
write_movie_button->set_icon(gui_base->get_theme_icon(SNAME("MainMovieWrite"), SNAME("EditorIcons")));
write_movie_button->set_focus_mode(Control::FOCUS_NONE);
write_movie_button->connect("pressed", callable_mp(this, &EditorNode::_menu_option), make_binds(RUN_WRITE_MOVIE));
write_movie_button->set_tooltip(TTR("Enable Movie Maker mode.\nThe project will run at stable FPS and the visual and audio output will be recorded to a video file."));
// Restore these values to something more useful so it ignores the theme
write_movie_button->add_theme_color_override("icon_normal_color", Color(1, 1, 1, 0.4));
write_movie_button->add_theme_color_override("icon_pressed_color", Color(1, 1, 1, 1));
write_movie_button->add_theme_color_override("icon_hover_color", Color(1.2, 1.2, 1.2, 0.4));
write_movie_button->add_theme_color_override("icon_hover_pressed_color", Color(1.2, 1.2, 1.2, 1));
write_movie_button->add_theme_color_override("icon_focus_color", Color(1, 1, 1, 1));
write_movie_button->add_theme_color_override("icon_disabled_color", Color(1, 1, 1, 0.4));

HBoxContainer *right_menu_hb = memnew(HBoxContainer);
menu_hb->add_child(right_menu_hb);

Expand Down
4 changes: 3 additions & 1 deletion editor/editor_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class EditorNode : public Node {
RUN_PLAY_CUSTOM_SCENE,
RUN_SETTINGS,
RUN_USER_DATA_FOLDER,
RUN_WRITE_MOVIE,
RELOAD_CURRENT_PROJECT,
RUN_PROJECT_MANAGER,
RUN_VCS_METADATA,
Expand Down Expand Up @@ -333,6 +334,7 @@ class EditorNode : public Node {
Button *play_scene_button = nullptr;
Button *play_custom_scene_button = nullptr;
Button *search_button = nullptr;
Button *write_movie_button = nullptr;
TextureProgressBar *audio_vu = nullptr;

Timer *screenshot_timer = nullptr;
Expand Down Expand Up @@ -667,7 +669,7 @@ class EditorNode : public Node {
void _pick_main_scene_custom_action(const String &p_custom_action_name);

void _immediate_dialog_confirmed();

void _update_write_movie_icon();
void _select_default_main_screen_plugin();

void _bottom_panel_switch(bool p_enable, int p_idx);
Expand Down
6 changes: 3 additions & 3 deletions editor/editor_properties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3747,11 +3747,11 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
EditorPropertyLocale *editor = memnew(EditorPropertyLocale);
editor->setup(p_hint_text);
return editor;
} else if (p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_FILE || p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE) {
} else if (p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_FILE || p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE) {
Vector<String> extensions = p_hint_text.split(",");
bool global = p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE;
bool global = p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE || p_hint == PROPERTY_HINT_GLOBAL_SAVE_FILE;
bool folder = p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_GLOBAL_DIR;
bool save = p_hint == PROPERTY_HINT_SAVE_FILE;
bool save = p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_SAVE_FILE;
EditorPropertyPath *editor = memnew(EditorPropertyPath);
editor->setup(extensions, folder, global);
if (save) {
Expand Down
12 changes: 11 additions & 1 deletion editor/editor_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ String EditorRun::get_running_scene() const {
return running_scene;
}

Error EditorRun::run(const String &p_scene) {
Error EditorRun::run(const String &p_scene, const String &p_write_movie) {
List<String> args;

String resource_path = ProjectSettings::get_singleton()->get_resource_path();
Expand All @@ -68,6 +68,16 @@ Error EditorRun::run(const String &p_scene) {
args.push_back("--debug-navigation");
}

if (p_write_movie != "") {
args.push_back("--write-movie");
args.push_back(p_write_movie);
args.push_back("--fixed-fps");
args.push_back(itos(GLOBAL_GET("editor/movie_writer/fps")));
if (bool(GLOBAL_GET("editor/movie_writer/disable_vsync"))) {
args.push_back("--disable-vsync");
}
}

int screen = EditorSettings::get_singleton()->get("run/window_placement/screen");
if (screen == 0) {
// Same as editor
Expand Down
2 changes: 1 addition & 1 deletion editor/editor_run.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class EditorRun {
public:
Status get_status() const;
String get_running_scene() const;
Error run(const String &p_scene);
Error run(const String &p_scene, const String &p_write_movie = "");
void run_native_notify() { status = STATUS_PLAY; }
void stop();

Expand Down
Loading

0 comments on commit 40c360b

Please sign in to comment.