Vitral
Vidros coloridos irregulares presos em linhas escuras de chumbo, com uma passada de ruído celular.
- 01ObjetivoPadrão
- 02TécnicaCélulas de Voronoi
- 03ExecuçãoVitral
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.
lengthTexture2Dlengthlengthlength UV01 Distância até as paredes (F2 − F1)
cosCoscoscoscos Entrada02 Uma cor por vidro
*Multiply*** Entrada03 Escurecer perto das bordas
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada04 Linhas de chumbo
mixMixmixmixmix Entrada05 Pôr o chumbo sobre o vidro
Como funciona
Um vitral é um diagrama de VoronoiUma divisão do plano numa região por ponto semente, em que cada lugar pertence ao ponto mais próximo dele. As bordas ficam onde dois pontos estão à mesma distância. Diagrama de Voronoi Uma divisão do plano numa região por ponto semente, em que cada lugar pertence ao ponto mais próximo dele. As bordas ficam onde dois pontos estão à mesma distância.
Passo a passo
- Distância até as paredes (F2 − F1).
cellular()amplia a UVUma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.FundamentoCoordenadas UV
Uma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.
uv = gl_FragCoord.xy / resolutionFontespela quantidade de vidros na largura e na altura, então cada célula da grade guarda um ponto semente, posicionado por um hashUma função que embaralha a entrada num valor que parece aleatório, mas é sempre o mesmo para a mesma entrada. Shaders a usam para ter aleatoriedade por pixel e por célula.- gl_FragCoord (abre em uma nova aba)Referência
- UV mapping (abre em uma nova aba)Referência
FundamentoHash
Uma função que embaralha a entrada num valor que parece aleatório, mas é sempre o mesmo para a mesma entrada. Shaders a usam para ter aleatoriedade por pixel e por célula.
h = fract(sin(dot(p, k)) × big)da célula. Ela mede comlengtha distância do pixel até os pontos da própria célula e das oito ao redor, e guarda o mais próximo (F1), o segundo mais próximo (F2) e qual célula venceu (pane).F2 − F1vale zero exatamente onde dois pontos estão à mesma distância, na borda entre os dois vidros, e cresce em direção ao meio de cada vidro. - Uma cor por vidro. As coordenadas da célula vencedora passam de novo pelo hash, deslocadas em 71 para a cor não
ficar presa a onde o ponto está. Um número aleatório escolhe um matiz numa roda de cores feita com
cos, o outro uma densidade. Elevar o matiz ao quadrado em espaço linear o empurra para os tons fundos e saturados do vidro de verdade, e a densidade separa dois vizinhos de matiz parecido. - Escurecer perto das bordas.
smoothstep(0.0, 0.5, walls)sobe de 0 na borda até 1 bem dentro do vidro, e misturar a sombra da borda em direção a 1 com esse valor escurece cada vidro perto do chumbo, uma escolha de sombreamento que dá a cada vidro um ar levemente almofadado e faz o chumbo parecer afundado. Suba a sombra da borda para 1 para ter vidros chapados. - Linhas de chumbo.
1.0 - smoothstep(p_leadWidth, p_leadWidth + 0.03, walls)vale 1 na faixa da borda e 0 no resto. A rampa de 0.03 é uma borda suave em unidades de célula, então a linha sai lisa neste tamanho sem uma segunda amostra. Com largura de chumbo 0 sobra só esse fio. - Pôr o chumbo sobre o vidro.
mix(lit, p_leadColor, lead)coloca a cor do chumbo por cima. Num vitral de verdade, as tiras de chumbo são perfis em forma de H que seguram o vidro cortado, e funcionam como o contorno escuro que o olho usa para achar cada forma.
Duas escolhas dentro de cellular() importam. Os pontos ficam a 0.1 dos lados da célula: com deslocamento total, um
ponto a duas células de distância pode ficar mais perto que os nove conferidos, e o chumbo mostra uma emenda reta onde
deveria fazer curva. E o hash trabalha com inteiros em vez do comum fract(sin(…)): o sin de um número grande perde
precisão de um jeito diferente em cada GPU, então os vidros sairiam diferentes de um aparelho para outro.
Notas de produção
Custo. Cerca de 370 operações por pixel, uma passada, nenhuma leitura de textura. As nove células custam umas 36
cada: o hash de inteiros (duas rodadas de multiplicação, deslocamento de bits e xor), o deslocamento do ponto, uma
subtração, um length e as comparações que guardam F1 e F2. A cor do vidro soma um hash, um cos por canal e algumas
multiplicações; a sombra e o chumbo são dois smoothstep e dois mix. É pesado para um fundo, mas tranquilo para uma
janela ou um objeto de cena, e não ocupa memória em resolução nenhuma.
O chumbo não é bem uniforme. F2 − F1 vale zero na borda, mas não é a distância até ela: cresce com o dobro da
velocidade da distância onde a borda cruza a reta entre os dois pontos, e mais devagar ao longo da borda, tanto mais
quanto mais próximos os pontos estiverem. Por isso o mesmo limiar dá chumbo fino no meio de uma borda e mais grosso
perto das pontas e entre pontos próximos. Num vitral isso parece feito à mão. Para linhas de uma largura exata, o artigo
sobre bordas de Voronoi nas fontes acha primeiro o ponto mais próximo e depois procura em 5 × 5 células ao redor dele a
mediatriz mais próxima, por umas quatro vezes o custo.
De longe. O chumbo é uma faixa dura como as bordas do tabuleiro de xadrez, então cintila quando um vidro fica com poucos pixels de largura. Antes disso, desbote o chumbo para a cor do vidro conforme a distância, ou diminua a quantidade de vidros.
Variações. Os mesmos três valores desenham outras células: só o F1 para escamas e bolhas, F2 − F1 com chumbo fino
e uma cor só para chão rachado, o id da célula para uma altura ou um material aleatório por ladrilho. Numa malha sem
UVs boas, a mesma busca em 3 × 3 × 3 células sobre a posição no mundo dá células sem emendas, pelo triplo do custo.
Monte no GLSL
O botão de copiar entrega um shader de fragmento WebGL2 completo: #version 300 es, os uniforms, a struct Inputs e um
main() que a preenche e chama effect(). Para usá-lo no seu próprio código WebGL2, desenhe um quad (ou um triângulo
de tela cheia) cujo shader de vértice escreve v_uv de 0 a 1, além de v_position, v_normal e v_view (este card só
lê a UV, então constantes servem), compile o par e defina os uniforms:
p_acrossep_upcomo floats: mais ou menos quantos vidros cabem na largura e na altura. Mantenha a proporção entre eles igual à do canvas (10 e 6 para um canvas de 1000 × 600), senão os vidros esticam junto.p_leadWidthep_edgeShadecomo floats,p_leadColorcomo cor linear. Uma cor tirada de um seletor está em sRGB; decodifique na CPU com a curva sRGB antes de enviar.
O shader escreve a cor final já codificada em sRGB, então ela vai direto para o canvas. Num pipeline que codifica
depois (renderizando antes num alvo linear), tire a codificação do fim do main().
Reaproveitando as partes. cellular() devolve F1 e F2 e escreve a célula vencedora no parâmetro out, então ela
serve em qualquer shader que precise de Voronoi: devolva distances.x para bolhas, aplique um limiar só em walls para
rachaduras. hash22() dá dois números entre 0 e 1 para qualquer célula inteira; some uma constante diferente à célula
para cada valor independente, como o effect() faz para a cor.
Outras sementes. Os vidros são fixados pelas coordenadas das células. Some um deslocamento à UV ampliada (um
uniform vec2) para mostrar outra parte do padrão infinito, o que funciona como uma semente nova.
Monte no Godot
O VisualShader não tem nó de Voronoi. Num grafo de nós, o primeiro passo é um nó Texture2D lendo um NoiseTexture2D cujo FastNoiseLite está em Cellular; os passos seguintes são os mesmos nós de cima.
O botão de copiar entrega um shader canvas_item. Numa cena 2D, adicione um ColorRect (ou qualquer Control ou
Sprite2D), dê a ele um novo ShaderMaterial e cole o código num novo Shader desse material. O vitral preenche o
retângulo do nó: across e up ficam no inspetor, em Shader Parameters, e a proporção entre eles deve bater com o
tamanho do nó (10 e 6 para um retângulo de 500 × 300) para os vidros não esticarem. leadWidth, leadColor e
edgeShade funcionam como no site. A linguagem de shader do Godot 4 tem uint, literais hexadecimais e deslocamento de
bits, então o hash de inteiros compila sem mudança e dá os mesmos vidros da versão web.
Numa malha 3D. O mesmo effect() funciona num shader spatial, sobre a UV da malha. Comece um novo Shader com
shader_type spatial; e render_mode unshaded;, cole os uniforms, as funções auxiliares e o effect() e escreva um
fragment() que preenche Inputs a partir de UV e faz ALBEDO = effect(i).rgb;. Shaders spatial decodificam os
uniforms source_color para linear e codificam a saída sozinhos, então tire a chamada a u_toLinear lá. Para uma
janela que brilha, escreva o vidro em EMISSION e deixe o ALBEDO preto.
Como grafo de nós. Num VisualShader no modo CanvasItem, leia um NoiseTexture2D com um nó Texture2D: o ruído dele é um FastNoiseLite com Noise Type Cellular e Return Type Distance2Sub (que é o F2 − F1), e Cellular Jitter em 1.0, que espalha os pontos mais ou menos como este card. Um SmoothStep no canal vermelho dá o chumbo. Para as cores, um segundo Texture2D sobre um NoiseTexture2D com as mesmas configurações, mas Return Type Cell Value, dá um número constante por vidro; passe-o por nós Cos como paleta, multiplique pela sombra e faça um Mix com a cor do chumbo, ligado à saída Color. A textura se repete sem emenda com Seamless ligado e custa uma ou duas leituras de textura no lugar das nove células de conta.
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// Voronoi cells → Stained glass, 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_across; // Panes across: 10.0 (2.0 to 40.0)uniform float p_up; // Panes up: 6.0 (2.0 to 40.0)uniform float p_leadWidth; // Lead width: 0.06 (0.0 to 0.3)uniform vec3 p_leadColor; // Lead color: vec3(0.1, 0.1, 0.11), sRGB; upload it decoded to linearuniform float p_edgeShade; // Edge shade: 0.45 (0.0 to 1.0)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));}// An integer hash rather than fract(sin(...)): sin loses precision differently on each GPU, so the panes would change// from one device to the next.uint mix32(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}vec2 hash22(vec2 cell) { uvec2 q = uvec2(ivec2(cell)); uint h = mix32(q.x ^ mix32(q.y)); return vec2(float(h >> 16u), float(h & 0xffffu)) / 65536.0;}vec2 cellular(vec2 p, out vec2 id) { vec2 base = floor(p); vec2 local = fract(p); float f1 = 8.0; float f2 = 8.0; id = base; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { vec2 o = vec2(float(x), float(y)); // Points kept 0.1 away from the cell's sides: a point two cells away then almost never beats the nine // checked, which would show as a straight seam in the lead vec2 point = o + 0.1 + 0.8 * hash22(base + o); float d = length(point - local); if (d < f1) { f2 = f1; f1 = d; id = base + o; } else if (d < f2) { f2 = d; } } } return vec2(f1, f2);}vec4 effect(Inputs i) { vec2 pane; vec2 distances = cellular(i.uv * vec2(p_across, p_up), pane); float walls = distances.y - distances.x; // Offset from the point hash, so a pane's color doesn't follow where its point sits vec2 pick = hash22(pane + 71.0); vec3 hue = 0.5 + 0.5 * cos(6.2831853 * (pick.x + vec3(0.0, 0.33, 0.67))); // Squaring in linear space deepens the colors toward the saturated tones of real glass, and a density per pane // keeps two neighbors of the same hue apart vec3 glass = hue * hue * mix(0.3, 0.85, pick.y); float inset = smoothstep(0.0, 0.5, walls); vec3 lit = glass * mix(p_edgeShade, 1.0, inset); float lead = 1.0 - smoothstep(p_leadWidth, p_leadWidth + 0.03, walls); vec3 color = mix(lit, p_leadColor, lead); 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);}// Voronoi cells → Stained glass, 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;uniform float across : hint_range(2.0, 40.0, 0.5) = 10.0;uniform float up : hint_range(2.0, 40.0, 0.5) = 6.0;uniform float leadWidth : hint_range(0.0, 0.3, 0.01) = 0.06;uniform vec3 leadColor : source_color = vec3(0.1, 0.1, 0.11);uniform float edgeShade : hint_range(0.0, 1.0, 0.05) = 0.45;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));}// An integer hash rather than fract(sin(...)): sin loses precision differently on each GPU, so the panes would change// from one device to the next.uint mix32(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}vec2 hash22(vec2 cell) { uvec2 q = uvec2(ivec2(cell)); uint h = mix32(q.x ^ mix32(q.y)); return vec2(float(h >> 16u), float(h & 0xffffu)) / 65536.0;}vec2 cellular(vec2 p, out vec2 id) { vec2 base = floor(p); vec2 local = fract(p); float f1 = 8.0; float f2 = 8.0; id = base; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { vec2 o = vec2(float(x), float(y)); // Points kept 0.1 away from the cell's sides: a point two cells away then almost never beats the nine // checked, which would show as a straight seam in the lead vec2 point = o + 0.1 + 0.8 * hash22(base + o); float d = length(point - local); if (d < f1) { f2 = f1; f1 = d; id = base + o; } else if (d < f2) { f2 = d; } } } return vec2(f1, f2);}vec4 effect(Inputs i) { vec2 pane; vec2 distances = cellular(i.uv * vec2(across, up), pane); float walls = distances.y - distances.x; // Offset from the point hash, so a pane's color doesn't follow where its point sits vec2 pick = hash22(pane + 71.0); vec3 hue = 0.5 + 0.5 * cos(6.2831853 * (pick.x + vec3(0.0, 0.33, 0.67))); // Squaring in linear space deepens the colors toward the saturated tones of real glass, and a density per pane // keeps two neighbors of the same hue apart vec3 glass = hue * hue * mix(0.3, 0.85, pick.y); float inset = smoothstep(0.0, 0.5, walls); vec3 lit = glass * mix(edgeShade, 1.0, inset); float lead = 1.0 - smoothstep(leadWidth, leadWidth + 0.03, walls); vec3 color = mix(lit, u_toLinear(leadColor), lead); return vec4(color, 1.0);}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)), 1.0);}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
- Cellular noise (abre em uma nova aba)Livro
- Voronoi edges (abre em uma nova aba)Artigo
- Voronoi diagram (abre em uma nova aba)Referência
- Worley noise (abre em uma nova aba)Referência
- Came glasswork (abre em uma nova aba)Referência
- length (abre em uma nova aba)Referência
- smoothstep (abre em uma nova aba)Referência
- FastNoiseLite (abre em uma nova aba)Documentação
- NoiseTexture2D (abre em uma nova aba)Documentação
- CanvasItem shaders (abre em uma nova aba)Documentação