Pular para o conteúdo
Forma · Formas 3D com raymarchingAvançado

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.

UVTempoTela cheia
Abrir no Playground
  1. 01ObjetivoForma
  2. 02TécnicaFormas 3D com raymarching
  3. 03ExecuçãoEsfera e caixa em fusão suave
Quad de tela cheia

Parâmetros

0.40
0.80
#ff8c4d
#4d99ff
#0f121a

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

    01 Um raio por pixel

  2. stepExpressionstepstepstep

    02 Avançar o raio

  3. normalizeExpressionnormalizenormalizenormalize

    03 Normal pelo gradiente

  4. dotDotProductdotdotdot

    04 Luz difusa

  5. mixMixmixmixmix

    05 Misturar as cores

  6. mixMixmixmixmix

    06 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. 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 sinal. 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. min(box, sphere) daria as duas formas com um vinco marcado onde se encontram. O mínimo suave mistura em vez disso: onde as duas distâncias estão a menos que o tamanho da fusão uma da outra, 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 tracing anda pelo raio em passos iguais à distância da cena. Nada está mais perto do que essa distância, então o passo nunca atravessa uma superfície, e ele encolhe conforme o raio se aproxima de uma. Quando a distância cai abaixo de 0,001, o raio acertou; depois de 20 unidades, errou. 96 passos bastam para esta cena; os raios rasantes ao longo de uma aresta são os que gastam mais.

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 normal. Mais seis chamadas a 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 escalar entre a normal e uma direção de luz fixa é o termo difuso de Lambert, com um pouco de luz constante somada para o lado de trás não ficar preto. 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.

  1. Desenhe dois triângulos cobrindo o canvas no espaço de recorte, com um shader de vértice que escreva v_uv de 0 a 1 e constantes em v_position, v_normal e v_view (o shader de fragmento declara os quatro).
  2. A cada quadro, defina u_time com os segundos desde o início e u_resolution com o tamanho do drawing buffer.
  3. Defina p_smoothness e p_speed e envie p_sphereColor, p_boxColor e p_background decodificadas de sRGB para linear.
  4. Desenhe com o blending desligado a partir de um laço de requestAnimationFrame e pare o laço enquanto o canvas estiver fora da tela (um IntersectionObserver avisa): 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.

  1. Adicione um ColorRect a uma cena 2D ou de interface e defina o preset de âncoras como Full Rect.
  2. Defina o Material dele como um novo ShaderMaterial com um novo Shader e cole o código.
  3. 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

sphere-box-blend.fragGLSL
#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);}
sphere-box-blend.gdshaderShader do Godot
// 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