Pular para o conteúdo
Lente · DesfoqueIntermediário

Gaussiano separável

Um desfoque suave de lente sobre o quadro inteiro, feito numa passada de linhas e numa de colunas.

UVTelaTexturaTela / pós
  1. 01ObjetivoLente
  2. 02TécnicaDesfoque
  3. 03ExecuçãoGaussiano separável
Cena de teste

Parâmetros

3.0
1.00

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.

  1. textureTexture2Dtexturetexturetexture
    UV
    Tela
    Textura

    01 Ler a cena

    Passe main

  2. texture * expExpressiontexture * exptexture * exptexture * exp

    02 Desfocar as linhas

    Passe blurH

  3. texture * expExpressiontexture * exptexture * exptexture * exp

    03 Desfocar as colunas

    Passe main

  4. mixMixmixmixmix

    04 Dosar o efeito

    Passe main

Como funciona

O card desfoca a cena de teste com um kernel gaussiano: cada pixel vira uma média ponderada dos pixels em volta, e os pesos seguem uma curva de sino, mais pesados no centro e sumindo suavemente com a distância. Em vez de ler o quadrado inteiro em volta de cada pixel, ele roda duas passadas. A primeira desfoca ao longo das linhas num render target; a segunda desfoca esse resultado ao longo das colunas.

Passo a passo

  1. Ler a cena. sharp é a cor sem desfoque do quadro neste pixel, guardada para o último passo. A cena chega ao shader como t_screen em luz linear, então a média abaixo mistura luz, não números codificados.
  2. Desfocar as linhas. A primeira passada, blurH, anda reach() texels para a esquerda e para a direita. O peso de cada amostra é 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.
  3. Desfocar as colunas. A última passada faz o mesmo no eixo y, lendo t_blurH em vez da cena. Cada amostra desta passada já guarda uma média horizontal, então blurred é a gaussiana 2D completa.
  4. 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().

  1. Desenhe a cena num framebuffer cujo anexo de cor é uma textura RGBA16F, em luz linear. Esta é a t_screen.
  2. Crie uma segunda textura RGBA16F do mesmo tamanho e anexe-a a outro framebuffer. Desenhe a passada blurH nela com a t_screen ligada e u_resolution igual ao tamanho dela. Esta é a t_blurH.
  3. Ligue o framebuffer padrão (o canvas) e desenhe a passada main com t_screen e t_blurH ligadas. Ela escreve sRGB, já limitado, pronto para o canvas.
  4. Defina p_sigma (a largura do desfoque em pixels) e p_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.

  1. Adicione um MeshInstance3D com um QuadMesh (tamanho 2×2) como filho da sua Camera3D. Dê a ele um ShaderMaterial com a passada main, deixe só a camada visual (Visual Layer) 3 ligada e aumente o Extra Cull Margin até o máximo, para ele nunca ser descartado.
  2. 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.
  3. Dentro dele, adicione uma Camera3D e dê à sua câmera principal um filho RemoteTransform3D apontando 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.
  4. Também dentro do SubViewport, adicione um segundo quad 2×2 com a passada blurH, só na camada visual 2.
  5. 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.
  6. No material da main, defina t_blurH como uma ViewportTexture do SubViewport. Ajuste sigma e amount nos 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

separable-gaussian.glslGLSL
#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);}
separable-gaussian.gdshaderShader do Godot
// 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.

Leitura complementar