Anime em duas faixas
Uma cor iluminada e uma cor de sombra tingida, separadas por uma linha nítida.
- 01ObjetivoEstilizar
- 02TécnicaToon / cel shading
- 03ExecuçãoAnime em duas faixas
Parâmetros
Copie para a sua plataforma
- GLSLCopiar
- GodotCopiar
Cadeia de nós
Montada passo a passo, no estilo do GLSL, com os nomes de nós dele.
Montada passo a passo, no estilo do Godot, com os nomes de nós dele.
Este card ainda não tem versão para Unreal Engine 5, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Unity, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Blender, então os nós mostram os nomes do GLSL.
dotDotProductdotdotdot Normal01 Quanto encara a luz
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada02 Duas faixas
*Multiply*** Entrada03 Cor da sombra
mixMixmixmixmix Entrada04 Cor de cada faixa
Como funciona
O card ilumina uma esfera com um sol e depois não deixa a luz se desfazer num degradê. Cada pixel está iluminado ou na sombra, e a única coisa que sobra da transição é onde cai a linha entre os dois.
1. Quanto a superfície encara a luz. O produto escalarMultiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos). Produto escalar Multiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos). Normal Um vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções.dot(a, b) = |a| |b| cos θ
2. Duas faixas. Um smoothstepUm limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas. Smoothstep Um limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas.t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)threshold - softness ele devolve 0, acima de threshold + softness devolve 1, e só a janela fina entre
os dois recebe uma mistura. Uma janela de zero seria um step seco, e a borda ficaria serrilhada nos pixels; alguns
centésimos a deixam limpa sem parecer macia. Subir o limiar acima de 0 puxa a sombra em direção à luz, encolhendo o
lado iluminado para uma área menor voltada para ela e entregando mais da forma à sombra, num visual mais dramático.
3. Cor da sombra. A sombra não é preta nem uma cor base mais escura. É a cor base multiplicada por um tom, um violeta frio por padrão. A multiplicação mantém parte do matiz da cor base na sombra, então um objeto laranja ganha uma sombra rosada e fosca em vez de marrom. É assim que animadores de cel pintam sombras, e é boa parte do que faz o resultado parecer anime em vez de uma luz quebrada.
4. A cor de cada faixa. Um mix entre a cor da sombra e a cor base, guiado pelo valor da faixa, dá a cor final: a
base onde a faixa vale 1, a sombra onde vale 0.
Por que o sol fica dentro do shader
A direção da luz é uma constante, então as faixas não se mexem quando a câmera gira, só quando o objeto vira. Isso deixa
o card portátil: o mesmo código roda como um material sem iluminação em qualquer engine. O preço é que as luzes da cena
não o afetam. No Godot, os mesmos quatro passos podem ir para uma função light(), onde a direção e a cor vêm de cada
luz real; o guia do Godot explica como.
Para parecer intencional
Sombras desenhadas à mão são planejadas, e a matemática da iluminação não sabe disso. Estúdios que lançam jogos nesse estilo corrigem o sombreado por vértice ou com um mapa de limiar pintado, para que a sombra num rosto caia onde o artista quer. O controle de limiar do card é a forma mais simples desse controle.
Monte no GLSL
O botão de copiar entrega um shader de fragmento GLSL ES 3.00 completo. Ele espera a normal em espaço de mundo em v_normal e os quatro parâmetros como uniforms, com as duas cores enviadas já decodificadas para linear; ele mesmo codifica a saída em sRGB.
Desenhe a malha com o shader no estágio de fragmento. O estágio de vértice precisa passar a normal em espaço de mundo
em v_normal; as outras varyings que o shader declara (v_uv, v_position, v_view) ainda precisam ser escritas, mas
este card nunca as lê, então qualquer valor serve, desde que v_view não seja zero (main() o normaliza).
Preencha p_base e p_shadowTint a partir de um seletor de cor, convertidos de sRGB para linear antes, e p_threshold
e p_softness como floats comuns. O shader converte o resultado de volta para sRGB no fim de main(), então desenhe
direto no framebuffer padrão, sem outra conversão.
Para iluminar com o sol da sua cena em vez do sol fixo, acrescente um uniform vec3 u_sunDirection (normalizado,
apontando para o sol, em espaço de mundo) e use-o no lugar da constante na linha de lambert.
Monte no Godot
O shader é unshaded, então as luzes e sombras da cena não o afetam, exatamente como na prévia. O StandardMaterial3D também tem um modo toon nativo (Diffuse Mode em Toon), que corta em faixas a iluminação do próprio Godot mas não dá controle sobre a cor da sombra.
Crie um MeshInstance3D, dê a ele um novo ShaderMaterial e cole o shader num novo Shader dentro dele. Os quatro parâmetros aparecem no inspetor em Shader Parameters, com as cores como seletores de cor.
O sol fixo dentro de effect() deixa o material igual em qualquer cena. Para as faixas seguirem as luzes do Godot,
tire unshaded do render mode, escreva a cor base em ALBEDO e acrescente uma função light(): ali,
dot(NORMAL, LIGHT) é o termo de Lambert de cada luz, e você soma em DIFFUSE_LIGHT a cor da faixa, o tom da sombra
abaixo do limiar e branco acima dele, vezes LIGHT_COLOR * ATTENUATION. O Godot chama light() uma vez por luz, então
cada uma desenha as próprias faixas.
No VisualShader, a mesma cadeia é a Input normal levada ao espaço de mundo com um TransformVecMult (Operator em A x B (3x3), para não somar a posição da câmera) pela Input inv_view_matrix, um DotProduct com um Vec3Constant, um SmoothStep, um Multiply para a cor da sombra e um Mix, ligados em Albedo, com a flag Unshaded ligada em Flags.
Ainda não há tutorial de Unreal Engine 5 para este card.
Ainda não há tutorial de Unity para este card.
Ainda não há tutorial de Blender para este card.
Código
#version 300 es// Toon / cel shading → Two-band anime, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform vec3 p_base; // Base color: vec3(0.96, 0.62, 0.42), sRGB; upload it decoded to linearuniform vec3 p_shadowTint; // Shadow tint: vec3(0.62, 0.52, 0.78), sRGB; upload it decoded to linearuniform float p_threshold; // Shadow line: 0.1 (-0.6 to 0.8)uniform float p_softness; // Edge softness: 0.02 (0.005 to 0.3)in vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// A fixed sun, the same for every pixel, so the bands stay put while the camera orbits.vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(p_threshold - p_softness, p_threshold + p_softness, lambert); // Anime shadows are a tint of the base color, not a darker gray of it, so the shadow side keeps its hue vec3 shadow = p_base * p_shadowTint; vec3 color = mix(shadow, p_base, lit); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Toon / cel shading → Two-band anime, Godot 4. From Creative Center, MIT license.shader_type spatial;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform vec3 base : source_color = vec3(0.96, 0.62, 0.42);uniform vec3 shadowTint : source_color = vec3(0.62, 0.52, 0.78);uniform float threshold : hint_range(-0.6, 0.8, 0.01) = 0.1;uniform float softness : hint_range(0.005, 0.3, 0.005) = 0.02;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// A fixed sun, the same for every pixel, so the bands stay put while the camera orbits.vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(threshold - softness, threshold + softness, lambert); // Anime shadows are a tint of the base color, not a darker gray of it, so the shadow side keeps its hue vec3 shadow = base * shadowTint; vec3 color = mix(shadow, base, lit); return vec4(color, 1.0);}void fragment() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // VERTEX, NORMAL and VIEW are in view space here; the effect works in world space i.position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; i.normal = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); i.view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); ALBEDO = frag_out.rgb;}Ainda não há código de Unreal Engine 5 para este card.
Ainda não há código de Unity para este card.
Ainda não há código de Blender para este card.