Vela
Uma pequena chama tremulante, com base azul e halo quente, desenhada como luz sobre o preto.
- 01ObjetivoElementos
- 02TécnicaFogo e chamas
- 03ExecuçãoVela
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.
fbm()Texture2Dfbm()fbm()fbm() UVTempo01 Ruído subindo
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada02 Máscara em gota
clampClampclampclampclamp Entrada03 Calor
mixMixmixmixmix Entrada04 Paleta quente
*Multiply*** Entrada05 Halo e intensidade
Como funciona
A chama de uma vela é o fogo mais simples de observar, e isso faz dela uma boa chama para construir: uma única língua, quase parada embaixo e inquieta na ponta. O card a monta a partir de uma máscara em gota, dois ruídos e uma paleta, e escreve o resultado como emissão: luz, sobre o preto.
Passo a passo
- Ruído subindo. Um fBmUma soma de várias cópias de um ruído, cada uma com frequência maior e força menor que a anterior, para que o resultado tenha detalhes em vários tamanhos ao mesmo tempo, como nuvens, fumaça ou terreno.Fundamento · Ruído
Movimento browniano fractal (fBm)
Uma soma de várias cópias de um ruído, cada uma com frequência maior e força menor que a anterior, para que o resultado tenha detalhes em vários tamanhos ao mesmo tempo, como nuvens, fumaça ou terreno.
fbm(p) = Σ gainᵏ · noise(lacunarityᵏ · p)de quatro oitavasUma camada de ruído numa soma fractal. Cada oitava costuma ter o dobro da frequência e metade da amplitude da anterior, acrescentando detalhes mais finos e mais fracos.FundamentoOitava
Uma camada de ruído numa soma fractal. Cada oitava costuma ter o dobro da frequência e metade da amplitude da anterior, acrescentando detalhes mais finos e mais fracos.
fbm(p) = Σ gainᵏ · noise(lacunarityᵏ · p)lê uma coordenada cujoysubtraitime * speed, então o padrão do ruído viaja para cima através da chama, como gás quente; a prévia mostra esse valor, em torno do cinza médio. Menos 0,5, ele vira a turbulência, centrada no zero. Um segundo fBm, de uma dimensão, só do tempo, dá o balanço lento da chama inteira de um lado para o outro. - Máscara em gota.
risevai de 0 no pavio a 1 na ponta. A meia largura da chama em cada altura éwidth * sqrt(rise) * (1 - rise): a raiz quadrada arredonda a parte de baixo, o1 - risefecha o topo numa ponta, e a parte mais larga fica a um terço da altura, onde a chama de uma vela estufa. O balanço e a turbulência deslocam oxdo pixel antes que ele seja comparado com essa largura, e os dois são multiplicados pela altura (o balanço, pelo quadrado dela), então a base fica no pavio e a ponta é o que mais se move. Um smoothstepUm limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas.FundamentoSmoothstep
Um limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas.
t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)Fontesda metade da largura até a largura inteira deixa a borda suave.- smoothstep (abre em uma nova aba)Referência
- Smoothstep (abre em uma nova aba)Referência
- Calor. Dentro da máscara, o calor é maior no meio e na parte baixa da chama, e cai em direção à borda e à ponta. A turbulência entra na soma, então as faixas de calor se quebram em línguas em vez de ficarem ovais lisas. Bem no pavio, o calor surge ao longo do primeiro quinto da altura, porque ali o vapor da cera ainda não pegou fogo.
- Paleta quente.
hotPalette()transforma calor em cor com três mixesMistura de a até b por um fator t, em linha reta. O GLSL chama de mix; o HLSL e a maioria dos editores de nós chamam de lerp.FundamentoInterpolação linear (mix)
Mistura de a até b por um fator t, em linha reta. O GLSL chama de mix; o HLSL e a maioria dos editores de nós chamam de lerp.
mix(a, b, t) = a (1 − t) + b tFontesencadeados: de uma versão escura da Cor externa à cor externa inteira, depois à Cor interna, depois a quase branco. É a ordem em que um objeto quente brilha, vermelho, laranja, amarelo, branco, e como um único número a controla, as cores nunca saem fora de ordem. Onde o calor é baixo perto do pavio, a cor se mistura com a Cor da base: o azul fraco no pé de uma chama de vela de verdade, onde o vapor queima com mais ar e faz pouca fuligem.- mix (abre em uma nova aba)Referência
- Linear interpolation (abre em uma nova aba)Referência
- Halo e intensidade. Um brilho suave, o
expda distância ao centro da chama, acrescenta um pouco da cor externa em volta dela, a luz que ela joga no ar. Tudo é multiplicado por Intensidade.
Sobre o preto, depois sobre uma cena
O Blending Combinar a cor que o shader de fragmento devolve com a cor que já está no framebuffer, em vez de substituí-la. Transparência, brilho aditivo e multiplicação vêm daí.render.json do card diz "output": "emission": o resultado é luz que a superfície emite, não uma cor base. A
prévia o desenha opaco sobre o próprio preto, que é como esta etapa da ordem de aprendizado mostra o fogo. Sobre uma cena
de verdade, o mesmo shader vai num quad com misturaCombinar a cor que o shader de fragmento devolve com a cor que já está no framebuffer, em vez de substituí-la. Transparência, brilho aditivo e multiplicação vêm daí.result = src × srcFactor + dst × dstFactorrender_mode blend_add; no shader
canvas_item (o guia do Godot mostra), e na web gl.blendFunc(gl.ONE, gl.ONE). Com Intensidade acima de 1, um
pipeline HDR com bloom e sem o clamp no main() copiado, o miolo brilha além do branco.
Custo
Duas chamadas de fBm com quatro oitavas são oito oitavas de ruído de valor, cada uma com quatro hashes inteiros e três
mixes, cerca de 95 operações, então cerca de 760. A máscara, o calor, os três smoothsteps e mixes da paleta, o sqrt, o
exp e o length do halo somam umas 90. São cerca de 850 operações por pixel, nenhuma leitura de textura, uma passada.
Uma vela cobre algumas centenas de pixels, então isso não é nada; uma parede de fogo cobrindo a tela é outra história, e
é aí que um flipbook ou uma textura de ruído rolada ganham.
Por que parece fogo
Três sinais convencem, e cada um é uma linha separada do shader. O movimento vai para cima e cresce com a altura (a
rolagem da turbulência e o fator rise). A cor segue o calor na ordem certa (a paleta). A chama soma luz em vez de cobrir
(emissão, depois mistura aditiva). Quebre qualquer um deles, um balanço que move a base, uma paleta que fica amarela nas
bordas, uma chama desenhada com alpha sobre um fundo claro, e ela deixa de parecer fogo muito antes de alguém reparar no
ruído em si.
Monte no GLSL
O shader copiado é um shader de fragmento GLSL ES 3.00 completo: os auxiliares, effect(), os uniforms e um main()
que preenche Inputs e escreve a cor codificada em sRGB.
- Desenhe um quad cujo UV vá de 0 a 1 a partir do canto inferior esquerdo, passado como
v_uv. A chama fica no centro, a partir de 20% da altura. - A cada quadro, defina
u_timeem segundos (performance.now() / 1000) eu_resolutioncomo o tamanho do canvas. - Defina os parâmetros uma vez: os floats como estão, as três cores convertidas de sRGB para linear.
Sobre o preto, como no card, desenhe opaco. Sobre uma cena, desenhe o quad depois de tudo o que é opaco, sem escrever profundidade e com mistura aditiva:
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
gl.depthMask(false);
O shader escreve alpha 1, então ONE, ONE soma a cor dele como está. O preto não soma nada, então as bordas do quad
somem e só a chama e o halo aparecem. Para uma vela em 3D, vire o quad para a câmera a cada quadro (um billboard) e dê a
cada vela um deslocamento de tempo diferente, para que não tremulem em sincronia.
Monte no Godot
A exportação desenha a chama opaca sobre o preto, como o card. Sobre uma cena, acrescente blend_add à linha de render_mode (render_mode unshaded, blend_add;). No VisualShader, o ruído subindo é um nó Texture2D lendo um NoiseTexture2D com UV mais Time, e a paleta é um GradientTexture1D lido pelo calor.
A exportação é um shader canvas_item, para uma chama desenhada em 2D.
- Adicione um
ColorRect(ou umSprite2Dcom qualquer textura) onde fica a vela. A chama é modelada no espaço de UV e estica junto com o retângulo: nas proporções da prévia, 1,6 vez mais largo que alto (por exemplo 160 por 100 pixels), ela fica como no site. Ela fica no meio, a partir de 20% da altura; num retângulo mais estreito, aumente a Largura. - No slot Material dele, crie um New ShaderMaterial, depois um New Shader, e cole o código copiado.
- Para desenhá-la sobre o resto da cena em vez de sobre o preto, troque a linha de render mode por
render_mode unshaded, blend_add;. O Godot passa a somar a cor da chama ao que está atrás, e os cantos pretos do retângulo somem.
O shader inverte o UV do Godot, que começa no canto superior esquerdo, então a chama aponta para cima como no site. Para
uma vela numa cena 3D, o mesmo corpo funciona num shader spatial sobre um QuadMesh com o modo billboard ligado e
blend_add, unshaded no render mode; o código gerado aqui é o de 2D, então isso é uma adaptação feita à mão.
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// Fire & flames → Candle, 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_height; // Height: 0.6 (0.2 to 0.8)uniform float p_width; // Width: 0.25 (0.05 to 0.6)uniform float p_speed; // Flicker speed: 2.5 (0.0 to 6.0)uniform float p_intensity; // Intensity: 1.2 (0.0 to 3.0)uniform vec3 p_outer; // Outer color: vec3(1.0, 0.42, 0.08), sRGB; upload it decoded to linearuniform vec3 p_inner; // Inner color: vec3(1.0, 0.82, 0.35), sRGB; upload it decoded to linearuniform vec3 p_baseColor; // Base color: vec3(0.12, 0.25, 0.85), sRGB; upload it decoded to linearin 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));}// Integer bit mixing instead of the one-line sine hash: it gives the same value on every GPU, and the rising// coordinates can grow for hours without the noise turning into stripes.uint scramble(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}float hashCell(ivec2 cell) { return float(scramble(uint(cell.x) ^ scramble(uint(cell.y)))) / 4294967295.0;}float valueNoise(vec2 p) { ivec2 cell = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); float a = hashCell(cell); float b = hashCell(cell + ivec2(1, 0)); float c = hashCell(cell + ivec2(0, 1)); float d = hashCell(cell + ivec2(1, 1)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);}// Four octaves are enough: the flame is small and the detail only has to read as flickerfloat fbm(vec2 p) { float sum = 0.0; float amplitude = 0.5; for (int k = 0; k < 4; k++) { sum += amplitude * valueNoise(p); p = p * 2.0 + vec2(17.3, 9.1); amplitude *= 0.5; } return sum;}// From cool to hot: dark red, the outer color, the inner color, then near white, each taking over a third of the rangevec3 hotPalette(float t) { vec3 color = mix(p_outer * 0.15, p_outer, smoothstep(0.0, 0.35, t)); color = mix(color, p_inner, smoothstep(0.3, 0.7, t)); return mix(color, vec3(1.0, 0.97, 0.9), smoothstep(0.75, 1.0, t));}vec4 effect(Inputs i) { // 0 at the wick, 1 at the tip float rise = (i.uv.y - 0.2) / p_height; vec2 scroll = vec2(i.uv.x * 6.0, i.uv.y * 4.0 - i.time * p_speed); float rising = fbm(scroll); float turbulence = rising - 0.5; // The tip is light and moves a lot; the base is anchored to the wick, so the sway grows with the square of the height float sway = (fbm(vec2(i.time * 0.7, 3.1)) - 0.5) * 0.12 * rise * rise; float x = i.uv.x - 0.5 - sway - turbulence * 0.08 * rise; // A teardrop: sqrt(rise) rounds the bottom and (1 - rise) closes the top to a point float halfWidth = p_width * sqrt(max(rise, 0.0)) * max(1.0 - rise, 0.0); float edge = abs(x) / max(halfWidth, 0.0001); float flame = (1.0 - smoothstep(0.5, 1.0, edge)) * step(0.0, rise) * step(rise, 1.0); // Hottest a little above the wick and in the middle, cooling toward the edge and the tip; the noise breaks the bands // into tongues. Right at the wick the vapor hasn't caught yet, so the heat fades in over the first fifth. float heat = flame * smoothstep(0.0, 0.2, rise) * clamp(1.1 - edge * 0.6 - rise * 0.8 + turbulence * 0.6, 0.0, 1.0); vec3 fire = hotPalette(heat) * flame; // Near the wick a real flame burns blue, where the vapor gets the most air float base = flame * (1.0 - smoothstep(0.0, 0.15, rise)) * (1.0 - heat * heat); fire = mix(fire, p_baseColor, base); float halo = exp(-length(vec2(i.uv.x - 0.5, (i.uv.y - 0.2 - p_height * 0.4) * 0.6)) * 9.0) * 0.25; vec3 glow = (fire + p_outer * halo) * p_intensity; return vec4(glow, 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);}// Fire & flames → Candle, 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 height : hint_range(0.2, 0.8, 0.01) = 0.6;uniform float width : hint_range(0.05, 0.6, 0.01) = 0.25;uniform float speed : hint_range(0.0, 6.0, 0.1) = 2.5;uniform float intensity : hint_range(0.0, 3.0, 0.05) = 1.2;uniform vec3 outer : source_color = vec3(1.0, 0.42, 0.08);uniform vec3 inner : source_color = vec3(1.0, 0.82, 0.35);uniform vec3 baseColor : source_color = vec3(0.12, 0.25, 0.85);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));}// Integer bit mixing instead of the one-line sine hash: it gives the same value on every GPU, and the rising// coordinates can grow for hours without the noise turning into stripes.uint scramble(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}float hashCell(ivec2 cell) { return float(scramble(uint(cell.x) ^ scramble(uint(cell.y)))) / 4294967295.0;}float valueNoise(vec2 p) { ivec2 cell = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); float a = hashCell(cell); float b = hashCell(cell + ivec2(1, 0)); float c = hashCell(cell + ivec2(0, 1)); float d = hashCell(cell + ivec2(1, 1)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);}// Four octaves are enough: the flame is small and the detail only has to read as flickerfloat fbm(vec2 p) { float sum = 0.0; float amplitude = 0.5; for (int k = 0; k < 4; k++) { sum += amplitude * valueNoise(p); p = p * 2.0 + vec2(17.3, 9.1); amplitude *= 0.5; } return sum;}// From cool to hot: dark red, the outer color, the inner color, then near white, each taking over a third of the rangevec3 hotPalette(float t) { vec3 color = mix(u_toLinear(outer) * 0.15, u_toLinear(outer), smoothstep(0.0, 0.35, t)); color = mix(color, u_toLinear(inner), smoothstep(0.3, 0.7, t)); return mix(color, vec3(1.0, 0.97, 0.9), smoothstep(0.75, 1.0, t));}vec4 effect(Inputs i) { // 0 at the wick, 1 at the tip float rise = (i.uv.y - 0.2) / height; vec2 scroll = vec2(i.uv.x * 6.0, i.uv.y * 4.0 - i.time * speed); float rising = fbm(scroll); float turbulence = rising - 0.5; // The tip is light and moves a lot; the base is anchored to the wick, so the sway grows with the square of the height float sway = (fbm(vec2(i.time * 0.7, 3.1)) - 0.5) * 0.12 * rise * rise; float x = i.uv.x - 0.5 - sway - turbulence * 0.08 * rise; // A teardrop: sqrt(rise) rounds the bottom and (1 - rise) closes the top to a point float halfWidth = width * sqrt(max(rise, 0.0)) * max(1.0 - rise, 0.0); float edge = abs(x) / max(halfWidth, 0.0001); float flame = (1.0 - smoothstep(0.5, 1.0, edge)) * step(0.0, rise) * step(rise, 1.0); // Hottest a little above the wick and in the middle, cooling toward the edge and the tip; the noise breaks the bands // into tongues. Right at the wick the vapor hasn't caught yet, so the heat fades in over the first fifth. float heat = flame * smoothstep(0.0, 0.2, rise) * clamp(1.1 - edge * 0.6 - rise * 0.8 + turbulence * 0.6, 0.0, 1.0); vec3 fire = hotPalette(heat) * flame; // Near the wick a real flame burns blue, where the vapor gets the most air float base = flame * (1.0 - smoothstep(0.0, 0.15, rise)) * (1.0 - heat * heat); fire = mix(fire, u_toLinear(baseColor), base); float halo = exp(-length(vec2(i.uv.x - 0.5, (i.uv.y - 0.2 - height * 0.4) * 0.6)) * 9.0) * 0.25; vec3 glow = (fire + u_toLinear(outer) * halo) * intensity; return vec4(glow, 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
- Flame (abre em uma nova aba)Referência
- Black-body radiation (abre em uma nova aba)Referência
- Palettes (abre em uma nova aba)Artigo
- Fractal Brownian Motion (abre em uma nova aba)Livro
- smoothstep (abre em uma nova aba)Referência
- mix (abre em uma nova aba)Referência
- CanvasItem shaders (abre em uma nova aba)Documentação
- Shading language (abre em uma nova aba)Documentação