Pular para o conteúdo
Distorcer · Redemoinho, compressão e lenteIniciante

Redemoinho d'água

Braços de espuma que espiralam até um olho escuro, desenhados num quad plano girando mais o ângulo quanto mais perto do centro.

UVTempoSuperfície
Abrir no Playground
  1. 01ObjetivoDistorcer
  2. 02TécnicaRedemoinho, compressão e lente
  3. 03ExecuçãoRedemoinho d'água
Quad de tela cheia

Parâmetros

6.0
0.45
1.5
5
#051a2e
#1a7399
#d9f2ff

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

    01 Distância do centro

  2. atanATan2atanatanatan

    02 Ângulo em volta

  3. *Multiply***

    03 Torcer pela distância

  4. cosCoscoscoscos

    04 Braços no ângulo girado

  5. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    05 Linhas de espuma

  6. mixMixmixmixmix

    06 Água e espuma

Como funciona

Um redemoinho é um padrão desenhado em coordenadas polares. Braços retos são fáceis ali: tudo o que depende só do ângulo sai do centro como os raios de uma roda. Girar esse ângulo numa quantidade que cresce em direção ao centro curva os raios em espirais.

1. Distância do centro. length(uv - 0.5) vale 0 no meio do quad e 0.5 no meio de cada borda. A prévia a mostra como uma rampa cinza que clareia para fora a partir de um centro preto.

2. Ângulo em volta. atan(p.y, p.x) é a direção do pixel vista do centro, de -π a π. A prévia o mostra como ele é, limitado a 0..1: preto abaixo do centro, onde o ângulo é negativo, e depois uma rampa a partir da borda direita que fica branca depois de um radiano.

3. Torcer pela distância. A queda 1.0 - smoothstep(0.0, reach, radius) vale 1 no centro e 0 no alcance e além dele. Elevada ao quadrado e multiplicada pela torção, ela diz quantos radianos cada anel gira. O quadrado concentra o giro perto do olho, como num vórtice de verdade, onde a água perto do ralo gira mais rápido. Fora do alcance o giro é exatamente 0, então a água calma encontra a espiral sem emenda. A prévia é um disco branco que escurece até o preto no alcance: dentro de cerca de metade do alcance o giro passa de um radiano.

4. Braços no ângulo girado. cos(turned * arms) se repete um número inteiro de vezes em volta do círculo, e cada repetição é um braço. O ângulo girado é o ângulo original mais a torção mais o tempo. Como a torção muda com o raio, a crista de cada braço fica num ângulo diferente em cada anel, e as cristas formam espirais. O número de braços é arredondado com floor: com um número fracionário, os braços não encontrariam a si mesmos na emenda onde o atan salta de π para -π, e apareceria um corte.

5. Linhas de espuma. smoothstep(0.55, 1.0, arms) mantém só a crista de cada braço, uma linha fina e clara. Mais dois smoothsteps apagam a espuma onde a queda termina, para a água calma de fora não ter nenhuma, e bem perto do centro, onde os braços se amontoam num ponto que cintilaria.

6. Água e espuma. Uma rampa da água funda para a da superfície, pela distância, escurece o olho, e a espuma é misturada por cima.

Por que o giro é somado fora da torção

O tempo é somado ao ângulo sozinho, não multiplicado na torção. Isso gira cada anel no mesmo ângulo a cada segundo, então o padrão inteiro roda como um corpo rígido. Se o giro crescesse em direção ao centro como a torção, os anéis de dentro ganhariam dos de fora a cada quadro e os braços se enrolariam cada vez mais, até cada braço ficar mais fino que um pixel e o centro virar ruído. A água de um redemoinho de verdade anda mais rápido perto do olho; a imagem aguenta porque os braços aqui são um padrão, não partículas de água acompanhadas.

Ajustes e limites

Cerca de 50 operações matemáticas por pixel, nenhuma leitura de textura, uma passada. O atan é a mais cara, um polinômio curto na maioria das GPUs; o resto é aritmética barata.

  • A torção define quantas voltas os braços dão entre o alcance e o olho: cerca de torção / 2π. Acima de uns 12 as voltas de dentro ficam mais finas que a linha de espuma e começam a cintilar; alargue o smoothstep da espuma ou desfoque o olho.
  • Uma torção ou velocidade negativa gira o vórtice para o outro lado.
  • O centro é uma singularidade. No centro exato o ângulo é indefinido; atan(0, 0) devolve 0 na maioria das GPUs, mas isso não é garantido. A espuma some antes de chegar lá, o que esconde isso e os braços amontoados.
  • Para girar uma textura em vez de desenhar braços: gire o ângulo do mesmo jeito, reconstrua uma coordenada com 0.5 + radius * vec2(cos(turned), sin(turned)) e amostre ali. É o nó de twirl dos editores de nós.

Monte no GLSL

O shader copiado é um shader de fragmento WebGL2 completo para um quad: ele declara as entradas, um uniform por parâmetro e u_time, e escreve a cor codificada em sRGB.

  1. Desenhe um quad quadrado cujo shader de vértice repasse a UV, de 0..1 a partir do canto inferior esquerdo, como v_uv.
  2. Compile o código copiado como shader de fragmento e ligue-o a esse shader de vértice.
  3. A cada quadro, ponha em u_time os segundos desde o início e desenhe. Defina os uniforms p_ uma vez, ou quando um controle mudar; as cores vão em linear.
  4. Para pôr o vórtice em outro lugar, troque o 0.5 de i.uv - 0.5 por um uniform vec2; para aumentá-lo ou diminuí-lo, mude o alcance.

Para um redemoinho sobre a sua própria água, devolva a quantidade de espuma como alfa em vez de misturá-la sobre a água, e desenhe com gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA).

Monte no Godot

O shader é um shader canvas_item que assume o HDR 2D desligado (o padrão do projeto); ele decodifica as cores e codifica o resultado por conta própria.

O shader exportado desenha em qualquer nó 2D com material. Um ColorRect quadrado é o mais simples: ele não tem textura própria e a UV dele o cobre de canto a canto.

  1. Adicione um ColorRect a uma cena 2D ou a uma interface e dê a ele um tamanho quadrado, como 512 por 512.
  2. No Inspector, ponha em Material um novo ShaderMaterial, e no Shader dele um novo Shader.
  3. Cole o código copiado no shader. Os braços começam a girar na hora: o TIME do Godot alimenta i.time.
  4. Ajuste os parâmetros em Shader Parameters: os mesmos nomes deste cartão, sem o prefixo p_.

Para montá-lo como VisualShader (um VisualShader canvas_item no mesmo material):

  1. UV menos uma constante vec2 de 0.5: a coordenada centrada. Inverta o y dela antes com um OneMinus se a espiral precisar girar para o mesmo lado daqui, porque a UV do Godot começa no canto superior esquerdo.
  2. Length dela: o raio. VectorDecompose e ATan2 com y primeiro: o ângulo.
  3. SmoothStep do raio de 0 até o alcance, depois OneMinus, depois Multiply por ele mesmo e pela torção: o giro.
  4. Add do giro e de Time vezes a velocidade ao ângulo, Multiply pelo número de braços e o Cos disso, remapeado para 0..1: os braços.
  5. SmoothStep nos braços para a espuma, depois dois nós Mix: da água funda para a da superfície pelo raio, e a espuma por cima, em Color.

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

whirlpool.fragGLSL
#version 300 es// Swirl, pinch & bulge → Whirlpool, 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_twist; // Twist: 6.0 (-15.0 to 15.0)uniform float p_reach; // Reach: 0.45 (0.1 to 0.7)uniform float p_speed; // Spin speed: 1.5 (-6.0 to 6.0)uniform float p_armCount; // Arms: 5.0 (1.0 to 12.0)uniform vec3 p_deep; // Deep water: vec3(0.02, 0.1, 0.18), sRGB; upload it decoded to linearuniform vec3 p_shallow; // Surface water: vec3(0.1, 0.45, 0.6), sRGB; upload it decoded to linearuniform vec3 p_foam; // Foam: vec3(0.85, 0.95, 1.0), 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));}vec4 effect(Inputs i) {	vec2 p = i.uv - 0.5;	float radius = length(p);	float angle = atan(p.y, p.x);	// 1 at the center, 0 at p_reach and beyond: the water outside the whirlpool isn't turned	float falloff = 1.0 - smoothstep(0.0, p_reach, radius);	// Squared, so the twist builds up sharply toward the eye, as in a real vortex	float swirl = falloff * falloff * p_twist;	// The spin turns every radius by the same angle: a spin that grew toward the center would wind the arms tighter	// every second, until they were thinner than a pixel	float turned = angle + swirl + i.time * p_speed;	// A whole number of arms, so cos() meets itself where atan() jumps from pi to -pi	float arms = 0.5 + 0.5 * cos(turned * floor(p_armCount));	float froth = smoothstep(0.55, 1.0, arms) * smoothstep(0.0, 0.3, falloff) * smoothstep(0.03, 0.15, radius);	float depth = smoothstep(0.02, p_reach * 0.7, radius);	vec3 water = mix(p_deep, p_shallow, depth);	vec3 color = mix(water, p_foam, froth * 0.8);	return vec4(color, 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);}
whirlpool.gdshaderShader do Godot
// Swirl, pinch & bulge → Whirlpool, 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 twist : hint_range(-15.0, 15.0, 0.1) = 6.0;uniform float reach : hint_range(0.1, 0.7, 0.01) = 0.45;uniform float speed : hint_range(-6.0, 6.0, 0.1) = 1.5;uniform float armCount : hint_range(1.0, 12.0, 1.0) = 5.0;uniform vec3 deep : source_color = vec3(0.02, 0.1, 0.18);uniform vec3 shallow : source_color = vec3(0.1, 0.45, 0.6);uniform vec3 foam : source_color = vec3(0.85, 0.95, 1.0);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));}vec4 effect(Inputs i) {	vec2 p = i.uv - 0.5;	float radius = length(p);	float angle = atan(p.y, p.x);	// 1 at the center, 0 at reach and beyond: the water outside the whirlpool isn't turned	float falloff = 1.0 - smoothstep(0.0, reach, radius);	// Squared, so the twist builds up sharply toward the eye, as in a real vortex	float swirl = falloff * falloff * twist;	// The spin turns every radius by the same angle: a spin that grew toward the center would wind the arms tighter	// every second, until they were thinner than a pixel	float turned = angle + swirl + i.time * speed;	// A whole number of arms, so cos() meets itself where atan() jumps from pi to -pi	float arms = 0.5 + 0.5 * cos(turned * floor(armCount));	float froth = smoothstep(0.55, 1.0, arms) * smoothstep(0.0, 0.3, falloff) * smoothstep(0.03, 0.15, radius);	float depth = smoothstep(0.02, reach * 0.7, radius);	vec3 water = mix(u_toLinear(deep), u_toLinear(shallow), depth);	vec3 color = mix(water, u_toLinear(foam), froth * 0.8);	return vec4(color, 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