Skip to content

Instantly share code, notes, and snippets.

@belzecue
Last active July 21, 2024 09:28
Show Gist options
  • Save belzecue/d465f07e486d5ea1eff777a6e7565e06 to your computer and use it in GitHub Desktop.
Save belzecue/d465f07e486d5ea1eff777a6e7565e06 to your computer and use it in GitHub Desktop.
"""
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()
@belzecue
Copy link
Author

belzecue commented Jul 6, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment