Shader CRT na Godot 4 para dar cara de TV retro no seu pixel art

Aprenda a criar um shader CRT Godot com scanlines, curvatura e vinheta para dar cara de TV antiga no seu jogo pixel art, com codigo pronto e ajustavel.
Um jogo pixel art ganha muito quando a imagem parece sair de uma tela antiga. As linhas horizontais, os cantos escurecidos e aquela leve curvatura de tubo lembram o console ligado na TV de tubo da sala. Neste tutorial voce vai montar um shader CRT Godot completo, com scanlines, vinheta e uma distorcao sutil de curvatura, tudo aplicado por cima do jogo inteiro e ajustavel no Inspector. Nada de biblioteca externa, so a linguagem de shader da propria Godot 4.
A ideia central e simples. Voce desenha o jogo normalmente e, por cima de tudo, coloca uma camada de tela cheia que le o que ja foi renderizado e reescreve cada pixel com o efeito de tela antiga. Na Godot 4 isso se faz com um CanvasLayer num layer alto, um ColorRect que ocupa a tela e um ShaderMaterial com o codigo do efeito.
Montando a camada de tela cheia
Primeiro a estrutura de nos. Crie um CanvasLayer como filho da sua cena principal (ou de uma cena de UI que sempre fica carregada). Dentro dele, um ColorRect. No Inspector do CanvasLayer, ajuste a propriedade layer para um valor alto, tipo 100, para garantir que ele fique acima de tudo. No ColorRect, configure o ancoramento para preencher a tela inteira (o preset "Full Rect" resolve).
O truque para o shader enxergar o jogo por baixo esta no hint_screen_texture. Esse sampler entrega o conteudo do framebuffer, ou seja, tudo que foi desenhado antes daquele ColorRect. Como o CanvasLayer esta num layer alto, o framebuffer ja contem o jogo completo quando o shader roda.
Agora o shader. Crie um novo ShaderMaterial no ColorRect, e dentro dele um novo Shader. Cole o codigo abaixo. Ele e um canvas_item porque atua na camada 2D de UI, mesmo que o jogo por baixo seja 3D.
shader_type canvas_item;
uniform sampler2D screen_tex : hint_screen_texture, repeat_disable, filter_linear;
uniform float scanline_intensity : hint_range(0.0, 1.0) = 0.25;
uniform float scanline_count : hint_range(100.0, 1200.0) = 480.0;
uniform float curvature : hint_range(0.0, 0.5) = 0.08;
uniform float vignette_strength : hint_range(0.0, 2.0) = 0.6;
uniform float brightness : hint_range(0.5, 2.0) = 1.1;
uniform float aberration : hint_range(0.0, 0.01) = 0.0015;
// Distorce a UV para simular a curvatura do tubo.
vec2 curve_uv(vec2 uv, float amount) {
uv = uv * 2.0 - 1.0;
vec2 offset = abs(uv.yx) / (1.0 / amount);
uv = uv + uv * offset * offset;
uv = uv * 0.5 + 0.5;
return uv;
}
void fragment() {
vec2 uv = curve_uv(SCREEN_UV, curvature);
// Fora da tela curva vira preto, evitando repetir a borda.
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
COLOR = vec4(0.0, 0.0, 0.0, 1.0);
} else {
// Leve aberracao cromatica separando os canais.
float r = texture(screen_tex, uv + vec2(aberration, 0.0)).r;
float g = texture(screen_tex, uv).g;
float b = texture(screen_tex, uv - vec2(aberration, 0.0)).b;
vec3 col = vec3(r, g, b);
// Scanlines: escurece linhas horizontais periodicas.
float line = sin(uv.y * scanline_count * 3.14159);
float scan = 1.0 - scanline_intensity * (0.5 + 0.5 * line) * (0.5 + 0.5 * line);
col *= scan;
// Vinheta: escurece as bordas.
vec2 vuv = uv * (1.0 - uv.yx);
float vig = vuv.x * vuv.y * 15.0;
vig = pow(vig, vignette_strength);
col *= vig;
col *= brightness;
COLOR = vec4(col, 1.0);
}
}
Alguns detalhes valem atencao. O repeat_disable no sampler impede que, ao distorcer a UV para fora do intervalo 0 a 1, a Godot repita a textura na borda. O filter_linear suaviza a amostragem quando a curvatura desloca os pixels. E o hint_range em cada uniform faz a Godot desenhar um controle deslizante no Inspector, entao voce afina o efeito sem tocar no codigo.
Entendendo cada parte do efeito
As scanlines saem de uma funcao seno aplicada sobre a coordenada vertical. Multiplicando uv.y por um numero grande (scanline_count), voce cria muitas oscilacoes ao longo da altura da tela. Cada oscilacao vira uma linha clara e uma escura. O scanline_intensity controla o quanto essas linhas escurecem a imagem. Elevar ao quadrado deixa a transicao mais parecida com a de um tubo real, com linhas mais definidas.
A curvatura empurra a UV para fora nas bordas, simulando o vidro curvo da tela antiga. A funcao curve_uv converte a coordenada para o intervalo de menos 1 a 1, aplica uma distorcao proporcional ao quadrado da distancia do centro, e volta ao intervalo 0 a 1. Quanto maior o curvature, mais barrigudo fica o efeito. Passar de 0.15 costuma ficar caricato demais.
A vinheta escurece os cantos. O truque uv * (1.0 - uv.yx) gera um valor alto no centro e baixo nas bordas, e o pow controla a queda. Isso da aquela sensacao de foco no meio da tela, como acontecia em monitores de tubo.
A aberracao cromatica separa levemente os canais vermelho e azul, amostrando a textura em posicoes ligeiramente diferentes. Mantenha esse valor minusculo. Um deslocamento grande vira borrao e cansa a vista rapido.
Ligando o shader por codigo com GDScript tipado
Voce nao precisa de codigo para o efeito rodar, ele funciona so com os nos configurados. Mas quase sempre vale controlar os parametros em tempo real, por exemplo diminuir a intensidade quando o jogador abre um menu ou reagir a eventos. Para isso, acesse o ShaderMaterial e altere os uniform via set_shader_parameter. Todo GDScript aqui vai tipado, do jeito que recomendamos sempre no CursoGame.Dev.
extends CanvasLayer
@onready var screen_rect: ColorRect = $ColorRect
var _material: ShaderMaterial
func _ready() -> void:
_material = screen_rect.material as ShaderMaterial
if _material == null:
push_warning("ColorRect sem ShaderMaterial configurado.")
return
set_crt_enabled(true)
func set_crt_enabled(enabled: bool) -> void:
visible = enabled
func set_scanline_intensity(value: float) -> void:
if _material == null:
return
_material.set_shader_parameter("scanline_intensity", clampf(value, 0.0, 1.0))
func set_curvature(value: float) -> void:
if _material == null:
return
_material.set_shader_parameter("curvature", clampf(value, 0.0, 0.5))
Repare no screen_rect.material as ShaderMaterial. O as faz a conversao segura, retornando null se o material nao for do tipo esperado, e por isso a checagem logo em seguida. O clampf garante que valores vindos de um slider de menu nunca estourem o intervalo esperado pelo shader. Manter tudo tipado assim evita erros silenciosos que so aparecem em runtime, um problema comum quando se compara com abordagens sem tipo. Se voce ainda esta decidindo a linguagem do projeto, vale ler nossa comparacao entre C sharp e GDScript na Godot para entender onde a tipagem estatica ajuda mais.
Ajustando para pixel art de baixa resolucao
Aqui mora a parte que muita gente erra. Se o seu jogo e pixel art de verdade, ele provavelmente renderiza numa resolucao baixa (algo como 320 por 180) e depois escala para a tela. Nesse caso, o CanvasLayer do CRT deve ficar por cima da imagem ja escalada, na resolucao final da janela, nao na resolucao interna do jogo.
Na pratica, configure o projeto com um SubViewport de baixa resolucao para o jogo, ou use o modo de esticamento viewport nas configuracoes de janela. O importante e que o hint_screen_texture capture a imagem depois do upscale, para que as scanlines fiquem finas e nitidas na tela grande. Se voce aplicar o CRT dentro do viewport de 320 por 180, as scanlines somem no upscale ou viram uma bagunca de moire.
Um valor de scanline_count proximo da altura real da janela costuma dar o resultado mais convincente. Para uma tela de 1080 pixels de altura, algo entre 400 e 540 linhas fica coerente com o espacamento de um monitor de tubo. Ajuste no slider ate parecer natural.
Nao exagere e respeite a acessibilidade
O efeito CRT e tempero, nao prato principal. Se as scanlines escurecerem demais, o jogador perde detalhe e a leitura do HUD sofre. Comece com scanline_intensity em 0.2, curvature em 0.08 e teste com o texto mais miudo da sua interface ligado. Se voce nao consegue ler confortavelmente, baixe os valores.
Curvatura forte tambem atrapalha jogos que dependem de precisao nas bordas, como plataformas onde a hitbox chega perto do limite da tela. E a aberracao cromatica, por mais charmosa que seja, pode dar dor de cabeca para quem tem sensibilidade visual. Por isso, ofereca sempre a opcao de desligar.
Uma implementacao minima de acessibilidade e uma caixa de selecao no menu de opcoes que chama set_crt_enabled(false). Salve essa preferencia num arquivo de config com ConfigFile para persistir entre sessoes. Alguns jogadores vao desligar, e tudo bem, o objetivo e que o efeito some por completo, sem residuo de vinheta ou distorcao.
Combinando com outros efeitos retro
O CRT conversa bem com outros shaders de tela ou de material. Se voce ja usa efeitos de dissolucao para transicoes, como no nosso guia de shader de dissolve na Godot, eles continuam funcionando por baixo do CRT, ja que o efeito de tela le o frame final. O mesmo vale para contornos de sprite. Um shader de outline para destacar personagens aplicado no material dos sprites aparece normalmente atraves da camada CRT, so ganhando as scanlines por cima.
Uma ordem que costuma funcionar bem: efeitos de material (dissolve, outline) rodam nos sprites, efeitos de post no viewport do jogo, e o CRT por ultimo, na camada de tela cheia acima de tudo. Assim cada camada faz seu trabalho sem competir com as outras.
Fechando
Com pouco codigo voce transforma a estetica do jogo. O CanvasLayer com ColorRect e ShaderMaterial da o alicerce, o shader de canvas_item com hint_screen_texture faz o trabalho pesado, e os uniform com hint_range deixam tudo ajustavel no Inspector sem recompilar nada. Comece sutil, teste com o HUD ligado, cuide da leitura e deixe o jogador desligar quando quiser.
O melhor jeito de sentir o efeito e brincar com os sliders no seu proprio jogo. Abra o Inspector, mexa na intensidade da scanline e na curvatura, e veja o pixel art ganhar aquela alma de tela antiga. A partir dai e so calibrar para o clima que voce quer.
Perguntas frequentes
O shader CRT deixa o jogo pixel art ilegivel?
So se voce exagerar nos parametros. Mantenha a intensidade de scanline abaixo de 0.3 e a curvatura sutil. O objetivo e sugerir uma tela antiga, nao esconder o jogo. Sempre teste com o texto do HUD ligado.
Preciso do SCREEN_TEXTURE ou dá para fazer sem?
Para um efeito de tela cheia que le o que ja foi desenhado, use hint_screen_texture. Isso captura tudo abaixo do ColorRect. Sem ele voce so consegue efeitos que nao dependem do frame renderizado.
Esse shader funciona em jogos 3D na Godot?
Sim. A abordagem de CanvasLayer com ColorRect e ShaderMaterial cobre a tela inteira, entao funciona por cima de 2D e 3D. O shader_type continua canvas_item porque atua na camada de UI.
Como deixo o jogador desligar o efeito CRT?
Coloque o CanvasLayer em uma variavel e alterne a propriedade visible por uma opcao no menu de acessibilidade. Guarde a escolha em um arquivo de config para persistir entre sessoes.
A curvatura de UV corta parte da imagem nas bordas?
Pode cortar se a distorcao empurrar a UV para fora do intervalo 0 a 1. Use repeat_disable no sampler e um clamp suave, ou reduza o fator de curvatura para manter tudo visivel.


