Pular para o conteúdo
Elementos · Fogo e chamasIntermediário

Vela

Uma pequena chama tremulante, com base azul e halo quente, desenhada como luz sobre o preto.

UVTempoSuperfície
Abrir no Playground
  1. 01ObjetivoElementos
  2. 02TécnicaFogo e chamas
  3. 03ExecuçãoVela
Quad de tela cheia

Parâmetros

0.60
0.25
2.5
1.20
#ff6b14
#ffd159
#1f40d9

Copie para a sua plataforma

  • GLSLCopiar
  • GodotCopiar

Cadeia de nós

Montada passo a passo, no estilo do GLSL, com os nomes de nós dele.

Montada passo a passo, no estilo do Godot, com os nomes de nós dele.

Este card ainda não tem versão para Unreal Engine 5, então os nós mostram os nomes do GLSL.

Este card ainda não tem versão para Unity, então os nós mostram os nomes do GLSL.

Este card ainda não tem versão para Blender, então os nós mostram os nomes do GLSL.

  1. fbm()Texture2Dfbm()fbm()fbm()
    UV
    Tempo

    01 Ruído subindo

  2. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    02 Máscara em gota

  3. clampClampclampclampclamp

    03 Calor

  4. mixMixmixmixmix

    04 Paleta quente

  5. *Multiply***

    05 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

  1. Ruído subindo. Um fBm de quatro oitavas lê uma coordenada cujo y subtrai time * 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.
  2. Máscara em gota. rise vai 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, o 1 - rise fecha 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 o x do 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 smoothstep da metade da largura até a largura inteira deixa a borda suave.
  3. 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.
  4. Paleta quente. hotPalette() transforma calor em cor com três mixes encadeados: 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.
  5. Halo e intensidade. Um brilho suave, o exp da 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 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 mistura aditiva: o preto não soma nada, então as bordas quadradas do quad somem, e o halo ilumina o que está atrás. No Godot isso é render_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.

  1. 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.
  2. A cada quadro, defina u_time em segundos (performance.now() / 1000) e u_resolution como o tamanho do canvas.
  3. 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.

  1. Adicione um ColorRect (ou um Sprite2D com 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.
  2. No slot Material dele, crie um New ShaderMaterial, depois um New Shader, e cole o código copiado.
  3. 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

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