Saltar al contenido principal

Laboratorio tech

Donde probamos lo que los navegadores pueden hacer de verdad. Todo funciona en vivo en tu dispositivo, escrito a mano: WebGL puro, nuestro propio motor de física y dos minijuegos. Sin plugins, sin motor de juegos, cero librerías.

  • WebGL + GLSL
  • Instanciado en GPU
  • Física propia
  • Canvas 2D
  • Ratón + táctil + teclado
  • 0 dependencias
Elige una demo
01 · WebGL

Miles de ladrillos, una sola llamada de dibujo

Shaders GLSL escritos a mano e instanciado en GPU animan cada ladrillo en la tarjeta gráfica. Arrastra para girar la cámara y mueve el puntero para empujar la ola.

PANEL DE CONTROL EN VIVO
Escena
ReconstruirHaz que todos los ladrillos vuelvan a caer del cielo.
—FPS
—Ladrillos
—Triángulos
1Llamada de dibujo
Cómo funciona Las matemáticas del campo de bloques

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]

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]

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]

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]

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]

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]
Fuentes
  1. Khronos Group. Especificación de la extensión de WebGL «ANGLE_instanced_arrays». registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/
  2. WebGL Fundamentals. «WebGL Optimization – Instanced Drawing». webglfundamentals.org/webgl/lessons/webgl-instanced-drawing.html
  3. MDN Web Docs. «WebGL model view projection». developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
  4. Diseño propio para esta demo (ola, bulto gaussiano, curvas de caída y rebote); sin fuente externa.
  5. Ley del coseno de Lambert (reflexión difusa). Wikipedia. en.wikipedia.org/wiki/Lambert%27s_cosine_law
  6. Blinn, J. F. (1977). «Models of light reflection for computer synthesized pictures». SIGGRAPH ’77, págs. 192–198. www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf
  7. MDN Web Docs. «Intersection Observer API». developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
02 · Motor de física

Ladrillos con peso de verdad

Nuestro propio pequeño motor: gravedad, masa, fricción, rebote y apilado, resueltos 1.200 veces por segundo. Toca un espacio vacío para soltar un ladrillo; agarra cualquier cosa y lánzala.

Gravedad
0Cuerpos
0Contactos / paso
0 msTiempo de cálculo
Cómo funciona Las matemáticas del simulador de física

Un reloj fijo para la física

La física avanza en pasos fijos de 1/120 de segundo, sea cual sea la frecuencia de la pantalla. Cada fotograma suma el tiempo real a un acumulador y lo gasta en pasos completos (6 como máximo), así que los bloques se comportan igual a 30 o a 144 fps.

accumulator += frame_dt
while accumulator ≥ step_dt and steps < 6:
    step(step_dt)            step_dt = 1/120 s
    accumulator −= step_dt
Fuentes: [1]

Mover cada cuerpo

Cada paso suma primero la gravedad a la velocidad y luego mueve el cuerpo con la nueva velocidad (Euler semiimplícito, que se mantiene estable al apilar). Un bloque agarrado sigue a tu puntero como un muelle rígido y actúa como si tuviera masa infinita.

vel += gravity · step_dt     gravity = 1900 (Earth) · 320 (Moon) · 0 (Zero-G) px/s²
pos += vel · step_dt
zero_g:  vel *= 0.999 each step
|vel| ≤ 2600 px/s
grabbed: vel = 16 · (pointer_pos − pos),  inv_mass = 0
Fuentes: [2]

Detectar contactos

Los bloques son cajas alineadas con los ejes, así que dos bloques se tocan cuando se solapan en ambos ejes; se separan a lo largo del eje con menor solapamiento. La bola de demolición se compara con el punto más cercano de cada caja.

overlap_x = half_w_a + half_w_b − |dx|
overlap_y = half_h_a + half_h_b − |dy|
contact if overlap_x > 0 and overlap_y > 0
normal = axis of the smaller overlap,  penetration = that overlap

closest = clamp(ball_pos, box_min, box_max)
normal  = (ball_pos − closest) / |ball_pos − closest|
penetration = radius − |ball_pos − closest|
Fuentes: [2] [3]

Separar y luego rebotar

Los cuerpos que se solapan se separan en proporción a su masa inversa, dejando un margen de 0,3 px para que las pilas en reposo no tiemblen. Después, un impulso anula la velocidad de acercamiento. Solo los golpes fuertes rebotan, así que las pilas se asientan en silencio.

inv_sum = inv_mass_a + inv_mass_b
correction = 0.8 · max(penetration − 0.3, 0) / inv_sum
pos_a −= normal · correction · inv_mass_a
pos_b += normal · correction · inv_mass_b

rel_vel_n = (vel_b − vel_a) · normal          (skip if > 0)
restitution = 0.22 if rel_vel_n < −260 else 0
impulse = −(1 + restitution) · rel_vel_n / inv_sum
vel_a −= impulse · normal · inv_mass_a
vel_b += impulse · normal · inv_mass_b
Fuentes: [2]

Fricción

El deslizamiento se frena a lo largo de la superficie de contacto, con un límite del 55% del impulso normal (fricción de Coulomb). Eso es lo que permite que un muro de bloques mantenga su forma hasta que lo golpea la bola.

tangent   = (−normal_y, normal_x)
rel_vel_t = (vel_b − vel_a) · tangent
friction  = clamp(−rel_vel_t / inv_sum, −0.55·impulse, 0.55·impulse)
vel_a −= friction · tangent · inv_mass_a
vel_b += friction · tangent · inv_mass_b
Fuentes: [2] [4]

Peso y pilas estables

Los bloques más grandes pesan más y la bola es lo más pesado. En cada paso los cuerpos se ordenan de abajo arriba y se recorren todos los contactos 10 veces, una iteración al estilo Gauss–Seidel que deja que el apoyo suba por la pila.

inv_mass = 1 / studs        (2-stud brick = ½,  4-stud = ¼)
ball inv_mass = 1/14
solver_iterations = 10,  bodies sorted bottom-first
floor: bounce 0.18 (bricks) / 0.35 (ball) when impact > 320 px/s
floor friction: vel_x *= 0.97
Fuentes: [4]
Fuentes
  1. Fiedler, G. (2004). «Fix Your Timestep!». Gaffer On Games. gafferongames.com/post/fix_your_timestep/
  2. Envato Tuts+. «How to Create a Custom 2D Physics Engine: The Basics and Impulse Resolution». code.tutsplus.com/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331t
  3. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann (solapamiento AABB y punto más cercano).
  4. Catto, E. (2005). «Iterative Dynamics with Temporal Coherence». box2d.org/files/ErinCatto_IterativeDynamics_GDC2005.pdf
030405 · Minijuego

Tómate un respiro

Tres minijuegos, cada uno de unos pocos kilobytes de JavaScript puro. Cambia entre ellos con el menú de arriba. Funcionan con ratón, pantalla táctil y teclado, y recuerdan tu mejor puntuación.

Apílalo

Récord 0

Toca o pulsa Espacio para soltar. Lo que sobresalga del borde se rompe. Encájalo a la perfección para mantener el ancho.

Cómo funciona Las matemáticas de Stack it

El bloque que se desliza

El nuevo bloque va y viene por la pantalla y acelera con cada bloque que colocas, hasta un límite.

speed = min(520, 170 + 10 · height) px/s
x += direction · speed · dt
turn around past −0.3·width (left) or canvas_w + 0.3·width (right)
Fuentes: [1]

Cortar lo que sobresale

Al soltar, solo se queda la parte que se solapa con el bloque de abajo. El resto se rompe; si no hay solapamiento, se acaba el juego.

left    = max(x_new, x_top)
right   = min(x_new + w_new, x_top + w_top)
overlap = right − left
overlap ≤ 0  → game over
kept brick = (left, overlap)     scrap = w_new − overlap
Fuentes: [1]

Caídas perfectas

Si caes a menos de 5 px del bloque de abajo, no se corta nada. Tres caídas perfectas seguidas empiezan a hacer crecer el bloque de nuevo.

perfect = |x_new − x_top| < 5 px
perfect → combo += 1, width unchanged
combo ≥ 3 → width += min(12, 0.6·canvas_w − width)
Fuentes: [1]

Restos que caen y la cámara

Los restos caen con la gravedad y salen girando. La cámara sube con un suavizado independiente de los fotogramas para que la cima de la torre siga a la vista.

scrap_vel_y += 1600 · dt        scrap_spin = ±2.4 rad/s
target = max(0, 30 · height − 0.5 · canvas_h)
camera += (target − camera) · min(1, 6 · dt)
Fuentes: [2]
Fuentes
  1. Implementación propia. La mecánica de tocar para apilar viene del género de apilar con un toque, popularizado por «Stack» de Ketchapp (2016).
  2. Suavizado exponencial (suavizado según los fotogramas). Wikipedia. en.wikipedia.org/wiki/Exponential_smoothing

Rompeladrillos

Récord 0

Mueve la paleta con el puntero, el dedo o las flechas. Toca o pulsa Espacio para lanzar. Tres vidas.

Cómo funciona Las matemáticas de Brick breaker

Lanzamiento y velocidad

La bola sale de la pala con un pequeño ángulo aleatorio, más rápido en cada nivel. Cada fotograma se divide en 3 subpasos para que una bola rápida no atraviese un bloque.

launch_angle ∈ [−0.45, 0.45] rad
speed = min(640, 400 + 40 · level) px/s
vel = speed · (sin launch_angle, −cos launch_angle)
sub_steps = 3 per frame
Fuentes: [1]

Paredes y pala

Las paredes reflejan la bola. La pala fija un nuevo ángulo según dónde la golpea la bola: el centro la manda recta hacia arriba y los bordes hasta unos 60° hacia los lados. Eso es lo que te da puntería.

walls:   vel_x = ±|vel_x|,  ceiling: vel_y = |vel_y|
hit_offset   = clamp((ball_x − paddle_x) / (paddle_w / 2), −1, 1)
bounce_angle = 1.05 · hit_offset rad
vel = |vel| · (sin bounce_angle, −cos bounce_angle)
Fuentes: [1]

Golpear bloques

Un bloque recibe un golpe cuando la bola está a menos de su radio del punto más cercano del bloque. La bola rebota por el lado con menor solapamiento y va un 1,2% más rápido en cada golpe. Desde el nivel 2, la fila de arriba necesita dos golpes.

closest = clamp(ball_pos, brick_min, brick_max)
hit if |ball_pos − closest|² ≤ radius²
flip vel_x if overlap_x < overlap_y, else flip vel_y
speed *= 1.012 while speed < 760
top_row_hits = 2 from level 2
Fuentes: [1] [2]

Estallidos y niveles

Un bloque roto lanza 6 trocitos que caen y se desvanecen. Al limpiar el tablero empieza el siguiente nivel con más filas.

pieces = 6,  vel_x ∈ [−180, 180],  vel_y ∈ [−260, 0] px/s
piece_vel_y += 900 · dt,   life = 0.7 s
rows = min(7, 4 + level),  lives = 3
Fuentes: [3]
Fuentes
  1. MDN Web Docs. «2D breakout game using pure JavaScript». developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript
  2. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann (círculo contra caja).
  3. Ajuste propio. El género viene de «Breakout» de Atari (1976).

Brick drop

Mejor nivel —

Sin puntos, solo niveles. Sube tu pila hasta la zona roja de peligro y luego limpia filas hasta que un bloque vuelva a tocar el suelo de la base: eso es una remontada, y 5 remontadas pasan un nivel. En cada nivel la línea de peligro sube una fila más cerca del techo, así que tienes que arriesgar más. Si un bloque se fija en la fila de arriba, se acaba el juego. Las filas completas se desprenden y los bloques sueltos caen. Formas: 2×1, 2×2, U, gancho cuadrado, doble T y m. Teclas: ← → mover, ↑ girar, ↓ bajar, Espacio soltar, P pausa. Táctil: toca para girar, arrastra para mover, desliza hacia abajo para soltar.

Cómo funciona Las matemáticas de Brick drop

El tablero y la bolsa de bloques

La base es una cuadrícula de 15 × 22. Los bloques salen de una bolsa de 8 que se baraja (Fisher–Yates) y se vacía antes de rellenarse, así que los bloques pequeños salen el doble que los grandes y ninguno tarda mucho en volver.

bag = [2×1, 2×1, 2×2, 2×2, u, square_hook, double_t, m]
for i from len(bag) − 1 down to 1:
    j = rand_int(0, i)
    swap(bag[i], bag[j])
Fuentes: [1]

Girar un bloque

Un bloque gira 90° reasignando sus celdas dentro de su cuadrado envolvente y luego se ajusta para seguir centrado. Si choca con algo, prueba pequeños empujones laterales y después una fila más arriba. El 2×2 nunca gira.

clockwise: (col, row) → (n − 1 − row, col)     n = max(width, height)
normalize to top-left, then
x += floor((width − new_width) / 2)
y += floor((height − new_height) / 2)
kicks = [0, −1, +1, −2, +2], then up 1
Fuentes: [3]

Caer y fijarse

Un bloque baja una fila por tic y acelera en cada nivel. Cabe si todas sus celdas están dentro del tablero y vacías; cuando ya no puede bajar, se fija y sus tetones hacen «clic».

fall_interval = max(0.08, 0.85 − 0.07 · (level − 1)) s per row
fits = every cell inside the board and empty
can’t fall → lock; click on each cell resting on floor or another brick
Fuentes: [4]

Filas que se desprenden y cascadas

Las filas completas se desprenden juntas en 0,5 s. Después, cualquier bloque que quede colgando cae entero: las celdas del mismo bloque que siguen tocándose se agrupan con un relleno por inundación, y un grupo baja una fila siempre que todo lo que tiene debajo esté vacío. Se repite cada 0,06 s hasta que nada se mueve y luego se vuelven a revisar las filas para las cadenas.

snap_offset = (t / 0.5)²
groups = flood_fill(same brick, 4 neighbours)
group falls if, for every cell, the cell below is empty or its own
repeat every 0.06 s until stable → check rows again (chain)
Fuentes: [2]

Línea de peligro y niveles

Sin puntos. Subir hasta la zona de peligro activa una remontada; que un bloque toque el suelo la completa. Cinco remontadas pasan un nivel, y cada nivel sube la línea una fila hacia el techo.

danger_line = max(1, 6 − (level − 1))          (rows from the top)
armed when stack_top ≤ danger_line, after being below it
comeback when armed and a locked brick touches the floor
comebacks = 5 → level += 1
locks in row 0 → topped out
Fuentes: [4]
Fuentes
  1. Barajado de Fisher–Yates (Durstenfeld 1964; Knuth, algoritmo P). Wikipedia. en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
  2. Relleno por inundación. Wikipedia. en.wikipedia.org/wiki/Flood_fill
  3. Matriz de rotación (giro de 90° en una cuadrícula). Wikipedia. en.wikipedia.org/wiki/Rotation_matrix
  4. Diseño de juego propio: tablero, juego de bloques, fijación por tetones, filas que se desprenden, cascadas, remontadas y la línea de peligro que sube.

¿Quieres esto en tu producto?

Visores 3D de productos, configuradores, onboarding divertido, juegos de marca. Creamos experiencias interactivas rápidas y accesibles, y las partes pesadas solo se cargan cuando alguien las abre, para que el resto de tu sitio siga siendo rápido.

Empezar un proyecto