Voltar para o Blog
Quest Log

Cel Shading na Godot 4: Visual Toon com Shader Próprio

Modelo 3D de personagem com iluminação em bandas de cor no estilo anime dentro do editor da Godot 4

Cel shading na Godot 4: monte um spatial shader com função light() que quantiza a luz em bandas, adiciona rim light e fecha com contorno toon.

Se você já tentou dar cara de anime ou cartoon a um jogo 3D, provavelmente esbarrou no mesmo problema: a iluminação padrão da engine deixa tudo com aquele degradê suave e realista, e o resultado não bate com a arte estilizada que você imaginou. A solução é o cel shading, e fazer cel shading na Godot 4 com um shader próprio é mais simples do que parece. Neste post você vai escrever um spatial shader que quantiza a luz em degraus, entender a função light(), controlar o número de bandas, somar um rim light e fechar o visual com um contorno. Público iniciante e intermediário, código real e comentado.

O que muda entre realista e toon

Vale entender o que está acontecendo por baixo antes de sair copiando código. No sombreamento realista, a quantidade de luz que uma superfície recebe depende do ângulo entre a normal da superfície e a direção da luz. Esse cálculo, chamado difusa Lambertiana, produz um valor contínuo: uma face virada de frente para a luz recebe quase 100 por cento, e conforme ela gira para longe, o valor cai suavemente até zero. Esse gradiente contínuo é o que dá o aspecto fotográfico.

O cel shading não muda esse cálculo. Ele muda o que a gente faz com o resultado. Em vez de usar o valor contínuo direto, a gente pega esse número entre 0 e 1 e força ele a cair em poucos degraus fixos. Um pedaço da superfície fica na banda "iluminado", outro pedaço na banda "sombra", e a transição entre elas é uma linha dura em vez de um degradê. É essa quebra em blocos chapados que o olho lê como desenho animado.

Se essa é sua primeira vez mexendo com código de shader, dá uma passada antes no guia de shaders na Godot para iniciantes, que cobre o básico de shader_type, uniforms e a estrutura das funções. Aqui eu assumo que você já sabe criar um recurso .gdshader e aplicar num material.

Cel shading na Godot 4 do zero

Na Godot 4, um material 3D usa shader_type spatial. O truque para o toon é que a gente não vai deixar a engine calcular a iluminação sozinha. A Godot permite escrever uma função light(), que roda uma vez para cada luz que atinge o objeto e te dá controle total sobre como aquela luz vira cor na tela. É dentro dela que a mágica do banding acontece.

Antes de escrever a light(), definimos a cor base do objeto na função fragment(), que roda por pixel. Comece com este esqueleto.

shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_toon, specular_toon;

// Cor base e textura do material
uniform vec4 albedo_color : source_color = vec4(0.85, 0.35, 0.35, 1.0);
uniform sampler2D albedo_texture : source_color, hint_default_white;

void fragment() {
    ALBEDO = albedo_color.rgb * texture(albedo_texture, UV).rgb;
}

O hint_default_white faz o shader funcionar mesmo sem uma textura atribuída: ele assume branco e a cor final vira só o albedo_color. Isso deixa o material testável na hora, sem precisar arrastar imagem nenhuma.

Quantizando a luz em bandas

Agora a parte principal. A função light() te entrega algumas variáveis prontas. As que interessam aqui são NORMAL (a normal da superfície), LIGHT (a direção até a luz), LIGHT_COLOR (a cor da luz, já com a energia embutida), ATTENUATION (quanto a luz chega naquele ponto, considerando sombras e distância) e ALBEDO (a cor que você definiu no fragment). O que a gente escreve em DIFFUSE_LIGHT é o que aparece na tela.

// Numero de degraus de luz (2 = cartoon duro, 4+ = mais suave)
uniform int bands : hint_range(1, 8) = 3;

void light() {
    // 1. Difusa Lambert: o quanto a superficie encara a luz
    float n_dot_l = dot(NORMAL, LIGHT);
    float diffuse = clamp(n_dot_l, 0.0, 1.0) * ATTENUATION;

    // 2. Quantiza em degraus (o coracao do cel shading)
    float steps = float(bands);
    float toon = floor(diffuse * steps) / steps;

    // 3. Escreve a luz final
    DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR * toon;
}

A linha que faz tudo acontecer é a do floor. Suponha que diffuse valha 0.62 e bands seja 3. A conta fica floor(0.62 * 3) / 3, ou seja floor(1.86) / 3, que dá 1 / 3, aproximadamente 0.33. Qualquer valor de difusa entre 0.33 e 0.66 vira o mesmo 0.33. É isso que agrupa a superfície em blocos: faixas inteiras de ângulos diferentes recebem exatamente a mesma cor, e a transição entre elas vira uma linha dura.

Troque o valor de bands no inspetor e observe. Com 2, você tem só claro e escuro, o visual mais cartoon possível. Com 4 ou 5, o degrau fica mais fino e o resultado se aproxima do sombreamento de anime, com uma ou duas sombras intermediárias.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Suavizando a borda dos degraus

Se você olhar de perto, a linha entre uma banda e outra fica serrilhada, com aquele efeito de escada nos pixels. Isso acontece porque o floor é um corte seco. Dá para pedir à GPU que suavize só aquela beirada usando fwidth, que mede o quanto o valor muda de um pixel para o vizinho. Com ele, a gente aplica um smoothstep estreito exatamente na transição.

void light() {
    float n_dot_l = dot(NORMAL, LIGHT);
    float diffuse = clamp(n_dot_l, 0.0, 1.0) * ATTENUATION;

    float steps = float(bands);
    float scaled = diffuse * steps;

    // largura de um pixel na transicao, para antialiasing
    float aa = fwidth(scaled) * 0.5;
    float lower = floor(scaled);
    float toon = (lower + smoothstep(0.5 - aa, 0.5 + aa, fract(scaled))) / steps;

    DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR * toon;
}

Aqui a gente separa a parte inteira (floor) da parte fracionária (fract) e usa o smoothstep só nos poucos pixels bem em cima da fronteira do degrau. O resultado mantém o visual chapado de bandas, mas com a linha de sombra limpa em vez de serrilhada. Se preferir o corte cru e nítido, fique com a versão anterior do floor. As duas são cel shading válido, é decisão de estilo.

Adicionando o rim light

O rim light, ou luz de borda, é aquele brilho fino que contorna a silhueta do objeto do lado oposto à câmera. Ele ajuda a destacar o personagem do fundo e reforça a leitura de anime. A conta é baseada no ângulo entre a normal e a direção da visão (VIEW): quanto mais a superfície aponta para longe da câmera, mais forte o brilho.

uniform vec4 rim_color : source_color = vec4(1.0);
uniform float rim_amount : hint_range(0.0, 1.0) = 0.6;
uniform float rim_width : hint_range(0.0, 1.0) = 0.7;

void light() {
    float n_dot_l = dot(NORMAL, LIGHT);
    float diffuse = clamp(n_dot_l, 0.0, 1.0) * ATTENUATION;

    float steps = float(bands);
    float toon = floor(diffuse * steps) / steps;
    DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR * toon;

    // Rim: mais forte nas bordas viradas pra fora da camera
    float rim = 1.0 - clamp(dot(NORMAL, VIEW), 0.0, 1.0);
    rim = smoothstep(rim_width - 0.05, rim_width + 0.05, rim);

    // so aparece no lado que recebe luz, e tambem em degrau
    rim *= step(0.001, diffuse) * rim_amount;
    DIFFUSE_LIGHT += rim_color.rgb * rim * LIGHT_COLOR;
}

O 1.0 - dot(NORMAL, VIEW) vale quase zero quando a superfície encara a câmera de frente e chega perto de um nas bordas. O smoothstep com o rim_width transforma isso numa faixa nítida, de novo no espírito toon. Multiplicar por step(0.001, diffuse) garante que o rim só apareça no lado iluminado do objeto, senão a silhueta inteira acenderia, inclusive na sombra, e o efeito perderia a graça. Ajuste rim_width para engrossar ou afinar a linha e rim_amount para a intensidade.

Se quiser ir além, vale conhecer também como as luzes de cena funcionam por dentro para posicionar melhor a fonte principal e a de recorte. O guia de iluminação 3D na Godot explica os tipos de luz e como energia e atenuação entram nessa mesma função light().

Fechando com o contorno (outline)

O shader que a gente montou cuida do preenchimento: as cores chapadas em bandas e o rim. O que ainda falta para o visual bater de vez com desenho animado é a linha preta ao redor da silhueta. Esse contorno é um efeito separado e não vem da função light().

A técnica mais usada em 3D é o casco invertido, ou inverted hull. Você duplica a malha, empurra os vértices dela um pouco para fora ao longo das normais, pinta de preto e renderiza só as faces de trás com cull_front. O resultado é uma casca preta que aparece exatamente na borda do modelo, sem cobrir a parte da frente. Como esse contorno tem suas próprias particularidades e vários ajustes, cobri o passo a passo dele com código completo no post sobre shader de contorno na Godot. Combine os dois materiais e o efeito toon fica completo: bandas por dentro, linha por fora.

Uma alternativa mais barata, quando você não quer duplicar a malha, é usar o efeito de pós-processamento com detecção de borda por profundidade e normal. Ele desenha o contorno lendo a tela inteira, o que pega também linhas internas do modelo, não só a silhueta. É mais elaborado e fica para outra hora, mas saiba que existe.

Ligando o toon a um material de verdade

Para testar rápido, crie um MeshInstance3D com uma malha qualquer (uma cápsula ou uma esfera já servem), atribua um ShaderMaterial, cole o shader completo e adicione uma DirectionalLight3D na cena. Gire a luz e você vê as bandas deslizando pela superfície em blocos duros. Se quiser trocar propriedades do shader por código, lembre de usar GDScript tipado e set_shader_parameter.

extends MeshInstance3D

@export var bandas: int = 3
@export var cor_rim: Color = Color.WHITE

func _ready() -> void:
    var material: ShaderMaterial = get_active_material(0) as ShaderMaterial
    if material == null:
        return
    material.set_shader_parameter("bands", bandas)
    material.set_shader_parameter("rim_color", cor_rim)

Repare no as ShaderMaterial e na checagem de null: se o material do índice 0 não for um ShaderMaterial, o cast devolve null e a gente sai da função sem erro. Tipar as variáveis não é firula, é o que deixa o editor te avisar de nome de parâmetro errado antes de rodar.

Resumo prático

O cel shading não reinventa a iluminação, ele reinterpreta o resultado dela. Você calcula a difusa Lambert normalmente, quantiza esse valor em poucos degraus com floor dentro da função light(), e o gradiente contínuo vira blocos chapados de cor. A partir daí, o número de bandas define o quão duro ou suave o visual fica, o rim light destaca a silhueta, o antialiasing com fwidth limpa a borda dos degraus e o contorno por casco invertido dá o acabamento de desenho.

Comece com a versão do floor, ajuste bands e as cores da luz até achar o tom que combina com sua arte, e só depois some o rim e o outline. Cada peça é independente, então dá para ir empilhando efeito por efeito e ver o personagem ganhar cara de anime a cada passo.

Perguntas frequentes

O que diferencia cel shading do sombreamento realista?

No sombreamento padrão (PBR) a luz varia de forma contínua sobre a superfície, criando um gradiente suave do claro ao escuro. No cel shading a gente pega esse mesmo cálculo de luz e quantiza o resultado em poucos degraus fixos, então em vez de um degradê você vê blocos chapados de cor, igual arte de desenho animado ou anime.

Para que serve a função light() no shader da Godot 4?

A função light() substitui o modelo de iluminação padrão da Godot e roda uma vez por luz que atinge o objeto. É nela que você tem acesso a NORMAL, LIGHT, LIGHT_COLOR e ATTENUATION e decide como escrever em DIFFUSE_LIGHT e SPECULAR_LIGHT. É exatamente o lugar certo para quantizar a luz em bandas e montar o efeito toon.

Como controlar quantas bandas de luz o cel shading tem?

Você multiplica o valor da difusa (entre 0 e 1) pelo número de bandas, aplica floor para arredondar para baixo e divide pelo mesmo número. Com 2 bandas o resultado é bem duro, estilo cartoon; com 4 ou 5 fica mais suave, mais perto do anime. Basta expor esse número como um uniform int e ajustar no inspetor.

Preciso de um contorno junto com o cel shading?

Não é obrigatório, mas o contorno preto (outline) é o que faz o objeto parecer desenhado de verdade. O shader de banda cuida do preenchimento das cores; o contorno cuida da silhueta. Os dois são efeitos separados que se somam, e você aplica a outline por cima, geralmente com a técnica de casco invertido em 3D.

Cel shading funciona com as luzes normais da cena na Godot?

Sim. Como o efeito é escrito dentro da função light(), ele reage a DirectionalLight3D, OmniLight3D e SpotLight3D normalmente, respeitando cor, energia e atenuação de cada uma. Você não precisa de uma iluminação especial, só de um material com o seu shader toon aplicado.