Gaussiano separável
Um desfoque suave de lente sobre o quadro inteiro, feito numa passada de linhas e numa de colunas.
- 01ObjetivoLente
- 02TécnicaDesfoque
- 03ExecuçãoGaussiano separável
Parâmetros
Copie para a sua plataforma
- GLSLAdaptar
- GodotAdaptar
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.
textureTexture2Dtexturetexturetexture UVTelaTextura01 Ler a cena
Passe main
texture * expExpressiontexture * exptexture * exptexture * exp Entrada02 Desfocar as linhas
Passe blurH
texture * expExpressiontexture * exptexture * exptexture * exp Entrada03 Desfocar as colunas
Passe main
mixMixmixmixmix Entrada04 Dosar o efeito
Passe main
Como funciona
O card desfoca a cena de teste com um kernelUma pequena grade de pesos. Cada pixel de saída é a soma ponderada dos vizinhos, o que desfoca, aumenta a nitidez ou encontra bordas, conforme os pesos. Kernel Uma pequena grade de pesos. Cada pixel de saída é a soma ponderada dos vizinhos, o que desfoca, aumenta a nitidez ou encontra bordas, conforme os pesos. Render target Uma textura em que um desenho escreve no lugar da tela, para uma passada seguinte ler o resultado. Blur, bloom e todo efeito com várias passadas usam isso.out = Σ weight(i, j) · texel(x + i, y + j)
Passo a passo
- Ler a cena.
sharpé a cor sem desfoque do quadro neste pixel, guardada para o último passo. A cena chega ao shader comot_screenem luz linear, então a média abaixo mistura luz, não números codificados. - Desfocar as linhas. A primeira passada,
blurH, andareach()texelsUm elemento de uma textura, a versão do pixel numa textura. Uma amostra mistura texels próximos ou devolve o mais próximo.FundamentoTexel
Um elemento de uma textura, a versão do pixel numa textura. Uma amostra mistura texels próximos ou devolve o mais próximo.
texel size = 1 / textureSize(sampler, 0)Fontespara a esquerda e para a direita. O peso de cada amostra é- texture (abre em uma nova aba)Referência
- texelFetch (abre em uma nova aba)Referência
exp(-k² / 2σ²), a gaussiana naquela distância;p_sigmaé a largura dela em pixels. As cores ponderadas são somadas e divididas pela soma dos pesos, o que mantém uma área lisa com exatamente o mesmo brilho. O resultado,across, vai para o alvo da própria passada,t_blurH: um quadro borrado só para os lados. - Desfocar as colunas. A última passada faz o mesmo no eixo y, lendo
t_blurHem vez da cena. Cada amostra desta passada já guarda uma média horizontal, entãoblurredé a gaussiana 2D completa. - Dosar o efeito.
mix(sharp, blurred, p_amount)mistura o desfoque sobre o original: 0 mostra o quadro nítido e 1 o desfoque inteiro.
Por que duas passadas dão a mesma imagem
Uma gaussiana 2D se fatora numa horizontal vezes uma vertical: G(x, y) = G(x) · G(y). Ponderar um quadrado de texels
por G(x) · G(y) e somar dá a mesma soma que primeiro somar cada linha com G(x) e depois somar essas somas de linha
ao longo da coluna com G(y). A segunda passada lê exatamente essas somas de linha. Desfoques de caixa se fatoram do
mesmo jeito; a maioria das outras formas, como um disco de bokeh redondo, não.
As amostras param em três sigmas (reach()), onde a curva já caiu para cerca de 1% do pico. Cortar ali perde uma
lasca das pontas; dividir pelos pesos de fato usados devolve essa energia, então a imagem não escurece.
Notas de produção
Na largura padrão (σ = 3), cada passada faz 19 amostras: 19 leituras na passada das linhas e 20 na das colunas,
contando sharp. Um kernel 19×19 numa passada só leria 361 texels por pixel. A matemática fica em cerca de 11 operações
por amostra (o exp do peso, o deslocamento, a multiplicação com soma), umas 420 por pixel; na maioria das GPUs as
leituras custam mais que a matemática. O preço de separar é uma segunda passada de tela cheia e um alvo de meio float do
tamanho do quadro, o que pesa mais na largura de banda de memória do que na aritmética.
Dobrar o sigma dobra as amostras de cada passada. Passando de uma dúzia de pixels de raio, desfoque uma cópia menor do quadro: metade da resolução cobre o dobro do raio com um quarto dos pixels, e o bloom faz exatamente isso. Aqui os pesos são recalculados a cada amostra para o código ficar legível; um desfoque de produção costuma guardá-los numa lista constante para um sigma fixo, e junta amostras vizinhas numa leitura bilinear para cortar a contagem pela metade.
Desfoque em luz linear, como aqui. Tirar a média de valores codificados em sRGB escurece as bordas entre áreas claras e escuras e apaga brilhos pequenos, que é o clássico desfoque lamacento.
Monte no GLSL
Dois shaders de fragmento, um por passada. Renderize a cena numa textura de meio float, desenhe a passada "blurH" numa segunda textura do mesmo tamanho e depois desenhe a passada "main" no canvas com as duas ligadas.
Cada passada é um desenho de tela cheia: um triângulo que cobre o viewport, com um shader de vértice que escreve v_uv
de 0 a 1 de ponta a ponta. O código copiado traz os dois shaders de fragmento, cada um com seu main().
- Desenhe a cena num framebuffer cujo anexo de cor é uma textura
RGBA16F, em luz linear. Esta é at_screen. - Crie uma segunda textura
RGBA16Fdo mesmo tamanho e anexe-a a outro framebuffer. Desenhe a passadablurHnela com at_screenligada eu_resolutionigual ao tamanho dela. Esta é at_blurH. - Ligue o framebuffer padrão (o canvas) e desenhe a passada
maincomt_screenet_blurHligadas. Ela escreve sRGB, já limitado, pronto para o canvas. - Defina
p_sigma(a largura do desfoque em pixels) ep_amount(0 nítido, 1 todo desfocado) nos dois programas; os dois os declaram.
Para desfocar uma textura em vez da tela, ligue-a como t_screen e pule o passo 1. Para um desfoque largo, rode as
duas passadas numa cópia do quadro com metade do tamanho e desenhe o resultado ampliado, como faz o Neon bloom: cada
amostra passa a cobrir dois pixels da tela.
Monte no Godot
Dois shaders spatial. A passada "blurH" roda dentro de um SubViewport que renderiza o mesmo mundo a partir de uma câmera que segue a sua; a passada "main" roda num quad de tela cheia na frente da sua câmera e lê a textura desse SubViewport como t_blurH.
O código copiado são dois shaders, um por passada. O Godot desenha uma passada de pós como uma malha na frente da
câmera cujo shader de vértice a estica sobre a tela, e uma passada só consegue ler a anterior por uma ViewportTexture.
- Adicione um
MeshInstance3Dcom umQuadMesh(tamanho 2×2) como filho da suaCamera3D. Dê a ele umShaderMaterialcom a passadamain, deixe só a camada visual (Visual Layer) 3 ligada e aumente o Extra Cull Margin até o máximo, para ele nunca ser descartado. - Adicione um
SubViewportà cena. Defina o Size com o tamanho da janela, ligue o Use HDR 2D e ponha o Update Mode em Always. Deixe o Own World 3D desligado, para ele ver a sua cena. - Dentro dele, adicione uma
Camera3De dê à sua câmera principal um filhoRemoteTransform3Dapontando para ela, para ela seguir cada movimento. Iguale o FOV dela ao da sua câmera. No Environment dela, ponha uma cópia do Environment do seu WorldEnvironment com Tonemap > Mode em Linear, Exposure 1 e o Glow desligado, para a passada das linhas continuar linear e só a sua câmera fazer o tone mapping. - Também dentro do
SubViewport, adicione um segundo quad 2×2 com a passadablurH, só na camada visual 2. - Ajuste o Cull Mask das câmeras: a câmera do SubViewport vê as camadas 1 e 2, a sua câmera vê as camadas 1 e 3.
- No material da
main, definat_blurHcomo umaViewportTexturedoSubViewport. Ajustesigmaeamountnos dois materiais.
A passada main escreve ALBEDO sem sombreamento, então o tone mapper e o Glow do seu Environment rodam uma vez só,
depois dela, sobre o quadro desfocado. A câmera do SubViewport tem o próprio Environment linear, para a passada das
linhas não passar duas vezes pelo tone mapping.
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// Blur → Separable Gaussian, pass "blurH" (1 of 2), 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_sigma; // Width (sigma, px): 3.0 (0.5 to 8.0)uniform float p_amount; // Amount: 1.0 (0.0 to 1.0)uniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D t_blurH; // pass "blurH"'s output: linear HDR (RGBA16F), the output's size, linear 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;};// A 2D Gaussian equals a horizontal 1D Gaussian followed by a vertical one, so two passes of 2r + 1 taps replace one// pass of (2r + 1)² taps. Each pass keeps its own loop instead of sharing a helper that takes a sampler2D: Godot rejects// a sampler argument that receives the screen texture in one call and a plain texture in another.// The taps reach three sigmas out, where the bell curve has fallen to about 1% of its peakint reach() { return int(ceil(3.0 * p_sigma));}float bell(int k) { return exp(-0.5 * float(k * k) / (p_sigma * p_sigma));}vec4 blurH(Inputs i) { vec2 texel = vec2(1.0 / i.resolution.x, 0.0); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_screen, i.uv + texel * float(k)).rgb * w; total += w; } // Dividing by the weights actually summed keeps a flat color exactly as bright, although the curve is cut off vec3 across = sum / total; return vec4(across, 1.0);}vec4 effect(Inputs i) { vec3 sharp = texture(t_screen, i.uv).rgb; vec2 texel = vec2(0.0, 1.0 / i.resolution.y); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_blurH, i.uv + texel * float(k)).rgb * w; total += w; } vec3 blurred = sum / total; vec3 color = mix(sharp, blurred, p_amount); 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; // Linear HDR into this pass's target, read by the passes after it fragColor = blurH(i);}#version 300 es// Blur → Separable Gaussian, pass "main" (2 of 2), 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_sigma; // Width (sigma, px): 3.0 (0.5 to 8.0)uniform float p_amount; // Amount: 1.0 (0.0 to 1.0)uniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D t_blurH; // pass "blurH"'s output: linear HDR (RGBA16F), the output's size, linear 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));}// A 2D Gaussian equals a horizontal 1D Gaussian followed by a vertical one, so two passes of 2r + 1 taps replace one// pass of (2r + 1)² taps. Each pass keeps its own loop instead of sharing a helper that takes a sampler2D: Godot rejects// a sampler argument that receives the screen texture in one call and a plain texture in another.// The taps reach three sigmas out, where the bell curve has fallen to about 1% of its peakint reach() { return int(ceil(3.0 * p_sigma));}float bell(int k) { return exp(-0.5 * float(k * k) / (p_sigma * p_sigma));}vec4 blurH(Inputs i) { vec2 texel = vec2(1.0 / i.resolution.x, 0.0); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_screen, i.uv + texel * float(k)).rgb * w; total += w; } // Dividing by the weights actually summed keeps a flat color exactly as bright, although the curve is cut off vec3 across = sum / total; return vec4(across, 1.0);}vec4 effect(Inputs i) { vec3 sharp = texture(t_screen, i.uv).rgb; vec2 texel = vec2(0.0, 1.0 / i.resolution.y); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_blurH, i.uv + texel * float(k)).rgb * w; total += w; } vec3 blurred = sum / total; vec3 color = mix(sharp, blurred, p_amount); 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);}// Blur → Separable Gaussian, pass "blurH" (1 of 2), Godot 4. From Creative Center, MIT license.// Godot has no pass chain on one material: draw this pass in its own SubViewport (or a CompositorEffect) and// give its ViewportTexture to the passes that read t_blurH.shader_type spatial;// A full-screen quad over the finished frame: no lighting, no culling, never hidden by depthrender_mode unshaded, cull_disabled, depth_test_disabled;uniform float sigma : hint_range(0.5, 8.0, 0.1) = 3.0;uniform float amount : hint_range(0.0, 1.0, 0.01) = 1.0;uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D t_blurH : filter_linear, repeat_disable; // pass "blurH"'s output: its SubViewport's ViewportTexturestruct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// 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);}// A 2D Gaussian equals a horizontal 1D Gaussian followed by a vertical one, so two passes of 2r + 1 taps replace one// pass of (2r + 1)² taps. Each pass keeps its own loop instead of sharing a helper that takes a sampler2D: Godot rejects// a sampler argument that receives the screen texture in one call and a plain texture in another.// The taps reach three sigmas out, where the bell curve has fallen to about 1% of its peakint reach() { return int(ceil(3.0 * sigma));}float bell(int k) { return exp(-0.5 * float(k * k) / (sigma * sigma));}vec4 blurH(Inputs i) { vec2 texel = vec2(1.0 / i.resolution.x, 0.0); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_screen, u_flipUv(i.uv + texel * float(k))).rgb * w; total += w; } // Dividing by the weights actually summed keeps a flat color exactly as bright, although the curve is cut off vec3 across = sum / total; return vec4(across, 1.0);}vec4 effect(Inputs i) { vec3 sharp = texture(t_screen, u_flipUv(i.uv)).rgb; vec2 texel = vec2(0.0, 1.0 / i.resolution.y); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_blurH, u_flipUv(i.uv + texel * float(k))).rgb * w; total += w; } vec3 blurred = sum / total; vec3 color = mix(sharp, blurred, amount); return vec4(color, 1.0);}void vertex() { // Put the shader on a MeshInstance3D with a 2x2 QuadMesh in front of the camera; it covers the screen wherever it is POSITION = vec4(VERTEX.xy, 1.0, 1.0);}void fragment() { Inputs i; // SCREEN_UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(SCREEN_UV.x, 1.0 - SCREEN_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 = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = blurH(i); ALBEDO = frag_out.rgb;}// Blur → Separable Gaussian, pass "main" (2 of 2), Godot 4. From Creative Center, MIT license.// The last pass: it reads the earlier passes' ViewportTextures through their t_ uniforms.shader_type spatial;// A full-screen quad over the finished frame: no lighting, no culling, never hidden by depthrender_mode unshaded, cull_disabled, depth_test_disabled;uniform float sigma : hint_range(0.5, 8.0, 0.1) = 3.0;uniform float amount : hint_range(0.0, 1.0, 0.01) = 1.0;uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D t_blurH : filter_linear, repeat_disable; // pass "blurH"'s output: its SubViewport's ViewportTexturestruct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// 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);}// A 2D Gaussian equals a horizontal 1D Gaussian followed by a vertical one, so two passes of 2r + 1 taps replace one// pass of (2r + 1)² taps. Each pass keeps its own loop instead of sharing a helper that takes a sampler2D: Godot rejects// a sampler argument that receives the screen texture in one call and a plain texture in another.// The taps reach three sigmas out, where the bell curve has fallen to about 1% of its peakint reach() { return int(ceil(3.0 * sigma));}float bell(int k) { return exp(-0.5 * float(k * k) / (sigma * sigma));}vec4 blurH(Inputs i) { vec2 texel = vec2(1.0 / i.resolution.x, 0.0); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_screen, u_flipUv(i.uv + texel * float(k))).rgb * w; total += w; } // Dividing by the weights actually summed keeps a flat color exactly as bright, although the curve is cut off vec3 across = sum / total; return vec4(across, 1.0);}vec4 effect(Inputs i) { vec3 sharp = texture(t_screen, u_flipUv(i.uv)).rgb; vec2 texel = vec2(0.0, 1.0 / i.resolution.y); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach(); k <= reach(); k++) { float w = bell(k); sum += texture(t_blurH, u_flipUv(i.uv + texel * float(k))).rgb * w; total += w; } vec3 blurred = sum / total; vec3 color = mix(sharp, blurred, amount); return vec4(color, 1.0);}void vertex() { // Put the shader on a MeshInstance3D with a 2x2 QuadMesh in front of the camera; it covers the screen wherever it is POSITION = vec4(VERTEX.xy, 1.0, 1.0);}void fragment() { Inputs i; // SCREEN_UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(SCREEN_UV.x, 1.0 - SCREEN_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 = 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.