Pular para o conteúdo
Estilizar · Contorno de spriteIniciante

Contorno de pixel

Uma borda de um pixel em volta de um sprite de pixel art, encontrada a partir do próprio alfa dele.

UVTextura2D / sprite
  1. 01ObjetivoEstilizar
  2. 02TécnicaContorno de sprite
  3. 03ExecuçãoContorno de pixel
Sprite de exemplo

Parâmetros

#ffcc33

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.

  1. textureTexture2DParametertexturetexturetexture
    UV
    Textura

    01 O sprite

  2. maxMaxmaxmaxmax

    02 Algum vizinho sólido?

  3. *Multiply***

    03 Só fora do sprite

  4. mixMixmixmixmix

    04 Cor da linha ali

Como funciona

O sprite é uma pequena gosma com uma folha na cabeça e pixels transparentes em volta. O shader desenha uma linha nesses pixels transparentes, mas só nos que encostam na gosma, então a linha abraça o formato dela com um pixel do sprite de largura.

1. O sprite. O shader lê o sprite no próprio pixel. Onde está a gosma, own.a vale 1; em volta, 0.

2. Algum vizinho sólido? Ele lê o alfa um texel à esquerda, à direita, abaixo e acima, e fica com o maior usando max. O deslocamento de um texel é 1.0 / textureSize(t_sprite, 0), então o shader funciona com qualquer tamanho de sprite sem precisar de parâmetro. A prévia do passo mostra uma gosma um pixel mais gorda em toda direção: tudo o que é sólido ou encosta em algo sólido.

3. Só fora do sprite. Multiplicar por 1.0 - own.a tira o próprio sprite disso, e sobra o anel de pixels transparentes que fazem divisa com ele.

4. A cor da linha ali. Um mix troca esses pixels pela cor da linha, com alfa cheio, e deixa o resto do sprite como está. O blending de alfa do card então põe o sprite contornado sobre o fundo.

Por que quatro vizinhos

Testar só os vizinhos da esquerda, da direita, de cima e de baixo deixa de fora o pixel na diagonal de cada canto externo, então a linha corta os cantos de leve, como pixel artists costumam desenhar, e custa cinco leituras. Somar as quatro diagonais preenche esses cantos, numa linha mais pesada e quadrada, por nove leituras. As duas têm um pixel de espessura; um contorno de pixel mais grosso precisa de vizinhos mais distantes, e de mais leituras de novo.

A armadilha do espaço em volta

A linha vai para fora da arte, em pixels que o sprite já precisa ter. Um sprite recortado rente à arte não tem espaço, e o contorno é cortado na borda do quad: o shader só roda nos pixels do quad. Deixe pelo menos um pixel transparente em volta da arte (esta gosma tem pelo menos dois), mais para uma linha mais grossa.

Numa sprite sheet (abre em uma nova aba) o mesmo problema tem um gêmeo: o vizinho do pixel da borda de um quadro é o primeiro pixel do quadro seguinte. Se os quadros se encostam, pixels sólidos do quadro seguinte vazam um contorno para dentro deste. Deixe espaço em volta de cada quadro, ou monte a folha com espaçamento entre eles.

Filtragem

A textura é lida com filtragem nearest, então cada leitura devolve o valor exato de um texel e o contorno fica nítido. Com filtragem linear, o alfa na borda da arte se mistura com os pixels transparentes em volta, e a linha borra numa faixa suave e meio transparente.

Monte no GLSL

O botão de copiar entrega um shader de fragmento para o quad de um sprite. Ligue o sprite em t_sprite com filtragem NEAREST e CLAMP_TO_EDGE, enviado como SRGB8_ALPHA8 para ser lido em linear, e desenhe com blending de alfa. A saída tem alfa direto (não pré-multiplicado).

Desenhe um quad com a proporção do sprite, com o shader de vértice passando coordenadas de 0 a 1 em v_uv, origem embaixo à esquerda. As outras varyings ainda precisam ser escritas; qualquer valor serve, desde que v_normal e v_view não sejam zero.

Envie o sprite como SRGB8_ALPHA8 para ele ser lido como cor linear, com filtros NEAREST e CLAMP_TO_EDGE, e ligue-o em t_sprite. O shader acha o tamanho do texel com textureSize, então não precisa de uniform de tamanho. Preencha p_lineColor decodificada para linear, ligue o blending e desenhe. A saída é codificada em sRGB no main().

Para uma sprite sheet, passe o retângulo do quadro para o shader e some-o a v_uv (ou calcule a UV de cada quadro no shader de vértice); os deslocamentos dos vizinhos continuam sendo um texel da folha inteira, então os quadros precisam de um espaço transparente entre eles.

Monte no Godot

Um shader canvas_item para um Sprite2D. Ele lê o sprite pelo próprio uniform t_sprite, que você preenche com a mesma textura do Sprite2D, porque o TEXTURE do Godot não pode ser lido dentro de uma função. Ele supõe o HDR 2D desligado (o padrão) e faz a própria decodificação e codificação sRGB.

Selecione o Sprite2D, crie um novo ShaderMaterial no espaço Material (seção CanvasItem) e cole o shader num novo Shader. Em Shader Parameters, arraste a mesma imagem que o Sprite2D mostra para t_sprite e escolha a cor da linha.

O próprio shader põe filtragem nearest em t_sprite, então o contorno fica nítido qualquer que seja o filtro de textura padrão do projeto. Deixe pelo menos um pixel transparente em volta da arte na imagem, ou a linha é cortada na borda do sprite. Para ligar o contorno só num sprite selecionado, acrescente um uniform bool e multiplique outline por ele, ou troque o material por script.

No VisualShader, use o modo CanvasItem: um Texture2DParameter alimentando cinco nós Texture2D (o pixel e os quatro vizinhos, deslocados pelo tamanho do texel vindo de um nó Input em texture_pixel_size), nós Max para os vizinhos, um OneMinus e um Multiply para o anel de fora e um Mix ligado em Color e Alpha.

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

pixel-outline.fragGLSL
#version 300 es// Sprite outline → Pixel outline, 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_lineColor; // Line color: vec3(1.0, 0.8, 0.2), sRGB; upload it decoded to linearuniform sampler2D t_sprite; // slime.png: sRGB color, uploaded as SRGB8_ALPHA8 so it samples linear; nearest filter, clamp to edgein 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));}// The four direct neighbours only, one texel away: the line is one sprite pixel thick and skips the pixel diagonal to a// corner, which softens corners the way pixel artists often do. Adding the four diagonals fills those corners in.vec4 effect(Inputs i) {	vec2 texel = 1.0 / vec2(textureSize(t_sprite, 0));	vec4 own = texture(t_sprite, i.uv);	float left = texture(t_sprite, i.uv - vec2(texel.x, 0.0)).a;	float right = texture(t_sprite, i.uv + vec2(texel.x, 0.0)).a;	float down = texture(t_sprite, i.uv - vec2(0.0, texel.y)).a;	float up = texture(t_sprite, i.uv + vec2(0.0, texel.y)).a;	float neighbors = max(max(left, right), max(down, up));	float outline = neighbors * (1.0 - own.a);	vec4 result = mix(own, vec4(p_lineColor, 1.0), outline);	return result;}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	fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), frag_out.a);}
pixel-outline.gdshaderShader do Godot
// Sprite outline → Pixel outline, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded, blend_mix;uniform vec3 lineColor : source_color = vec3(1.0, 0.8, 0.2);uniform sampler2D t_sprite : source_color, filter_nearest, repeat_disable; // the sprite: the Sprite2D's own texture again (TEXTURE can't be read inside a function)struct Inputs {	vec2 uv;	vec3 position;	vec3 normal;	vec3 view;	float time;	vec2 fragCoord;	vec2 resolution;};// Godot's 2D renderer works in sRGB (HDR 2D off, the default), so the frame decodes color params and sRGB// textures to linear itself and encodes the result. With HDR 2D on, make these return c unchanged.vec3 u_toLinear(vec3 c) {	return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec4 u_toLinear4(vec4 c) {	return vec4(u_toLinear(c.rgb), c.a);}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));}// Godot's textures start at the top-left and the effect's uv at the bottom-left: each sample flips v backvec2 u_flipUv(vec2 uv) {	return vec2(uv.x, 1.0 - uv.y);}// The four direct neighbours only, one texel away: the line is one sprite pixel thick and skips the pixel diagonal to a// corner, which softens corners the way pixel artists often do. Adding the four diagonals fills those corners in.vec4 effect(Inputs i) {	vec2 texel = 1.0 / vec2(textureSize(t_sprite, 0));	vec4 own = u_toLinear4(texture(t_sprite, u_flipUv(i.uv)));	float left = u_toLinear4(texture(t_sprite, u_flipUv(i.uv - vec2(texel.x, 0.0)))).a;	float right = u_toLinear4(texture(t_sprite, u_flipUv(i.uv + vec2(texel.x, 0.0)))).a;	float down = u_toLinear4(texture(t_sprite, u_flipUv(i.uv - vec2(0.0, texel.y)))).a;	float up = u_toLinear4(texture(t_sprite, u_flipUv(i.uv + vec2(0.0, texel.y)))).a;	float neighbors = max(max(left, right), max(down, up));	float outline = neighbors * (1.0 - own.a);	vec4 result = mix(own, vec4(u_toLinear(lineColor), 1.0), outline);	return result;}void fragment() {	Inputs i;	// UV starts at the top-left; the effect expects GLSL's bottom-left	i.uv = vec2(UV.x, 1.0 - UV.y);	// A flat quad facing the viewer, the same as the site's 2D preview	i.position = vec3(i.uv * 2.0 - 1.0, 0.0);	i.normal = vec3(0.0, 0.0, 1.0);	i.view = vec3(0.0, 0.0, 1.0);	i.time = TIME;	// FRAGCOORD's origin is the top-left too	i.resolution = 1.0 / SCREEN_PIXEL_SIZE;	i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y);	vec4 frag_out = effect(i);	COLOR = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), frag_out.a);}

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.

Leitura complementar