Nuvens no céu
Cúmulos suaves cruzando um gradiente azul, feitos de três chamadas de fBm e um limiar.
- 01ObjetivoElementos
- 02TécnicaNuvens e fumaça (2D)
- 03ExecuçãoNuvens no céu
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.
mixMixmixmixmix UVTempo01 Gradiente do céu
fbm()Texture2Dfbm()fbm()fbm() Entrada02 Campo de distorção
fbm()Texture2Dfbm()fbm()fbm() Entrada03 fBm distorcido
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada04 Limiar
mixMixmixmixmix Entrada05 Sombrear os miolos
mixMixmixmixmix Entrada06 Nuvens sobre o céu
Como funciona
O card é a receita em quatro partes da sua técnica, com cada parte o menor possível: um ruído de valor, um fBm feito com ele, uma distorção, um limiar e três mixes.
O ruído por baixo
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.valueNoise() coloca um valor aleatório em cada canto de uma grade unitária e mistura os quatro cantos em volta do
pixel com uma transição suave (a mistura bilinear do fundamento de ruído de valor). Os valores aleatórios vêm de
hashCell(), um hashUma 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.h = fract(sin(dot(p, k)) × big) inteiro: ele embaralha os bits das coordenadas da célula, então a mesma
célula dá o mesmo valor em qualquer GPU. O hash comum de uma linha, fract(sin(…)), depende da precisão com que a GPU
calcula o seno de um número grande, e aqui as coordenadas crescem sem parar com o tempo, então depois de um tempo ele
começaria a mostrar listras.
Oitava Uma camada de ruído numa soma fractal. Cada oitava costuma ter o dobro da frequência e metade da amplitude da anterior, acrescentando detalhes mais finos e mais fracos.fbm() soma cinco oitavasUma camada de ruído numa soma fractal. Cada oitava costuma ter o dobro da frequência e metade da amplitude da anterior, acrescentando detalhes mais finos e mais fracos.fbm(p) = Σ gainᵏ · noise(lacunarityᵏ · p) desse ruído, cada uma com o dobro da frequência e metade da força da
anterior. Entre uma oitava e outra, a coordenada também gira cerca de 37 graus. O ruído de valor tem suas formas
alinhadas à grade, e sem o giro a grade de cada oitava se alinharia com as outras e apareceria como fileiras fracas,
horizontais e verticais. O resultado fica em torno de 0,5, raramente abaixo de 0,2 ou acima de 0,8.
Passo a passo
- Gradiente do céu.
mix(horizon, zenith, sqrt(uv.y))mistura as duas cores do céu pela altura. A raiz quadrada puxa a mistura para baixo, então a maior parte do quadro fica com o azul mais fundo do alto e a faixa clara fica perto do horizonte, como num céu de verdade. - Campo de distorção. Duas chamadas de fBm em deslocamentos sem relação entre si dão um vetor 2D aleatório e suave em cada pixel, mostrado em vermelho e verde. Antes, a coordenada foi achatada na vertical por 1,8, porque nuvens vistas de baixo parecem mais largas que altas.
- fBm distorcido. O terceiro fBm lê a coordenada empurrada pelo campo de distorção vezes Distorção, mais o vento. Essa é a densidade da nuvem. O campo de distorção anda mais devagar que o vento e em outra direção, então as formas se dobram e mudam enquanto se movem, em vez de deslizar pela tela como uma imagem. Com Distorção em 0 você vê exatamente essa imagem deslizando.
- Limiar.
smoothstep(threshold, threshold + softness, density)transforma a densidade em cobertura, comthreshold = 1 - coverage. Abaixo dele é céu limpo, acima é nuvem sólida, e Suavidade da borda é a largura da transição: pequena para cúmulos nítidos, grande para névoa. - Sombrear os miolos. Um segundo smoothstep, mais largo, da mesma densidade diz quão espessa é a nuvem. Os miolos espessos vão em direção a Sombra da nuvem, as bordas finas ficam com Nuvem iluminada, que é como uma nuvem iluminada de cima aparece vista do chão.
- Nuvens sobre o céu. Um último mix coloca a nuvem sombreada sobre o gradiente pela cobertura.
Custo
Cada amostra de ruído são quatro hashes e três mixes, cerca de 90 operações; uma oitava acrescenta o giro e a soma, então cerca de 95. Três chamadas de fBm com cinco oitavas são quinze oitavas, cerca de 1.425 operações, e o gradiente, o limiar e os mixes somam umas 15 a mais. Nenhuma leitura de textura, uma passada. Tudo bem para um fundo numa GPU de desktop, e pesado para a tela inteira num celular. As saídas baratas, em ordem: reduzir o campo de distorção a três oitavas (ele só precisa ser suave), renderizar o céu em meia resolução, ou gerar a densidade numa textura que se repete e manter só o limiar e o sombreamento ao vivo.
Precisão
O vento soma time * speed à coordenada. Depois de algumas horas esse número fica grande o bastante para que a parte
fracionária que fract() devolve perca precisão, e as nuvens ficam quadriculadas. Num jogo que roda por muito tempo,
faça o tempo que você envia dar a volta (por exemplo, módulo 1000 segundos) e aceite um salto de vez em quando, ou mova
as nuvens com um deslocamento uniforme que o próprio jogo faz dar a volta.
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.
- Desenhe um triângulo ou quad de tela cheia e passe o UV dele, de 0 a 1 a partir do canto inferior esquerdo, como
v_uv. O shader também declarav_position,v_normalev_view; o céu não os lê, então qualquer valor serve. - A cada quadro, defina
u_timecomo os segundos desde que a página começou (performance.now() / 1000) eu_resolutioncomo o tamanho do canvas. - Defina os parâmetros uma vez, ou quando um controle mudar: os floats como estão, as quatro cores convertidas de sRGB
para linear (
c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4em cada canal).
O UV estica junto com o canvas, então num canvas largo as nuvens também ficam mais largas. Isso combina com um céu; se
você quiser a mesma forma em qualquer proporção, multiplique i.uv.x por i.resolution.x / i.resolution.y antes do ruído.
Para pôr as nuvens sobre outra coisa em vez do gradiente, retorne vec4(cloud, cover) de effect(), troque a
última linha de main() por fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), frag_out.a); e desenhe com
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA): cover já é um alpha suave.
Monte no Godot
No VisualShader não existe nó de ruído; a montagem comum amostra um NoiseTexture2D (FastNoiseLite, tipo fractal FBM, seamless ligado) com um nó Texture2D, e a distorção de domínio do próprio FastNoiseLite faz o papel do campo de distorção. Essa versão é uma leitura de textura, então é mais barata, mas repete.
A exportação é um shader canvas_item, para qualquer coisa desenhada em 2D: um ColorRect, um TextureRect, um
Sprite2D ou um Panel.
- Adicione um
ColorRectque preencha o fundo (Layout → Full Rect) e mova-o para o topo da árvore da cena, para que ele seja desenhado primeiro. - No slot Material dele, crie um New ShaderMaterial, depois um New Shader dentro dele, e troque o código pelo shader copiado.
- Os parâmetros aparecem em Shader Parameters. As cores usam
source_color, então o seletor mostra os mesmos valores sRGB do site.
O shader inverte o UV do Godot, que começa no canto superior esquerdo, então a cor do alto fica no topo do retângulo,
como no site. TIME move o vento; para pausar ou controlar as nuvens por um script, troque i.time = TIME; em
fragment() por um uniform que você define.
Para um céu 3D, o mesmo corpo do efeito funciona num shader shader_type sky; com EYEDIR convertido numa coordenada 2D
(EYEDIR.xz / EYEDIR.y para uma camada plana de nuvens no alto), mas isso pede um template próprio e não é o código
copiado.
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// Clouds & smoke (2D) → Sky clouds, 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_scale; // Scale: 3.0 (1.0 to 8.0)uniform float p_coverage; // Coverage: 0.5 (0.0 to 1.0)uniform float p_softness; // Edge softness: 0.15 (0.01 to 0.5)uniform float p_warp; // Warp: 1.5 (0.0 to 4.0)uniform float p_speed; // Wind speed: 0.08 (0.0 to 0.5)uniform vec3 p_zenith; // Sky overhead: vec3(0.22, 0.45, 0.82), sRGB; upload it decoded to linearuniform vec3 p_horizon; // Sky at the horizon: vec3(0.68, 0.82, 0.95), sRGB; upload it decoded to linearuniform vec3 p_cloudLit; // Sunlit cloud: vec3(1.0, 1.0, 1.0), sRGB; upload it decoded to linearuniform vec3 p_cloudShade; // Cloud shadow: vec3(0.6, 0.65, 0.74), 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 drifting// 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);}// Each octave is turned by about 37 degrees, so the value noise's grid lines don't line up across octaves and show// as rows in the sky.float fbm(vec2 p) { const mat2 turn = mat2(vec2(0.8, 0.6), vec2(-0.6, 0.8)); float sum = 0.0; float amplitude = 0.5; for (int k = 0; k < 5; k++) { sum += amplitude * valueNoise(p); p = turn * p * 2.0 + vec2(17.3, 9.1); amplitude *= 0.5; } return sum;}vec4 effect(Inputs i) { vec3 sky = mix(p_horizon, p_zenith, sqrt(i.uv.y)); // Clouds seen from below look wider than they are tall, so the noise is squashed vertically vec2 p = vec2(i.uv.x, i.uv.y * 1.8) * p_scale; float drift = i.time * p_speed; // The warp field moves slower than the wind, so the clouds churn as they travel instead of sliding like a picture vec2 warpField = vec2(fbm(p + vec2(0.0, drift * 0.3)), fbm(p + vec2(5.2, 1.3) - drift * 0.2)); float density = fbm(p + p_warp * warpField + vec2(drift, 0.0)); float threshold = 1.0 - p_coverage; float cover = smoothstep(threshold, threshold + p_softness, density); // Thick cores block the sun and thin edges let it through, so shade grows with density above the threshold float thickness = smoothstep(threshold, threshold + 0.35, density); vec3 cloud = mix(p_cloudLit, p_cloudShade, thickness * 0.8); vec3 color = mix(sky, cloud, cover); 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);}// Clouds & smoke (2D) → Sky clouds, 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 scale : hint_range(1.0, 8.0, 0.1) = 3.0;uniform float coverage : hint_range(0.0, 1.0, 0.01) = 0.5;uniform float softness : hint_range(0.01, 0.5, 0.01) = 0.15;uniform float warp : hint_range(0.0, 4.0, 0.05) = 1.5;uniform float speed : hint_range(0.0, 0.5, 0.01) = 0.08;uniform vec3 zenith : source_color = vec3(0.22, 0.45, 0.82);uniform vec3 horizon : source_color = vec3(0.68, 0.82, 0.95);uniform vec3 cloudLit : source_color = vec3(1.0, 1.0, 1.0);uniform vec3 cloudShade : source_color = vec3(0.6, 0.65, 0.74);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 drifting// 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);}// Each octave is turned by about 37 degrees, so the value noise's grid lines don't line up across octaves and show// as rows in the sky.float fbm(vec2 p) { const mat2 turn = mat2(vec2(0.8, 0.6), vec2(-0.6, 0.8)); float sum = 0.0; float amplitude = 0.5; for (int k = 0; k < 5; k++) { sum += amplitude * valueNoise(p); p = turn * p * 2.0 + vec2(17.3, 9.1); amplitude *= 0.5; } return sum;}vec4 effect(Inputs i) { vec3 sky = mix(u_toLinear(horizon), u_toLinear(zenith), sqrt(i.uv.y)); // Clouds seen from below look wider than they are tall, so the noise is squashed vertically vec2 p = vec2(i.uv.x, i.uv.y * 1.8) * scale; float drift = i.time * speed; // The warp field moves slower than the wind, so the clouds churn as they travel instead of sliding like a picture vec2 warpField = vec2(fbm(p + vec2(0.0, drift * 0.3)), fbm(p + vec2(5.2, 1.3) - drift * 0.2)); float density = fbm(p + warp * warpField + vec2(drift, 0.0)); float threshold = 1.0 - coverage; float cover = smoothstep(threshold, threshold + softness, density); // Thick cores block the sun and thin edges let it through, so shade grows with density above the threshold float thickness = smoothstep(threshold, threshold + 0.35, density); vec3 cloud = mix(u_toLinear(cloudLit), u_toLinear(cloudShade), thickness * 0.8); vec3 color = mix(sky, cloud, cover); 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
- fBM (abre em uma nova aba)Artigo
- Domain warping (abre em uma nova aba)Artigo
- Fractal Brownian Motion (abre em uma nova aba)Livro
- Noise (abre em uma nova aba)Livro
- Value noise (abre em uma nova aba)Referência
- smoothstep (abre em uma nova aba)Referência
- mix (abre em uma nova aba)Referência
- NoiseTexture2D (abre em uma nova aba)Documentação
- FastNoiseLite (abre em uma nova aba)Documentação
- CanvasItem shaders (abre em uma nova aba)Documentação
- Shading language (abre em uma nova aba)Documentação