How To Centre A Texture Inside A Fragment Shader

Godot Version

4.4.1

Question

I am working on a shader for a new project of mine, and for the fact it is the most advanced shader I have written myself it is going well, except for one thing;

(Code)

shader_type canvas_item;

uniform sampler2D icon;
uniform float icon_aspect_ratio = 1.0;
uniform float self_aspect_ratio = 1.0;
uniform float scroll_progress : hint_range(0.0, 1.0, 0.005) = 0.0;

void fragment() {
	// Preserve icon aspect ratio
	vec2 adjusted_uv = UV;
	self_aspect_ratio > icon_aspect_ratio ? adjusted_uv.x *= self_aspect_ratio / icon_aspect_ratio : adjusted_uv.y *= icon_aspect_ratio / self_aspect_ratio;
	
	// As we scroll down, shift the texture up
	vec4 icon_texture = texture(icon, vec2(adjusted_uv.x, adjusted_uv.y + scroll_progress));
	
	// As we scroll down, dim the icon
	icon_texture.r *= 0.75 - scroll_progress;
	icon_texture.g *= 0.75 - scroll_progress;
	icon_texture.b *= 0.75 - scroll_progress;
	
	// As we scroll down, fade to transparent
	COLOR = mix(icon_texture, vec4(0, 0, 0, 0), scroll_progress);
	
	// Stops long lines from the edge of the texture being sampled
	// from appearing if the texture edge isn't transparent
	adjusted_uv.y >= 1.0 - scroll_progress ? COLOR = vec4(0, 0, 0, 0) : COLOR = COLOR;
	adjusted_uv.x >= 1.0 ? COLOR = vec4(0, 0, 0, 0) : COLOR = COLOR;
}

The one problem is that the icon isn’t centred, it is off to one side;
(The current shader with the aspect ratios set, 0 scroll progress and an icon from one of my projects)

So how can I figure the offset to centre it?
Annoyingly not all of the icon’s are the same size so I can’t use a constant value, so I need to figure one out dynamically, how can I achieve this?

Thanks.

Hi!

UVs go from 0 to 1, regardless of initial texture size, so to center horizontally, you could try offset the X axis by 0.5.
Something like this:

vec2 adjusted_uv = UV;
adjusted_uv.x += 0.5; // Maybe you need to subtract, but the idea is the same.

Also, is your node a Sprite2D or a TextureRect? If it’s a TextureRect node, you should have a look at Stretch Mode options, as some of them are made to center a texture and keep its ratio inside the control rect, whatever dimensions the rect has.

I have tried this, and interestingly it doesn’t result in it being in the centre;

(With the adjusted_uv.x being subtracted by 0.5 after the aspect ratio setting and before texture sampling, and a white line to show the centre of the node easier)

I am using a ColorRect node, so all the icon-related things are handled solely by the shader and its parameters.
Though if this doesn’t end up being possible I could change it to a TextureRect to use that as you said.

Thanks.

I suppose this is due to your texture not being centered in a first place. If you need your texture to be centered somehow, the first thing to do is to ensure the .png file itself has a centered image. Then, the -= 0.5 computation should work fine. Otherwise, there’s no way you can do it dynamically for any icon of any size.

I believe that would be a better fit, but I may not be aware of everything concerning your use case, so maybe using a ColorRect is fine here. Worth a try, though!

My texture is centred I believe, here is the file I am using;

(Its 129x129)

And opening it inside a drawing programme and putting a line in the centre (Two-wide since the centre isn’t one line due to it not being an even size) shows this;

The centre it a little to the right of the M, but as the image shows above when offsetting it by 0.5 the centre line is through the N, so I don’t believe it is an issue with the texture.
But I might be misunderstanding the issue.

I am going to try and write a shader using a TextureRect as even if a way to do it using a ColorRect is found I imagine using a TextureRect would be simpler, but hopefully we can still find a way to do with with a ColorRect.

Thanks.

Thanks for sharing the icon. Indeed I was wrong, it’s centered correctly.
I believe I forgot to consider the ratio parameter; it’s something like 0.6, right?
I tried adding the ratio into the offset calculation, like so:

vec2 adjusted_uv = UV;
self_aspect_ratio > icon_aspect_ratio ? adjusted_uv.x *= self_aspect_ratio / icon_aspect_ratio : adjusted_uv.y *= icon_aspect_ratio / self_aspect_ratio;
adjusted_uv.x -= 0.5 * icon_aspect_ratio;

Using a ColorRect. Would that be correct with your other icons?

Since the icon is 129x129, wouldn’t that result in an aspect ratio of 1?

So it would be 0.5 * (129.0 / 129.0 = 1.0) = 0.5, would it not?
Unless I don’t understand what the aspect ratio is; I thought it was width divided by height?

It is!
But you’re looking to center an icon into a ColorRect that is not a squared (thus not having an aspect ratio of 1). You defined a parameter called icon_aspect_ratio, so I assumed it was a way of getting the aspect ratio of the ColorRect, but maybe it’s not why you’re using that parameter?

Not gonna lie, but the more we’re talking about this, the more I think you should be using a TextureRect :laughing:
If you’re just looking to center the icon into the rect, it’s a piece of cake with a TextureRect. The rest of your effects should be done the same way.

texture rect

1 Like

As @sixrobin mentioned, there’s no reason to render from a texture yourself, TextureRect can handle textures and the aspect ratio too. You only need the scroll_progress, so go ahead with a TextureRect, and simplify the shader as shown below.

shader_type canvas_item;

uniform float scroll_progress : hint_range(0.0, 1.0, 0.005) = 0.0;

void fragment() {
	// As we scroll down, dim the icon
	vec3 dimmed = COLOR.rgb * 0.75 - scroll_progress;
	// As we scroll down, fade to transparent
	COLOR = mix(dimmed, vec4(0), scroll_progress);
}
1 Like

That is true, though doing it that way I am then unsure how to replicate the effect from the line;

vec4 icon_texture = texture(icon, vec2(adjusted_uv.x, adjusted_uv.y + scroll_progress))

Which moves the texture up as you scroll down, I don’t know how to replicate this without having the icon as a parameter, can you access the current texture inside the fragment shader? Something like this for example;

// Assuming TEXTURE was the current TextureRect texture
// including it being centred and with the correct AspectRatio
COLOR = texture(TEXTURE, vec2(UV.x, UV.y + scroll_progress));
// Rest of the shader...

Because this is the one reason why after @sixrobin first mentioned it I didn’t then switch to using this instead.

Thanks.

You have TEXTURE in the fragment shader indeed! The doc below said about this clearly.

But since you only want to offset the y axis, why not use a plain Node?

1 Like

I (With retrospect naively) thought it wouldn’t be too much more complex and more performant to offset the y of the texture using a shader then just moving a node.

But yes, I was able to use…

COLOR = texture(TEXTURE, vec2(UV.x, UV.y + scroll_progress))

…to get the correct image, I had tried to use it before and it hadn’t worked but I had tried to then assign the value back to TEXTURE opposed to COLOR which is why it didn’t work, and then I assumed TEXTURE meant something else and moved on lol.

Thank both of you very much, now I have the far simpler shader of this on a TextureRect;

shader_type canvas_item;

uniform float scroll_progress : hint_range(0.0, 1.0, 0.005) = 0.0;

void fragment() {
	COLOR = texture(TEXTURE, vec2(UV.x, UV.y + scroll_progress));
	
	COLOR.r *= 0.75 - scroll_progress;
	COLOR.g *= 0.75 - scroll_progress;
	COLOR.b *= 0.75 - scroll_progress;
	COLOR.a *= 1.0 - scroll_progress;
	
	// Prevent long lines from the texture being sampled repeatedly
	UV.y >= 1.0 - scroll_progress ? COLOR = vec4(0) : COLOR = COLOR;
}
1 Like

Even simpler with COLOR.rgb *= 0.75 - scroll_progress; instead of 3 lines! :nerd_face:
Glad we could help.

1 Like

I fully understand what you’re talking about as a former performance-eager noob…and that’s what we called

In most of the mostly-most cases (triple most), you don’t need any optimization, even code that runs every frame. Only optimize after something is bottlenecking, analyze the performance bottleneck by profiling, etc. In this case, the position offsetting can be totally ignored for two reasons

  • There’s only a single node here with simple arithmetic (division 100 times is still simple). In reality, only in scenarios like a thousand Node all performing really complex logics (or physics) will affect the overall performance.
  • No matter what, the Node' is rendered at its position`, and the calculation is always being performed.

And that’s the story of how the noobs always messed up the performance with optimizing…