Capítulo Mundo de la guía Neblla — el motor 3D: escena, cámaras, controles, física, eventos, modelos, el catálogo de piezas y el muñeco. El contrato base está en core.txt; el criterio de cuándo usar Neblla, en empezar.txt. # Mundo, el motor 3D **Mundo** es el motor 3D de Neblla (el nombre no se traduce; en el código es `neblla.world`). Lo usas desde `client.js` para un juego, una app o una web: un plataformas, un juego en tercera persona, una maqueta de un piso o un visor 3D de un producto en una tienda. Dibuja con three.js y simula con Rapier, y ya viene con luz, cielo, sombras, cámaras y controles que se ven bien sin tocar nada. Tú escribes las reglas; Mundo pone la escena, la física y los controles de teclado, ratón, mando y pantalla táctil. Requiere el módulo `world` encendido en Config → Módulos (o `neblla_update_app` con `features: { world: true }`). Está **apagado por defecto**. Con el módulo apagado, `neblla.world.create()` rechaza con `feature_disabled`. El motor es un fichero aparte del SDK: se descarga la primera vez que la página llama a `neblla.world.create()`, nunca antes. Una página sin 3D no lo paga. Son unos 1,8 MB comprimidos (la física es la mayor parte), en caché para siempre. ```javascript const world = await neblla.world.create({ container: '#juego', sky: 'day', camera: 'third' }); world.add({ shape: 'box', size: [20, 1, 20], position: [0, -0.5, 0], color: '#7fa65a' }); const yo = world.add({ character: true, position: [0, 1, 0] }); world.control(yo); // el teclado, el ratón, el mando o el joystick táctil lo mueven; la cámara lo sigue ``` `neblla.world.create(opts)` **(CLIENT)** — devuelve `Promise`. Opciones: - `container`: el elemento (o su selector) donde va el lienzo; el lienzo ocupa todo el contenedor y lo sigue al cambiar de tamaño. **Dale una altura con CSS** (un `div` vacío mide 0). Sin `container`, ocupa la ventana entera. - `sky`: `'day'` (por defecto), `'sunset'`, `'night'` o `'studio'` (luz neutra, sin niebla y un suelo que solo muestra la sombra: para un producto). Cada cielo trae su luz, su niebla y su sombra. - `camera`: un preajuste (`'side'`, `'top'`, `'iso'`, `'third'` por defecto, `'first'`, `'chase'`) o `{ preset, distance, height, yaw, pitch, target }`. - `quality`: `'auto'` (por defecto), `'high'`, `'medium'`, `'low'` o `'lowest'`; se cambia luego con `world.quality.set()`. - `fog`: `[color, cerca, lejos]`, `{ color, near, far }` o `false`; la del cielo por defecto. `far`: hasta dónde ve la cámara (1000). `shadow: { range }`: los metros que cubre la sombra (20). Ver «Mundos grandes en un móvil». - `touch`: `'auto'` (por defecto: el joystick y los botones salen en un móvil o una tableta), `true` o `false`. - `gravity`: `[x, y, z]`, por defecto `[0, -9.81, 0]`. - `lock`: un eje que nada puede dejar, como `'z'`. Con `camera: 'side'` es `'z'` por defecto: el plataformas es el mismo mundo visto de lado. - `fallY`: por debajo de esta altura salta `fall` (por defecto `-30`). - `headless: true`: solo la simulación, sin dibujo (también en Node). ## Las capas Mundo va por capas y puedes bajar de capa cuando quieras: `world.three` es three.js (`{ THREE, scene, camera, renderer }`), `world.rapier` es Rapier y `world.sim` es la simulación sin dibujo. Lo que Mundo no trae lo añades tú con three.js o Rapier sobre la misma escena. `await neblla.world.engine()` devuelve el módulo del motor (`createWorld`, `PRESETS`, `SKIES`, `STEP`…). ## Cosas: formas, cuerpos y modelos `world.add(spec)` **(CLIENT)** — añade una cosa y la devuelve. Es síncrono. - Forma: `shape: 'box'` con `size: [ancho, alto, fondo]`, `'sphere'` con `radius`, `'capsule'` con `radius` y `height`, `'convex'` con `points` o `'mesh'` con `vertices` e `indices`. - Cuerpo: `body: 'static'` (por defecto: suelo, paredes), `'dynamic'` (cae, rueda, choca), `'kinematic'` (lo mueves tú: una plataforma que va y viene) o `'none'` (solo se ve). - Sitio: `position: [x, y, z]` (metros; 1 unidad = 1 metro), `rotation: [x, y, z]` (radianes). - Aspecto: `color` (`'#ffd23f'` o `0xffd23f`), `material: 'matte'` (por defecto), `'glossy'`, `'metal'` o `'emissive'`, `glow`, `opacity`, `visible`. - Física: `friction`, `restitution` (rebote), `density`, `lock` (ejes que esta cosa no deja, como `'xz'`). - Zonas: `sensor: true` (no choca, avisa de quién entra y sale) y `collect: true` (una moneda: la recoge un personaje, salta `collect` y desaparece; con `keep: true` se queda). - `name` y `data`: lo tuyo. `world.find(name)` la busca. La cosa devuelta tiene `id`, `name`, `data`, `object` (su objeto de three.js), `position` y `velocity` (leer y escribir, `[x, y, z]`), `rotation` (un cuaternión `{ x, y, z, w }`; se escribe también en radianes), `impulse([x, y, z])` (un empujón a un cuerpo `dynamic`), `moveTo(position, rotation?)` (el siguiente sitio de un cuerpo `kinematic`, que empuja lo que encuentra) y `remove()`. ### Personajes `world.add({ character: true, position, speed: 5, runSpeed: 8, jump: 6.5, radius: 0.35, height: 1.8 })` — una cápsula con el controlador de personaje de Rapier: sube escalones (`step`, 0,35 m), sube pendientes hasta `slope` grados (50), se pega al suelo al bajar y salta con margen (un poco después de dejar un borde y un poco antes de tocar el suelo). `grounded` dice si pisa suelo. `world.control(personaje)` — los controles lo mueven y la cámara lo sigue. Sin `control()`, lo mueves tú con `personaje.input = { x, z, jump, run }` (`x` y `z` entre −1 y 1, en los ejes del mundo). ## Cámaras `world.camera.use(preajuste, { target, transition, distance, height, yaw, pitch })` **(CLIENT)** — cambia de cámara, también a mitad de partida, con una transición suave (`transition`: segundos, 0,6 por defecto; 0 es un corte). `target` es una cosa o un punto `[x, y, z]`; con `control()` es el personaje. | Preajuste | Para | Controles | |---|---|---| | `side` | plataformas, de lado | izquierda y derecha, saltar | | `top` | una arena, desde arriba | las cuatro direcciones en el plano | | `iso` | una granja, estrategia | las cuatro direcciones, en diagonal como se ve | | `third` | tipo Roblox, un visor de producto | hacia donde mira la cámara; arrastrar con el botón derecho gira (con el izquierdo también en `third`) y la rueda acerca | | `first` | primera persona | el ratón queda capturado y mira; WASD camina | | `chase` | carreras | arriba acelera, los lados giran | `world.camera.preset` dice cuál está puesta y `world.camera.orbit` (`{ yaw, pitch, zoom }`) es el giro que mueven los controles. ## Controles Teclado (WASD o flechas, espacio salta, Mayús corre, E o Intro es la acción), ratón (**arrastrar con el botón derecho gira la cámara**, como en Roblox, sin el menú del navegador; en `third` también con el izquierdo, y en `chase` la cámara se queda donde la dejas, y al andar «adelante» es hacia donde mira la cámara; **doble clic con el derecho** la pone otra vez detrás; en `first` el ratón queda capturado), mando (stick izquierdo mueve, el derecho mira, A salta, X acción) y, en pantallas táctiles, un joystick a la izquierda y los botones de saltar y de acción a la derecha; un dedo en el resto de la pantalla gira la cámara. Cada preajuste de cámara trae sus controles. `world.player.action` dice si la acción está pulsada. ## Eventos y temporizadores Los nombres son fijos: son los mismos que usarán los géneros en JSON. - `world.on('touch', (a, b) => …)` — dos cosas se tocan (empiezan a tocarse). - `world.on('enter', (zona, cosa) => …)` y `world.on('leave', (zona, cosa) => …)` — algo entra o sale de un `sensor`. - `world.on('collect', (moneda, quien) => …)` — un personaje recoge un `collect`. - `world.on('fall', (cosa) => …)` — algo cae por debajo de `fallY`. - `world.on('step', (dt) => …)` — cada paso de la simulación: aquí van las reglas. Cada cosa tiene los suyos: `cosa.onTouch(fn)`, `cosa.onEnter(fn)`, `cosa.onLeave(fn)`, `cosa.onCollect(fn)`, `cosa.onFall(fn)`. Todos devuelven una función que lo quita. `world.after(segundos, fn)` y `world.every(segundos, fn)` — temporizadores en el reloj de la simulación, no en el de la pared: la misma partida da los mismos tiempos. Devuelven una función que los cancela. `world.time` es el tiempo simulado. `world.onFrame((dt) => …)` — cada dibujo, para lo que solo se ve (una pieza que gira en un escaparate), nunca para las reglas. ## La simulación La física va a 60 pasos por segundo fijos y el dibujo, a lo que dé la pantalla, entre los dos últimos pasos. Mundo usa la versión determinista de Rapier: la misma simulación, con las mismas entradas, da el mismo estado en cualquier dispositivo. `world.state()` devuelve el sitio, el giro y la velocidad de cada cosa, y `world.hash()`, una huella corta de ese estado: dos dispositivos que la comparan saben si van iguales. El multijugador está más abajo, en «Multijugador». `world.stop()` y `world.start()` paran y siguen el bucle; `world.update(dt)` avanza a mano y `world.step()`, un paso. `world.destroy()` lo quita todo. ## Calidad automática Con `quality: 'auto'` Mundo mide los fotogramas por segundo: si no llega a 45 baja la nitidez y las sombras, y si pasa de 58 las sube. En un móvil empieza más ligero. `world.quality.level` dice el nivel de ahora. Cada nivel tiene también un presupuesto de draw calls (400, 250, 150 y 100, de `high` a `lowest`): un fotograma que lo pasa cuenta como lento y baja un nivel aunque vaya a 60. **Cambiarla en marcha** (C524). `world.quality.set('auto' | 'high' | 'medium' | 'low' | 'lowest')` la cambia al momento y devuelve el nivel; `'auto'` deja otra vez que Mundo decida, desde donde esté. Es lo que llama el menú de ajustes gráficos de un juego. `world.quality.modes` lista los valores y se emite el evento `quality` (`world.on('quality', (nivel, auto) => …)`). `world.stats()` dice qué cuesta un fotograma: `{ drawCalls, triangles, fps, entities, zones, lods, quality, auto, budget, overBudget }`. `drawCalls` y `triangles` son del último dibujo (0 sin dibujo) y `fps` va suavizado. ## Efectos y sonido Para el dibujo y el oído, nunca para las reglas: no tocan la simulación, así que dos dispositivos que no coinciden en una salpicadura siguen coincidiendo en el juego. `world.effect(nombre, { at, follow, color, scale, count, loop })` lanza partículas dibujadas en la tarjeta gráfica (un solo draw call por efecto). Los nombres: `splash` (salpicadura), `spark` (chispas), `sparkle` (destellos que se quedan flotando), `smoke` y `dust`. `at` es `[x, y, z]` o una cosa (un barco que salpica); `follow` hace que el efecto siga a una cosa mientras vive; `color` es un número (`0xffaa00`) o `'#ffaa00'`; `scale` agranda; `count` cambia cuántas (hasta 200; la calidad deja un 100 %, 70 %, 50 % o 30 %); `loop: true` no acaba hasta `stop()`. Devuelve `{ stop(), done, count, object }`; acaba solo, y `world.effects` lista los vivos. Un nombre que no existe, sin sitio, o con `scale`/`count` fuera de rango falla con `world_bad_effect` diciendo qué escribir. `world.sound(nombre | dirección | hoja.json, { at, follow, loop, loopStart, loopEnd, volume, rolloff })` suena **desde su sitio** (Web Audio con un panner; el oyente sigue a la cámara). Sin `at` ni `follow` suena sin posición (música, menús). Los sonidos de Neblla, generados por Neblla —sin ficheros ni licencias que citar— son `splash`, `hit`, `slash`, `coin`, `step`, `wave` y `gull`; con una dirección (`/media/olas.mp3`, una URL) suena cualquier sonido de tu app. Arranca tras el primer toque o tecla de la página (el navegador no deja antes); lo que se pide antes suena en cuanto se puede. Con la **hoja `.json` que escribe Eco** (la de una canción o una música de fondo) basta una dirección: Mundo escoge la fuente más ligera que el aparato sabe tocar (Opus antes que MP3) y, con `loop: true`, repite **entre los puntos de esa fuente** sin salto (`AudioBufferSourceNode`, no `