Borda em brasa
Uma superfície devorada por buracos que crescem, cada um cercado por uma faixa carbonizada e uma linha fina de brasa.
- 01ObjetivoRevelar
- 02TécnicaDissolução
- 03ExecuçãoBorda em brasa
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.
tiledNoiseTexture2DtiledNoisetiledNoisetiledNoise UVTempo01 Ruído
mixMixmixmixmix Entrada02 Limiar
stepStepstepstepstep Entrada03 Corte
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada04 Faixa carbonizada
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada05 Faixa de brasa
mixMixmixmixmix Entrada06 Cor
Como funciona
O card abre buracos numa esfera. Arraste Queimado e os buracos crescem de alguns pontinhos até a superfície inteira; cada um é cercado por uma faixa escura carbonizada e, bem na borda, uma linha fina de brasa. Tudo sai de um único valor de ruído por pixel e de três limiares sobre ele.
1. Ruído
Ruído Uma função pseudoaleatória e suave da posição. Valores ou gradientes aleatórios ficam numa grade e são misturados entre os pontos, então pontos próximos recebem valores parecidos e o resultado não tem padrão visível. Hash Uma função que embaralha a entrada num valor que parece aleatório, mas é sempre o mesmo para a mesma entrada. Shaders a usam para ter aleatoriedade por pixel e por célula. Coordenadas UV Uma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.tiledNoise é o ruído de valor do fundamento de ruídoUma função pseudoaleatória e suave da posição. Valores ou gradientes aleatórios ficam numa grade e são misturados entre os pontos, então pontos próximos recebem valores parecidos e o resultado não tem padrão visível.h = fract(sin(dot(p, k)) × big) nos
quatro cantos de cada célula da grade, misturados com a curva do smoothstep. Ele roda sobre o UVUma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.uv = gl_FragCoord.xy / resolutionhashCell, e não o hash de seno de uma linha: ele dá os mesmos bits em qualquer GPU, então os buracos abrem nos
mesmos lugares na web e no Godot. A mistura de inteiros dele é a maior parte da conta do card, cerca de 90 das suas
125 operações.
Uma mudança o adapta à esfera. A coordenada u da esfera dá uma volta em torno dela e salta de 1 de volta para 0 num dos
lados, e um ruído comum saltaria junto, desenhando uma emenda de polo a polo. Aqui cada índice de célula passa por
cell % period antes do hash, então a última coluna de células reaproveita os cantos da primeira e o ruído dá a
volta sem quebra. O período tem o dobro de células na volta em relação ao sentido de polo a polo porque a volta da
esfera é duas vezes maior que a distância de cima a baixo, o que mantém as manchas redondas em vez de esticadas.
Quantidade de manchas define o número de células.
2. Limiar
Queimado vai de 0 a 1, mas o limiar com que o ruído é comparado vai de um pouco abaixo de 0 até 1. Em 0, a faixa
da borda ficaria em cima dos valores mais baixos do ruído e apareceria como pontinhos escuros; começar abaixo de 0
mantém a superfície intacta limpa. Em 1, o limiar fica acima de qualquer valor que o ruído alcança, então não sobra
nada. mix(-3.0 * p_width, 1.0, p_amount) faz esse remapeamento numa chamada só.
3. Corte
Recorte por alfa Descartar todo fragmento cujo alfa fica abaixo de um limiar e desenhar o resto totalmente opaco. A superfície continua na passagem opaca, grava profundidade e não precisa de ordenação, mas as bordas ficam duras.step(n, threshold) vale 1 onde o ruído é menor ou igual ao limiar: os pixels que já se foram. O shader devolve
1.0 - burnt como alfa, e a passagem do card tem um recorte por alfa em 0,5, então esses pixels são descartados em vez
de desenhados transparentes. O recorte por alfaDescartar todo fragmento cujo alfa fica abaixo de um limiar e desenhar o resto totalmente opaco. A superfície continua na passagem opaca, grava profundidade e não precisa de ordenação, mas as bordas ficam duras.if (alpha < threshold) discard;
4. Faixa carbonizada
Os pixels logo acima do limiar são os próximos a sumir. smoothstep(threshold, threshold + 3.0 * p_width, n) sobe de
0 na borda do buraco até 1 um pouco mais para dentro, então um menos ele é uma faixa que vale 1 na borda e se dissolve
na superfície. Misturar a cor da superfície em direção a quase preto com ela dá o halo chamuscado em volta de cada
buraco.
5. Faixa de brasa
O mesmo 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. Smoothstep 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)
6. Cor
O último mix pinta a cor da brasa sobre a superfície carbonizada ao longo dessa faixa fina. A prévia não tem
iluminação, então a brasa já parece brilhar; numa engine, mandar a faixa também para a emissão deixa o bloom pegá-la.
Todas as faixas seguem o ruído, então elas se movem juntas conforme o limiar sobe, e só o formato dos buracos decide onde está o fogo.
Monte no GLSL
O botão de copiar entrega um shader de fragmento WebGL2 completo: o efeito, os Inputs que ele lê e um main() que
descarta os pixels queimados. Junte-o a um shader de vértice que escreva o UV da malha em v_uv. Ele ainda precisa declarar
e escrever v_position, v_normal e v_view, que o main() lê, mas zeros servem: o efeito só usa uv. Desenhe a
malha com o estado que o card usa:
gl.enable(gl.DEPTH_TEST);
gl.depthMask(true);
gl.disable(gl.BLEND);
gl.enable(gl.CULL_FACE); // faces de trás descartadas: os buracos mostram o que está atrás
Atualize p_amount a cada quadro pela sua própria animação. Uma queima que leva dois segundos depois de um golpe é uma
rampa limitada:
const amount = Math.min((now - hitTime) / 2000, 1);
gl.uniform1f(gl.getUniformLocation(program, "p_amount"), amount);
Os uniforms de cor são lineares: converta os valores sRGB de um seletor de cor antes de enviá-los, como faz o
renderizador deste site. A saída é limitada e codificada em sRGB no main(), então desenhe direto no canvas.
Monte no Godot
Num VisualShader, ligue um nó Texture2D com um NoiseTexture2D (Seamless ativado) no lugar do tiledNoise, monte o limiar e as duas faixas com nós Mix e SmoothStep, e conecte o alfa em Alpha e um FloatConstant de 0,5 em Alpha Scissor Threshold no nó de saída.
Crie um ShaderMaterial no MeshInstance3D, cole o shader num novo Shader, e a malha queima na quantidade padrão. O shader
escreve ALPHA_SCISSOR_THRESHOLD, o que mantém o material no pipeline opaco. Numa malha fina e aberta, como uma carta
ou uma bandeira, acrescente cull_disabled ao render_mode para o verso queimar também.
Anime o uniform amount por um script. Um Tween é o jeito mais curto de queimar um inimigo em dois segundos e
liberá-lo em seguida:
func burn_away() -> void:
var material := $Mesh.get_active_material(0) as ShaderMaterial
var tween := create_tween()
tween.tween_method(func(v: float) -> void: material.set_shader_parameter("amount", v), 0.0, 1.0, 2.0)
tween.tween_callback(queue_free)
Cada instância que queima no seu próprio ritmo precisa do seu próprio valor. Duplique o material por instância
(material.duplicate()), ou troque uniform float amount por instance uniform float amount e defina-o com
$Mesh.set_instance_shader_parameter("amount", v), o que mantém um único material compartilhado.
Os parâmetros de cor usam source_color, então o seletor de cor do inspetor entrega sRGB e o Godot decodifica para
linear antes de o efeito ver, igual a este site. Com o Glow ligado no WorldEnvironment e a brasa escrita também em
EMISSION, a borda brilha.
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// Dissolve → Burning edge, 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_amount; // Burnt: 0.4 (0.0 to 1.0)uniform float p_scale; // Blotch count: 10.0 (2.0 to 16.0)uniform float p_width; // Edge width: 0.04 (0.005 to 0.15)uniform vec3 p_base; // Surface: vec3(0.62, 0.66, 0.72), sRGB; upload it decoded to linearuniform vec3 p_glow; // Ember: vec3(1.0, 0.55, 0.12), 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));}// The integer hash from the hash fundamental (MurmurHash3's finalizer, public domain): the same bits on every GPU,// where the one-line sine hash depends on how precisely each GPU computes sin of a large argumentuint mix32(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}float hashCell(ivec2 cell) { uint h = mix32(uint(cell.x) ^ mix32(uint(cell.y))); return float(h >> 8u) / 16777216.0;}// Value noise whose cells wrap every `period` cells. The sphere's u runs once around it, so without the wrap the noise// would jump where u goes from 1 back to 0 and draw a seam down its side. The wrap is integer %, which is exact where// float mod can round 20/20 below 1 and bring the seam back; cells are never negative on a 0..1 UV, so % is defined.float tiledNoise(vec2 p, ivec2 period) { ivec2 cell = ivec2(floor(p)); vec2 f = fract(p); float a = hashCell(cell % period); float b = hashCell((cell + ivec2(1, 0)) % period); float c = hashCell((cell + ivec2(0, 1)) % period); float d = hashCell((cell + ivec2(1, 1)) % period); vec2 u = f * f * (3.0 - 2.0 * f); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);}vec4 effect(Inputs i) { // Around the sphere is twice as far as pole to pole, so twice the cells keep the blotches round ivec2 period = ivec2(2, 1) * int(p_scale); float n = tiledNoise(i.uv * vec2(period), period); // Starts below 0 so that at amount 0 not even the charred band shows, and reaches 1, past every noise value float threshold = mix(-3.0 * p_width, 1.0, p_amount); float burnt = step(n, threshold); float charred = 1.0 - smoothstep(threshold, threshold + 3.0 * p_width, n); float edge = 1.0 - smoothstep(threshold, threshold + p_width, n); vec3 surface = mix(p_base, p_base * 0.08, charred); vec3 color = mix(surface, p_glow, edge); return vec4(color, 1.0 - burnt);}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); // A cut-out: fragments under the threshold are dropped and the rest stay opaque if (frag_out.a < 0.5) discard; // 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);}// Dissolve → Burning edge, Godot 4. From Creative Center, MIT license.shader_type spatial;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform float amount : hint_range(0.0, 1.0, 0.01) = 0.4;uniform float scale : hint_range(2.0, 16.0, 1.0) = 10.0;uniform float width : hint_range(0.005, 0.15, 0.005) = 0.04;uniform vec3 base : source_color = vec3(0.62, 0.66, 0.72);uniform vec3 glow : source_color = vec3(1.0, 0.55, 0.12);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// The integer hash from the hash fundamental (MurmurHash3's finalizer, public domain): the same bits on every GPU,// where the one-line sine hash depends on how precisely each GPU computes sin of a large argumentuint mix32(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}float hashCell(ivec2 cell) { uint h = mix32(uint(cell.x) ^ mix32(uint(cell.y))); return float(h >> 8u) / 16777216.0;}// Value noise whose cells wrap every `period` cells. The sphere's u runs once around it, so without the wrap the noise// would jump where u goes from 1 back to 0 and draw a seam down its side. The wrap is integer %, which is exact where// float mod can round 20/20 below 1 and bring the seam back; cells are never negative on a 0..1 UV, so % is defined.float tiledNoise(vec2 p, ivec2 period) { ivec2 cell = ivec2(floor(p)); vec2 f = fract(p); float a = hashCell(cell % period); float b = hashCell((cell + ivec2(1, 0)) % period); float c = hashCell((cell + ivec2(0, 1)) % period); float d = hashCell((cell + ivec2(1, 1)) % period); vec2 u = f * f * (3.0 - 2.0 * f); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);}vec4 effect(Inputs i) { // Around the sphere is twice as far as pole to pole, so twice the cells keep the blotches round ivec2 period = ivec2(2, 1) * int(scale); float n = tiledNoise(i.uv * vec2(period), period); // Starts below 0 so that at amount 0 not even the charred band shows, and reaches 1, past every noise value float threshold = mix(-3.0 * width, 1.0, amount); float burnt = step(n, threshold); float charred = 1.0 - smoothstep(threshold, threshold + 3.0 * width, n); float edge = 1.0 - smoothstep(threshold, threshold + width, n); vec3 surface = mix(base, base * 0.08, charred); vec3 color = mix(surface, glow, edge); return vec4(color, 1.0 - burnt);}void fragment() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // VERTEX, NORMAL and VIEW are in view space here; the effect works in world space i.position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; i.normal = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); i.view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz); 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; // A cut-out: Godot drops fragments under the threshold and keeps the rest opaque ALPHA = frag_out.a; ALPHA_SCISSOR_THRESHOLD = 0.5;}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
- Noise (abre em uma nova aba)Livro
- step (abre em uma nova aba)Referência
- smoothstep (abre em uma nova aba)Referência
- Standard Material 3D and ORM Material 3D (abre em uma nova aba)Documentação
- Spatial shaders (abre em uma nova aba)Documentação
- Blending (abre em uma nova aba)Artigo