Bloom de neon
Letreiros que brilham e vazam luz no escuro em volta, a partir de um desfoque em meia resolução só dos pixels mais claros.
- 01ObjetivoLente
- 02TécnicaBloom
- 03ExecuçãoBloom de neon
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.
dotDotProductdotdotdot UVTela01 Brilho
Passe bright
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada02 Manter só o que brilha
Passe bright
texture * expExpressiontexture * exptexture * exptexture * exp Entrada03 Espalhar com desfoque
Passe blurV
+Add+++ Entrada04 Somar sobre a cena
Passe main
/Divide/// Entrada05 Mapear os tons
Passe main
Como funciona
A cena de teste tem formas emissivas muito mais claras que 1. Sem ajuda, elas virariam traços brancos chapados. Este
card guarda só esses pixels claros, desfoca-os com um raio largo em meia resolução, soma o desfoque de volta ao quadro e
depois aplica o tone mappingA renderização mantém valores de luz acima de 1 em buffers float (HDR), e depois uma curva de tone mapping os comprime nos 0–1 que uma tela consegue mostrar. HDR e tone mapping A renderização mantém valores de luz acima de 1 em buffers float (HDR), e depois uma curva de tone mapping os comprime nos 0–1 que uma tela consegue mostrar.Reinhard = c / (1 + c)
Passo a passo
- Brilho. A passada
brightlê a cena e tira a luminânciaO quanto uma cor parece clara, a partir do vermelho, verde e azul lineares, pesados pela sensibilidade do olho a cada um.FundamentoLuminância
O quanto uma cor parece clara, a partir do vermelho, verde e azul lineares, pesados pela sensibilidade do olho a cada um.
Y = 0.2126 R + 0.7152 G + 0.0722 BFontes,luma: o produto escalar da cor com o quanto cada canal parece claro ao olho (o verde pesa mais, o azul menos). Um número por pixel decide se ele brilha, então uma luz azul saturada e uma branca com o mesmo brilho se comportam igual. - Manter só o que brilha.
smoothstep(p_threshold, p_threshold + 0.5, luma)vale 0 abaixo do limiar e 1 meia unidade acima dele, ekepté a cor da cena multiplicada por isso. A rampa suave é um joelho: uma superfície que clareia devagar entra no brilho aos poucos, em vez de acender de uma vez num valor exato. - Espalhar com desfoque. Duas passadas rodam um gaussiano separável sobre
kept, as linhas emt_blurHe depois as colunas emt_blurV, cujo resultado églow. As três passadas têm tamanhohalf: um quarto dos pixels, e cada amostra cobre dois pixels da tela, então o halo vai duas vezes mais longe com o mesmo número de leituras. - Somar sobre a cena. A última passada soma
glow × p_strengthà cena em resolução cheia e aplica a exposição,exp2(p_exposure), em stops. Agorahdrpassa bem de 1 em volta de cada luz. - Mapear os tons.
hdr / (1.0 + hdr)(a curva de Reinhard) traz qualquer valor para 0–1 sem cortar: 1 vira 0,5 e 4 vira 0,8. O núcleo de cada letreiro continua mais claro que o halo, com um degradê entre os dois em vez de um platô branco e chapado.
Por que o limiar precisa de HDR
Uma passada de brilho só funciona se “claro” ainda quiser dizer alguma coisa. Num quadro de 8 bits, uma camisa branca e um tubo de neon valem os dois 1,0, e os dois brilhariam. No alvo de meio float o tubo vale 6,0 e a camisa 0,8, então um limiar de 1 escolhe só o tubo. O mesmo vale para o tone mapping: ele tem que rodar depois da soma, sobre o valor em HDR, senão o brilho não tem folga.
Notas de produção
Contando por pixel de cada passada, a cadeia lê 1 + 25 + 25 + 2 = 53 texels e faz cerca de 565 operações no alcance
padrão (σ = 4, 25 amostras por passada de desfoque). Só que três das quatro passadas rodam em meia resolução, então
por pixel da tela fica mais perto de 15 leituras: o truque da meia resolução é a maior parte do motivo de o bloom caber
no orçamento. A memória são três alvos de meio float em meia resolução, além da cena em HDR que o quadro já tem.
A passada de brilho lê a tela uma vez, no centro de um pixel de meia resolução, que cai bem entre quatro texels da
tela, então o filtro bilinear tira a média deles de graça. Essa média ainda pode piscar quando um único pixel muito
claro atravessa a grade (os fireflies); um bloom de produção pondera essa redução por 1 / (1 + luma) para domá-los.
Um único tamanho de desfoque dá um halo com uma única queda. Lentes de verdade espalham a luz com um núcleo forte e uma cauda bem longa, que as engines constroem desfocando uma cadeia de cópias cada vez menores e somando todas. Use isso quando um gaussiano só parecer uma névoa uniforme em volta da luz.
Reinhard em cada canal dessatura as cores claras em direção ao branco, o que combina com neon. Num jogo inteiro, use o mesmo tone mapper do resto do quadro (ACES, AgX, Khronos PBR Neutral) e ponha o bloom logo antes dele.
Monte no GLSL
Quatro shaders de fragmento, um por passada. Renderize a cena numa textura de meio float, desenhe as passadas "bright", "blurH" e "blurV" em sequência em três texturas de meio float em meia resolução, cada uma lendo a anterior, e depois desenhe a passada "main" no canvas com a cena e o último desfoque ligados.
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 quatro shaders de fragmento, cada um com seu main().
- Desenhe a cena num framebuffer cujo anexo de cor é uma textura
RGBA16F, em luz linear, com as superfícies emissivas bem acima de 1. Esta é at_screen. - Crie três texturas
RGBA16Fcom metade do tamanho do canvas (arredondado para cima), cada uma no seu framebuffer, com filtragemLINEAReCLAMP_TO_EDGE. - Desenhe a passada
brightna primeira com at_screenligada: at_bright. - Desenhe a passada
blurHna segunda com at_brightligada: at_blurH. Depois a passadablurVna terceira com at_blurHligada: at_blurV. As passadas de desfoque acham o tamanho do texel comtextureSize(), então funcionam em qualquer tamanho. - Ligue o canvas, volte o viewport ao tamanho cheio e desenhe a passada
maincom at_screene at_blurVligadas. Ela soma o brilho, aplica a exposição e a curva de Reinhard e escreve sRGB. - Defina
p_threshold,p_spread,p_strengthep_exposureem todo programa que os declara.
Para uma cauda mais longa, repita os passos 3 e 4 numa cópia com um quarto do tamanho e some os dois desfoques na última passada, cada um com sua força.
Monte no Godot
Quatro shaders spatial, encadeados por três SubViewports em meia resolução. A passada "bright" renderiza o mundo a partir de uma câmera que segue a sua; "blurH" e "blurV" desenham cada uma só o seu quad num viewport com Own World 3D, lendo o anterior por uma ViewportTexture; a passada "main" roda num quad de tela cheia na frente da sua câmera. Na maioria dos projetos o Glow do próprio Godot (no Environment) é a melhor escolha; esta cadeia serve para um visual que o Glow não dá, ou para igualar a versão web.
O código copiado são quatro shaders, um por passada. O Godot desenha uma passada de pós como uma malha na frente de uma
câmera cujo shader de vértice a estica sobre a tela, e uma passada só lê a anterior por uma ViewportTexture.
- Adicione um
MeshInstance3Dcom umQuadMesh2×2 como filho da suaCamera3D. Dê a ele umShaderMaterialcom a passadamain, deixe-o só na camada visual (Visual Layer) 3 e aumente o Extra Cull Margin até o máximo, para ele nunca ser descartado. - Adicione um
SubViewportpara abright: Size com metade do tamanho da janela, Use HDR 2D ligado, Update Mode em Always, Own World 3D desligado. Dentro dele, adicione umaCamera3Dque a sua câmera conduz por um filhoRemoteTransform3D(iguale o FOV) e um quad 2×2 com a passadabright, só na camada visual 2. - Adicione mais dois
SubViewports, para ablurHe ablurV: mesmo tamanho e mesmas opções, mas com Own World 3D ligado. Dentro de cada um, adicione umaCamera3De um quad 2×2 com a sua passada. Os mundos deles não têm mais nada, então eles desenham só o quad. - Ligue as texturas:
t_brightno material dablurHé umaViewportTexturedo viewport dabright,t_blurHnablurVé o viewport dablurH, et_blurVnamainé o viewport dablurV. - Ajuste os Cull Masks: a câmera da
brightvê as camadas 1 e 2, a sua câmera vê as camadas 1 e 3. - Defina
threshold,spread,strengtheexposureem todos os materiais; cada um declara todos eles.
Para a passada de brilho pegar alguma coisa, os materiais emissivos precisam de Emission Energy bem acima de 1: o Godot guarda o quadro 3D em HDR, então um tubo de neon com energia 6 aparece como 6 na textura da tela.
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// Bloom → Neon bloom, pass "bright" (1 of 4), 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_threshold; // Threshold: 1.0 (0.0 to 4.0)uniform float p_spread; // Spread (sigma, half-size px): 4.0 (1.0 to 8.0)uniform float p_strength; // Strength: 1.0 (0.0 to 3.0)uniform float p_exposure; // Exposure (stops): 0.5 (-2.0 to 2.0)uniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D t_bright; // pass "bright"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurH; // pass "blurH"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurV; // pass "blurV"'s output: linear HDR (RGBA16F), half 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;};// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, i.uv).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(p_threshold, p_threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * p_spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (p_spread * p_spread)); sum += texture(source, uv + texel * float(k)).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, i.uv).rgb; vec3 hdr = (scene + texture(t_blurV, i.uv).rgb * p_strength) * exp2(p_exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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 = bright(i);}#version 300 es// Bloom → Neon bloom, pass "blurH" (2 of 4), 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_threshold; // Threshold: 1.0 (0.0 to 4.0)uniform float p_spread; // Spread (sigma, half-size px): 4.0 (1.0 to 8.0)uniform float p_strength; // Strength: 1.0 (0.0 to 3.0)uniform float p_exposure; // Exposure (stops): 0.5 (-2.0 to 2.0)uniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D t_bright; // pass "bright"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurH; // pass "blurH"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurV; // pass "blurV"'s output: linear HDR (RGBA16F), half 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;};// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, i.uv).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(p_threshold, p_threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * p_spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (p_spread * p_spread)); sum += texture(source, uv + texel * float(k)).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, i.uv).rgb; vec3 hdr = (scene + texture(t_blurV, i.uv).rgb * p_strength) * exp2(p_exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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// Bloom → Neon bloom, pass "blurV" (3 of 4), 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_threshold; // Threshold: 1.0 (0.0 to 4.0)uniform float p_spread; // Spread (sigma, half-size px): 4.0 (1.0 to 8.0)uniform float p_strength; // Strength: 1.0 (0.0 to 3.0)uniform float p_exposure; // Exposure (stops): 0.5 (-2.0 to 2.0)uniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D t_bright; // pass "bright"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurH; // pass "blurH"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurV; // pass "blurV"'s output: linear HDR (RGBA16F), half 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;};// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, i.uv).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(p_threshold, p_threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * p_spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (p_spread * p_spread)); sum += texture(source, uv + texel * float(k)).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, i.uv).rgb; vec3 hdr = (scene + texture(t_blurV, i.uv).rgb * p_strength) * exp2(p_exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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 = blurV(i);}#version 300 es// Bloom → Neon bloom, pass "main" (4 of 4), 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_threshold; // Threshold: 1.0 (0.0 to 4.0)uniform float p_spread; // Spread (sigma, half-size px): 4.0 (1.0 to 8.0)uniform float p_strength; // Strength: 1.0 (0.0 to 3.0)uniform float p_exposure; // Exposure (stops): 0.5 (-2.0 to 2.0)uniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D t_bright; // pass "bright"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurH; // pass "blurH"'s output: linear HDR (RGBA16F), half the output's size, linear filter, clamp to edgeuniform sampler2D t_blurV; // pass "blurV"'s output: linear HDR (RGBA16F), half 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));}// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, i.uv).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(p_threshold, p_threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * p_spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (p_spread * p_spread)); sum += texture(source, uv + texel * float(k)).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, i.uv).rgb; vec3 hdr = (scene + texture(t_blurV, i.uv).rgb * p_strength) * exp2(p_exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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);}// Bloom → Neon bloom, pass "bright" (1 of 4), 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_bright.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 threshold : hint_range(0.0, 4.0, 0.05) = 1.0;uniform float spread : hint_range(1.0, 8.0, 0.1) = 4.0;uniform float strength : hint_range(0.0, 3.0, 0.05) = 1.0;uniform float exposure : hint_range(-2.0, 2.0, 0.1) = 0.5;uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D t_bright : filter_linear, repeat_disable; // pass "bright"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurH : filter_linear, repeat_disable; // pass "blurH"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurV : filter_linear, repeat_disable; // pass "blurV"'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);}// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(threshold, threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (spread * spread)); sum += texture(source, u_flipUv(uv + texel * float(k))).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; vec3 hdr = (scene + texture(t_blurV, u_flipUv(i.uv)).rgb * strength) * exp2(exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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 = bright(i); ALBEDO = frag_out.rgb;}// Bloom → Neon bloom, pass "blurH" (2 of 4), 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 threshold : hint_range(0.0, 4.0, 0.05) = 1.0;uniform float spread : hint_range(1.0, 8.0, 0.1) = 4.0;uniform float strength : hint_range(0.0, 3.0, 0.05) = 1.0;uniform float exposure : hint_range(-2.0, 2.0, 0.1) = 0.5;uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D t_bright : filter_linear, repeat_disable; // pass "bright"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurH : filter_linear, repeat_disable; // pass "blurH"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurV : filter_linear, repeat_disable; // pass "blurV"'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);}// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(threshold, threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (spread * spread)); sum += texture(source, u_flipUv(uv + texel * float(k))).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; vec3 hdr = (scene + texture(t_blurV, u_flipUv(i.uv)).rgb * strength) * exp2(exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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;}// Bloom → Neon bloom, pass "blurV" (3 of 4), 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_blurV.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 threshold : hint_range(0.0, 4.0, 0.05) = 1.0;uniform float spread : hint_range(1.0, 8.0, 0.1) = 4.0;uniform float strength : hint_range(0.0, 3.0, 0.05) = 1.0;uniform float exposure : hint_range(-2.0, 2.0, 0.1) = 0.5;uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D t_bright : filter_linear, repeat_disable; // pass "bright"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurH : filter_linear, repeat_disable; // pass "blurH"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurV : filter_linear, repeat_disable; // pass "blurV"'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);}// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(threshold, threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (spread * spread)); sum += texture(source, u_flipUv(uv + texel * float(k))).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; vec3 hdr = (scene + texture(t_blurV, u_flipUv(i.uv)).rgb * strength) * exp2(exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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 = blurV(i); ALBEDO = frag_out.rgb;}// Bloom → Neon bloom, pass "main" (4 of 4), 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 threshold : hint_range(0.0, 4.0, 0.05) = 1.0;uniform float spread : hint_range(1.0, 8.0, 0.1) = 4.0;uniform float strength : hint_range(0.0, 3.0, 0.05) = 1.0;uniform float exposure : hint_range(-2.0, 2.0, 0.1) = 0.5;uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D t_bright : filter_linear, repeat_disable; // pass "bright"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurH : filter_linear, repeat_disable; // pass "blurH"'s output: its SubViewport's ViewportTextureuniform sampler2D t_blurV : filter_linear, repeat_disable; // pass "blurV"'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);}// Bloom: keep only what is brighter than the threshold, blur it wide at half size, add it back over the scene, then// tone-map, since the sum goes well above 1.vec4 bright(Inputs i) { // This pass is half size: each of its pixels sits between four screen texels, so one bilinear read averages them vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722)); // A soft knee: light fades in over half a unit above the threshold, so a glow doesn't pop on as a surface brightens vec3 kept = scene * smoothstep(threshold, threshold + 0.5, luma); return vec4(kept, 1.0);}// One axis of a Gaussian, taps one texel apart out to three sigmas; both callers pass a pass's own texturevec3 gaussian(sampler2D source, vec2 uv, vec2 axis) { vec2 texel = axis / vec2(textureSize(source, 0)); int reach = int(ceil(3.0 * spread)); vec3 sum = vec3(0.0); float total = 0.0; for (int k = -reach; k <= reach; k++) { float w = exp(-0.5 * float(k * k) / (spread * spread)); sum += texture(source, u_flipUv(uv + texel * float(k))).rgb * w; total += w; } return sum / total;}vec4 blurH(Inputs i) { vec3 wide = gaussian(t_bright, i.uv, vec2(1.0, 0.0)); return vec4(wide, 1.0);}vec4 blurV(Inputs i) { vec3 glow = gaussian(t_blurH, i.uv, vec2(0.0, 1.0)); return vec4(glow, 1.0);}vec4 effect(Inputs i) { vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; vec3 hdr = (scene + texture(t_blurV, u_flipUv(i.uv)).rgb * strength) * exp2(exposure); // Reinhard: 1 maps to 0.5 and anything brighter approaches 1 without clipping, so the glow's core keeps a gradient vec3 mapped = hdr / (1.0 + hdr); return vec4(mapped, 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
- Bloom (abre em uma nova aba)Artigo
- Bloom (shader effect) (abre em uma nova aba)Referência
- Next Generation Post Processing in Call of Duty: Advanced Warfare (abre em uma nova aba)Paper
- HDR (abre em uma nova aba)Artigo
- Relative luminance (abre em uma nova aba)Referência
- smoothstep (abre em uma nova aba)Referência