3D Hover Shader for Godot – Make Your UI Elements Pop!


Introduction

Ever wanted to add a 3D hover effect to your UI elements in Godot? With this shader, you can make any CanvasItem like TextureRect appear as if they are tilting and moving in 3D space when hovered! This effect gives a sleek, interactive feel to menus, buttons, and images, making them stand out beautifully!

In this devlog, I'll break down how this shader works and how you can use it in your Godot projects. 

How the Shader Works

This is a  shader_type of canvas_item, which means it works only on 2D canvasitem nodes and not on a 3D object. It basically manipulates the vertex positions of the UI elements based on mouse movement. The effect creates a tilt and perspective shift, making the object appear like it’s moving in 3D space.

Key Features

  • Realistic 3D tilting based on mouse position.
  • Customizable tilt intensity to control how strong the effect is.
  • Optional specular lighting to simulate light reflections.
  • Optimized for performance—works smoothly in UI-heavy projects.

Shader Breakdown

As you would know, any shader code is made of two functions - vertex() for manipulating the vertex position of a mesh and fragment() for rendering the pixel color onto the screen.

Vertex() shader– Simulating 3D Rotation

Here we going to change the VERTEX position of the canvasitem. 

In Godot, the vertex position is nothing but the pixel position on the 2D screen. In our textureRect, the local VERTEX position at the top left is (0, 0) and at the bottom right is (TextureSize.x,  TextureSize.y). So we first normalize the vertex position to range between (0, 0) to (1, 1). This is done by dividing the VERTEX by the texture size. We can simply multiply by the TEXTURE_PIXEL_SIZE built-in variable, which is equal to 1/Texture size.

Note: Refer to built-in variables in the Godot documentation
// Normalize texture coordinates
vTexCoord = VERTEX.xy * TEXTURE_PIXEL_SIZE;

We want the (0, 0) value to be at the centre of the textureRect instead of at the top right. This will help us to rotate the textureRect around the origin and make our calculations easy. Thus, we shift the origin to the center by subtracting (0.5, 0.5). 

(after subtracting, the values will range between (-0.5 -0.5) in the top left to (0.5, 0.5) in the bottom right and (0.0, 0.0) in the centre)

// Center the coordinates around the origin
vec2 centeredCoord = vTexCoord - vec2(0.5, 0.5);

We now calculate the mouse position. 

The mouse position is passed as a uniform _mousePos from a gdscript attached to the canvasitem. (This script is explained in the next section.) The gdscript passes the mouse position without normalizing. So we normalize it and center its origin, but with a range (-1, -1) to (1, 1). This range is useful for matrix transformation.

We also calculate vMouseoffset, a varying that will be used in the fragment shader

Note: For any values that need to be calculated in the vertex shader and passed to the fragment shader, we use varying
vec2 mouse_centered = ((_mousePos + 0.5/TEXTURE_PIXEL_SIZE) * TEXTURE_PIXEL_SIZE) * 2.0 - 1.0;
vMouseoffset = mouse_centered / 2.0; // varying to be passed to fragment

Now we have the normalized centered position of the vertex (centeredCoord) and the normalized centered position of the mouse. All we have to do is tilt the vertex position based on the mouse position, The vertex position can be rotated in any axis using matrices. We create three matrices that rotate the textureRect in the X, Y, and Z axes, respectively. Then we combine them to get a single matrix. This matrix will then be multiplied to our centeredCoord to transform or tilt in 3D space.

The amount of tilt/rotation angle is calculated based on the mouse position. For example, rotation along the X axis is determined by the amount of mouse movement in the Y axis relative to the origin.

Note: To get an in-depth understanding of matrix transformation, I would recommend this tutorial by Jasper Flick

Let's do the matrix transformations!

// Rotation matrices around the X, Y, and Z axes 
//Rotation along X-axis
float cosX = cos(mouse_centered.y * _tilt_Scale);
float sinX = sin(mouse_centered.y * _tilt_Scale);
mat3 rotationX;
rotationX[0] = vec3(1.0, 0.0, 0.0);
rotationX[1] = vec3(0.0, cosX, -sinX);
rotationX[2] = vec3(0.0, sinX, cosX);
//Rotation along Y-axis 
float cosY = cos(-mouse_centered.x * _tilt_Scale); 
float sinY = sin(-mouse_centered.x * _tilt_Scale); 
mat3 rotationY; 
rotationY[0] = vec3(cosY, 0.0, sinY); 
rotationY[1] = vec3(0.0, 1.0, 0.0); 
rotationY[2] = vec3(-sinY, 0.0, cosY);
//Rotation along Z-axis 
//no rotation along Z-axis, so the angle is zero 
float cosZ = cos(0.); 
float sinZ = sin(0.); 
mat3 rotationZ; 
rotationZ[0] = vec3(cosZ, -sinZ, 0.0); 
rotationZ[1] = vec3(sinZ, cosZ, 0.0); 
rotationZ[2] = vec3(0.0, 0.0, 1.0);
// Combine rotations  
mat3 rotation = rotationZ * rotationY * rotationX;  
// Apply the rotation to the vertex position  
vec3 transformedCoord = rotation * vec3(centeredCoord, 0.0);

Now we have successfully transformed the vertex positions (transformedCoord). But since we are in a 2D space in canvasitem, we don't get the 3D perspective with tilt/depth. In a 3D space, the X, Y positions appear to change with depth. To simulate the 3D perspective, we simply need to multiply the perspective correction value along the z-axis.

We also multiply the vTexCoord by the perspective. This will ensure the TexCoord varying is interpolated correctly by the vertex shader. Also, we pass the perspective value to the vFragPerspective varying.

// Apply perspective projection
float perspective = 1.0 / (1.0 - transformedCoord.z * 0.5);
transformedCoord.xy *= perspective;
vTexCoord *= perspective;
vFragPerspective = perspective;

Finally, we will assign this transformed vertex position (transformedCoord) to the VERTEX position of the shader. But before assigning, we will transform back to the original range by de-centering and de-normalizing it.

// Transform back to screen coordinates
vec2 screenPosition = (transformedCoord.xy + vec2(0.5)) / TEXTURE_PIXEL_SIZE;
VERTEX = screenPosition;


Fragment() shader – Texture color and Specular Light

The fragment shader is pretty simple. We just sample the texture color and add a secular light relative to the mouse position.

The TextureRect's main texture is accessed using the TEXTURE built-in variable. To sample any texture, we need a UV coordinate, and here we can use the vTexCoord as uv coordinate that we calculated from the vertex shader. But this needs to be perspective corrected before we use it.

Here is how we get the color to be rendered on the screen.

//perspective correction of UV
vec2 finalTexCoord = vTexCoord / vFragPerspective; 
vec4 texColor = texture(TEXTURE, finalTexCoord);

Now we going to add a specular highlight that will move based on the mouse position. The formula below just calculates the distance (length) from the origin of the UV coordinate (finalTexCoord) and offsets it based on the mouse movement. Also, its power and intensity are calculated based on the _speularLightPower and _speularLightIntensity inputs.

//sepcular light
float specularvalue = pow(clamp(1.0 - length(finalTexCoord - 0.5 + vMouseoffset), 0.0, 1.0), _speularLightPower) * _speularLightIntensity;
vec3 specularCol = vec3(specularvalue);

Finally, if specular lighting is enabled, it is added to the final color.

if(_isSpecularLight)   
    COLOR = texColor + vec4(specularCol, 0.0);
else
COLOR = texColor;


Hover3D gdscript

This script is attached to the canvasitem that we want to hover. It passes the mouse position value to the _ mousePos uniform of the shader.

When the mouse is hovering on the canvasitem, we set the mouse position relative to the canvasitem. Also, to make the position independent of the canvasitem's scale, we divide the relative position by scale and centre it by subtracting half the texture size.

func _input(event):
    if event is InputEventMouseMotion and is_mouse_inside:
        var mouse_position = event.position
        var relative_mouse_position = mouse_position - position
        material.set_shader_parameter("_mousePos", relative_mouse_position/scale - size/2.0)

At the default state, the _mousePos value is 0.0 (origin at the centre). So when the mouse exits the hovering, we set it to 0.0

func _on_mouse_exited():
    is_mouse_inside = false
    material.set_shader_parameter("_mousePos", 0.0)

How to Use It

  • Create a new ShaderMaterial in Godot.
  • Attach the shader code to the material.
  • Assign the material to a TextureRect or any other CanvasItem in your scene.
    • Assign an image texture to the TexureRect. It is recommended to use a texture with some transparent space at the borders so that the image is not clipped when tilted.
  • Attach the Hover3D.gd script to the canvasitem.
  • Tweak the shader parameters (_tilt_Scale, _isSpecularLight, _speularLightIntensity, etc.) to customize the effect.

I’d love to see what you create with this shader! Feel free to download, experiment, and share your ideas about features to add to this shader in the comments.  Happy coding!

Get 3D Hover Canvasitem Shader Godot

Leave a comment

Log in with itch.io to leave a comment.