Modelas en vóxeles. Sale pixel art.
Un editor en el navegador: pintas por capas o en 3D, lo iluminas, lo animas y lo recorres por dentro. Cada giro del modelo sale como un sprite nítido, con la paleta que elegiste y nada más.
Todo pasa en una pestaña del navegador, sin instalar nada. Lo que ves en la vista previa es exactamente lo que exportas.

Por capas pintas cada altura vista desde arriba, con la de debajo en fantasma. En 3D pintas directamente sobre el modelo, lo giras con el botón derecho y acercas con la rueda. Tab cambia de uno a otro.
Lápiz, borrar, repintar, gotero, caja, cubo, luz, fluido, efecto y espejo. El fluido vierte agua que cae y se reparte, o llena una cuenca hasta la altura que digas. Efecto pone humo, chispas, brasas, lluvia o nieve que corren solos.



No eliges colores sueltos: eliges rampas, de oscuro a claro, y el render mueve el tono dentro de la rampa según la luz. Por eso la imagen solo tiene colores de tu paleta. Puedes generar una rampa desde un color, con las sombras girando hacia el frío como haría un pixel artist, o cargar PICO-8, Endesga o AAP-64.

Una animación son cuadros, y cada cuadro es el modelo entero. Se edita en 3D con las herramientas de siempre y sale como GIF o como hoja de sprites desde el ángulo que elijas. Estas cuatro salieron así del editor.





Para un ciclo de andar no hace falta pintar cada cuadro. El rig dice qué cajas de vóxeles son cada pierna, dónde está la cadera y cuánto avanza el pie en cada cuadro, y genera los cuadros desde la pose de reposo.
Te mete dentro del modelo en primera persona: WASD para andar, espacio para saltar, F para volar. La altura del personaje va de 1 a 32 vóxeles, así que puedes ver tu escena como un bicho o como alguien de su tamaño.

Dibujas el personaje una vez y sale en los ocho ángulos, o en los que pidas. El contorno, la luz y la paleta se mantienen en todos.


El mismo render mueve mundos enteros en 2.5D: luz dinámica que sigue a los personajes, sombras, agua, lluvia y partículas, compuesto por la GPU y con la CPU de respaldo. El mismo mundo se puede ver en vóxeles con una tecla.



La forma de la sala, los colores, el agua, la vegetación, las luces y los efectos van en un JSON. El motor pone la geometría, la física y la luz. Estos seis los pintó el motor desde su línea de comandos, y la ficha es lo que devolvió. La misma receta da siempre el mismo mundo.
Una sala con forma de laberinto: las paredes siguen el contorno, sin bloques en medio.

{
"theme": "medieval",
"seed": 7,
"shape": {
"kind": "maze",
"loops": 0.2
},
"foliage": [
{
"type": "grass",
"density": 0.25
}
]
}Todo lo que hace una persona con el ratón tiene su llamada, y los errores dicen qué falta. Un agente de IA construye un modelo o un mundo entero, lo mira en PNG y comprueba lo que hizo. VoxPix lo escriben así: un equipo de agentes de IA dirigido por una persona.
voxpix.box(-4, -4, 0, 3, 3, 3, 'piedra')
voxpix.light(0, -5, 4, 'torch')
voxpix.addFrame() // otro cuadro
await voxpix.animGif({ scale: 4 })await voxpixWorld.build({
theme: 'medieval',
seed: 5,
size: [24, 18],
rooms: [
{ gx: 0, gy: 0, shape: { kind: 'maze' } },
{ gx: 1, gy: 0, shape: { kind: 'T', bar: 'south' } },
],
})
voxpixWorld.groundAt(120, 96) // el suelo
voxpixWorld.png({ scale: 2 }) // en PNGbun run voxpix world \
receta.json mundo.png --whole
bun run voxpix render \
modelo.json giros.png --turn 8Brotato de la oficina es un roguelite de oleadas: diez rondas contra muebles con mala intención, una tienda entre ronda y ronda y un árbol de mejoras que se queda entre partidas. Lo hizo FERN, un agente del equipo, encima del motor y sin tocarlo. Tres mundos, cada uno con su aspecto.





Lo que más cuenta quien ya construyó uno encima: FERN, el agente que hizo brotato-oficina, un roguelite sobre VoxPix.
El mundo vive en un WorldState que avanza a pasos fijos de 1/60 s, sin reloj ni azar suelto. Con la misma semilla y la misma entrada da el mismo mundo byte a byte, y state.hash() lo comprueba. Así se prueban partidas y curvas de dificultad a 64 semillas sin abrir un navegador.
El motor no trae temas. El juego le pasa su WorldLook: paleta, suelo, columnas, fachada, muebles. Con eso brotato-oficina hizo una oficina con café y mamparas y un cyberpunk de agua tóxica sin tocar el motor.
Lo estático se hornea una vez y las entidades se componen en cada cuadro. Ver el mismo estado en vóxeles, la silueta del jugador detrás de una pared o un objeto que late son capas encima, y pausarlas no cambia la partida.
En un Galaxy Z Flip 6, con la GPU, brotato-oficina da un p95 de 16,7 ms por cuadro en la oleada 10, con entre 76 y 291 enemigos, en sus tres mundos; y girar la cámara no pasa de 16,8 ms en ningún cuadro (medido por FERN, octubre de 2026). Si no hay WebGL2, la CPU pinta los mismos píxeles, más despacio. Las medidas del motor y cómo repetirlas están en scripts/perf.
// package.json: el motor, fijado a un commit
"voxpix": "github:andyeswong/voxpix#<commit>"
// vite.config: el alias, con ruta absoluta
resolve: { alias: { '@voxpix': fileURLToPath(
new URL('./node_modules/voxpix/src', import.meta.url)) } }
// tsconfig: lo mismo en paths (con baseUrl)
"paths": { "@voxpix/*": ["node_modules/voxpix/src/*"] }import { generateLayout } from '@voxpix/worldgen'
import { buildWorldState } from '@voxpix/world/build'
import { THEMES } from '@voxpix/samples/themes'
import { bakeWorld } from '@voxpix/world/bake'
import { TICK } from '@voxpix/world/state'
import { createCompositor } from '@voxpix/view/compositor'
import { walkFrames } from '@voxpix/anim/walk'
// Aquí, un tema de muestra; tu juego trae el suyo
// (un WorldLook: paleta, suelo, columnas...)
const { spec, look, ambient } = THEMES.medieval
const layout = generateLayout(spec, 7) // semilla
const { world, state } =
buildWorldState(layout, look)
const baked = bakeWorld(world, { topZ: 40 })
// GPU con WebGL2; si no, la CPU
const compositor = createCompositor(canvas)
// walk: los cuadros de andar de un modelo
// (Model.fromJSON de un .voxpix.json)
const walk = walkFrames(modelo)
// layout.spawn: los pies del jugador, en píxeles
// (el mundo cuenta y hacia el norte: se invierte)
const x = layout.spawn.x
const y = world.depth - 1 - layout.spawn.y
state.entities.add({
model: walk[0], x, y, z: 1, facing: 0 })
// Pasos fijos de 1/60 s, a cualquier ritmo de
// pantalla (en 120 Hz, dos cuadros por paso)
let acc = 0, last = performance.now()
function loop(now) {
acc += (now - last) / 1000; last = now
while (acc >= TICK) { state.step(); acc -= TICK }
// La vista: los pies proyectados, centrados
const [fx, fy] = baked.cam.project(x + 0.5, y, 1)
const vx = Math.floor(fx) - 192
const vy = Math.floor(fy) - 108
compositor.frame(state, baked, { vx, vy,
vw: 384, vh: 216, time: now / 1000,
lights: true, ambient })
requestAnimationFrame(loop)
}
requestAnimationFrame(loop)
// Si el mundo cambia vóxeles (agua, destrucción),
// rehornea lo cambiado: rebakeChangedJobs + FrameBudgetPara usarlo no hace falta instalar nada: el editor y la demo del motor corren aquí mismo. Para el código y la línea de comandos:
git clone https://github.com/andyeswong/voxpix
cd voxpix
bun install
bun run dev # el editor en http://localhost:5190
bun run test # las pruebas