Godot Version
v4.6.1
Question
I have this shader and it is being applied to a CanvasGroup Node so I can apply it to multiple sprites at once. It is a modified version of a shader I made that can be applied to Sprites. I had to modify it because CanvasGroups seem like they can't use normal UV and TEXTURE, and have to use SCREEN_UV and SCREEN_TEXTURE. For some reason this cuases the waves on the shader to change speed when the screen is moved, creating an unwanted effect. Also, if you zoom in or zoom out, it changes the size of the wave. I know this is because of using SCREEN_UV and SCREEN_TEXTURE, but I don't know how else to do it.
Here is the shader code itself (and as a note I am extremely new to shaders. Please explain your solutions with that in mind.)
shader_type canvas_item;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_nearest;
uniform float amount: hint_range(0,1);
uniform float wave_speed = 5.0;
uniform float wave_freq = 1.0;
uniform float wave_width = 2;
uniform vec4 color: source_color;
uniform float transparency: hint_range(0.0, 1.0);
uniform float scale = 1;
void fragment(){
if (amount > 0.0) {
vec2 wave_uv_offset;
vec2 uv_scaled = (SCREEN_UV - 0.5) / scale + 0.5;
wave_uv_offset.x = (cos((TIME*wave_speed)+uv_scaled.x+uv_scaled.y*wave_freq*2.0)*wave_width*0.01) * amount;
vec2 new_UV = vec2(uv_scaled.x+wave_uv_offset.x, uv_scaled.y);
COLOR = texture(SCREEN_TEXTURE,new_UV);
COLOR.a -= 1.0 - clamp(transparency/amount, 0.0, 1.0);
if (COLOR.a > 0.0) {
vec4 mixed_color = mix(COLOR, color, amount);
COLOR = mixed_color;
COLOR.a -= 1.0 - clamp(transparency/amount, 0.0, 1.0);
}
} else {
vec2 uv_scaled = (SCREEN_UV - 0.5) / scale + 0.5;
COLOR = texture(SCREEN_TEXTURE,uv_scaled);
}
if (SCREEN_UV.x > 1.0 || SCREEN_UV.y > 1.0) {
COLOR.a = 0.0;
}
Are you applying this shader to the whole viewport?
I’m applying it to a canvas group, but I have to use SCREEN_UV and SCREEN_TEXTURE because normal UV and TEXTURE don’t work on canvas groups. I need to use a canvas group because it is made up of multiple sprites but needs it applied to all as if they were one.
Okay, that might explain the issues you’re having. It doesn’t look like the shader effect changes when you zoom or move the camera, it looks to me like the effect is being applied based on screen position. The size of the waves aren’t changing, the waves are staying the same but you’re changing the relative size and position of the sprite when compared to the camera.
I’m not sure how to fix this other than suggest you find a way to do this without using SCREEN_TEXTURE and SCREEN_UV as that’s making the shader be applied relative to the screen rather than relative to the canvas item.
Yeah, that’s why I had to post this. I have no idea how to do this without those.
Transform UVs to world space and do the displacement there.
1 Like
Sorry, but could you explain how to do that?
When you say inverse do you mean (for example) -x or -1/x. Or something completely different
For a scaling factor it’s 1/x, for a matrix it’s matrix inverse, so in GDScript that’s Transform3D::affine_inverse() and inverse() in shader.
Have in mind though that if you move individual sprites the effect may still slide over them, depending on the movement direction, although it will stay in place when you move the camera/viewport. The only way to keep it “tied” to an individual sprite is to run the shader individually on each sprite.
1 Like
Ok, thanks. That sounds like the solution I need, and I’ll try and implement it once I have the time.
How do you multiply the mat4 and the SCREEN_UV (which is a vec2). I just get an error. I’m trying the solution with mat2 because that doesn’t give an error. I’m not sure if I’m doing this right though. This is my script:
@tool
extends CanvasGroup
func _process(delta: float) -> void:
var viewport = get_viewport()
if Engine.is_editor_hint():
viewport = EditorInterface.get_editor_viewport_2d()
var canvas_transform = get_global_transform_with_canvas()
canvas_transform *= Vector2(1/viewport.size.x, 1/viewport.size.y)
material.set("shader_parameter/transform", canvas_transform)
And this is my shader now:
uniform float wave_speed = 5.0;
uniform float wave_freq = 1.0;
uniform float wave_width = 2;
uniform vec4 color: source_color;
uniform float transparency: hint_range(0.0, 1.0);
uniform float scale = 1;
uniform mat2 transform;
void fragment(){
if (amount > 0.0) {
vec2 wave_uv_offset;
vec2 uv_scaled = SCREEN_UV * transform;
wave_uv_offset.x = (cos((TIME*wave_speed)+uv_scaled.x+uv_scaled.y*wave_freq*2.0)*wave_width*0.01) * amount;
vec2 new_UV = vec2(uv_scaled.x+wave_uv_offset.x, uv_scaled.y);
COLOR = texture(SCREEN_TEXTURE,new_UV * inverse(transform));
COLOR.a -= 1.0 - clamp(transparency/amount, 0.0, 1.0);
if (COLOR.a > 0.0) {
vec4 mixed_color = mix(COLOR, color, amount);
COLOR = mixed_color;
COLOR.a -= 1.0 - clamp(transparency/amount, 0.0, 1.0);
}
} else {
vec2 uv_scaled = SCREEN_UV * transform;
COLOR = texture(SCREEN_TEXTURE,uv_scaled * inverse(transform));
}
if (SCREEN_UV.x > 1.0 || SCREEN_UV.y > 1.0) {
COLOR.a = 0.0;
}
}
vec2 result = (matrix * vec4(SCREEN_UV, 0.0, 1.0)).xy;
Ok, it isnt throwing errors any more, but the mat4 doesn’t seem to change at all when moving the viewport. Is my script set up right?
@tool
extends CanvasGroup
func _process(delta: float) -> void:
var viewport = get_viewport()
if Engine.is_editor_hint():
viewport = EditorInterface.get_editor_viewport_2d()
var canvas_transform = get_global_transform_with_canvas()
canvas_transform *= Vector2(1/viewport.size.x, 1/viewport.size.y)
material.set("shader_parameter/transform", canvas_transform
var world_to_uv = viewport.global_canvas_transform.scaled(Vector2(1.0, 1.0) / Vector2(viewport.size))
# send world_to_uv to the shader...
I added that line and the shader started doing this…
And this is with the “amount” variable only set to .2
Well you need to implement the shader part properly as well.
I think I did
shader_type canvas_item;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_nearest;
uniform float amount: hint_range(0,1);
uniform float wave_speed = 5.0;
uniform float wave_freq = 1.0;
uniform float wave_width = 2;
uniform vec4 color: source_color;
uniform float transparency: hint_range(0.0, 1.0);
uniform float scale = 1;
uniform mat4 matrix; //Heres the matrix
void fragment(){
if (amount > 0.0) {
vec2 wave_uv_offset;
vec2 uv_scaled = (matrix * vec4(SCREEN_UV, 0.0, 1.0)).xy; //Heres the scaling
wave_uv_offset.x = (cos((TIME*wave_speed)+uv_scaled.x+uv_scaled.y*wave_freq*2.0)*wave_width*0.01) * amount;
vec2 new_UV = vec2(uv_scaled.x+wave_uv_offset.x, uv_scaled.y);
COLOR = texture(SCREEN_TEXTURE,(inverse(matrix) * vec4(new_UV, 0.0, 1.0)).xy); // Heres it being applied
COLOR.a -= 1.0 - clamp(transparency/amount, 0.0, 1.0);
if (COLOR.a > 0.0) {
vec4 mixed_color = mix(COLOR, color, amount);
COLOR = mixed_color;
COLOR.a -= 1.0 - clamp(transparency/amount, 0.0, 1.0);
}
} else {
vec2 uv_scaled = (matrix * vec4(SCREEN_UV, 0.0, 1.0)).xy;
COLOR = texture(SCREEN_TEXTURE,(inverse(matrix) * vec4(uv_scaled, 0.0, 1.0)).xy);
}
if (SCREEN_UV.x > 1.0 || SCREEN_UV.y > 1.0) {
COLOR.a = 0.0;
}
}
It has to be mat4, not mat2