Pular para o conteúdo
Revelar · Varredura no espaço do mundoIntermediário

Pulso de scanner

Um anel de luz que se expande a partir de um ponto do mundo e deixa uma grade que se apaga em toda superfície por onde passou.

PosiçãoTempoSuperfície
Abrir no Playground
  1. 01ObjetivoRevelar
  2. 02TécnicaVarredura no espaço do mundo
  3. 03ExecuçãoPulso de scanner
Malha de teste

Parâmetros

2.5
2.8
0.35
0.30
#4dd9ff
1.50

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. distanceDistance3Ddistancedistancedistance
    Posição
    Tempo

    01 Distância do scanner

  2. -Subtract---

    02 Atrás do pulso

  3. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    03 Frente do pulso

  4. expExpexpexpexp

    04 Rastro que se apaga

  5. fractFractfractfractfract

    05 Grade do mundo

  6. *Multiply***

    06 Brilho

Como funciona

O card põe um scanner logo abaixo da esfera de teste, como se estivesse no chão embaixo dela, e solta um pulso a cada Segundos por pulso. A esfera fica invisível até o pulso chegar: uma casca de luz forte sobe por ela, e atrás da casca uma grade desenhada em unidades do mundo brilha por um instante e se apaga. Tudo sai de um único número por pixel, a distância dele até o scanner.

1. Distância do scanner

i.position é o ponto da superfície no espaço do mundo, que o shader de vértice passou adiante como um varying (veja a página da técnica). distance(i.position, origin) é o quanto esse ponto está longe do scanner. A posição do scanner aqui é uma constante para a prévia não precisar de script; num jogo, é um uniform que o script do scanner atualiza a cada quadro, e nada mais muda. Como a distância é medida no mundo, não importa a que objeto o pixel pertence: um chão, uma parede e uma caixa na mesma distância acendem juntos.

2. Atrás do pulso

O raio do pulso cresce com o tempo: fract(i.time / p_period) * p_range sobe de 0 até Alcance unidades do mundo uma vez por período e recomeça. radius - dist é positivo onde o pulso já passou pela superfície e negativo à frente dele, e o tamanho dele diz há quanto tempo, em unidades do mundo. Os próximos três passos são todos curvas de modelagem desse único número.

3. Frente do pulso

A frente é uma casca fina bem no raio. Um smoothstep sobe de 0 a 1 nos últimos 0,02 de unidade antes dele, outro desce de volta a 0 nos 0,08 depois, e o produto dos dois é um pico estreito e um pouco torto: afiado do lado que avança, mais suave do lado de trás, como uma onda de choque.

4. Rastro que se apaga

Atrás da frente, exp(-behind / p_decay) começa em 1 e cai pela metade a cada 0,7 × Comprimento do rastro unidades, mais ou menos, então a área que o pulso acabou de cobrir brilha e depois se apaga suavemente. step(0.0, behind) limita o rastro à área atrás da frente; à frente do pulso não há rastro.

5. Grade do mundo

Só o rastro iluminaria a superfície por igual. Multiplicá-lo por uma grade faz ele parecer uma varredura. gridLines confere o quanto cada coordenada da posição no mundo está perto de um múltiplo do Espaçamento da grade: fract(p / spacing + 0.5) - 0.5 é a distância com sinal até o plano da grade mais próximo, em células, e um smoothstep transforma “a menos de 8% de uma célula” numa linha. A grade também está no espaço do mundo, então as linhas têm o mesmo tamanho em todo objeto e se alinham entre eles; elas não esticam com o UV do objeto.

6. Brilho

A frente mais o rastro com a grade, vezes Cor e Intensidade, é a luz que o pulso emite. A passagem é aditiva e o papel de saída do card é emissão, então a luz é somada por cima do que estiver atrás, e onde o pulso não está a superfície não soma nada e continua invisível. Um último fator apaga o pulso nos últimos 40% do alcance, para que o salto do raio de volta a 0 nunca apareça.

Monte no GLSL

O botão de copiar entrega um shader de fragmento WebGL2 completo. O main() dele escreve a luz pré-multiplicada pelo alfa, com o canal mais claro como alfa, então desenhe-o com mesclagem aditiva e sem gravar profundidade, depois da geometria opaca:

gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
gl.depthMask(false);
gl.depthFunc(gl.LEQUAL); // quando desenha por cima da mesma malha

A única coisa que o shader de vértice precisa acertar é a posição. O efeito mede distâncias no mundo, então v_position precisa ser o vértice transformado pela matriz do modelo e nada mais:

uniform mat4 u_model;
uniform mat4 u_viewProjection;
layout(location = 0) in vec3 a_position;
out vec3 v_position;
// ...declare e escreva também v_uv, v_normal e v_view; o main() os lê

void main() {
	vec4 world = u_model * vec4(a_position, 1.0);
	v_position = world.xyz;
	gl_Position = u_viewProjection * world;
}

Numa cena, o scanner se move. Troque a linha const vec3 origin por um uniform e defina-o pelo código do jogo; toda malha que usa o shader passa a compartilhar um único pulso:

uniform vec3 origin;

Para disparar um pulso sob demanda em vez de um loop, troque o i.time do raio por um uniform seu que conte os segundos desde que a varredura começou.

Monte no Godot

Num VisualShader, a posição no mundo é a entrada Vertex multiplicada pela entrada InvViewMatrix (um TransformVectorMult com w = 1); um Distance3D até a posição do scanner, um Subtract do raio, dois nós SmoothStep e um Exp montam o pulso, e o resultado vai para Emission, com blend_add no render mode do shader.

O shader foi feito para ir por cima do material do objeto, e não para substituí-lo. Selecione um MeshInstance3D e, na seção Geometry, dê a ele um novo ShaderMaterial como Material Overlay, depois cole o shader nele. O overlay é desenhado depois dos materiais da própria malha com blend_add, então a luz do pulso é somada por cima do objeto iluminado e não soma nada onde o pulso não está. O shader escreve um albedo preto e o pulso em EMISSION, então, com o Glow ligado no WorldEnvironment, a frente brilha.

A posição do scanner é uma constante no shader. Para movê-la, e para compartilhá-la com todos os objetos de uma vez, transforme-a num uniform global. Adicione scanner_origin (um vec3) em Project Settings > Globals > Shader Globals, depois troque a linha const vec3 origin por

global uniform vec3 scanner_origin;

renomeie origin para scanner_origin em effect() e defina-o pelo script do scanner:

func _process(_delta: float) -> void:
	RenderingServer.global_shader_parameter_set("scanner_origin", global_position)

Todo material que lê o global segue o mesmo scanner, sem script nenhum por objeto. Para um pulso disparado sob demanda em vez de um loop, adicione um segundo global, scan_age (um float), que o script do scanner zera quando uma varredura começa e avança em delta a cada quadro, e use-o no lugar de i.time. Um script não consegue ler o TIME do shader, então a idade precisa vir do relógio do próprio script. Perto do fim de um período o pulso já se apagou; segure a idade logo abaixo do período até a próxima varredura, senão o raio volta a 0 e o pulso recomeça.

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

scanner-pulse.fragGLSL
#version 300 es// World-space scan → Scanner pulse, 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_period; // Seconds per pulse: 2.5 (0.5 to 6.0)uniform float p_range; // Range: 2.8 (1.0 to 6.0)uniform float p_decay; // Trail length: 0.35 (0.05 to 2.0)uniform float p_spacing; // Grid spacing: 0.3 (0.05 to 1.0)uniform vec3 p_color; // Color: vec3(0.3, 0.85, 1.0), sRGB; upload it decoded to linearuniform float p_intensity; // Intensity: 1.5 (0.0 to 4.0)in 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 scanner sits just under the test sphere, as if on the ground below it. In a game this is a uniform the script// sets to the scanner's world position.const vec3 origin = vec3(0.0, -1.2, 0.0);// 1 on the world planes where a coordinate is a whole multiple of the spacing, fading out within 8% of a cellfloat gridLines(vec3 p) {	vec3 offset = abs(fract(p / p_spacing + 0.5) - 0.5);	vec3 line = 1.0 - smoothstep(vec3(0.0), vec3(0.08), offset);	return max(line.x, max(line.y, line.z));}vec4 effect(Inputs i) {	float radius = fract(i.time / p_period) * p_range;	float dist = distance(i.position, origin);	// Positive where the pulse has already passed, negative ahead of it	float behind = radius - dist;	float front = smoothstep(-0.02, 0.0, behind) * (1.0 - smoothstep(0.0, 0.08, behind));	float trail = step(0.0, behind) * exp(-behind / p_decay);	float lines = gridLines(i.position);	// The pulse dies out before it restarts, so the jump back to radius 0 never shows	float fade = 1.0 - smoothstep(0.6 * p_range, p_range, radius);	vec3 glow = p_color * p_intensity * (front + trail * lines) * fade;	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	// Additive light over a transparent target: premultiplied, as bright as its alpha, so dark adds nothing	vec3 frag_light = u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)) * frag_out.a;	fragColor = vec4(frag_light, max(max(frag_light.r, frag_light.g), frag_light.b));}
scanner-pulse.gdshaderShader do Godot
// World-space scan → Scanner pulse, Godot 4. From Creative Center, MIT license.shader_type spatial;// Emission: a black base color, no specular or ambient light, and the effect as EMISSION, which glow catchesrender_mode specular_disabled, ambient_light_disabled, blend_add;uniform float period : hint_range(0.5, 6.0, 0.1) = 2.5;uniform float range : hint_range(1.0, 6.0, 0.1) = 2.8;uniform float decay : hint_range(0.05, 2.0, 0.05) = 0.35;uniform float spacing : hint_range(0.05, 1.0, 0.01) = 0.3;uniform vec3 color : source_color = vec3(0.3, 0.85, 1.0);uniform float intensity : hint_range(0.0, 4.0, 0.05) = 1.5;struct Inputs {	vec2 uv;	vec3 position;	vec3 normal;	vec3 view;	float time;	vec2 fragCoord;	vec2 resolution;};// The scanner sits just under the test sphere, as if on the ground below it. In a game this is a uniform the script// sets to the scanner's world position.const vec3 origin = vec3(0.0, -1.2, 0.0);// 1 on the world planes where a coordinate is a whole multiple of the spacing, fading out within 8% of a cellfloat gridLines(vec3 p) {	vec3 offset = abs(fract(p / spacing + 0.5) - 0.5);	vec3 line = 1.0 - smoothstep(vec3(0.0), vec3(0.08), offset);	return max(line.x, max(line.y, line.z));}vec4 effect(Inputs i) {	float radius = fract(i.time / period) * range;	float dist = distance(i.position, origin);	// Positive where the pulse has already passed, negative ahead of it	float behind = radius - dist;	float front = smoothstep(-0.02, 0.0, behind) * (1.0 - smoothstep(0.0, 0.08, behind));	float trail = step(0.0, behind) * exp(-behind / decay);	float lines = gridLines(i.position);	// The pulse dies out before it restarts, so the jump back to radius 0 never shows	float fade = 1.0 - smoothstep(0.6 * range, range, radius);	vec3 glow = color * intensity * (front + trail * lines) * fade;	return vec4(glow, 1.0);}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 = vec3(0.0);	EMISSION = frag_out.rgb;	ALPHA = frag_out.a;}

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