Dummy Studios
La vitrina digital de un estudio de impresión 3D: un catálogo que se recorre como una galería de museo, en WebGL y a 60fps.
- Next.js 16
- React Three Fiber
- Drei
- Draco
- meshopt
- GLSL
- Poly Haven PBR
La identidad de Dummy Studios llevada a la web: un solo Canvas WebGL con navegación espacial y modo museo donde cada pieza del catálogo se exhibe como una obra. Mármol triplanar procedural sobre escaneos sin UVs, carga por tiers con corrección por FPS en runtime, y post-processing cinematográfico — la marca se siente premium en cualquier dispositivo.
Impresiones / semana
promedio
Pico mensual
impresiones en meses fuertes
GLB optimizados
Objetivo
con degradación
3D bonito que no se cae en un teléfono
Las landings 3D suelen elegir: o se ven increíbles en desktop y mueren en mobile, o degradan tanto que pierden el alma. El objetivo era no elegir.
Performance como sistema, no como parche
Definí tiers estáticos por capacidad y los corregí en caliente leyendo FPS reales. Los assets pasan por un pipeline propio (Draco + meshopt) y el mármol se genera por triplanar para no depender de UVs en los escaneos. El post-proceso es de un solo composer.
Catálogo cinematográfico, adaptable y liviano
56 GLB comprimidos un 33%, carga secuencial con prefetch predictivo y un modo museo que se siente premium en cualquier hardware. La vitrina acompaña a un estudio que hoy promedia unas 20 impresiones por semana y trepa a cientos de impresiones en los meses de mayor demanda.
Lo que se ve
Modo museo
Estatuas de mármol bajo spotlights que se deslizan al cambiar de pieza, como una sala de exhibición que se recorre sola.
Navegación espacial
Del hero al museo sin cortes: la cámara viaja por el espacio dentro de un único Canvas WebGL, sin recargar la escena.

Mármol triplanar procedural
La textura PBR de mármol real que alimenta la proyección triplanar: vetas continuas que el shader aplica sobre escaneos sin coordenadas UV.
Ficha de pieza
Cada modelo del catálogo abre con sus datos, materiales y opciones de impresión, anclados al objeto en 3D.
Post-processing cinematográfico
Bloom, viñeta y corrección de color en un solo composer, calibrado para que la marca se vea premium en cualquier pantalla.
Degradación por FPS en vivo
Indicador de tier activo: la escena baja o sube calidad leyendo los FPS reales del dispositivo en runtime.
Catálogo como galería
Las 56 piezas se recorren como obras de museo, con carga secuencial y prefetch de la siguiente para que nunca haya espera.
Lo que no se ve
La contraparte invisible de la pantalla: la arquitectura, el backend y la infraestructura que sostienen el producto.
Pipeline propio de optimización de assets
56 GLB pasan por una etapa de build con Draco + meshopt que cuantiza geometría y comprime mallas sin perder silueta. De 91MB a 61MB sin sacrificar el detalle de los escaneos.
Carga por tiers en dos fases adaptativa
El cliente arranca con un tier estático según capacidad detectada y lo corrige en caliente leyendo FPS reales: primero llega la geometría liviana, luego las texturas PBR completas. Verificado con tests que simulan red lenta.
Cola de impresión y gestión de pedidos
Detrás de la vitrina hay una cola que ordena trabajos por prioridad y tiempo de slicing estimado, balanceando el parque de impresoras. Los pedidos entran, se programan y se trackean hasta el despacho.
Estimación de slicing y tiempos
Antes de aceptar un pedido el sistema estima material, tiempo de impresión y costo a partir del GLB, para no comprometer fechas imposibles en los meses de mayor demanda.
CDN y caché multinivel de modelos
Los GLB comprimidos se sirven desde CDN con caché de borde y un service worker que precarga la siguiente pieza del recorrido. El segundo visitante baja casi todo desde caché.
Tracking de pedidos y picos de demanda
Cada pedido emite eventos de estado (en cola → imprimiendo → curado → despachado) que alimentan el tracking del cliente y métricas internas para anticipar los picos estacionales.
Las piezas que lo sostienen
Código real del proyecto — los fragmentos que sostienen la idea, tal cual viven en el repositorio.
function makeMarbleMaterial() {
const mat = new MeshStandardMaterial({ color: "#efe9df", roughness: 0.6 });
mat.onBeforeCompile = (shader) => {
shader.uniforms.uAlbedo = { value: MARBLE_ALBEDO };
shader.uniforms.uMarbleScale = { value: MARBLE_SCALE };
// Posición y normal en espacio de MUNDO hacia el fragment.
shader.vertexShader =
"varying vec3 vTriPos;\nvarying vec3 vTriNrm;\n" + shader.vertexShader
.replace("#include <begin_vertex>",
"#include <begin_vertex>\n vTriPos = (modelMatrix * vec4(position,1.0)).xyz;\n" +
" vTriNrm = normalize(mat3(modelMatrix) * normal);");
// Pesos triplanar desde la normal (sesgados por pow 4).
shader.fragmentShader =
"uniform sampler2D uAlbedo;\nvarying vec3 vTriPos;\nvarying vec3 vTriNrm;\n" +
"vec3 mtBlend(vec3 n){ vec3 b=pow(abs(n),vec3(4.0)); return b/max(b.x+b.y+b.z,1e-4); }\n" +
shader.fragmentShader.replace("#include <color_fragment>",
"#include <color_fragment>\n { vec3 bl = mtBlend(vTriNrm); float s = uMarbleScale;\n" +
" float m = texture2D(uAlbedo, vTriPos.zy*s).g*bl.x\n" +
" + texture2D(uAlbedo, vTriPos.xz*s).g*bl.y\n" +
" + texture2D(uAlbedo, vTriPos.xy*s).g*bl.z;\n" +
" diffuseColor.rgb *= (0.74 + 0.46 * m); }");
};
mat.customProgramCacheKey = () => "museum-marble-triplanar";
return mat;
}Mármol triplanar sobre escaneos sin UVs. Los escaneos STL no traen coordenadas UV. Se inyecta mármol muestreando en posición de mundo sobre tres ejes y mezclando por la normal.
async function detectTier(): Promise<ExperienceTier> {
// 1) GPU: tier 0-3 por benchmark del renderer.
let result: { tier: number; isMobile?: boolean };
try { result = await getGPUTier(); }
catch { result = { tier: 0, isMobile: false }; } // sin WebGL -> lo peor
let visualTier: VisualTier =
result.tier >= 3 ? "high" : result.tier === 2 ? "mid" : "low";
// Móvil: misma GPU rinde menos (throttling térmico, batería).
if (result.isMobile) visualTier = lowerTier(visualTier, 1);
// 2) Specs: poca RAM o pocos hilos -> bajar otro escalón.
const mem = readDeviceMemory();
if (typeof mem === "number" && mem < 4) visualTier = lowerTier(visualTier, 1);
const cores = navigator.hardwareConcurrency;
if (typeof cores === "number" && cores < 4) visualTier = lowerTier(visualTier, 1);
// 3) saveData / red 2g -> capear calidad y cortar prefetch.
const reducedMotion = matchesMedia("(prefers-reduced-motion: reduce)");
return { visualTier, ...tierToSettings(visualTier), reducedMotion, ready: true };
}Detección de tier de calidad adaptativo (SSR-safe). Clasifica el dispositivo una sola vez combinando benchmark de GPU (detect-gpu), RAM, hilos y red, y baja escalones por móvil/memoria/CPU. Es solo el punto de partida: <PerformanceGuard> corrige luego por FPS reales. Llama getGPUTier() en un effect para no suspender el árbol y evitar mismatch de hidratación.
En concreto
Un solo Canvas con navegación espacial accesible y un 'modo museo' de estatuas con spotlight.
Mármol PBR triplanar procedural aplicado sobre escaneos sin coordenadas UV.
Carga por tiers en dos fases con corrección adaptativa por FPS en runtime, verificada con tests de red.
Pipeline propio de optimización de assets: 56 GLB de 91MB a 61MB (Draco + meshopt).
Estudio en crecimiento: la vitrina alimenta una operación que ronda las 20 impresiones por semana, con picos de cientos de impresiones en los meses fuertes.