Efecto de nubes dinámicas con Three.js y shaders personalizados

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.

Etiquetas: three.js WebGL Shaders frontend efectos-visuales

Publicado el 6-20 05:19