Slice – 2D pixel perfect slice shader

Creates two halves of a texture and allows you to move ’em. You can change the orgin of the angle of the slice, the angle of the slice, the pixel size of your image, and the distance from the center of each slice.

Shader code
shader_type canvas_item;

// Center of the slice, normalized [0,1]
uniform vec2 slice_center = vec2(0.5, 0.5);

// Angle of the slice in radians
uniform float slice_angle = 0.0;

// Distance to move the slices apart, normalized [0,1]
uniform float slice_distance = 0.0;

// Pixel size for pixel-perfect rendering
uniform float pixel_size = 1.0;

// Expansion factor for the sprite bounds
uniform float expansion_factor = 0.5;

// Border color for the expanded area
uniform vec4 border_color = vec4(0.0, 0.0, 0.0, 0.0);

// Convert UV to pixel-perfect UV
vec2 pixel_uv(vec2 uv, float pixel_sizee) {
    return floor(uv * pixel_sizee) / pixel_sizee;
}

// Rotate a point around another point
vec2 rotate(vec2 point, vec2 center, float angle) {
    vec2 translated_point = point - center;
    float sin_angle = sin(angle);
    float cos_angle = cos(angle);
    vec2 rotated_point = vec2(
        translated_point.x * cos_angle - translated_point.y * sin_angle,
        translated_point.x * sin_angle + translated_point.y * cos_angle
    );
    return rotated_point + center;
}

void vertex() {
    // Expand the vertex positions and UVs
    VERTEX *= (1.0 + expansion_factor * 2.0);
    UV = (UV - 0.5) * (1.0 + expansion_factor * 2.0) + 0.5;
}

void fragment() {
    vec2 uv = UV;

    // Adjust UV to handle pixel-perfect rendering
    uv = pixel_uv(uv, pixel_size);

    // Calculate the vector from the slice center to the current UV
    vec2 offset = uv - slice_center;

    // Rotate the offset vector by the negative slice angle
    vec2 rotated_offset = rotate(offset, vec2(0.0), -slice_angle);

    // Determine which side of the slice the current UV is on
    bool is_top = rotated_offset.y > 0.0;

    // Move the UV based on the slice distance
    vec2 movement = vec2(
        slice_distance * cos(slice_angle),
        slice_distance * sin(slice_angle)
    );

    if (is_top) {
        uv += movement;
    } else {
        uv -= movement;
    }

    // Check if UV is outside the [0, 1] range and set the border color if it is
    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        COLOR = border_color;
    } else {
        // Sample the texture with the modified UV
        COLOR = texture(TEXTURE, uv);
    }
}
Tags
2d, effect, pixel, pixel perfect, slice, transition
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Buggit

Radial / Cooldown shader

Related shaders

Perfect Retro Pixel Shader – Godot 4

Clean pixel perfect outline via material

Pixel perfect dissolving

Subscribe
Notify of
guest

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
DinoMC
DinoMC
2 months ago

Damn, the gif looks AMAZING but I can’t get it to work at all.

As soon as I apply the shader to anything, my entire texture just disappears.
And if I set a border color, I just get a large square of that color covering the entirety of my node (+ more).

Looking at the code I think it means the shader thinks every single pixel is outside of the [0,1] range so it just draw the border everywhere… but I don’t understand why.

Even if I just create a new project and add a sprite then add the shader, I get the issue.

Which Godot version are you using? (4.2 stable here)

Edit : Ah, I got it. I was confused by the Pixel Size parameter. For anyone else with the same issue : you gotta input the amount of pixels in your texture, basically.
So if your texture is 32x16px, for example, then set Pixel Size to 512 and the shader will work

Last edited 2 months ago by DinoMC
DinoMC
DinoMC
2 months ago

Here’s a modified version that allows you to slice everything on screen rather than one texture! If you want to have a boss that slice your whole game in half or something like that 🙂

Create a textured area (eg. TextureRect, ColorRect, etc…, but it over what you want to slice (and with an higher z-index) then add a material to the area with this shader :

shader_type canvas_item;


// Center of the slice, normalized [0,1]
uniform vec2 slice_center = vec2(0.5, 0.5);


// Angle of the slice in radians
uniform float slice_angle = 0.0;


// Distance to move the slices apart, normalized [0,1]
uniform float slice_distance = 0.0;


// Pixel size for pixel-perfect rendering
uniform float pixel_size = 1.0;


// Expansion factor for the sprite bounds
uniform float expansion_factor = 0.5;


// Border color for the expanded area
uniform vec4 border_color = vec4(0.0, 0.0, 0.0, 0.0);


uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap;


// Convert UV to pixel-perfect UV
vec2 pixel_uv(vec2 uv, float pixel_sizee) {
    return floor(uv * pixel_sizee) / pixel_sizee;
}


// Rotate a point around another point
vec2 rotate(vec2 point, vec2 center, float angle) {
    vec2 translated_point = point - center;
    float sin_angle = sin(angle);
    float cos_angle = cos(angle);
    vec2 rotated_point = vec2(
        translated_point.x * cos_angle - translated_point.y * sin_angle,
        translated_point.x * sin_angle + translated_point.y * cos_angle
    );
    return rotated_point + center;
}


void vertex() {
    // Expand the vertex positions and UVs
    VERTEX *= (1.0 + expansion_factor * 2.0);
    UV = (UV - 0.5) * (1.0 + expansion_factor * 2.0) + 0.5;
}


void fragment() {
    vec2 uv = SCREEN_UV;


    // Adjust UV to handle pixel-perfect rendering
    uv = pixel_uv(uv, pixel_size);


    // Calculate the vector from the slice center to the current UV
    vec2 offset = uv - slice_center;


    // Rotate the offset vector by the negative slice angle
    vec2 rotated_offset = rotate(offset, vec2(0.0), -slice_angle);


    // Determine which side of the slice the current UV is on
    bool is_top = rotated_offset.y > 0.0;


    // Move the UV based on the slice distance
    vec2 movement = vec2(
        slice_distance * cos(slice_angle),
        slice_distance * sin(slice_angle)
    );


    if (is_top) {
        uv += movement;
    } else {
        uv -= movement;
    }


    // Check if UV is outside the [0, 1] range and set the border color if it is
    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        COLOR = border_color;
    } else {
        // Sample the texture with the modified UV
        COLOR = texture(TEXTURE, uv);
    }
	vec3 c = textureLod(screen_texture, uv, 0.0).rgb;


	COLOR.rgb = c;
}

(Added a screen sampler, changed UV to SCREEN_UV in fragment, and added Color.rgb = textureLod(screen_texture, uv, 0.0).rgb to the end of fragment)

DinoMC
DinoMC
2 months ago
Reply to  DinoMC

The shader I posted above is mostly useful if you want to slice the whole screen, because the “slice center” you set in the shader parameters will be based on the center of the camera/viewport, NOT the center of your Texture node. So, it’s useless if you want to slice something that’s not near the center of the screen.

Here’s another version that actually center on your node’s center ( + whatever offset you set in the parameters). Allowing you to slice anything, anywhere. Yay!

shader_type canvas_item;


// Center of the slice, normalized [0,1]
uniform vec2 slice_center = vec2(0.5, 0.5);




// Angle of the slice in radians
uniform float slice_angle = 0.0;




// Distance to move the slices apart, normalized [0,1]
uniform float slice_distance = 0.0;




// Pixel size for pixel-perfect rendering
uniform float pixel_size = 1.0;




// Expansion factor for the sprite bounds
uniform float expansion_factor = 0.5;




// Border color for the expanded area
uniform vec4 border_color = vec4(0.0, 0.0, 0.0, 0.0);




uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap;






// Convert UV to pixel-perfect UV
vec2 pixel_uv(vec2 uv, float pixel_sizee) {
    return floor(uv * pixel_sizee) / pixel_sizee;
}




// Rotate a point around another point
vec2 rotate(vec2 point, vec2 center, float angle) {
    vec2 translated_point = point - center;
    float sin_angle = sin(angle);
    float cos_angle = cos(angle);
    vec2 rotated_point = vec2(
        translated_point.x * cos_angle - translated_point.y * sin_angle,
        translated_point.x * sin_angle + translated_point.y * cos_angle
    );
    return rotated_point + center;
}




void vertex() {
    // Expand the vertex positions and UVs
    VERTEX *= (1.0 + expansion_factor * 2.0);
    UV = (UV - 0.5) * (1.0 + expansion_factor * 2.0) + 0.5;
}




void fragment() {
	// Calculate screen resolution from SCREEN_PIXEL_SIZE
	vec2 screen_resolution = vec2(1.0) / SCREEN_PIXEL_SIZE;


	// Convert the fragment coordinates to normalized screen UV coordinates
	vec2 uv = FRAGCOORD.xy / screen_resolution;




    // Adjust UV to handle pixel-perfect rendering
    // uv = pixel_uv(uv, pixel_size);




    // Calculate the vector from the slice center to the current UV
    vec2 offset = (UV - slice_center) / screen_resolution;




    // Rotate the offset vector by the negative slice angle
    vec2 rotated_offset = rotate(offset, vec2(0.0), -slice_angle);




    // Determine which side of the slice the current UV is on
    bool is_top = rotated_offset.y > 0.0;




    // Move the UV based on the slice distance
    vec2 movement = vec2(
        slice_distance * cos(slice_angle),
        slice_distance * sin(slice_angle)
    );




    if (is_top) {
        uv += movement;
    } else {
        uv -= movement;
    }




    // Check if UV is outside the [0, 1] range and set the border color if it is
    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        COLOR = border_color;
    } else {
        // Sample the texture with the modified UV
        COLOR = texture(TEXTURE, uv);
    }
	vec3 c = textureLod(screen_texture, uv, 0.0).rgb;
	// Sample the screen texture at the calculated UV coordinates


	COLOR.rgb = c;
}

I think there’s still a small bug where the angle won’t be the same depending on the viewport aspect ratio (because SCREEN_PIXEL_SIZE and FRAGCOORD use the windows size, including the space outside the viewport. what I’d need is something like VIEWPORT_PIXEL_SIZE and a fragcoord equivalent too, but that doesnt seem to exist).

If anyone have a fix idea, please comment :). But that bug doesn’t seem like a big deal, try it out.

DinoMC
DinoMC
2 months ago
Reply to  DinoMC

OK, discovered some more bugs afterward…. sadly it seems I can’t edit or delete my previous comment to remove the bugged wall of code :(.

Here’s a Node version where everything works correctly :

  • Slice Center is relative to your node’s size and won’t move no matter the camera position, zoom, etc…
  • Slice angle won’t change with window size or aspect ratio.
  • Expansion factor expands on all sides (it sometimes didn’t with the previous one and broke everything.

NOTE : It seems to ONLY work if you use a Sprite2D. ColorRect or TextureRect won’t expand correctly for some reason.
You can just use the Godot icon as a texture and then scale the Sprite to whatever size you need, it works well.

Final version (?) :

shader_type canvas_item;


// Center of the slice, normalized [0,1]
uniform vec2 slice_center = vec2(0.5, 0.5);


// Angle of the slice in radians
uniform float slice_angle = 0.0;


// Distance to move the slices apart, normalized [0,1]
uniform float slice_distance = 0.0;


// Pixel size for pixel-perfect rendering
uniform float pixel_size = 1.0;


// Expansion factor for the sprite bounds
uniform float expansion_factor = 0.5;


// Border color for the expanded area
uniform vec4 border_color = vec4(0.0, 0.0, 0.0, 0.0);

// Get the color of pixels on screen, including behind the current Sprite2D
uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap;


// Convert UV to pixel-perfect UV
vec2 pixel_uv(vec2 uv, float pixel_sizee) {
    return floor(uv * pixel_sizee) / pixel_sizee;
}


// Rotate a point around another point
vec2 rotate(vec2 point, vec2 center, float angle) {
    vec2 translated_point = point - center;
    float sin_angle = sin(angle);
    float cos_angle = cos(angle);
    vec2 rotated_point = vec2(
        translated_point.x * cos_angle - translated_point.y * sin_angle,
        translated_point.x * sin_angle + translated_point.y * cos_angle
    );
    return rotated_point + center;
}


void vertex() {
    // Expand the vertex positions and UVs
    VERTEX *= (1.0 + expansion_factor * 2.0);
    UV = (UV - 0.5) * (1.0 + expansion_factor * 2.0) + 0.5;
}


void fragment() {
	// Calculate screen resolution from SCREEN_PIXEL_SIZE
	vec2 screen_resolution = vec2(1.0) / SCREEN_PIXEL_SIZE;
	// Get coordinates of current fragment relative to the window
	vec2 pixelxy = FRAGCOORD.xy / screen_resolution;
	
    vec2 uv = UV;


    // Adjust UV to handle pixel-perfect rendering
    // uv = pixel_uv(uv, pixel_size);


    // Calculate the vector from the slice center to the current UV
    vec2 offset = uv - slice_center;


    // Rotate the offset vector by the negative slice angle
    vec2 rotated_offset = rotate(offset, vec2(0.0), -slice_angle);


    // Determine which side of the slice the current UV is on
    bool is_top = rotated_offset.y > 0.0;


    // Move the UV based on the slice distance
    vec2 movement = vec2(
        slice_distance * cos(slice_angle),
        slice_distance * sin(slice_angle)
    );


    if (is_top) {
        pixelxy += movement;
    } else {
        pixelxy -= movement;
    }


    // Check if UV is outside the [0, 1] range and set the border color if it is
    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        COLOR = border_color;
    } else {
        // Sample the texture with the modified UV
        COLOR = texture(TEXTURE, uv);
    }
    
    // Get pixel color from behind the Sprite2D and apply it to the current fragment (in the new position)
	vec3 c = textureLod(screen_texture, pixelxy, 0.0).rgb;
	// Sample the screen texture at the calculated UV coordinates
	COLOR.rgb = c;
}
Laozz
1 month ago
Reply to  DinoMC

Thank you very much, the modified code is very good, but there are some bugs that I can not use, so I made some modifications.

shader_type canvas_item;

// 切片的中心,规范化到 [0,1] 范围内
uniform vec2 slice_center = vec2(0.5, 0.5);

// 切片的角度,以弧度为单位
uniform float slice_angle = 0.0;

// 切片分开的距离,规范化到 [0,1] 范围内
uniform float slice_distance = 0.0;

// 像素大小,用于像素完美渲染
uniform float pixel_size = 1.0;

// 精灵边界的扩展系数
uniform float expansion_factor = 0.5;

// 扩展区域的边框颜色
uniform vec4 border_color = vec4(0.0, 0.0, 0.0, 0.0);

// 围绕另一个点旋转一个点
vec2 rotate(vec2 point, vec2 center, float angle) {
  vec2 translated_point = point – center;
  float sin_angle = sin(angle);
  float cos_angle = cos(angle);
  vec2 rotated_point = vec2(
    translated_point.x * cos_angle – translated_point.y * sin_angle,
    translated_point.x * sin_angle + translated_point.y * cos_angle
  );
  return rotated_point + center;
}

void fragment() {
  vec2 uv = UV;

  // 计算从切片中心到当前 UV 的向量
  vec2 offset = uv – slice_center;

  // 通过负切片角度旋转偏移向量
  vec2 rotated_offset = rotate(offset, vec2(0.0), -slice_angle);

  // 确定当前 UV 位于切片的哪一侧
  bool is_top = rotated_offset.y > 0.0;

  // 根据切片距离移动 UV
  vec2 movement = vec2(
    slice_distance * cos(slice_angle),
    slice_distance * sin(slice_angle)
  );

  if (is_top) {
    uv += movement;
  } else {
    uv -= movement;
  }

  // 检查 UV 是否在 [0, 1] 范围之外,如果是则设置边框颜色
  if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
    COLOR = border_color;
  } else {
    // 使用修改后的 UV 采样纹理
    COLOR = texture(TEXTURE, uv);
  }
}

TheYellowArchitect
1 month ago
Reply to  DinoMC

Bro these are 4 posts and the code is 3 times the size of OP and its functionality. It deserves its own shader post here, or if you want to reach more people, a youtube video (no webcam, no voice, just show step by step to setup with text)

TheYellowArchitect
1 month ago

If I had a 3D card (so Z is completely non-existent), how would this be converted?