Search code examples
godotgdscript

Tween the texture on a TextureButton / TextureRect. Fade out Image1 while simultaneously fade in Image2


Character portrait selection. Clicking next loads the next image in an array, clicking back loads the previous image. Instead of a sharp change from one image to another, I want a variable-speed fading out of the current image and fading in of the new image. Dissolve/Render effects would be nice, but even an opacity tween 100->0 / 0-> 100 in x Seconds.

I really prefer not to use multiple objects on top of each other and alternating between them for "current texture".

Is this possible?


Solution

  • We can do Fade-in and Fade-out by animation modulate. Which is the simple solution.

    For dissolve we can use shaders. And there is a lot we can do with shaders. There are plenty of dissolve shaders you can find online... I'll explain some useful variations. I'm favoring variations that are easy to tinker with.


    Fade-in and Fade-out

    We can do this with a Tween object and either the modulate or self-modulate properties.

    I would go ahead and create a Tween in code:

    var tween:Tween
    
    func _ready():
        tween = Tween.new()
        add_child(tween)
    

    Then we can use interpolate_property to manipulate modulate:

    var duration_seconds = 2
    tween.interpolate_property(self, "modulate",
        Color.white, Color.transparent, duration_seconds)
    

    Don't forget to call start:

    tween.start()
    

    We can take advantage of yield, to add code that will execute when the tween is completed:

    yield(tween, "tween_completed")
    

    Then we change the texture:

    self.texture = target_texture
    

    And then interpolate modulate in the opposite direction:

    tween.interpolate_property(self, "modulate",
        Color.transparent, Color.white, duration_seconds)
    tween.start()
    

    Note that I'm using self but you could be manipulating another node. Also target_texture is whatever texture you want to transition into, loaded beforehand.


    Dissolve Texture

    For any effect that require both textures partially visible, use a custom shader. Go ahead and add a ShaderMaterial to your TextureRect (or similar), and give it a new Shader file.

    This will be our starting point:

    shader_type canvas_item;
    
    void fragment()
    {
        COLOR = texture(TEXTURE, UV);
    }
    

    That is a shader that simply shows the texture. Your TextureRect should look the same it does without this shader material. Let us add the second texture with an uniform:

    shader_type canvas_item;
    uniform sampler2D target_texture;
    
    void fragment()
    {
        COLOR = texture(TEXTURE, UV);
    }
    

    You should see a new entry on Shader Param on the Inspector panel for the new texture.

    We also need another parameter to interpolate. It will be 0 to display the original Texture, and 1 for the alternative texture. In Godot we can add a hint for the range:

    shader_type canvas_item;
    uniform sampler2D target_texture;
    uniform float weight: hint_range(0, 1);
    
    void fragment()
    {
        COLOR = texture(TEXTURE, UV);
    }
    

    In Shader Param on the Inspector Panel you should now see the new float, with a slider that goes from 0 to 1.

    It does nothing, of course. We still need the code to mix the textures:

    shader_type canvas_item;
    uniform sampler2D target_texture;
    uniform float weight: hint_range(0, 1);
    
    void fragment()
    {
        vec4 color_a = texture(TEXTURE, UV);
        vec4 color_b = texture(target_texture, UV);
        COLOR = mix(color_a, color_b, weight);
    }
    

    That will do. However, I'll do a little refactor for ease of modification, later on this answer:

    shader_type canvas_item;
    uniform sampler2D target_texture;
    uniform float weight: hint_range(0, 1);
    
    float adjust_weight(float input, vec2 uv)
    {
        return input;
    }
    
    void fragment()
    {
        vec4 color_a = texture(TEXTURE, UV);
        vec4 color_b = texture(target_texture, UV);
        float adjusted_weight = adjust_weight(weight, UV);
        COLOR = mix(color_a, color_b, adjusted_weight);
    }
    

    And now we manipulate it, again with Tween. I'll assume you have a Tween created the same way as before. Also that you already have your target_texture loaded.

    We will start by setting the weight to 0, and target_texture:

    self.material.set("shader_param/weight", 0)
    self.material.set("shader_param/target_texture", target_texture)
    

    We can tween weight:

    var duration_seconds = 4
    tween.interpolate_property(self.material, "shader_param/weight",
        0, 1, duration_seconds)
    tween.start()
    yield(tween, "tween_completed")
    

    And then change the texture:

    self.texture = target_texture
    

    Making Dissolve Fancy

    We can get fancy we our dissolve effect. For example, we can add another texture to control how fast different parts transition form one texture to the other:

    uniform sampler2D transition_texture;
    

    Set it to a new NoiseTexture (and don't forget to set the Noise property of the NoiseTexture). I'll be using the red channel of the texture.

    A simple solution looks like this:

    float adjust_weight(float input, vec2 uv)
    {
        float transition = texture(transition_texture, uv).r;
        return min(1.0, input * (transition + 1.0));
    }
    

    Where the interpolation is always linear, and the transition controls the slope.

    We can also do something like this:

    float adjust_weight(float input, vec2 uv)
    {
        float transition = texture(transition_texture, uv).r;
        float input_2 = input * input;
        return input_2 + (input - input_2) * transition;
    }
    

    Which ensure that an input of 0 returns 0, and an input of 1 returns 1. But transition controls the curve in between.

    If you plot x * x + (x - x * x) * y in the range from 0 to 1 in both axis, you will see that when y (transition) is 1, you have a line, but when y is 0 you have a parabola.

    Alternatively, we can change adjusted_weight to an step function:

    float adjust_weight(float input, vec2 uv)
    {
        float transition = texture(transition_texture, uv).r;
        return smoothstep(transition, transition, input);
    }
    

    Using smoothstep instead of step to avoid artifacts near 0.

    Which will not interpolate between the textures, but each pixel will change from one to the other texture at a different instant. If your noise texture is continuous, then you will see the dissolve advance through the gradient.

    Ah, but it does not have to be a noise texture! Any gradient will do. *You can create a texture defining how you want the dissolve to happen (example, under MIT license).

    You probably can come up with other versions for that function.


    Making Dissolve Edgy

    We also could add an edge color. We need, of course, to add a color parameter:

    uniform vec4 edge_color: hint_color;
    

    And we will add that color at an offset of where we transition. We need to define that offset:

    uniform float edge_weight_offset: hint_range(0, 1);
    

    Now you can add this code:

    float adjusted_weight = adjust_weight(max(0.0, weight - edge_weight_offset * (1.0 - step(1.0, weight))), UV);
    float edge_weight = adjust_weight(weight, UV);
    color_a = mix(color_a, edge_color, edge_weight);
    

    Here the factor (1.0 - step(1.0, weight)) is making sure that when weight is 0, we pass 0. And when weight is 1, we pass a 1. Sadly we also need to make sure the difference does not result in a negative value. There must be another way to do it… How about this:

    float weight_2 = weight * weight;
    float adjusted_weight = adjust_weight(weight_2, UV);
    float edge_weight = adjust_weight(weight_2 + (weight - weight_2) * edge_weight_offset, UV);
    color_a = mix(color_a, edge_color, edge_weight);
    

    Ok, feel free to inline adjust_weight. Whichever version you are using (this makes edges with the smoothstep version. With the other it blends a color with the transition).


    Dissolve Alpha

    It is not hard to modify the above shader to dissolve to alpha instead of dissolving to another texture. First of all, remove target_texture, also remove color_b, which we don't need and should not use. And instead of mix, we can do this:

    COLOR = vec4(color_a.rgb, 1.0 - adjusted_weight);
    

    And to use it, do the same as before to transition out:

    self.material.set("shader_param/weight", 0)
    var duration_seconds = 2
    tween.interpolate_property(self.material, "shader_param/weight",
        0, 1, duration_seconds)
    tween.start()
    yield(tween, "tween_completed")
    

    Which will result in making it transparent. So you can change the texture:

    self.texture = target_texture
    

    And transition in (with the new texture):

    tween.interpolate_property(self.material, "shader_param/weight",
        1, 0, duration_seconds)
    tween.start()