Botão de interface arredondado
Um botão em forma de pílula com borda interna, um ícone de seta (chevron) e uma sombra projetada suave, todos recortados de distâncias com sinal.
- 01ObjetivoForma
- 02TécnicaComposição de SDF 2D
- 03ExecuçãoBotão de interface arredondado
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.
lengthExpressionlengthlengthlength UV01 Distância da caixa arredondada
maxMaxmaxmaxmax Entrada02 Subtrair: borda interna
minMinminminmin Entrada03 União: seta
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada04 Sombra suave
mixMixmixmixmix Entrada05 Empilhar as cores
fwidthDerivativeFuncfwidthfwidthfwidth Entrada06 Borda sem serrilhado
Como funciona
Um botão é uma pequena pilha de formas: um corpo, uma borda, um ícone e uma sombra. Desenhar cada uma como imagem
significa quatro imagens que borram em outro tamanho. Desenhá-las como distâncias com sinalUma função que devolve a distância de um ponto até a borda de uma forma, negativa dentro e positiva fora. O zero é o próprio contorno. Campo de distância com sinal (SDF) Uma função que devolve a distância de um ponto até a borda de uma forma, negativa dentro e positiva fora. O zero é o próprio contorno.circle(p) = |p| − r
significa quatro fórmulas curtas, combinadas com os operadores da composição de SDF, e toda borda continua com
exatamente um pixel de suavidade em qualquer tamanho.
O corpo: uma caixa arredondada. As coordenadas são centralizadas e corrigidas pela proporção da tela, como nos cards
de Forma anteriores. sdRoundBox primeiro mede a distância até uma caixa encolhida pelo raio do canto e depois subtrai
o raio do resultado. Subtrair uma constante faz um campo de distância crescer para fora nessa medida, e fazer um canto
vivo crescer r o transforma num quarto de círculo de raio r; assim, a caixa volta ao tamanho cheio com os cantos
arredondados. O raio é limitado à menor das meias medidas, o que transforma um botão baixo e largo numa pílula em vez de
quebrar a forma.
A borda: uma subtração. body + borderWidth é a mesma caixa encolhida para dentro pela espessura da borda.
Subtrair uma forma de outra é max(a, -b), então max(body, -(body + borderWidth)) guarda a parte do corpo que não
está dentro da cópia menor: um contorno que acompanha o lado de dentro da borda, com os mesmos cantos arredondados.
O ícone: uma união. Cada braço da seta é a distância até um segmento de reta, que projeta o pixel sobre o segmento e
mede até o ponto mais próximo. O min() das duas distâncias é os dois segmentos ao mesmo tempo, e subtrair uma
espessura faz as linhas crescerem até virar cápsulas de pontas redondas. O tamanho do ícone acompanha a altura do botão,
então ele escala junto.
A sombra: a mesma distância, borrada. A fórmula do corpo é calculada mais uma vez num ponto deslocado para cima, o
que desloca a forma para baixo, e recortada com 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)
Empilhando as cores. Cada distância vira uma cobertura com a mesma função auxiliar, fill, e as coberturas
escolhem as cores com mix na ordem do pintor: a borda sobre a face, o ícone sobre as duas.
Uma borda de um pixel. fill não usa uma suavidade fixa. fwidth(d) é quanto a distância muda entre este pixel e
os vizinhos, então é o tamanho de um pixel nas unidades da própria distância. Um smoothstep de menos metade a mais
metade disso é uma borda sem serrilhado de um pixel, por mais que o botão seja escalado ou ampliado. (As derivadas têm o
próprio fundamento.)
Alfa. O botão é desenhado com alpha blending sobre o que estiver atrás dele. O alfa é a cobertura do corpo mais a sombra onde não há corpo; como a sombra é preta, ela soma opacidade mas não cor, e a cor é dividida pelo alfa porque a moldura espera alfa direto, não pré-multiplicado.
As mesmas distâncias dão mais visuais com uma linha cada: um brilho de hover é exp(-k * max(body, 0.0)), um estado
pressionado aproxima a sombra, e um anel de foco é abs(body - gap) - width.
Monte no GLSL
O código copiado é um shader de fragmento WebGL2 completo que desenha o botão, com a sombra, sobre o que já estiver no canvas.
- Desenhe um retângulo onde o botão vai ficar, maior que o botão no tamanho da sombra, com um shader de vértice que escreva
v_uvde 0 a 1 ao longo dele e constantes emv_position,v_normalev_view. - Defina
u_resolutioncom o tamanho, em pixels, da área que as coordenadas do shader cobrem. O código centraliza as coordenadas no retângulo e as corrige pela proporção deu_resolution, então, para um retângulo menor que o canvas, passe o tamanho do próprio retângulo em pixels. - Defina os cinco uniforms float (a meia largura e a meia altura estão em unidades de metade da altura do retângulo) e envie as três cores decodificadas de sRGB para linear.
- Ligue o blending com
gl.enable(gl.BLEND)egl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)e desenhe.
O shader só desenha; os cliques continuam sendo trabalho da página. Teste o ponteiro contra a mesma caixa arredondada em JavaScript (ou ponha um botão HTML transparente sobre o canvas) e devolva o estado de hover ou de pressionado como um uniform.
Monte no Godot
O shader exportado supõe o HDR 2D desligado (o padrão). Com ele ligado, faça u_toLinear e u_toSrgb devolverem a entrada sem mudança. Para um botão de menu comum, o StyleBoxFlat do próprio Godot (raio de canto, borda, sombra) faz o mesmo sem shader; use este quando a forma precisar se animar ou se transformar.
O código copiado é um shader canvas_item completo. Os canvas items do Godot misturam com alfa por padrão, então a
sombra cai sobre o que estiver atrás do nó.
- Adicione um
ColorRect(ou qualquerControl) a uma cena de interface. Para o tutorial, defina o preset de âncoras como Full Rect, para a proporção dele ser a da viewport. - Defina o Material dele como um novo
ShaderMaterialcom um novoShadere cole o código. - Ajuste a meia largura, a meia altura, o raio do canto, a espessura da borda, o tamanho da sombra e as cores em Shader Parameters. O botão se atualiza no editor.
- Para torná-lo clicável, faça do
ColorRectfilho de umButtoncom todos os estilos emStyleBoxEmpty. No retângulo, ponha o Mouse Filter em Ignore e ligue Visibility › Show Behind Parent, para ele ser desenhado por baixo do texto do botão, e não por cima. O Godot cuida da entrada, do foco e do texto; o shader só desenha por trás deles. Não ponha o material no próprioButton: o texto dele também passaria pelo shader.
Num VisualShader, cada distância é um nó Expression com a função correspondente, e a composição usa nós comuns: Max
para a borda, Min para o ícone, SmoothStep para a sombra, um DerivativeFunc em Sum (fwidth) para as bordas e
Mix para empilhar as cores.
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// 2D SDF composition → Rounded UI button, 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 float p_width; // Half width: 0.75 (0.1 to 1.4)uniform float p_height; // Half height: 0.25 (0.05 to 0.8)uniform float p_radius; // Corner radius: 0.12 (0.0 to 0.8)uniform float p_borderWidth; // Border width: 0.02 (0.0 to 0.15)uniform float p_shadowSize; // Shadow size: 0.1 (0.01 to 0.4)uniform vec3 p_faceColor; // Face color: vec3(0.27, 0.36, 0.95), sRGB; upload it decoded to linearuniform vec3 p_borderColor; // Border color: vec3(0.55, 0.62, 1.0), sRGB; upload it decoded to linearuniform vec3 p_iconColor; // Icon color: vec3(1.0, 1.0, 1.0), sRGB; upload it decoded to linearin 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 box shrunk by r, then grown back by r: growing a distance field rounds every convex cornerfloat sdRoundBox(vec2 p, vec2 halfSize, float r) { vec2 q = abs(p) - halfSize + r; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;}float sdSegment(vec2 p, vec2 a, vec2 b) { vec2 pa = p - a; vec2 ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h);}// fwidth() is how much d changes across one pixel, so the edge is one pixel soft at any size or zoomfloat fill(float d) { float w = fwidth(d); return 1.0 - smoothstep(-0.5 * w, 0.5 * w, d);}vec4 effect(Inputs i) { vec2 p = (i.uv * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0); vec2 halfSize = vec2(p_width, p_height); float r = min(p_radius, min(halfSize.x, halfSize.y)); float body = sdRoundBox(p, halfSize, r); // Subtraction: the body without a copy of itself shrunk by the border width, which leaves an inner stroke float border = max(body, -(body + p_borderWidth)); // Union: two capsules meeting at the tip make a chevron float iconSize = 0.4 * p_height; vec2 tip = vec2(0.4 * iconSize, 0.0); float upper = sdSegment(p, vec2(-0.3 * iconSize, iconSize), tip); float lower = sdSegment(p, vec2(-0.3 * iconSize, -iconSize), tip); float icon = min(upper, lower) - 0.12 * iconSize; // The same box, lower and blurred by widening the smoothstep: a drop shadow costs one more distance float shadowDistance = sdRoundBox(p - vec2(0.0, -0.4 * p_shadowSize), halfSize, r); float shadow = 0.45 * (1.0 - smoothstep(-p_shadowSize, p_shadowSize, shadowDistance)); vec3 framed = mix(p_faceColor, p_borderColor, fill(border)); vec3 face = mix(framed, p_iconColor, fill(icon)); float coverage = fill(body); // The black shadow under the face, as straight alpha: it adds opacity, and only the face adds color float alpha = coverage + shadow * (1.0 - coverage); vec3 color = face * coverage / max(alpha, 0.0001); return vec4(color, alpha);}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);}// 2D SDF composition → Rounded UI button, 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 float width : hint_range(0.1, 1.4, 0.01) = 0.75;uniform float height : hint_range(0.05, 0.8, 0.01) = 0.25;uniform float radius : hint_range(0.0, 0.8, 0.01) = 0.12;uniform float borderWidth : hint_range(0.0, 0.15, 0.005) = 0.02;uniform float shadowSize : hint_range(0.01, 0.4, 0.01) = 0.1;uniform vec3 faceColor : source_color = vec3(0.27, 0.36, 0.95);uniform vec3 borderColor : source_color = vec3(0.55, 0.62, 1.0);uniform vec3 iconColor : source_color = vec3(1.0, 1.0, 1.0);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));}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 box shrunk by r, then grown back by r: growing a distance field rounds every convex cornerfloat sdRoundBox(vec2 p, vec2 halfSize, float r) { vec2 q = abs(p) - halfSize + r; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;}float sdSegment(vec2 p, vec2 a, vec2 b) { vec2 pa = p - a; vec2 ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h);}// fwidth() is how much d changes across one pixel, so the edge is one pixel soft at any size or zoomfloat fill(float d) { float w = fwidth(d); return 1.0 - smoothstep(-0.5 * w, 0.5 * w, d);}vec4 effect(Inputs i) { vec2 p = (i.uv * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0); vec2 halfSize = vec2(width, height); float r = min(radius, min(halfSize.x, halfSize.y)); float body = sdRoundBox(p, halfSize, r); // Subtraction: the body without a copy of itself shrunk by the border width, which leaves an inner stroke float border = max(body, -(body + borderWidth)); // Union: two capsules meeting at the tip make a chevron float iconSize = 0.4 * height; vec2 tip = vec2(0.4 * iconSize, 0.0); float upper = sdSegment(p, vec2(-0.3 * iconSize, iconSize), tip); float lower = sdSegment(p, vec2(-0.3 * iconSize, -iconSize), tip); float icon = min(upper, lower) - 0.12 * iconSize; // The same box, lower and blurred by widening the smoothstep: a drop shadow costs one more distance float shadowDistance = sdRoundBox(p - vec2(0.0, -0.4 * shadowSize), halfSize, r); float shadow = 0.45 * (1.0 - smoothstep(-shadowSize, shadowSize, shadowDistance)); vec3 framed = mix(u_toLinear(faceColor), u_toLinear(borderColor), fill(border)); vec3 face = mix(framed, u_toLinear(iconColor), fill(icon)); float coverage = fill(body); // The black shadow under the face, as straight alpha: it adds opacity, and only the face adds color float alpha = coverage + shadow * (1.0 - coverage); vec3 color = face * coverage / max(alpha, 0.0001); return vec4(color, alpha);}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
- 2D distance functions (abre em uma nova aba)Artigo
- Distance functions (abre em uma nova aba)Artigo
- Improved alpha-tested magnification for vector textures and special effects (abre em uma nova aba)Paper
- fwidth (abre em uma nova aba)Referência
- Shapes (abre em uma nova aba)Livro
- CanvasItem shaders (abre em uma nova aba)Documentação