5
\$\begingroup\$

I'm currently stuck on trying to write a shader for Godot 4.x. and get it to look right.

I'd like to take an image, pixelate a bit so that particles are a bit chunky, and then explode it into component chunky pixels. So it looks like the sprite has broken apart.

pixels explosion slowed down

This is simple 3d animation which I mocked up in Blender to show what I want. It's basically a grid of 16 squares that shrink and fade as they move away from the centre. However they are all mapped so that together at the start they represent a whole image.

and being very new to shaders, this is what I've got so far in Godot:

pixel explosion shader

shader_type canvas_item;

uniform sampler2D tex_diffuse; // explosion image
uniform sampler2D tex_explode; // grayscale circle
uniform float progress : hint_range(0.0, 1.0);

void fragment() {
    vec2 direction = texture(tex_diffuse, UV).xy;
    direction = direction  * progress;
    
    // Get UV and step them up by 4
    vec2 tex_size = 1.0 / TEXTURE_PIXEL_SIZE; // texture size
    vec2 uv = floor(UV * tex_size/4.0) / (tex_size/4.0 - 1.0); // snap to step

    // Texture with exploded UV
    vec4 tex = texture(TEXTURE, uv); 
    
    // Dissolve with alpha
    float dissolve = texture(tex_explode, uv).x;
    dissolve = step(progress, dissolve);
    tex.a *= dissolve;
    
    // set tex 
    COLOR = tex;
}

tex_explode texture:

tex_explode image

As you can see from the two animations, I'm not quite there. This is where I need help. I need to divide the image up into pixelated chunks - let's call them pixel particles. "Move" those pixel particles apart from one another radiating from centre. scale down and fade out the pixel particles.

Just to be clear, I'm using a scale animation on the sprite (not yet included in shader)

\$\endgroup\$
6
  • \$\begingroup\$ What is the purpose of the texture? \$\endgroup\$
    – Theraot
    Sep 25 at 5:55
  • \$\begingroup\$ It's gonna be part of a stylised explosion for an old school arcade game. \$\endgroup\$
    – Ghoul Fool
    Sep 25 at 8:12
  • \$\begingroup\$ My current understanding is that want the top animation, don't you? I do not see how or why you would need tex_explode to accomplish the top animation. However, you have gone a path that uses tex_explode. Thus, I'm guessing that top animation is not actually what you want. What effect do you want to accomplish? If you do actually want the top animation, then what is tex_explode for? \$\endgroup\$
    – Theraot
    Sep 25 at 9:07
  • \$\begingroup\$ Whoops! Pasted wrong version of code. Updated now. \$\endgroup\$
    – Ghoul Fool
    Sep 25 at 9:39
  • \$\begingroup\$ @Theraot I believe they want the pixels separating like the first animation AND pixels vanishing in a pseudorandom pattern from the inside out like the second animation — both effects occurring simultaneously. If that's accurate, Ghoul Fool, it would help to state that more explicitly in your question. \$\endgroup\$
    – DMGregory
    Sep 25 at 9:55

1 Answer 1

3
\$\begingroup\$

This is what I have been able to come up with:

shader_type canvas_item;

uniform float progress : hint_range(0.0, 1.0) = 0.0;
uniform float sprite_scale_begin : hint_range(0.0, 5.0) = 1.0;
uniform float sprite_scale_end : hint_range(0.0, 5.0) = 1.0;
uniform float particle_scale_begin : hint_range(0.0, 5.0) = 1.0;
uniform float particle_scale_end : hint_range(0.0, 5.0) = 1.0;
uniform sampler2D dissolve_texture;

void vertex() {
    // GROW
    VERTEX *= mix(sprite_scale_begin, sprite_scale_end, progress);
}

void fragment() {
    // MOSAIC EFFECT
    const float pixel_step = 4.0;
    vec2 texure_size = 1.0 / TEXTURE_PIXEL_SIZE;
    vec2 steps = texure_size/pixel_step;
    vec2 uv = floor(UV * steps) / (steps - 1.0);
    COLOR = texture(TEXTURE, uv);

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);
    vec2 direction = UV - 0.5;
    vec2 quantized_direction = sign(direction);
    float angle = PI * 1.25 - atan(quantized_direction.y, quantized_direction.x);
    float angle_sin = sin(angle);
    float angle_cos = cos(angle);
    mat2 rotation_matrix = mat2(vec2(angle_cos, angle_sin), vec2(-angle_sin, angle_cos));
    vec2 rotated_fract_uv = (rotation_matrix * (fract_uv - 0.5)) + 0.5; 
    float inverse_particle_scale = 1.0 / mix(particle_scale_begin, particle_scale_end, progress);
    if (isinf(inverse_particle_scale)){
        COLOR.a = 0.0;
    }
    else
    {
        float step_position = max(rotated_fract_uv.x, rotated_fract_uv.y) * inverse_particle_scale;
        float inverse_sprite_scale = 1.0 / mix(sprite_scale_begin, sprite_scale_end, progress);
        COLOR.a *= step(step_position, inverse_sprite_scale);
    }

    // DISSOLVE EFFECT
    float dissolve = texture(dissolve_texture, uv).x;
    dissolve = step(progress, dissolve);
    COLOR.a *= dissolve;
}

Let us go part by part...


First of all, just as in your code progress controls the animation:

uniform float progress : hint_range(0.0, 1.0) = 0.0;

Now, to make the sprite grow, I have defined these uniforms:

uniform float sprite_scale_begin : hint_range(0.0, 5.0) = 1.0;
uniform float sprite_scale_end : hint_range(0.0, 5.0) = 1.0;

So I can compute what should be the scale of the sprite using progress, and the code to actually scale it is in vertex:

void vertex() {
    // GROW
    VERTEX *= mix(sprite_scale_begin, sprite_scale_end, progress);
}

The mosaic effect in fragment begins with the pixel_step constant, which I promoted from literals in your code:

    // MOSAIC EFFECT
    const float pixel_step = 4.0;

We continue with the texture_size as you do:

    // MOSAIC EFFECT
    const float pixel_step = 4.0;
    vec2 texure_size = 1.0 / TEXTURE_PIXEL_SIZE;

And how many steps does the texture has:

    // MOSAIC EFFECT
    const float pixel_step = 4.0;
    vec2 texure_size = 1.0 / TEXTURE_PIXEL_SIZE;
    vec2 steps = texure_size/pixel_step;

So we can get the uv (which is at integers steps):

    // MOSAIC EFFECT
    const float pixel_step = 4.0;
    vec2 texure_size = 1.0 / TEXTURE_PIXEL_SIZE;
    vec2 steps = texure_size/pixel_step;
    vec2 uv = floor(UV * steps) / (steps - 1.0);

I was tempted to extract UV * steps as a variable, as we will use it later, but I couldn't figure out a good name for it.

And we can set the output COLOR:

    // MOSAIC EFFECT
    const float pixel_step = 4.0;
    vec2 texure_size = 1.0 / TEXTURE_PIXEL_SIZE;
    vec2 steps = texure_size/pixel_step;
    vec2 uv = floor(UV * steps) / (steps - 1.0);
    COLOR = texture(TEXTURE, uv);

From here on, we will only worry about transparency. I will be setting COLOR.a (using discard is also an option, since we will not have partial transparency).


OK, so far it grows, and it has the mosaic effect. However, the elements of the mosaic grow, and there are no visible gaps between them.

I'm going to explain how to keep the size of the particles the same...

First we need to figure out a coordinate system inside of the particle. So we start with the fraction of the UV we didn't use when we rounded it:

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);

Here is where we use UV * steps again.

This has a couple problems: It also grows, and it is in the regular orientation. We will handle the orientation first...

So, we figure out the direction from the center of the texture:

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);
    vec2 direction = UV - 0.5;

By substracting UV - 0.5 we have the UV coordinates shifted, so the origin is at the center (since UV goes from (0, 0) to (1, 1), UV - 0.5 goes from (-0.5, -0.5) to (0.5, 0.5)).

We do not need to normalize it... Instead we will quantize it. That is, we will "round" the direction to a few possible ones, in this case I want the directions we get by simply getting the sign of the components:

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);
    vec2 direction = UV - 0.5;

From this I expect four directions:

  • Top Left: (-1.0, -1.0)
  • Top Right: (1.0, -1.0)
  • Bottom Left: (-1.0, 1.0)
  • Bottom Right: (-1.0, 1.0)

That is: I expect the diagonals. I do not expect straight up, down, left and right, unless you have set a pixel_step and texure_size that result in an odd number of particles so that there are particles aligned with the center of the texture.

Now, I figure out an angle...

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);
    vec2 direction = UV - 0.5;
    vec2 quantized_direction = sign(direction);
    float angle = PI * 1.25 - atan(quantized_direction.y, quantized_direction.x);

Here atan(quantized_direction.y, quantized_direction.x) is the angle of the quantized_direction measured from the positive horizontal axis (this is the standard)… Except I want the angle for the Top Left corner to be zero (that part is already oriented how I wanted it), and, well, It might make more sense with the next steps: I rotate the fract_uv:

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);
    vec2 direction = UV - 0.5;
    vec2 quantized_direction = sign(direction);
    float angle = PI * 1.25 - atan(quantized_direction.y, quantized_direction.x);
    float angle_sin = sin(angle);
    float angle_cos = cos(angle);
    mat2 rotation_matrix = mat2(vec2(angle_cos, angle_sin), vec2(-angle_sin, angle_cos));
    vec2 rotated_fract_uv = (rotation_matrix * (fract_uv - 0.5)) + 0.5;

As you can figure out, I made a rotation matrix from the computed angle. The idea here is to get a rotated version of fract_uv, such that the zero is on the outer corner of the particle.

Now, I need to take the difference with PI * 1.25 (225º) so the top left has a rotation of zero. And - via trial an error - I figured out that I need to do PI * 1.25 minus the angle, and not the other way around.

I do substract 0.5 before rotating, and add it again after the rotation, so the rotation happens around the center of the particle.

Next I need to do this to keep the particles of the same size:

    float step_position = max(rotated_fract_uv.x, rotated_fract_uv.y);
    float inverse_sprite_scale = 1.0 / mix(sprite_scale_begin, sprite_scale_end, progress);
    COLOR.a *= step(step_position, inverse_sprite_scale);

Here step_position is a measure of where on the particle the current fragment is... measured from the outer corner (which was the point of all the rotation stuff). And - again via trial and error - I figured that comparing it with inverse_sprite_scale allows me to cut the particle so it retains its original size (the size it had when the sprite scale was 1.0).


Ah, but in your mock up animation I notice the particles shrink! So to add that we have two more uniforms:

uniform float particle_scale_begin : hint_range(0.0, 5.0) = 1.0;
uniform float particle_scale_end : hint_range(0.0, 5.0) = 1.0;

And we do this instead:

    // PARTICLE SIZE
    vec2 fract_uv = fract(UV * steps);
    vec2 direction = UV - 0.5;
    vec2 quantized_direction = sign(direction);
    float angle = PI * 1.25 - atan(quantized_direction.y, quantized_direction.x);
    float angle_sin = sin(angle);
    float angle_cos = cos(angle);
    mat2 rotation_matrix = mat2(vec2(angle_cos, angle_sin), vec2(-angle_sin, angle_cos));
    vec2 rotated_fract_uv = (rotation_matrix * (fract_uv - 0.5)) + 0.5; 
    float inverse_particle_scale = 1.0 / mix(particle_scale_begin, particle_scale_end, progress);
    if (isinf(inverse_particle_scale)){
        COLOR.a = 0.0;
    }
    else
    {
        float step_position = max(rotated_fract_uv.x, rotated_fract_uv.y) * inverse_particle_scale;
        float inverse_sprite_scale = 1.0 / mix(sprite_scale_begin, sprite_scale_end, progress);
        COLOR.a *= step(step_position, inverse_sprite_scale);
    }

The reason I'm checking for an infinite inverse_particle_scale is because you might have set the particle scale to zero (so they shrink to nothing), which result in a division by zero.

Otherwise, what I do to scale the particles is scale step_position by inverse_particle_scale.


Finally, you had a dissolve effect which uses a texture, so we have a texture uniform:

uniform sampler2D dissolve_texture;

And the code is basically what you had:

    // DISSOLVE EFFECT
    float dissolve = texture(dissolve_texture, uv).x;
    dissolve = step(progress, dissolve);
    COLOR.a *= dissolve;

Hopefully this is the effect you want. Barring that, I hope I gave you the tools to figure it out.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much for doing this. Great explanation as well. \$\endgroup\$
    – Ghoul Fool
    Sep 27 at 16:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .