Pular para o conteúdo
Padrão · Células de VoronoiIntermediário

Vitral

Vidros coloridos irregulares presos em linhas escuras de chumbo, com uma passada de ruído celular.

UVSuperfície
Abrir no Playground
  1. 01ObjetivoPadrão
  2. 02TécnicaCélulas de Voronoi
  3. 03ExecuçãoVitral
Quad de tela cheia

Parâmetros

10.0
6.0
0.06
#1a1a1c
0.45

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. lengthTexture2Dlengthlengthlength
    UV

    01 Distância até as paredes (F2 − F1)

  2. cosCoscoscoscos

    02 Uma cor por vidro

  3. *Multiply***

    03 Escurecer perto das bordas

  4. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    04 Linhas de chumbo

  5. mixMixmixmixmix

    05 Pôr o chumbo sobre o vidro

Como funciona

Um vitral é um diagrama de Voronoi desenhado de três formas ao mesmo tempo: cada vidro ganha sua cor a partir do id da célula, o chumbo fica onde duas células se encontram, e o vidro escurece um pouco perto das bordas.

Passo a passo

  1. Distância até as paredes (F2 − F1). cellular() amplia a UV pela quantidade de vidros na largura e na altura, então cada célula da grade guarda um ponto semente, posicionado por um hash da célula. Ela mede com length a 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 − F1 vale 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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_across e p_up como 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_leadWidth e p_edgeShade como floats, p_leadColor como 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

stained-glass.fragGLSL
#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);}
stained-glass.gdshaderShader do Godot
// 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