Esfera e caixa em fusão suave
Uma esfera atravessa uma caixa que gira e se derrete nela, com raymarching e luz num só shader de fragmento.
- 01ObjetivoForma
- 02TécnicaFormas 3D com raymarching
- 03ExecuçãoEsfera e caixa em fusão suave
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.
normalizeNormalizenormalizenormalizenormalize UVTempo01 Um raio por pixel
stepExpressionstepstepstep Entrada02 Avançar o raio
normalizeExpressionnormalizenormalizenormalize Entrada03 Normal pelo gradiente
dotDotProductdotdotdot Entrada04 Luz difusa
mixMixmixmixmix Entrada05 Misturar as cores
mixMixmixmixmix Entrada06 Sobre o fundo
Como funciona
Não há malha neste card. O quad só dá a cada pixel um lugar para rodar, e o shader monta a cena, descobre o que o pixel vê e o ilumina, tudo a partir de uma única função que mede distância.
A cena é uma distância. Campo de distância com sinal (SDF) Uma função que devolve a distância de um ponto até a borda de uma forma, negativa dentro e positiva fora. O zero é o próprio contorno.scene() recebe um ponto em 3D e devolve a que distância ele está da superfície mais
próxima: a forma 3D de um campo de distância com sinalUma função que devolve a distância de um ponto até a borda de uma forma, negativa dentro e positiva fora. O zero é o próprio contorno.circle(p) = |p| − r. Uma esfera é a distância até o centro
menos o raio. Uma caixa dobra o ponto para um único canto com abs e depois mede até as faces desse canto; subtrair um
pouco no final arredonda as arestas, o mesmo truque de crescer a forma do botão arredondado. A caixa gira porque o
ponto é girado ao contrário antes da medida, e a esfera desliza no eixo x com um seno do tempo.
Derretendo uma na outra. Mínimo suave Uma versão do min que arredonda o canto onde dois valores se cruzam. Aplicado a dois campos de distância, funde as formas numa só, com a largura da fusão definida por k.min(box, sphere) daria as duas formas com um vinco marcado onde se encontram. O
mínimo suaveUma versão do min que arredonda o canto onde dois valores se cruzam. Aplicado a dois campos de distância, funde as formas numa só, com a largura da fusão definida por k.h = clamp(0.5 + 0.5 (b − a) / k, 0, 1); smin = mix(b, a, h) − k h (1 − h)h vai de uma forma à outra, e o termo extra - smoothness * h * (1 - h) puxa a superfície para fora,
formando um filete. Longe da junta, h vale 0 ou 1 e cada forma mantém a distância exata. O mesmo h diz quanto de
cada forma um ponto tem, então ele também mistura as duas cores.
Um raio por pixel. A posição do pixel, centralizada e corrigida pela proporção da tela, vira uma direção que sai de uma câmera acima e à frente das formas. Os três eixos da câmera (para a frente, rumo à origem; para a direita; para cima) transformam essa posição 2D num raio 3D; o 1.8 no eixo da frente é a lente, e um número maior aproxima a imagem.
Avançando o raio. O sphere tracingDesenhar uma cena lançando um raio por cada pixel e avançando por ele pela distância até a superfície mais próxima, lida de uma função de distância com sinal, até acertar. Raymarching Desenhar uma cena lançando um raio por cada pixel e avançando por ele pela distância até a superfície mais próxima, lida de uma função de distância com sinal, até acertar.
A normal pelo gradiente. A distância cresce mais rápido na direção que se afasta da superfície, então o gradiente
dela aponta ao longo da normalUm vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções. Normal Um vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções.scene(), um passo minúsculo para cada
lado em cada eixo, dão o gradiente, e normalize o transforma numa normal unitária.
Luz e cor. O produto escalarMultiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos). Produto escalar Multiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos).dot(a, b) = |a| |b| cos θhit escolhe entre a cor iluminada e
o fundo.
Quanto custa. Cada chamada a scene() custa umas 34 operações. A maioria dos pixels, acertando ou errando, termina
a marcha em uns dez passos: um raio que erra dá passos cada vez maiores depois de passar pelas formas e logo passa das
20 unidades. O pior caso é um raio rasante a uma aresta, que roda os 96 passos (umas 3.550 operações), e a normal e a
cor somam mais 7 chamadas, perto de 3.830 operações por pixel, contra uma dúzia no Ponto suave. Cada forma acrescentada
a scene() encarece todos os passos, já que cada passo avalia todas elas. Um teste de volume envolvente antes do laço
deixa os pixels longe das formas pularem a marcha inteira, e é a primeira otimização num projeto de verdade.
Monte no GLSL
O código copiado é um shader de fragmento WebGL2 completo. A cena, a câmera e a luz moram todas nele, então a página só precisa cobrir o canvas e contar o tempo.
- Desenhe dois triângulos cobrindo o canvas no espaço de recorte, com um shader de vértice que escreva
v_uvde 0 a 1 e constantes emv_position,v_normalev_view(o shader de fragmento declara os quatro). - A cada quadro, defina
u_timecom os segundos desde o início eu_resolutioncom o tamanho do drawing buffer. - Defina
p_smoothnessep_speede enviep_sphereColor,p_boxColorep_backgrounddecodificadas de sRGB para linear. - Desenhe com o blending desligado a partir de um laço de
requestAnimationFramee pare o laço enquanto o canvas estiver fora da tela (umIntersectionObserveravisa): ao contrário da maioria dos cards, este é pesado o bastante para isso fazer diferença.
Para acrescentar formas, escreva as funções de distância delas ao lado de sdSphere e sdBox e combine-as dentro de
scene() com min ou com o mínimo suave. A marcha, a normal e a iluminação não mudam.
Monte no Godot
O shader exportado supõe o HDR 2D desligado (o padrão). Com ele ligado, faça u_toLinear e u_toSrgb devolverem a entrada sem mudança. Um VisualShader não consegue expressar o laço a não ser dentro de um único nó Expression com o mesmo código, então aqui o shader em texto é o caminho.
O código copiado é um shader canvas_item completo; i.time lê o TIME do Godot, então as formas também se mexem no
editor.
- Adicione um
ColorRecta uma cena 2D ou de interface e defina o preset de âncoras como Full Rect. - Defina o Material dele como um novo
ShaderMaterialcom um novoShadere cole o código. - Ajuste o tamanho da fusão, a velocidade e as três cores em Shader Parameters.
As funções auxiliares (sdSphere, sdBox, scene, march, normalAt) entram no Godot sem mudança; só a moldura em
volta delas muda. Para montar uma cena sua, edite scene() e mantenha o resto.
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// Raymarched 3D shapes → Sphere–box smooth blend, 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_smoothness; // Blend size: 0.4 (0.01 to 1.0)uniform float p_speed; // Speed: 0.8 (0.0 to 3.0)uniform vec3 p_sphereColor; // Sphere color: vec3(1.0, 0.55, 0.3), sRGB; upload it decoded to linearuniform vec3 p_boxColor; // Box color: vec3(0.3, 0.6, 1.0), sRGB; upload it decoded to linearuniform vec3 p_background; // Background: vec3(0.06, 0.07, 0.1), 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));}const float FAR = 20.0;float sdSphere(vec3 p, float r) { return length(p) - r;}float sdBox(vec3 p, vec3 halfSize) { vec3 q = abs(p) - halfSize; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);}vec3 rotateY(vec3 p, float angle) { float c = cos(angle); float s = sin(angle); return vec3(c * p.x + s * p.z, p.y, c * p.z - s * p.x);}// x: the scene's distance; y: how much of it is sphere, 1 on the sphere, 0 on the box, in between where they meltvec2 scene(vec3 p, float time) { // Subtracting a little from the box's distance rounds its edges, so the light catches them float box = sdBox(rotateY(p, 0.5 * time), vec3(0.55)) - 0.06; float sphere = sdSphere(p - vec3(1.3 * sin(time * p_speed), 0.0, 0.0), 0.6); // Smooth minimum: where the two distances are within p_smoothness of each other, they blend instead of creasing float h = clamp(0.5 + 0.5 * (box - sphere) / p_smoothness, 0.0, 1.0); float d = mix(box, sphere, h) - p_smoothness * h * (1.0 - h); return vec2(d, h);}// Sphere tracing: nothing is closer than the scene's distance, so the ray can always step that far safelyfloat march(vec3 origin, vec3 ray, float time) { float t = 0.0; for (int k = 0; k < 96; k++) { float d = scene(origin + ray * t, time).x; if (d < 0.001 || t > FAR) break; t += d; } return t;}// The distance grows fastest straight away from the surface, so its gradient is the normalvec3 normalAt(vec3 p, float time) { vec2 e = vec2(0.001, 0.0); return normalize(vec3( scene(p + e.xyy, time).x - scene(p - e.xyy, time).x, scene(p + e.yxy, time).x - scene(p - e.yxy, time).x, scene(p + e.yyx, time).x - scene(p - e.yyx, time).x ));}vec4 effect(Inputs i) { vec2 p = (i.uv * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0); vec3 origin = vec3(0.0, 1.2, 3.6); vec3 forward = normalize(-origin); vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0))); vec3 up = cross(right, forward); // The 1.8 is the lens: larger is a narrower field of view vec3 ray = normalize(p.x * right + p.y * up + 1.8 * forward); float t = march(origin, ray, i.time); float hit = step(t, FAR); vec3 point = origin + ray * t; vec3 normal = normalAt(point, i.time); float diffuse = max(dot(normal, normalize(vec3(0.6, 0.8, 0.4))), 0.0); vec3 albedo = mix(p_boxColor, p_sphereColor, scene(point, i.time).y); // A little constant light so the side facing away isn't black vec3 lit = albedo * (0.15 + 0.85 * diffuse); vec3 color = mix(p_background, lit, hit); 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);}// Raymarched 3D shapes → Sphere–box smooth blend, 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 smoothness : hint_range(0.01, 1.0, 0.01) = 0.4;uniform float speed : hint_range(0.0, 3.0, 0.05) = 0.8;uniform vec3 sphereColor : source_color = vec3(1.0, 0.55, 0.3);uniform vec3 boxColor : source_color = vec3(0.3, 0.6, 1.0);uniform vec3 background : source_color = vec3(0.06, 0.07, 0.1);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));}const float FAR = 20.0;float sdSphere(vec3 p, float r) { return length(p) - r;}float sdBox(vec3 p, vec3 halfSize) { vec3 q = abs(p) - halfSize; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);}vec3 rotateY(vec3 p, float angle) { float c = cos(angle); float s = sin(angle); return vec3(c * p.x + s * p.z, p.y, c * p.z - s * p.x);}// x: the scene's distance; y: how much of it is sphere, 1 on the sphere, 0 on the box, in between where they meltvec2 scene(vec3 p, float time) { // Subtracting a little from the box's distance rounds its edges, so the light catches them float box = sdBox(rotateY(p, 0.5 * time), vec3(0.55)) - 0.06; float sphere = sdSphere(p - vec3(1.3 * sin(time * speed), 0.0, 0.0), 0.6); // Smooth minimum: where the two distances are within smoothness of each other, they blend instead of creasing float h = clamp(0.5 + 0.5 * (box - sphere) / smoothness, 0.0, 1.0); float d = mix(box, sphere, h) - smoothness * h * (1.0 - h); return vec2(d, h);}// Sphere tracing: nothing is closer than the scene's distance, so the ray can always step that far safelyfloat march(vec3 origin, vec3 ray, float time) { float t = 0.0; for (int k = 0; k < 96; k++) { float d = scene(origin + ray * t, time).x; if (d < 0.001 || t > FAR) break; t += d; } return t;}// The distance grows fastest straight away from the surface, so its gradient is the normalvec3 normalAt(vec3 p, float time) { vec2 e = vec2(0.001, 0.0); return normalize(vec3( scene(p + e.xyy, time).x - scene(p - e.xyy, time).x, scene(p + e.yxy, time).x - scene(p - e.yxy, time).x, scene(p + e.yyx, time).x - scene(p - e.yyx, time).x ));}vec4 effect(Inputs i) { vec2 p = (i.uv * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0); vec3 origin = vec3(0.0, 1.2, 3.6); vec3 forward = normalize(-origin); vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0))); vec3 up = cross(right, forward); // The 1.8 is the lens: larger is a narrower field of view vec3 ray = normalize(p.x * right + p.y * up + 1.8 * forward); float t = march(origin, ray, i.time); float hit = step(t, FAR); vec3 point = origin + ray * t; vec3 normal = normalAt(point, i.time); float diffuse = max(dot(normal, normalize(vec3(0.6, 0.8, 0.4))), 0.0); vec3 albedo = mix(u_toLinear(boxColor), u_toLinear(sphereColor), scene(point, i.time).y); // A little constant light so the side facing away isn't black vec3 lit = albedo * (0.15 + 0.85 * diffuse); vec3 color = mix(u_toLinear(background), lit, hit); 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
- Sphere tracing: a geometric method for the antialiased ray tracing of implicit surfaces (abre em uma nova aba)Paper
- Distance functions (abre em uma nova aba)Artigo
- Smooth minimum (abre em uma nova aba)Artigo
- Normals for an SDF (abre em uma nova aba)Artigo
- Ray marching (abre em uma nova aba)Referência
- Signed distance function (abre em uma nova aba)Referência