How to force to Godot to play at constant framerate and not skip any? (to use Godot as a renderer for 3D animation)

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By samuelsantos

Hi.

I was trying to use Godot as a renderer for my 3D animations to speed up everything. But the code to capture screen and save in a PNG file is too slow, and of course all of it relies on not heavy scene to be fast enough to “play”.

So I’m looking for a way to achieve that. The thing is: to be reliable should be possible to force Godot to render every frame (I know the concept of frames in a game engine is quite different, but that’s part of the problem). So I’m trying to make Godot play at a constant frame rate (and save the PNG file fast enough to be usable), and/or when the engine can’t handle heavy scene or animation it wait the “render and save image” and go for what should be the next “frame” in animation, not the correct timing for the viewer.

Is that possible inside Godot?

The goal is in the future create some kind of plugin to setup the output files easily, so the art team can use without having to learn anything new. But to be reliable the frames rendered should be the same everytime we render it again, and be agnostic for the machine performance even though it takes a little longer to finish.

:bust_in_silhouette: Reply From: Magso

Godot doesn’t have a way to render an animation like a movie and taking a screenshot of the viewport takes two frames so you would have to stop and start the animation every time.

Animation_player.stop(false)
var image = get_viewport().get_texture().get_data()
yield(get_tree(), "idle_frame")
yield(get_tree(), "idle_frame")
image.flip_y()
image.save_png(file path)
Animation_player.play()

It will probably be just as well to play Godot fullscreen and have a screen recorder running.

I’m sorry, but I don’t understand why this code doesn’t work. I receive this error:

Parser Error: The identifier “Animation_player” isn’t declared in the
current scope.

samuelsantos | 2020-02-18 14:53

Animation_player would be the reference to the AnimationPlayer that plays the animation file. In this case Animation_player is the variable to save using get_node every time.

Magso | 2020-02-18 15:27

Thanks for your help.

The code you proposed didn’t work better than the previous, but with what I learned from that I could fix it with the seek(). It’s not perfect yet cause the engine doesn’t jump exactly to the “animation time position”. So what’s “rendered” in Godot it’s not pixel prefectly/exactly what the 3D software would render (the objects often appears a little offset in time inside what’s expected and all the animation is offset by 2 frames. But for now it’s gonna do the job.

As far as I could test the time offset of objects animated looks like to have some consistency. So maybe the only problem is how I’m calculating the time position based on a division: 1/(frame per second from the animation).

samuelsantos | 2020-02-19 05:26

:bust_in_silhouette: Reply From: samuelsantos

With the help from Magso in this thread, was possible to achieved (at least partially) the goal.

extends Spatial

var frame = 0
var file_name = ""
var path
var path_start = "c:/Screenshots/screenshot_" #make sure the path exist before play
var path_end = ".png"
var go_ahead =  0.0
var fps = 24.0
var framerate = 0.0

func _ready():  #start the animation
	var Animation_player = get_node('AnimationPlayer')
	Animation_player.play("Animation")  #don't know why, but I need to put the name of the animation here to start the animation.
	Animation_player.stop(true) #stops and reset the animation to the begin.

func _process(_delta):
	frame = frame + 1  #calculate the actual frame to render
	file_name = str(frame) #convert to sting and store in file_name to compose the name of png file.
	path = path_start + file_name + path_end #compose the path with the name.
	
	var image = get_viewport().get_texture().get_data()  #get the image
	image.flip_y() # flips the image
	image.save_png(path) #save the image
	
	var Animation_player = get_node('AnimationPlayer')
	Animation_player.seek(go_ahead, true) #go to the position in time
	
	framerate = 1/fps #calcaultes how much increment the time position for the next time
	go_ahead = go_ahead + framerate  #update the next time position

It’s not perfect yet cause the engine doesn’t jump exactly to the “animation time position”. So what’s “rendered” in Godot it’s not pixel prefectly/exactly what the 3D software would render (the objects often appears a little offset in time inside what’s expected) and all the animation is offset by 2 frames. But for now it’s gonna do the job. (update)

As far as I could test the time offset of objects animated looks like to have some consistency. So maybe the only problem is how I’m calculating the time position based on a division: 1/(frame per second from the animation). So problably it’s not a bug, it’s a feature. Would be nice to achieve that, so we could mix renders (like render smoke in other software and compose with what Godot render.

So if anybody know how to solve this would be great! (I added the code here so maybe it can help somebody and fix this last issue.

update: I found 2 problems in my workflow. The first I didn’t know it was necessary to change the FPS of the scene imported inside Godot. So to things works better go to “Import” tab in godot, go down to find “Fps” value and change to the framrate you have in your 3D aplication. And the second (and was what was generating the offset time problem) and most important, in the “Import” tab in godot yet, go a little bit down and disable the “Optimizer”. It was deleting and interpolating the frames the engine doesn’t consider necessary. With this two things right, the code run perfectly without any visibly delay in the animation, thoug when I try to use the command line option (–fixex-fps), continue to produce some errors.

:bust_in_silhouette: Reply From: Calinou

Godot does have a way to do this since 3.0. You can disable real-time synchronization by running the project with the --fixed-fps <fps> command-line argument (e.g. --fixed-fps 30).

See GitHub - Calinou/godot-video-rendering-demo: Demo project for high-quality offline video rendering with Godot for an example of this. Videos like this one were recorded using a script very similar to the demo above.

Thanks! Was really helpful.

I did several tests here with your option and it’s a more simple method. Even thoug I don’t understand why it works. Save png takes to much time, and we are forcing Godot to keep 30 fps, but this part of the code don’t know its taking too long to precess, but it waits, just so we can complet the task. But works, and thats amazing!

But I found it a little bit inconsistent when compared with my method. I did also tests with other formats ( glTF and Collada - with collada with best results), and when the engine render the animation with the exact timing provide by the 3D aplication (blender here), the command line keeps a little offset in time. In other words: in my code we control where in time/position the next loop should be in animation and render, in yours we leave this job to the engine. So I have better results controling everything in code. In many cases it won’t matters, I know, but cause I want to mix both renders (3D aplication and Godot), as I said, to make it more realiable, for now I’ll go with it.

Thanks one more time for your help, I learned a lot in this small journey with you people!

samuelsantos | 2020-02-20 04:36

Saving PNGs is quite slow and should be sped up significantly by moving the saving work to a thread. I started working on it a while ago, but I couldn’t test it enough so I never committed those changes.

Also, adding an option to save uncompressed PNGs should further speed up image saving (as you generally don’t care about lossless compression before converting to a lossy video).

Calinou | 2020-02-25 08:47