Pular para o conteúdo
Revelar · DissoluçãoIniciante

Borda em brasa

Uma superfície devorada por buracos que crescem, cada um cercado por uma faixa carbonizada e uma linha fina de brasa.

UVTempoSuperfície
Abrir no Playground
  1. 01ObjetivoRevelar
  2. 02TécnicaDissolução
  3. 03ExecuçãoBorda em brasa
Malha de teste

Parâmetros

0.40
10
0.040
#9ea8b8
#ff8c1f

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. tiledNoiseTexture2DtiledNoisetiledNoisetiledNoise
    UV
    Tempo

    01 Ruído

  2. mixMixmixmixmix

    02 Limiar

  3. stepStepstepstepstep

    03 Corte

  4. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    04 Faixa carbonizada

  5. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    05 Faixa de brasa

  6. mixMixmixmixmix

    06 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

tiledNoise é o ruído de valor do fundamento de ruído: um hash nos quatro cantos de cada célula da grade, misturados com a curva do smoothstep. Ele roda sobre o UV da malha, então as manchas ficam presas à superfície enquanto ela gira. O hash é o inteiro do fundamento de hash, hashCell, 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

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 alfa mantém a esfera na passagem opaca: ela grava profundidade, é ordenada como qualquer objeto sólido e projeta uma sombra correta numa engine. O preço é uma borda dura, do tamanho de um pixel, que a faixa de brasa disfarça. As faces de trás continuam descartadas, então os buracos deixam ver através da esfera o que está atrás dela; uma malha fina e aberta, como uma carta ou uma bandeira, desliga esse descarte, para o verso queimar também.

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 smoothstep num intervalo três vezes mais estreito dá uma linha fina bem na borda. Largura da borda escala as duas faixas juntas, então a carbonizada é sempre mais larga que a de brasa.

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

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