Last active
July 21, 2024 09:28
-
-
Save belzecue/d465f07e486d5ea1eff777a6e7565e06 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Based on Calinou's Godot3 movie render script: | |
https://github.com/Calinou/godot-video-rendering-demo/blob/master/camera.gd | |
# Copyright © 2019 Hugo Locurcio and contributors - MIT License | |
# See `LICENSE.md` included in the source distribution for details. | |
This script asynchronously renders gameplay or camera animation | |
to PNG files which can then be combined into a video via ffmpeg. | |
IMPORTANT! | |
If you use real time in your project, i.e through Time, you will see | |
strange speed issues in the video of your rendered project. This is because the | |
rendering process described below works asynchronously to export every frame no matter | |
how long it takes to render each frame. One frame could take two seconds on an old | |
computer, which means time has effectively doubled from the point of view of your | |
code running inside your project. Therefore, you will need to allow for switching | |
your project between realtime-based timing (i.e. using Time and get_ticks_msec) and | |
asynchronous fixed timing when you are rendering out your project to a movie at 60fps. | |
In that case you need to use frame-based timing where every 60 frames generated equals | |
one second, no matter how long each individual frame took to render. | |
For example, if you will be using --fixed-fps 60 then in _process you would have: | |
var time: float = Engine.get_frames_drawn() / 60.0 * 1000.0 | |
First, set up the project ready for rendering: | |
- Set the project render dimensions, e.g. 1920x1080 | |
- Turn vsync ON to cap framerate to 60 | |
- Add this script to the Camera (or a plain node in the tree, just change the 'extends' first line to match) | |
- Set up the animation player and track that will define the start/end of the capture, set it to autoplay on load. Select it for script var 'anim_player'. | |
- If you just want to capture e.g. the first 30 seconds of play, simply set the trackless animation player length to 30. | |
Next, export the build as normal. | |
Run the build to capture the frames: | |
e.g. in a console run with these required params: | |
./build3.x86_64 --fixed-fps 60 --no-window | |
This outputs frames as numbered PNG files in the project runtime folder. | |
You will use the same fps when compiling frames in ffpmeg. | |
Combine the frames with ffmpeg: | |
e.g. | |
ffmpeg -r 60 -f image2 -s 1920x1080 -i %d.png -vcodec libx264 -crf 15 video.mp4 | |
The '-r 60' fps value must match the render value. | |
The '-s 1920x1080' must match the project window resolution. | |
The output quality of '-crf 15' can be changed. Lower is better quality but longer to render. | |
""" | |
extends Node2D | |
export var active: bool = true | |
export var print_frames: bool = true | |
export var anim_player_path: NodePath | |
onready var anim_player: AnimationPlayer = get_node_or_null(anim_player_path) as AnimationPlayer | |
onready var viewport: Viewport = get_viewport() | |
const viewport_clear_mode: int = Viewport.CLEAR_MODE_ONLY_NEXT_FRAME | |
func _ready() -> void: | |
# Set viewport features here | |
#viewport.msaa = Viewport.MSAA_2X | |
if not active: | |
queue_free() | |
return | |
var err: int = 0 | |
if not OS.has_feature("standalone"): err = 1 | |
if not anim_player: err = 2 | |
if err > 0: | |
if err == 1: printerr("Must be run as standalone build with required params, e.g. './build3.x86_64 --fixed-fps 60 --no-window'") | |
elif err == 2: printerr("Missing animation node!") | |
get_tree().quit() | |
return | |
# Connect to animation player | |
anim_player.connect("animation_finished", self, "_on_animation_player_animation_finished") | |
var directory: = Directory.new() | |
directory.make_dir("user://render") | |
func _process(delta: float) -> void: | |
var frames_drawn: int = Engine.get_frames_drawn() | |
# The first frame is always black, there's no point in saving it | |
if frames_drawn == 0: return | |
# CLEAR_MODE_ONLY_NEXT_FRAME must be done every frame | |
# because it switches to VIEWPORT_CLEAR_NEVER thereafter. | |
viewport.set_clear_mode(viewport_clear_mode) | |
if print_frames: print(str("Rendering frame ...", frames_drawn)) | |
var image := viewport.get_texture().get_data() | |
# The viewport must be flipped to match the rendered window | |
image.flip_y() | |
var error := image.save_png("user://render/" + str(Engine.get_frames_drawn()) + ".png") | |
if error != OK: | |
printerr(str("Failed to save frame image. Err num: ", error)) | |
get_tree().quit() | |
return | |
func _on_animation_player_animation_finished(anim_name: String) -> void: | |
get_tree().quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Check out Calinou's original script which has additional code that handles frame upscaling for better quality. I removed it here as I didn't need it, but some might.