Shader Changing Speed When Moving Up And Down! Please Help!

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?

In script:

  • get the current viewport (if you want it to run in the editor you’ll need to test if you’re in the editor using Engine.is_editor_hint() and get it via EditorInterface.get_viewport_2d()
  • get global_canvas_transform from it as well as viewport’s size
  • scale the transform by the inverse of the size
  • send this transform to the shader via an uniform mat4

In shader:

  • transform SCREEN_UV to global space by multiplying it with mat4 you received from the script
  • deform the result using all values in global coordinates
  • transform the result back into uv space by multiplying it with inverse of that mat4
1 Like

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

It is mat4