Shader de Dissolucao (Dissolve) no Godot 4 com GDShader

Aprenda a montar um shader dissolve godot do zero usando GDShader, ruido com NoiseTexture2D, borda emissiva e animacao com Tween no material.
Quando um inimigo morre, uma porta se abre ou um item some do mundo, mostrar o sprite simplesmente desaparecendo de uma vez parece barato. O efeito de dissolucao resolve isso: a textura vai sumindo aos pedacos, como se queimasse ou se desintegrasse, geralmente com uma borda brilhante na linha que avanca. Neste tutorial voce vai montar um shader dissolve godot completo, do zero, e vai entender por que cada linha existe, nao so copiar e colar.
Shader de Dissolucao (Dissolve) no Godot 4 com GDShader
A ideia central e simples e vale entender antes de escrever qualquer codigo. Voce nao apaga o sprite gradualmente no tempo. Em vez disso, voce gera um mapa de ruido (uma imagem com valores aleatorios entre 0 e 1) e decide, pixel a pixel, se ele continua visivel ou nao comparando o ruido com um valor de progresso. Quando o progresso e 0, nada some. Quando ele chega em 1, tudo sumiu. A "aleatoriedade" do ruido e o que faz a dissolucao parecer organica em vez de uma cortina reta descendo.
Se shaders ainda sao terreno novo pra voce, vale ler antes o guia de shaders no Godot para iniciantes, porque aqui eu assumo que voce ja sabe criar um ShaderMaterial e colar um Shader nele.
Por que usar um shader e nao animacao de frames
Voce poderia desenhar a dissolucao quadro a quadro num spritesheet. Funciona, mas trava o efeito numa unica arte, numa unica direcao e num unico ritmo. Com shader, o mesmo codigo serve para qualquer sprite, voce controla a velocidade em tempo de execucao e a forma do desaparecimento vem do ruido, que voce troca quando quiser. Tambem economiza memoria: nada de dezenas de frames extras.
O custo e que voce precisa pensar em termos de matematica de imagem. A boa noticia e que para 2D isso e bem direto.
Estrutura basica de um canvas_item shader
Sprites 2D no Godot 4 usam shaders do tipo canvas_item. Crie um novo recurso Shader (ou escreva direto no editor de material) e comece com isto:
shader_type canvas_item;
void fragment() {
// COLOR ja vem preenchido com a cor do sprite (TEXTURE * MODULATE) neste ponto.
// Por enquanto so deixamos passar, sem alterar nada.
}
A funcao fragment() roda uma vez para cada pixel desenhado. A variavel COLOR e a saida: o que voce escrever nela e o que aparece na tela. Quando o fragment() comeca, COLOR ja contem a cor amostrada da textura do sprite, entao se voce nao mexer em nada o sprite aparece normal. Nosso trabalho e zerar o COLOR.a (o canal alpha, a transparencia) nos pixels que devem sumir.
Gerando o ruido com NoiseTexture2D
Precisamos de um valor pseudo-aleatorio por pixel. Da pra escrever uma funcao de ruido na mao, mas o Godot ja entrega um recurso pronto e configuravel: NoiseTexture2D. Voce gera uma vez, ele vira uma textura em escala de cinza, e o shader so le esse valor.
No shader, declare um uniform de textura para receber esse ruido:
shader_type canvas_item;
uniform sampler2D noise_texture;
uniform float dissolve_progress : hint_range(0.0, 1.0) = 0.0;
void fragment() {
float noise_value = texture(noise_texture, UV).r;
if (noise_value < dissolve_progress) {
COLOR.a = 0.0;
}
}
Repare em tres coisas. Primeiro, texture(noise_texture, UV).r le o canal vermelho do ruido na coordenada UV do pixel atual. Como o ruido e cinza, .r ja basta (vermelho, verde e azul tem o mesmo valor). Segundo, dissolve_progress e um uniform com hint_range, o que faz ele aparecer como um slider de 0 a 1 no inspetor, util para testar na mao. Terceiro, a regra: se o ruido naquele pixel for menor que o progresso, o pixel some. Conforme dissolve_progress sobe, mais pixels passam a satisfazer a condicao e o sprite se desfaz.
Para criar o ruido pelo editor: no slot noise_texture do material, escolha "Novo NoiseTexture2D", e dentro dele crie um "Novo FastNoiseLite". Ajuste a frequencia do FastNoiseLite para mudar o tamanho dos "graos" da dissolucao. Frequencia alta da pedacinhos pequenos, frequencia baixa da manchas grandes.
A borda emissiva que da o acabamento
So sumir e meio sem graca. O que vende o efeito e uma linha brilhante na fronteira entre o que sumiu e o que ficou, como brasa. A logica: alem de apagar pixels abaixo do progresso, voce destaca os pixels que estao numa faixa estreita logo acima do progresso.
shader_type canvas_item;
uniform sampler2D noise_texture;
uniform float dissolve_progress : hint_range(0.0, 1.0) = 0.0;
uniform float edge_width : hint_range(0.0, 0.2) = 0.05;
uniform vec4 edge_color : source_color = vec4(1.0, 0.55, 0.1, 1.0);
void fragment() {
float noise_value = texture(noise_texture, UV).r;
// Pixel abaixo do progresso: ja sumiu.
if (noise_value < dissolve_progress) {
COLOR.a = 0.0;
} else if (noise_value < dissolve_progress + edge_width) {
// Pixel na faixa de borda: pinta com a cor da brasa.
// Mantem o alpha original do sprite para nao vazar fora da silhueta.
COLOR.rgb = edge_color.rgb;
}
}
A hint source_color no uniform vec4 faz o Godot mostrar um seletor de cor de verdade no inspetor e tratar o valor no espaco de cor correto. O edge_width define a espessura da brasa: valores pequenos dao uma linha fina, valores maiores dao uma faixa larga e mais suave de transicao.
Um detalhe que confunde iniciante: por que checar noise_value < dissolve_progress + edge_width no else if? Porque o primeiro if ja capturou tudo que e menor que dissolve_progress. Quem chega no else if ja e maior ou igual ao progresso. Entao a unica coisa que falta testar e o limite de cima da faixa. Os pixels acima dessa faixa nao entram em nenhum ramo e ficam com a cor original do sprite.
Suavizando a borda com smoothstep
O corte duro do COLOR.a = 0.0 deixa serrilhado nas beiradas. Para amaciar, troque o if de apagar por uma transicao gradual no alpha usando smoothstep, que devolve um valor entre 0 e 1 de forma suave dentro de um intervalo.
shader_type canvas_item;
uniform sampler2D noise_texture;
uniform float dissolve_progress : hint_range(0.0, 1.0) = 0.0;
uniform float edge_width : hint_range(0.0, 0.2) = 0.05;
uniform vec4 edge_color : source_color = vec4(1.0, 0.55, 0.1, 1.0);
void fragment() {
float noise_value = texture(noise_texture, UV).r;
// Alpha vai de 0 (sumiu) a 1 (visivel) numa faixa estreita de transicao.
float alpha = smoothstep(dissolve_progress, dissolve_progress + 0.02, noise_value);
COLOR.a *= alpha;
// Pinta a borda na faixa logo acima do progresso.
if (noise_value > dissolve_progress && noise_value < dissolve_progress + edge_width) {
COLOR.rgb = edge_color.rgb;
}
}
Multiplicar COLOR.a *= alpha em vez de atribuir COLOR.a = alpha preserva a transparencia original do sprite. Se a arte tinha cantos arredondados ou areas semitransparentes, eles continuam respeitados, porque voce esta reduzindo o alpha existente, nao sobrescrevendo.
Animando o progresso com Tween no material
Ate aqui voce mexe o slider na mao. Em jogo, voce quer disparar a dissolucao por codigo, por exemplo quando o inimigo morre. O dissolve_progress e um parametro do ShaderMaterial, e da pra anima-lo com um Tween, que e a forma recomendada de animacao por codigo no Godot 4.
A chave e o nome do parametro. No GDScript voce acessa um uniform do shader pelo caminho shader_parameter/nome_do_uniform. Entao o dissolve_progress vira "shader_parameter/dissolve_progress".
extends Sprite2D
@onready var _material: ShaderMaterial = material as ShaderMaterial
func dissolve() -> void:
var tween: Tween = create_tween()
tween.tween_property(
_material,
"shader_parameter/dissolve_progress",
1.0,
0.8
).from(0.0)
# Quando a animacao terminar, remove o no da cena.
tween.tween_callback(queue_free)
O create_tween() cria um tween ligado ao no, que se limpa sozinho ao terminar. O tween_property anima o parametro de 0.0 (definido pelo .from) ate 1.0 ao longo de 0.8 segundos. O tween_callback(queue_free) so roda depois que a animacao acabou, garantindo que o sprite some por completo antes de o no sair da cena. Se voce chamar queue_free() direto, o no desaparece na hora e o efeito nem aparece.
Um cuidado importante com ShaderMaterial: por padrao varios sprites que usam o mesmo .tres de material compartilham a mesma instancia. Se voce animar o progresso de um, anima de todos. Quando cada inimigo precisa dissolver no seu proprio tempo, duplique o material por instancia:
extends Sprite2D
func _ready() -> void:
# Garante um material proprio para este no, sem afetar os outros.
if material:
material = material.duplicate()
func die() -> void:
var mat := material as ShaderMaterial
var tween := create_tween()
tween.tween_property(mat, "shader_parameter/dissolve_progress", 1.0, 0.8).from(0.0)
tween.tween_callback(queue_free)
Disparando a dissolucao a partir de um sinal
Na pratica voce nao chama die() na sorte. Normalmente um sistema de vida emite um sinal quando a barra zera, e o sprite reage. Conectar por sinal mantem as responsabilidades separadas: o componente de vida nao precisa saber que existe shader, e o sprite nao precisa saber as regras de dano. Se voce ja trabalhou com knockback e invencibilidade, esse padrao de sinais vai parecer familiar.
extends Sprite2D
signal dissolved
func _ready() -> void:
if material:
material = material.duplicate()
# Supondo um no de vida filho que emite "died" ao zerar a barra.
var health := $Health
health.died.connect(_on_died)
func _on_died() -> void:
var mat := material as ShaderMaterial
var tween := create_tween()
tween.tween_property(mat, "shader_parameter/dissolve_progress", 1.0, 0.8).from(0.0)
tween.tween_callback(func() -> void:
dissolved.emit()
queue_free()
)
Aqui o health.died.connect(_on_died) liga o sinal do filho ao metodo local. Quando a vida zera, _on_died roda, anima a dissolucao e, so no fim, emite o proprio sinal dissolved (caso um spawner ou contador de inimigos queira reagir) antes de liberar o no. Usar uma lambda no tween_callback permite encadear as duas acoes no momento certo, sem criar outro metodo so para isso.
Dissolucao reversa para aparicoes
O mesmo shader serve para o efeito contrario: fazer algo surgir. Basta animar o progresso de 1.0 para 0.0. Util para teleporte, materializacao de inimigo ou uma porta secreta aparecendo.
func appear() -> void:
var mat := material as ShaderMaterial
var tween := create_tween()
tween.tween_property(mat, "shader_parameter/dissolve_progress", 0.0, 0.6).from(1.0)
Como o shader nao tem nocao de "sumir" ou "aparecer", apenas de progresso, voce ganha os dois efeitos com zero linha extra de GDShader. Vale lembrar de comecar o no com dissolve_progress = 1.0 no inspetor, senao ele aparece visivel por um frame antes da animacao iniciar.
Ajustes finos que mudam o resultado
Alguns parametros costumam fazer mais diferenca do que parece. A frequencia do FastNoiseLite controla a granularidade: experimente valores entre 0.01 e 0.1 e veja a diferenca no tamanho dos pedacos. O tipo de ruido tambem conta. Perlin da uma dissolucao mais suave e nebulosa, enquanto Cellular cria fronteiras que lembram celulas ou rachaduras, otimo para efeito de cristal quebrando.
A duracao do Tween define o tom: 0.3 segundos passa urgencia (morte rapida), 1.2 segundos da um sumico lento e dramatico. E a edge_color muda completamente a leitura: laranja parece fogo, ciano parece energia, branco parece luz. Se quiser que a borda brilhe de verdade num projeto com glow, ative o glow no WorldEnvironment e use valores de cor acima de 1.0 no edge_color, porque cores acima do branco "estouram" e ativam o bloom.
Esse mesmo raciocinio de usar ruido e UV para controlar pixels e a base de muitos outros efeitos. Se gostou de manipular textura no fragment, o tutorial de shader de agua 2D usa ideias proximas, com distorcao de UV no tempo em vez de recorte por progresso.
Fechando
A dissolucao parece sofisticada, mas se apoia em uma so decisao por pixel: comparar um valor de ruido com um progresso e escolher entre apagar, pintar de borda ou manter. A partir dai, tudo e ajuste. Voce viu como montar o canvas_item shader, ler o NoiseTexture2D por um uniform, criar a borda emissiva com source_color, suavizar com smoothstep e animar o progresso com Tween no material, inclusive duplicando o material para cada no agir sozinho. Pegue o shader, jogue num sprite de teste, mova o slider e brinque com a frequencia do ruido. Quando o efeito estiver do jeito que voce quer parado, e so trocar o slider pela animacao por sinal e ele vai funcionar em qualquer inimigo do seu jogo.


