Este tutorial muestra cómo crear un efecto de fondo con nubes en movimeinto utilizando Three.js y shaders personalizados. El resultado es una atmósfera atmosférica perfecta para interfaces de login o páginas splash.
Estructura del proyecto
La implementación requiere varios componentes: una página HTML con estilos CSS para el formulario, las librerías base de Three.js, y el código JavaScript que genera el efecto visual.
HTML y estilos CSS
Comenzamos con la estructura básica y los estilos del formulario de acceso:
<html>
<head>
<meta charset="UTF-8">
<title>Interfaz de acceso</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(230deg, #1a3a5c 0%, #0d1b2a 100%);
}
.formulario-acceso {
position: absolute;
width: 320px;
height: 320px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(40, 45, 60, 0.95);
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 40px;
box-sizing: border-box;
}
.formulario-acceso h2 {
color: #fff;
text-align: center;
margin-bottom: 30px;
}
.campo-entrada {
width: 100%;
padding: 12px;
margin-bottom: 15px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
color: #fff;
box-sizing: border-box;
}
.boton-enviar {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
font-weight: bold;
}
</style>
</head>
<body>
<div class="formulario-acceso">
<h2>Acceso</h2>
<input type="text" class="campo-entrada" placeholder="Usuario">
<input type="password" class="campo-entrada" placeholder="Contraseña">
<button class="boton-enviar">Entrar</button>
</div>
<canvas id="lienzo-nubes"></canvas>
</body>
</html>
Shader de vértices
El shader de vértices es straightforward - pasa las coordenadas UV al fragment shader:
const shaderVertice = `
varying vec2 coordenadasUV;
void main() {
coordenadasUV = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(posicion, 1.0);
}
`;
Shader de fragmentos con efecto niebla
El fragment shader implementa el efecto de profundidad y niebla:
const shaderFragmento = `
uniform sampler2D texturaNube;
uniform vec3 colorNiebla;
uniform float nieblaCerca;
uniform float nieblaLejos;
varying vec2 coordenadasUV;
void main() {
// Calcular profundidad
float profundidad = gl_FragCoord.z / gl_FragCoord.w;
// Factor de niebla basado en profundidad
float factorNiebla = smoothstep(nieblaCerca, nieblaLejos, profundidad);
// Obtener color de la textura
vec4 colorTexto = texture2D(texturaNube, coordenadasUV);
// Aplicar niebla
colorTexto *= pow(gl_FragCoord.z, 15.0);
gl_FragColor = mix(colorTexto, vec4(colorNiebla, colorTexto.w), factorNiebla);
}
`;
Inicialización de Three.js
Configuración de la escena, cámara y renderer:
function inicializarEscena() {
// Crear contenedor
const contenedor = document.createElement('div');
document.body.appendChild(contenedor);
// Configurar cámara
const camara = new THREE.PerspectiveCamera(
30,
window.innerWidth / window.innerHeight,
1,
3000
);
camara.position.z = 6000;
// Crear escena
const escena = new THREE.Scene();
// Geometría para las nubes
const geometria = new THREE.PlaneGeometry(64, 64);
const geometriaCombinada = new THREE.Geometry();
// Generar múltiples planos para efecto de nubes
for (let i = 0; i < 8000; i++) {
const plano = new THREE.PlaneGeometry(64, 64);
plano.position.set(
Math.random() * 1000 - 500,
-Math.random() * 200 - 15,
i
);
plano.rotation.z = Math.random() * Math.PI;
const escala = Math.random() * 1.5 + 0.5;
plano.scale.set(escala, escala, 1);
GeometryUtils.merge(geometriaCombinada, plano);
}
// Niebla
const niebla = new THREE.Fog(0x4a7a9f, -100, 3000);
// Material shader
const material = new THREE.MeshShaderMaterial({
uniforms: {
texturaNube: { type: 't', value: 2, texture: texturaBase },
colorNiebla: { type: 'c', value: niebla.color },
nieblaCerca: { type: 'f', value: niebla.near },
nieblaLejos: { type: 'f', value: niebla.far }
},
vertexShader: shaderVertice,
fragmentShader: shaderFragmento,
depthTest: false
});
// Crear malla y añadir a escena
const malla = new THREE.Mesh(geometriaCombinada, material);
escena.addObject(mala);
// Duplicar para efecto continuo
const malla2 = new THREE.Mesh(geometriaCombinada.clone(), material);
malla2.position.z = -8000;
escena.addObject(malla2);
// Renderer WebGL
const renderer = new THREE.WebGLRenderer({
antialias: false
});
renderer.setSize(window.innerWidth, window.innerHeight);
contenedor.appendChild(renderer.domElement);
return { escena, camara, renderer, material };
}
Bucle de animación
La animación crea el movimiento de las nubes:
function iniciarAnimacion(escena, camara, renderer) {
let posicionAnterior = 0;
let tiempoInicio = Date.now();
// Variables para movimiento del mouse
let posicionRatonX = 0;
let posicionRatonY = 0;
// Listener para movimiento del mouse
document.addEventListener('mousemove', (evento) => {
posicionRatonX = (evento.clientX - window.innerWidth / 2) * 0.25;
posicionRatonY = (evento.clientY - window.innerHeight / 2) * 0.15;
});
// Listener para redimensionar ventana
window.addEventListener('resize', () => {
camara.aspect = window.innerWidth / window.innerHeight;
camara.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
function bucleAnimacion() {
requestAnimationFrame(bucleAnimacion);
// Calcular posición basada en tiempo
const tiempoActual = (Date.now() - tiempoInicio) * 0.03;
const posicion = tiempoActual % 8000;
// Mover cámara suavemente
camara.position.x += (posicionRatonX - camara.target.position.x) * 0.01;
camara.position.y += (-posicionRatonY - camara.target.position.y) * 0.01;
// Mover a lo largo del eje Z
camara.position.z = -posicion + 8000;
// Actualizar objetivo de la cámara
camara.target.position.x = camara.position.x;
camara.target.position.y = camara.position.y;
camara.target.position.z = camara.position.z - 1000;
// Renderizar
renderer.render(escena, camara);
}
bucleAnimacion();
}
Fondo degradado estático
Antes de cargar WebGL, generamos un fondo degradado como fallback:
function crearFondoDegradado() {
const lienzo = document.createElement('canvas');
lienzo.width = 32;
lienzo.height = window.innerHeight;
const contexto = lienzo.getContext('2d');
const degradado = contexto.createLinearGradient(0, 0, 0, lienzo.height);
degradado.addColorStop(0, '#1e4877');
degradado.addColorStop(0.5, '#4584b4');
contexto.fillStyle = degradado;
contexto.fillRect(0, 0, lienzo.width, lienzo.height);
document.body.style.background = `url(${lienzo.toDataURL('image/png')})`;
}
Resultado
El código produce un efecto visual donde:
- Las nubes se mueven horizontalmente creando sensación de profundidad
- La niebla aparece gradualmente en la distancia
- El movimiento del mouse añade parallax a la escena
- El formulario de acceso flota sobre el fondo animado
Este efecto es ideal para páginas de presentación, pantallas de carga o interfaces de usuario que requieran una atmósfera visual distintiva.