0.1 Resultado final y alcance
Tu proyecto tendrá un configurador tridimensional con cámara orbital, piezas seleccionables, vista explosionada, materiales realistas, precios, enlaces de compra y varias configuraciones.
Aprende a crear una experiencia como el comparador de servidores: modelos procedurales, materiales PBR, iluminación controlada, raycasting, etiquetas, fichas de producto, cotizaciones dinámicas, animaciones y diseño responsive.
0 de 26 lecciones completadas
Un sitio comercial con una escena 3D navegable y una interfaz de producto conectada a datos reales.
Tu proyecto tendrá un configurador tridimensional con cámara orbital, piezas seleccionables, vista explosionada, materiales realistas, precios, enlaces de compra y varias configuraciones.
Organiza los archivos para que la escena 3D, la interfaz y los datos puedan crecer sin convertirse en un solo bloque inmanejable.
El proyecto entregado sigue esta separación: index.html contiene la interfaz, styles.css el sistema visual y app.js toda la escena.
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/"
}
}
</script>
<script type="module" src="app.js"></script>python -m http.server 8080 y abre http://localhost:8080.Construye el núcleo WebGL, configura color, sombras y controles orbitales.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x070a10);
scene.fog = new THREE.FogExp2(0x070a10, 0.018);
const camera = new THREE.PerspectiveCamera(36, 1, 0.05, 100);
camera.position.set(14.5, 10.2, 15.5);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: 'high-performance'
});
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.target.set(0, 4.35, 0);
controls.minDistance = 7;
controls.maxDistance = 34;
function resize() {
const rect = viewport.getBoundingClientRect();
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
renderer.setSize(rect.width, rect.height, false);
}
new ResizeObserver(resize).observe(viewport);El objetivo es que la escena ocupe únicamente el área central, no toda la ventana. Por eso se mide el contenedor viewport.
Aprende a conservar reflejos y RGB sin convertir las superficies claras en manchas blancas.
El brillo final no depende de una sola luz. Es el resultado de la suma de luces directas, entorno, emisivos, exposición y bloom.
const LIGHTING_PRESETS = {
balanced: {
exposure: 0.72,
hemi: 0.72,
key: 1.85,
rim: 24,
fill: 14,
accent: 3.2,
secondary: 1.5,
emissive: 0.34,
floor: 0.09,
bloomHigh: 0.11
}
};
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.72;Los números no son universales, pero sirven como punto de partida consistente para una escena pequeña con materiales metálicos y vidrio.
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(1, 1),
0.11, // fuerza
0.28, // radio
0.92 // threshold: solo altas luces
);Construye gabinete, GPU, motherboard y ventiladores a partir de cajas, cilindros, toros y grupos.
function createGPU(accent, isSecond = false) {
const gpu = new THREE.Group();
gpu.name = isSecond ? 'GPU 2' : 'GPU 1';
const body = new THREE.Mesh(
new THREE.BoxGeometry(5.45, 0.95, 2.20),
new THREE.MeshStandardMaterial({
color: 0x202a38,
metalness: 0.78,
roughness: 0.27
})
);
gpu.add(body);
return gpu;
}Cada grupo conserva posición base, posición objetivo de explosión, identificador de producto y un punto de anclaje para la etiqueta HTML.
Carcasas, heatsinks, módulos RAM y fuente.
Ventiladores, tornillos, capacitores y plataforma.
Anillos RGB y bordes luminosos.
Cables de alimentación y heatpipes.
Para abrir el panel de vidrio, el grupo debe tener su pivote cerca de la bisagra. Para girar un ventilador, solo se anima el rotor, no todo el marco.
Da identidad visual a las piezas sin depender de texturas descargadas.
const glass = new THREE.MeshPhysicalMaterial({
color: 0x8fdce8,
transmission: 0.82,
transparent: true,
opacity: 0.24,
roughness: 0.12,
metalness: 0,
thickness: 0.18,
ior: 1.45,
side: THREE.DoubleSide
});El vidrio debe seguir permitiendo leer los componentes. Demasiada opacidad o iluminación interna intensa produce una caja lechosa.
function labelTexture(title, subtitle) {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 1024;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#111821';
ctx.fillRect(0, 0, 1024, 1024);
ctx.fillStyle = '#f7fafc';
ctx.font = '900 78px system-ui';
ctx.fillText(title, 92, 390);
ctx.font = '700 29px system-ui';
ctx.fillText(subtitle, 96, 445);
return new THREE.CanvasTexture(canvas);
}Separa la información comercial de la geometría para poder crear varias propuestas con el mismo motor 3D.
const products = [
{
id: 'gpu1',
name: 'RTX 3090 · GPU principal',
model: 'NVIDIA GeForce RTX 3090 · 24 GB',
price: 24500,
phase: 1,
retailer: 'Mercado Libre',
url: 'https://...',
specs: [
['VRAM', '24 GB'],
['Consumo', '350 W']
]
}
];const quoteProfiles = {
full: {
name: 'Servidor IA completo',
products: fullProducts,
tags: ['LLM', 'Imagen', 'Video']
},
compact: {
name: 'Servidor LLM + imagen',
products: compactProducts,
tags: ['Qwen', 'DeepSeek', 'ComfyUI']
}
};Al cambiar de perfil, reconstruyes el modelo, actualizas la lista y recalculas el total. La interfaz no necesita duplicarse.
Conecta el clic sobre una geometría con su ficha comercial en el DOM.
mesh.userData.componentId = 'gpu1';
function pickAt(clientX, clientY) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObject(modelRoot, true);
return hits.find(hit => hit.object.userData.componentId);
}const point = anchor.getWorldPosition(new THREE.Vector3());
point.project(camera);
const x = (point.x * 0.5 + 0.5) * viewport.clientWidth;
const y = (-point.y * 0.5 + 0.5) * viewport.clientHeight;
label.style.left = `${x}px`;
label.style.top = `${y}px`;Los hotspots son HTML, así que pueden tener textos nítidos, tooltips, accesibilidad y eventos normales. Three.js únicamente calcula dónde colocarlos.
Añade movimiento solo donde comunica algo: ensamblaje, apertura, rotación y foco.
function setExploded(enabled) {
for (const group of componentGroups.values()) {
const base = group.userData.basePosition.clone();
const offset = enabled
? group.userData.explode
: new THREE.Vector3();
group.userData.targetPosition = base.add(offset);
}
}
// En el loop:
group.position.lerp(group.userData.targetPosition, 0.105);Calcula el Box3 de la pieza, obtiene centro y tamaño, y anima cámara y target. El encuadre debe ajustarse al volumen real, no usar una distancia fija.
Lista izquierda, escena central e inspector derecho.
Inspector flotante y escena con más espacio.
Lista lateral deslizable e inspector inferior.
Botones, etiquetas, foco y contenido equivalente fuera del canvas.
Entrega una experiencia estable en diferentes GPUs y navegadores.
El sitio puede publicarse como contenido estático en Vercel, Netlify, GitHub Pages, Cloudflare Pages o un hosting convencional. Si usas modelos GLB, activa compresión y cache de larga duración.
# Python
python -m http.server 8080
# Node.js
npx serve .Elige un producto distinto y aplica la misma arquitectura de principio a fin.
Micrófono, interfaz, monitores y acústica.
Paneles, inversor, baterías y protecciones.
Muebles, cubierta, electrodomésticos y acabados.
Router, switch, servidor, NAS y UPS.