1
Un bloque, dibujado miles de veces
Todos los bloques comparten una sola malla (una caja más cuatro tetones de 12 lados = 462 vértices), que se sube a la GPU una sola vez. Cada bloque solo añade 8 números: posición, capa, giro y color. El instancing de la GPU lee un conjunto por bloque, así que todo el campo es una única llamada de dibujo.
brick_count = grid_size × grid_size (wave) e.g. 24 × 24 = 576
per_brick_data = (brick_x, brick_z, layer, brick_yaw) + (red, green, blue, rand)
vertex_attrib_divisor(attr, 1) → advance once per brick
draw_arrays_instanced(triangles, 0, 462, brick_count) → draw_calls = 1
Fuentes: [1]
[2]
2
Colocar cada bloque en la GPU
El vertex shader gira cada bloque alrededor del eje vertical, lo sube a su capa, suma la altura de la animación y lo proyecta a través de la cámara. La proyección usa la matriz de perspectiva estándar.
x_rot = x·cos(brick_yaw) − z·sin(brick_yaw)
z_rot = x·sin(brick_yaw) + z·cos(brick_yaw)
world_pos = (x_rot, y, z_rot) + (brick_x, 1.15·layer + anim_height, brick_z)
clip_pos = proj_matrix · view_matrix · world_pos
focal = 1 / tan(field_of_view / 2)
field_of_view = 0.75 rad near = 0.5 far = 600
proj_matrix = | focal/aspect 0 0 0 |
| 0 focal 0 0 |
| 0 0 (near+far)/(near−far) 2·near·far/(near−far) |
| 0 0 −1 0 |
Fuentes: [3]
3
La ola y el bulto del cursor
La altura de la ola es un seno de la distancia de cada bloque al centro, así que los anillos se propagan hacia fuera. Tu puntero añade un bulto gaussiano, medido en el espacio de la pantalla para que siga al cursor desde cualquier ángulo de cámara. Con el movimiento reducido activado, ambos se atenúan.
dist_center = √(x² + z²)
wave_height = 2.8 · (½ + ½·sin(0.3·dist_center − 1.8·time)) · amplitude
pointer_dist = |brick_screen_pos − pointer_pos| (aspect_corrected)
bump_height = 4 · e^(−14·pointer_dist²) · amplitude
anim_height = wave_height + bump_height
amplitude = 1 (0.35 with prefers-reduced-motion)
Fuentes: [4]
4
La caída y el rebote
En Pirámide y Hélice, cada bloque empieza tras un pequeño retraso aleatorio, más un poco por capa, así que la construcción llueve de abajo arriba. Cae siguiendo una parábola y luego rebota con un seno que se amortigua.
drop_time = 1.4·(time − drop_start) − 1.4·rand − 0.16·layer rand ∈ [0, 1) per brick
fall = clamp(1 − drop_time, 0, 1)
bounce_time = max(drop_time − 1, 0)
anim_height = 36·fall² + 0.9·|sin(9·bounce_time)|·e^(−6·bounce_time)
drop_time < 0 → brick not drawn yet
Fuentes: [4]
5
Luz, brillo y niebla
La iluminación se calcula por píxel: difusa de Lambert para el plástico mate, un término hemisférico para que las caras de arriba brillen más que los lados, un reflejo de Blinn–Phong para el brillo de juguete y niebla según la distancia hacia el color del cielo.
color = albedo · (0.34 + 0.24·(½ + ½·normal_y) + 0.6·max(normal·light_dir, 0))
+ 0.22 · max(normal·half_vec, 0)^36
light_dir = normalize(0.45, 0.85, 0.32)
half_vec = normalize(light_dir + (0, 0.5, 0.85)) (fixed view direction)
fog_amount = clamp((clip_w − fog_start) / (1.4·fog_start), 0, 1)
fog_start = 0.95 · camera_dist
final_color = mix(color, sky_color, 0.8 · fog_amount)
Fuentes: [5]
[6]
6
Cámara y bucle de fotogramas
La cámara orbita sobre una esfera alrededor de la escena y gira despacio sola hasta que arrastras. Cada fotograma se programa con requestAnimationFrame, y un IntersectionObserver pausa el bucle cuando la demo está fuera de la pantalla.
eye_pos = target + camera_dist · (sin(cam_yaw)·cos(cam_pitch), sin(cam_pitch), cos(cam_yaw)·cos(cam_pitch))
cam_yaw += 0.07 rad/s (auto_orbit)
cam_pitch ∈ [0.15, 1.35] rad
fps = frame_count / elapsed_sec (updated every 0.5 s)
Fuentes: [3]
[7]