CanvasGroup shader UV is too large with rotated node

Godot Version

v4.5.stable.official [876b29033]

Question

I’m creating a custom shader for CanvasGroups. Basically, it’s a playing card with multiple elements inside it. I’m using the shader for multiple effects, including rounding the corners.

I got everything working properly when the card is in upright position (or rotated 90, 180, 270 degrees), but the UVs mess things up with any angle between those. UV is getting bigger when rotated, making its coordinates unreliable. Any ideas on how to fix this?


Node tree

  • CanvasGroup (shader applied to this node)
    • Sprite2D
    • Sprite2D
    • Label
    • etc.

Shader code

shader_type canvas_item;
render_mode unshaded;

// An image with transparent rounded corners
uniform sampler2D rounded_corners_bitmap : repeat_disable;

void fragment() {
	// CanvasGroup texture
	vec4 color = textureLod(screen_texture, SCREEN_UV, 0.0);
	if (color.a > 0.0001) { color.rgb /= color.a; }
	COLOR *= color;

	// Rounded corners
	vec4 rounded_corners_texture = textureLod(rounded_corners_bitmap, UV, 0.0);
	float alpha = rounded_corners_texture.a;
	COLOR.a = alpha;
}

Outcome

What is rotated and who runs the shader?

1 Like

CanvasGroup has the shader and is the one being rotated. It doesn’t make a difference if I rotate a parent of the CanvasGroup either. The outcome is the same.

It’s a weird way of doing this. Why not contain everything in a control container like panel? With panel you get rounded cornered box for free, no shaders or textures needed.

But if you insist on doing it like this, simply leave the canvas group node be and put another sprite inside it that will draw the card background/frame

1 Like

I’m still pretty new to Godot. If there’s a better way to achieve what I want, I’m absolutely open to new implementation ideas! Here’s what I want to be able to do:

  1. Round corders (using an image mask or any other way of achieving it)
  2. Build the card using multiple nodes (sprites and labels)
  3. Be able to apply a singular shader to the whole card

I’m not sure how I could use a panel with rounded corners to achieve all 3 goals, especially the last one. That’s why I’m currently placing them into a CanvasGroup, to be able to apply a shader to the card as a single entity.

I’m not sure what you mean by this. By nature, CanvasGroup cannot have graphics applied to itself, but it works as a container for other nodes, like Sprite2Ds. I described by node tree in the opening post.

  • CanvasGroup (shader applied to this node)
    • Sprite2D
    • Sprite2D
    • Label
    • etc.

Well just add another sprite as a first child and assign that rounded corner texture to that sprite.

1 Like

I fail to see how that changes anything. Can you post a list of your suggested node tree? I believe it won’t change the outcome of applying a shader to said CanvasGroup.

The shader in the canvas group doesn’t need to do the rounded corner part then. Just read the screen texture and assign to COLOR. It won’t have to deal with UV which gets extended to encompassing bounding box of all rotated children.

CanvasGroup
   Background Sprite
   Other stuff you currently have
1 Like

That won’t accomplish what I show and explain, where I use the rounded corner image as a mask for everything inside the card. I can bake the rounded corners to any elements inside, but that doesn’t fix the UV issues. I want to have other shader effects on the card too, of which many use bitmaps to achieve said effects. They are stretched in a similar fashion whenever the card is rotated. The rounded corners is just the most visible one.

I would really want to get the UVs match the rotated CanvasGroup. That is the core issue I have.

Afaik, you can’t. CanvasGroup’s bounding box is not exposed to script so you have nothing to work with if you wanted to recalculate UVs. If the bounding box was known it could be possible with a bit of simple math to calculate the UV area that covers the content in rotated state.

If you need masking you can use canvas item’s clip children feature. Drop the canvas group and use a sprite as a root node, add an additional effect sprite on top of everything:

Clipping Sprite (rounded corner texture, enable clip children)
    Your content
    Effect sprite (shader drawing the effect)
1 Like

Using the clipping sprite should do what you need:

The result without and with effect sprite on top:

1 Like

That doesn’t let me apply a shader to your example’s root_mask (one of the main things I wanted to accomplish). Still, thank you so much for trying to help me with the problem.

Here’s the shader code we wrote to properly shrink UV for a rotated CanvasGroup in case someone else has the same problem.

vec2 shrink_rotated_canvas_group_uv(vec2 uv, vec2 original_size, float rotation) {
	float pixel_total_width = original_size.x + sin(2.0 * rotation) * original_size.y;
	float pixel_total_height = original_size.y + sin(2.0 * rotation) * original_size.x;
	
	// Turn pixel values to shader values
	float shader_total_width = pixel_total_width / original_size.x;
	float shader_total_height = pixel_total_height / original_size.y;
	
	float shader_offset_x = (shader_total_width - 1.0) / 2.0;
	float shader_offset_y = (shader_total_height - 1.0) / 2.0;
	
	return uv * vec2(shader_total_width, shader_total_height) - vec2(shader_offset_x, shader_offset_y);
}

// Example use
void fragment() {
	vec2 size = vec2(100.0, 200.0);
	float rotation = radians(20);
	vec2 adjusted_uv = shrink_rotated_canvas_group_uv(UV, size, rotation);
}

The adusted_uv can now be used with any texture() call and it will properly shrink it to the correct size.

  • size has to match the total size of the nodes in the CanvasGroup
  • rotation has to in radians and match the rotation of the CanvasGroup (I use a parameter that I set in code whenever I rotate the CanvasGroup)

Which the system doesn’t know and you need to guess/input it manually. If something is larger than the mask and sticks out (which is the sole purpose of using a mask) you’ll have hard time keeping track of that size.

Why do you need to run a shader on the canvas group? If you need to overlay a shader effect, just put an additional sprite on top of everything and run the shader on that sprite. Much simpler to manage than hacking the canvas group and guessing its bounding box size.

I’m setting it via code. I’m using this for my cards, which are always the same size. Scaling doesn’t affect this behaviour.

As far as I know, I cannot do ‘destructive’ shaders when I only overlay it to a new child node, like fading the card out using noise. I find it very helpful to be able to apply the shader directly to the group, not its children nodes. I’m not guessing the size of my CanvasGroup as it’s always the same size (and can be set in code too).

Right, the alpha output from shader has no effect on children mask, sadly.

Well then. You’ve got your solution.

A request for exposing canvas group bounding box to script should be made, to make canvas groups more useful.

1 Like