I'm trying to apply a pixelation shader to my textures and I need it to be applied only once, after that I can reuse my shader generated images as textures over and over without having to calculate every single time.
so how do I take a few images -> apply a shader and render them only once every time the game loads -> and use them as my textures?
so far I've managed to find the shader to apply:
shader_type canvas_item;
uniform int amount = 40;
void fragment()
{
vec2 grid_uv = round(UV * float(amount)) / float(amount);
vec4 text = texture(TEXTURE, grid_uv);
COLOR = text;
}
but I have no idea how to render out the images using it
Shaders reside in the GPU, and their output goes to the screen. To save the image, the CPU would have to see the GPU output, and that does not happen… Usually. And since it does not go through the CPU, the performance is good. Usually. Well, at least it is better than if the CPU was doing it all the time.
Also, are you sure you don't want to get a pixel art look by other means? Such as removing filter from the texture, changing the stretch mode and working on a small resolution, and perhaps enable pixel snap? No? Watch How to make a silky smooth camera for pixelart games in Godot. Still No? Ok...
Anyway, for what you want, you are going to need a Viewport
.
What you will need is to create a Viewport
. Don't forget to set its size
. Also may want to set render_target_v_flip
to true
, this flips the image vertically. If you find the output image is upside down it is because you need to toggle render_target_v_flip
.
Then place as child of the Viewport
what you want to render.
Rendering
Next, you can read the texture form the Viewport
, convert it to an image, and save it to a png. I'm doing this on a tool script attached to the Viewport
, so I'll have a workaround to trigger the code from the inspector panel. My code looks like this:
tool
extends Viewport
export var save:bool setget do_save
func do_save(new_value) -> void:
var image := get_texture().get_data()
var error := image.save_png("res://output.png")
if error != OK:
push_error("failed to save output image.")
You can, of course, export
a FILE
path String
to ease changing it in the inspector panel. Here I'm handing common edge cases:
tool
extends Viewport
export(String, FILE) var path:String
export var save:bool setget do_save
func do_save(_new_value) -> void:
var target_path := path.strip_edges()
var folder := target_path.get_base_dir()
var file_name := target_path.get_file()
var extension := target_path.get_extension()
if file_name == "":
push_error("empty file name.")
return
if not (Directory.new()).dir_exists(folder):
push_error("output folder does not exist.")
return
if extension != "png":
target_path += "png" if target_path.ends_with(".") else ".png"
var image := get_texture().get_data()
var error := image.save_png(target_path)
if error != OK:
push_error("failed to save output image.")
return
print("image saved to ", target_path)
Another option is to use ResourceSaver
:
tool
extends Viewport
export var save:bool setget do_save
func do_save(new_value) -> void:
var image := get_texture().get_data()
var error := ResourceSaver.save("res://image.res", image)
if error != OK:
push_error("failed to save output image.")
This will only work from the Godot editor, and will only work for Godot, since you get a Godot resource file. Although I find interesting the idea of using Godot to generate images. I'm going to suggest going with ResourceSaver
if you want to automate generating them for Godot.
In the examples above, I'm assuming you are saving to a resource path. This is because the intention is to use the output image as a resource in Godot. Using a resource path has a couple implications:
We can deal with the second point from an EditorPlugin
, if that is what you are doing, you can do this to tell Godot to scan for changes:
get_editor_interface().get_resource_filesystem().scan()
And if you are not, you can cheat by creating an empty EditorPlugin
. The idea is to do this:
var ep = EditorPlugin.new()
ep.get_editor_interface().get_resource_filesystem().scan()
ep.free()
By the way, you will want to cache cache the EditorPlugin
instead of making a new one each time. Or better yet, cache the EditorFileSystem
you get from get_resource_filesystem
.
Now, I'm aware that it can be cumbersome to have to place things inside the Viewport
. It might be Ok for your workflow if you don't need to do it all the time.
But what about automating it? Well, regardless of the approach, you will need a tool
script that makes a hidden Viewport
, takes a Node
, checks if it has a shader, if it does, it moves it temporarily to the Viewport
, get the rendered texture (get_texture()
) sets it as the texture of the Node
, removes the shader, and returns the Node
to its original position in the scene. Or instead of looking for a shader in the Node, always apply a shader to whatever Node
, perhaps loaded as a resource instead of hard-coded.
Note: I believe you need to let an idle frame pass between adding the Node
to the Viewport
and getting the texture, so the texture updates. Or was it two idle frames? Well, if one does not work, try adding another one.
About making an EditorPlugin
As you know, you can create an addon from project settings. This will create an EditorPlugin
script for you. There you can either add an option to the tools menu (with add_tool_menu_item
), or add it to the tool bar of the editor (with add_control_to_container
). And have it act on the current selection in the edited scene (you can either use get_selection
, or overwrite the edit
and handles
methods). You may also want to make an undo entry for that, see get_undo_redo
.
Or, alternatively you can have it keep track (or look for) the Node
s it has to act upon, and then work on the build
virtual method, which runs when the project is about to run. I haven't worked with the build
virtual method, so I don't know if it has any quirks to gotchas to be aware of.