Ejercicios resueltos — Capítulo 29¶
Volver al índice · Capítulo original
Ejercicio 1: Escena VR con A-Frame¶
Crea una escena VR con 5 objetos de colores.
Solución¶
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Mi escena VR</title>
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<a-box position="-1 0.5 -3" color="#6a5acd"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#22d3ee"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#ef4444"></a-cylinder>
<a-cone position="-2 0.75 -4" radius-bottom="1" height="1.5" color="#f59e0b"></a-cone>
<a-torus position="2 1 -5" color="#10b981"></a-torus>
<a-plane position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#f8fafc"></a-plane>
<a-sky color="#0f172a"></a-sky>
</a-scene>
</body>
</html>
Ejercicio 2: Botón "Enter VR"¶
Añade un botón para entrar en VR.
Solución¶
Con A-Frame: aparece automáticamente si el dispositivo lo soporta. No hay que hacer nada.
Con Three.js:
import { VRButton } from 'https://unpkg.com/three@0.160.0/examples/jsm/webxr/VRButton.js';
renderer.xr.enabled = true;
document.body.appendChild(VRButton.createButton(renderer));
Ejercicio 3: Three.js a WebXR¶
Convierte una escena Three.js a WebXR.
Solución¶
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>WebXR</title>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { VRButton } from 'https://unpkg.com/three@0.160.0/examples/jsm/webxr/VRButton.js';
const escena = new THREE.Scene();
escena.background = new THREE.Color(0x0f172a);
const camara = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 100);
camara.position.set(0, 1.6, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
escena.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));
const cubo = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0x6a5acd })
);
cubo.position.set(0, 1.6, -2);
escena.add(cubo);
renderer.setAnimationLoop((tiempo) => {
cubo.rotation.y = tiempo / 1000;
renderer.render(escena, camara);
});
addEventListener('resize', () => {
camara.aspect = innerWidth / innerHeight;
camara.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
Ejercicio 4: AR con ARCore¶
Prueba AR en un móvil Android.
Solución¶
<script type="module">
import * as THREE from 'three';
import { ARButton } from 'https://unpkg.com/three@0.160.0/examples/jsm/webxr/ARButton.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
document.body.appendChild(
ARButton.createButton(renderer, { requiredFeatures: ['hit-test'] })
);
const escena = new THREE.Scene();
const camara = new THREE.PerspectiveCamera();
escena.add(new THREE.HemisphereLight(0xffffff, 0xbbbbff, 3));
const cubo = new THREE.Mesh(
new THREE.BoxGeometry(0.2, 0.2, 0.2),
new THREE.MeshStandardMaterial({ color: 0x6a5acd })
);
cubo.visible = false;
escena.add(cubo);
const reticle = new THREE.Mesh(
new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2),
new THREE.MeshBasicMaterial({ color: 0x22d3ee })
);
reticle.matrixAutoUpdate = false;
reticle.visible = false;
escena.add(reticle);
let hitTestSource = null;
renderer.xr.addEventListener('sessionstart', async () => {
const session = renderer.xr.getSession();
const viewerSpace = await session.requestReferenceSpace('viewer');
hitTestSource = await session.requestHitTestSource({ space: viewerSpace });
session.addEventListener('select', () => {
if (reticle.visible) {
const clon = cubo.clone();
clon.position.setFromMatrixPosition(reticle.matrix);
clon.visible = true;
escena.add(clon);
}
});
});
renderer.setAnimationLoop((tiempo, frame) => {
if (frame && hitTestSource) {
const refSpace = renderer.xr.getReferenceSpace();
const hits = frame.getHitTestResults(hitTestSource);
if (hits.length) {
reticle.visible = true;
reticle.matrix.fromArray(hits[0].getPose(refSpace).transform.matrix);
}
}
renderer.render(escena, camara);
});
</script>
Ejercicio 5: Audio espacial¶
Añade audio espacial a un objeto.
Solución¶
const listener = new THREE.AudioListener();
camara.add(listener);
const sonido = new THREE.PositionalAudio(listener);
const loader = new THREE.AudioLoader();
loader.load('ambiente.mp3', (buffer) => {
sonido.setBuffer(buffer);
sonido.setRefDistance(1);
sonido.setLoop(true);
sonido.play();
});
cubo.add(sonido); // el sonido viene del cubo
Al moverte por la escena VR, el sonido cambia de volumen e intensidad según la distancia.
Ejercicio 6: Hand tracking en Quest¶
Implementa hand tracking básico.
Solución¶
const session = renderer.xr.getSession();
session.addEventListener('inputsourceschange', (event) => {
event.added.forEach((input) => {
if (input.hand) {
console.log('Mano detectada:', input.handedness);
}
});
});
renderer.setAnimationLoop((tiempo, frame) => {
if (!frame) return;
for (const input of session.inputSources) {
if (input.hand) {
for (const joint of input.hand.values()) {
// joint.transform es una matriz 4x4
// joint.jointName: 'wrist', 'thumb-tip', 'index-finger-tip', ...
}
}
}
renderer.render(escena, camara);
});
Ejercicio 7: Optimizar para 90 FPS¶
Optimiza una escena VR para alcanzar 90 FPS.
Solución¶
Checklist:
- [ ] Bajo poligonaje: cada triángulo cuenta.
- [ ] Texturas comprimidas: KTX2 o Basis.
- [ ] Instancing para objetos repetidos.
- [ ] LOD (Level of Detail) según distancia.
- [ ] Evita transparencias (caras de renderizar).
- [ ] Sin sombras dinámicas complejas.
- [ ] Sin post-processing pesado.
- [ ]
frustumCulled = truepor defecto.
Medir FPS:
let frames = 0;
let inicio = performance.now();
renderer.setAnimationLoop(() => {
frames++;
const ahora = performance.now();
if (ahora - inicio >= 1000) {
console.log('FPS:', frames);
frames = 0;
inicio = ahora;
}
renderer.render(escena, camara);
});
Objetivo: 90 FPS estables (11 ms por frame).