Create 666.html: A Cyberpunk Voxel Matrix Gas Station

by Alex Johnson 54 views

In this article, we'll guide you through creating a mesmerizing cyberpunk voxel matrix gas station using HTML, CSS, and JavaScript with the Three.js library. This project will demonstrate how to build a visually stunning and interactive 3D environment directly in your browser. Let's dive into the steps and code snippets needed to bring this futuristic scene to life.

Setting Up the HTML Structure

To begin, we need to establish the basic structure of our HTML file. This involves setting up the viewport, linking the necessary libraries, and creating the containers for our 3D scene and user interface. Understanding the foundational HTML elements is key to structuring your web application effectively. Let's break down the essential components:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Posto Voxel Matrix</title>
    <style>
        /* CSS Styles will go here */
    </style>
</head>
<body>

    <div id="loading">CARREGANDO A MATRIX...</div>
    <div id="ui-layer">
        <h1>Posto 0101</h1>
        <p>MOUSE ESQUERDO: ORBITAR</p>
        <p>MOUSE DIREITO: PAN</p>
        <p>SCROLL: ZOOM</p>
    </div>
    <div id="canvas-container"></div>

    <!-- Three.js e OrbitControls -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

    <script>
        // JavaScript Code will go here
    </script>
</body>
</html>
  • DOCTYPE html: Declares the document type and version of HTML being used.
  • html lang="pt-BR": The root element of the page, specifying the language as Portuguese (Brazilian).
  • head: Contains metadata, character set, viewport settings, title, and CSS styles.
  • meta charset="UTF-8": Specifies the character encoding for the document.
  • meta name="viewport" content="width=device-width, initial-scale=1.0": Configures the viewport for responsive design.
  • title: Sets the title of the webpage displayed in the browser tab.
  • style: An internal CSS block for styling the elements.
  • body: Contains the visible content of the page.
  • div id="loading": Displays a loading message while the 3D scene initializes.
  • div id="ui-layer": An overlay for displaying user interface elements, such as instructions.
  • div id="canvas-container": The container where the Three.js scene will be rendered.
  • script src="...": Includes the Three.js library and OrbitControls for camera manipulation.
  • script: An internal JavaScript block for the application logic.

Styling the Scene with CSS

Next, we apply CSS styles to create the desired cyberpunk aesthetic. The styles will set up a dark background, neon text, and other visual elements to enhance the scene's atmosphere. By understanding the role and usage of CSS, you can effectively control the visual presentation of your web content. Here are the key CSS styles used in our project:

<style>
    body { margin: 0; overflow: hidden; background-color: #000; font-family: 'Consolas', 'Courier New', monospace; }
    #canvas-container { width: 100vw; height: 100vh; }
    #ui-layer {
        position: absolute;
        top: 20px;
        left: 20px;
        color: #0f0;
        background: rgba(0, 20, 0, 0.8);
        border: 1px solid #0f0;
        padding: 15px;
        border-radius: 4px;
        box-shadow: 0 0 10px #0f0;
        pointer-events: none;
    }
    h1 { margin: 0 0 5px 0; font-size: 1.2rem; text-transform: uppercase; letter-spacing: 2px; }
    p { margin: 0; font-size: 0.8rem; opacity: 0.8; }
    #loading {
        position: absolute;
        top: 50%; left: 50%;
        transform: translate(-50%, -50%);
        color: #000;
        background: #0f0;
        padding: 20px 40px;
        font-weight: bold;
        font-family: monospace;
        box-shadow: 0 0 20px #0f0;
        z-index: 10;
    }
</style>
  • body:
    • margin: 0;: Removes default margins from the body.
    • overflow: hidden;: Hides scrollbars.
    • background-color: #000;: Sets a black background.
    • font-family: 'Consolas', 'Courier New', monospace;: Uses a monospace font for a futuristic look.
  • #canvas-container:
    • width: 100vw;: Sets the width to 100% of the viewport width.
    • height: 100vh;: Sets the height to 100% of the viewport height.
  • #ui-layer:
    • position: absolute;: Positions the UI layer absolutely, allowing it to overlay the canvas.
    • top: 20px; left: 20px;: Sets the position from the top-left corner.
    • color: #0f0;: Sets the text color to neon green.
    • background: rgba(0, 20, 0, 0.8);: Sets a semi-transparent dark green background.
    • border: 1px solid #0f0;: Adds a neon green border.
    • padding: 15px;: Adds padding around the content.
    • border-radius: 4px;: Rounds the corners.
    • box-shadow: 0 0 10px #0f0;: Adds a neon green shadow effect.
    • pointer-events: none;: Allows mouse events to pass through the UI layer to the canvas.
  • h1:
    • margin: 0 0 5px 0;: Sets margins for the heading.
    • font-size: 1.2rem;: Sets the font size.
    • text-transform: uppercase;: Converts text to uppercase.
    • letter-spacing: 2px;: Adds letter spacing.
  • p:
    • margin: 0;: Removes default margins from paragraphs.
    • font-size: 0.8rem;: Sets the font size.
    • opacity: 0.8;: Sets the opacity for a subtle look.
  • #loading:
    • position: absolute;: Positions the loading message absolutely.
    • top: 50%; left: 50%;: Centers the message.
    • transform: translate(-50%, -50%);: Adjusts the position for precise centering.
    • color: #000;: Sets the text color to black.
    • background: #0f0;: Sets a neon green background.
    • padding: 20px 40px;: Adds padding around the text.
    • font-weight: bold;: Sets the font weight to bold.
    • font-family: monospace;: Uses a monospace font.
    • box-shadow: 0 0 20px #0f0;: Adds a neon green shadow effect.
    • z-index: 10;: Ensures the loading message is on top of other elements.

Building the 3D Scene with Three.js

Now, let's get into the heart of the project: creating the 3D scene using Three.js. This involves setting up the scene, camera, renderer, and implementing the voxel-based world. Mastery of Three.js concepts is crucial for developing interactive 3D web applications. We’ll cover the core components and how they fit together:

<script>
    // --- Configurações Matrix ---
    const TAMANHO_VOXEL = 1;
    
    // Paleta Cyberpunk / Matrix
    const CORES = {
        PRETO: 0x050505,
        ASFALTO: 0x1a1a1a,
        CONCRETO: 0x333333,
        METAL_ESCURO: 0x222222,
        NEON_VERDE: 0x00ff00,
        NEON_VERDE_ESCURO: 0x008800,
        NEON_CIANO: 0x00ffff,
        NEON_VERMELHO: 0xff0033, // Luzes de freio
        VIDRO: 0x112211,
        LUZ_BRANCA: 0xccffcc,
        PAREDE_LOJA: 0x2a2a2a,
        LAMPADA: 0xffeebb // Nova cor para lâmpada comum
    };

    class ConstrutorVoxel {
        constructor() {
            this.voxels = []; 
        }

        add(x, y, z, cor) {
            this.voxels.push({ x: Math.round(x), y: Math.round(y), z: Math.round(z), c: cor });
        }
    }

    const construtor = new ConstrutorVoxel();

    // --- Utilitários de Construção ---
    function preencherCaixa(x, y, z, w, h, d, cor) {
        for(let i=0; i<w; i++) {
            for(let j=0; j<h; j++) {
                for(let k=0; k<d; k++) {
                    construtor.add(x+i, y+j, z+k, cor);
                }
            }
        }
    }

    // --- Geração de Cenário ---

    // 1. Chão e Estrada
    function construirChao(largura, profundidade) {
        for (let x = -largura / 2; x < largura / 2; x++) {
            for (let z = -profundidade / 2; z < profundidade / 2; z++) {
                let cor = CORES.ASFALTO;
                
                // Marcações de estrada
                if (Math.abs(z) < 4) { // Estrada principal passando na frente
                    if (Math.abs(z) < 0.5 && x % 4 !== 0) cor = 0xffff00; // Faixa amarela
                } else {
                    // Área do posto
                    if (x > -20 && x < 20 && z > 5 && z < 35) cor = CORES.CONCRETO;
                }

                // Efeito de "Grid" sutil no chão longe
                if ((x % 10 === 0 || z % 10 === 0) && cor === CORES.ASFALTO) {
                    cor = 0x111111; 
                }

                construtor.add(x, 0, z, cor);
            }
        }
    }

    // 2. Cobertura do Posto e Bombas
    function construirPosto(x, y, z) {
        // Colunas
        preencherCaixa(x-10, y, z, 2, 8, 2, CORES.METAL_ESCURO);
        preencherCaixa(x+10, y, z, 2, 8, 2, CORES.METAL_ESCURO);
        
        // Teto Grande
        preencherCaixa(x-15, y+8, z-6, 34, 2, 14, CORES.METAL_ESCURO);
        
        // Faixa de Luz (Lâmpadas) no teto - Agora amarelo/branco
        preencherCaixa(x-15, y+8, z-6, 34, 1, 1, CORES.LAMPADA);
        preencherCaixa(x-15, y+8, z+7, 34, 1, 1, CORES.LAMPADA);
        preencherCaixa(x-15, y+8, z-6, 1, 1, 14, CORES.LAMPADA);
        preencherCaixa(x+18, y+8, z-6, 1, 1, 14, CORES.LAMPADA);

        // Bombas de Gasolina
        const posBombas = [-6, 0, 6];
        posBombas.forEach(bx => {
            preencherCaixa(x+bx, y, z, 3, 4, 2, CORES.METAL_ESCURO); // Base
            construtor.add(x+bx, y+2, z-1, CORES.NEON_VERDE); // Display (mantém tech)
            construtor.add(x+bx+2, y+2, z-1, CORES.NEON_VERDE); // Display
            construtor.add(x+bx+1, y+4, z, CORES.NEON_VERDE_ESCURO); // Topo
        });
    }

    // 3. Loja de Conveniência
    function construirLoja(x, y, z) {
        // Estrutura principal
        preencherCaixa(x, y, z, 20, 7, 10, CORES.PAREDE_LOJA);
        
        // Vitrine de Vidro
        preencherCaixa(x+2, y+1, z-1, 16, 4, 1, CORES.VIDRO);
        
        // Letreiro "MART" em binário
        const letreiroY = y + 7;
        construtor.add(x+5, letreiroY, z, CORES.NEON_VERDE); 
        construtor.add(x+7, letreiroY, z, CORES.NEON_VERDE);
        construtor.add(x+9, letreiroY, z, 0x003300); // Apagado
        construtor.add(x+11, letreiroY, z, CORES.NEON_VERDE);

        // Interior básico (luzes)
        construtor.add(x+5, y+3, z+5, CORES.LUZ_BRANCA);
        construtor.add(x+15, y+3, z+5, CORES.LUZ_BRANCA);
    }

    // 4. Carros Futuristas
    function construirCarro(x, y, z, cor, direcao) {
        // Chassi
        if(direcao === 'H') { // Horizontal
            preencherCaixa(x-4, y+1, z-2, 9, 2, 4, cor);
            // Cabine
            preencherCaixa(x-2, y+2, z-2, 4, 2, 4, CORES.VIDRO);
            // Rodas (sem rodas, flutuando levemente ou rodas pretas)
            preencherCaixa(x-3, y, z-2, 2, 1, 1, 0x000000);
            preencherCaixa(x+2, y, z-2, 2, 1, 1, 0x000000);
            preencherCaixa(x-3, y, z+1, 2, 1, 1, 0x000000);
            preencherCaixa(x+2, y, z+1, 2, 1, 1, 0x000000);
            
            // Luzes
            construtor.add(x+4, y+1, z-1, CORES.NEON_CIANO); // Farol
            construtor.add(x+4, y+1, z, CORES.NEON_CIANO);
            construtor.add(x-4, y+1, z-1, CORES.NEON_VERMELHO); // Traseira
            construtor.add(x-4, y+1, z, CORES.NEON_VERMELHO);

        } else { // Vertical
            preencherCaixa(x-2, y+1, z-4, 4, 2, 9, cor);
            preencherCaixa(x-2, y+2, z-2, 4, 2, 5, CORES.VIDRO);
            // Luzes
            construtor.add(x-1, y+1, z-4, CORES.NEON_CIANO);
            construtor.add(x, y+1, z-4, CORES.NEON_CIANO);
        }
    }

    // --- Montagem do Cenário ---
    
    // Chão
    construirChao(60, 60);

    // Estruturas
    construirPosto(0, 0, 10);
    construirLoja(-5, 0, 25);

    // Carros
    // Carro abastecendo
    construirCarro(-6, 0, 10, 0x3333cc, 'V');
    
    // Carros na estrada
    construirCarro(15, 0, -2, 0xcc3333, 'H');
    construirCarro(-20, 0, 2, 0xcccc33, 'H');

    // Detalhes extras: Postes de luz
    function construirPosteLuz(x, z) {
        preencherCaixa(x, 0, z, 1, 10, 1, CORES.METAL_ESCURO);
        preencherCaixa(x, 10, z, 3, 1, 1, CORES.METAL_ESCURO);
        construtor.add(x+2, 9, z, CORES.LUZ_BRANCA);
        // Cone de luz simulado com voxels translúcidos seria complexo, vamos usar PointLight depois
    }
    construirPosteLuz(-25, -25);
    construirPosteLuz(25, -25);


    // --- Setup Three.js ---
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000500); // Preto esverdeado
    // Reduzindo neblina para ver mais longe
    scene.fog = new THREE.FogExp2(0x000500, 0.015);

    const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(-30, 20, 40);
    camera.lookAt(0, 5, 0);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    document.getElementById('canvas-container').appendChild(renderer.domElement);

    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.maxPolarAngle = Math.PI / 2 - 0.05; // Não deixar ir para baixo do chão

    // --- Iluminação ---
    // AUMENTANDO LUZ AMBIENTE: Cor mais clara e maior intensidade para não ficar tudo escuro
    // Mudando para um cinza neutro e aumentando a intensidade para clarear a cena
    const luzAmbiente = new THREE.AmbientLight(0xAAAAAA, 1.5);
    scene.add(luzAmbiente);

    // Luz principal (Luar) - Alterado de Verde (0x00ff00) para Azulado/Neutro (0xaaccff)
    const luzDir = new THREE.DirectionalLight(0xaaccff, 0.5);
    luzDir.position.set(-20, 50, -20);
    luzDir.castShadow = true;
    scene.add(luzDir);

    // Luzes Pontuais (Neons e Lâmpadas)
    const lights = [
        // Luz do teto do posto: Branco amarelado (lâmpada fluorescente e acolhedora), mais forte e abrangente
        { x: 0, y: 7, z: 10, c: 0xffeebb, i: 1.5, d: 35 }, 
        // Luz da loja: Amarelo suave, iluminando a frente
        { x: 5, y: 5, z: 30, c: 0xffdea0, i: 1.2, d: 25 }, 
        
        { x: -23, y: 9, z: -25, c: 0xffffff, i: 0.8, d: 20 }, // Poste Rua
        { x: -6, y: 2, z: 10, c: 0x00ffff, i: 2, d: 8 } // Carro abastecendo (glow)
    ];

    lights.forEach(l => {
        const pl = new THREE.PointLight(l.c, l.i, l.d);
        pl.position.set(l.x, l.y, l.z);
        scene.add(pl);
    });


    // --- Renderização dos Voxels Estáticos ---
    const geometriaVoxel = new THREE.BoxGeometry(TAMANHO_VOXEL, TAMANHO_VOXEL, TAMANHO_VOXEL);
    const materialVoxel = new THREE.MeshStandardMaterial({ 
        roughness: 0.2, 
        metalness: 0.8 
    });
    
    const malhaMundo = new THREE.InstancedMesh(geometriaVoxel, materialVoxel, construtor.voxels.length);
    malhaMundo.castShadow = true;
    malhaMundo.receiveShadow = true;

    const dummy = new THREE.Object3D();
    const corTemp = new THREE.Color();

    construtor.voxels.forEach((v, i) => {
        dummy.position.set(v.x, v.y, v.z);
        dummy.updateMatrix();
        malhaMundo.setMatrixAt(i, dummy.matrix);
        corTemp.setHex(v.c);
        // Se for neon, aumentar emissividade (gambiarra visual)
        if (v.c === CORES.NEON_VERDE || v.c === CORES.NEON_CIANO) {
            // Em StandardMaterial, a cor define albedo, brilho real precisa de emissive map ou bloom (post-processing)
            // Aqui vamos apenas clarear
            corTemp.addScalar(0.2); 
        }
        malhaMundo.setColorAt(i, corTemp);
    });
    scene.add(malhaMundo);


    // --- CHUVA MATRIX (Partículas Voxel) ---
    const contaChuva = 3000;
    // Voxels menores para a chuva
    const geomChuva = new THREE.BoxGeometry(0.15, 0.8, 0.15); 
    const matChuva = new THREE.MeshBasicMaterial({ 
        color: 0x00ff00, 
        transparent: true, 
        opacity: 0.8 
    });
    const sistemaChuva = new THREE.InstancedMesh(geomChuva, matChuva, contaChuva);
    
    const chuvaDados = [];

    for(let i=0; i<contaChuva; i++) {
        let x = (Math.random() - 0.5) * 80;
        let y = Math.random() * 40;
        let z = (Math.random() - 0.5) * 80;
        
        dummy.position.set(x, y, z);
        dummy.updateMatrix();
        sistemaChuva.setMatrixAt(i, dummy.matrix);
        
        chuvaDados.push({
            x: x, y: y, z: z,
            velocidade: Math.random() * 0.5 + 0.2
        });
    }
    scene.add(sistemaChuva);


    // Remover loading
    document.getElementById('loading').style.display = 'none';

    // --- Animação ---
    let tempo = 0;
    function animar() {
        requestAnimationFrame(animar);
        controls.update();
        tempo += 0.01;

        // Animar chuva
        for(let i=0; i<contaChuva; i++) {
            let p = chuvaDados[i];
            p.y -= p.velocidade;

            // Resetar chuva
            if(p.y < 0) {
                p.y = 40;
                // Randomizar levemente a posição x/z ao respawnar para não ficar estático
                p.x = (Math.random() - 0.5) * 80; 
            }

            dummy.position.set(p.x, p.y, p.z);
            dummy.updateMatrix();
            sistemaChuva.setMatrixAt(i, dummy.matrix);
        }
        sistemaChuva.instanceMatrix.needsUpdate = true;
        
        // Piscar levemente a luz do posto para efeito cyberpunk
        if (Math.random() > 0.95) {
             luzDir.intensity = 0.5 + (Math.random() * 0.2);
        }

        renderer.render(scene, camera);
    }

    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });

    animar();

</script>
  • Constants and Color Palette:
    • TAMANHO_VOXEL: Defines the size of each voxel.
    • CORES: An object containing color definitions for the cyberpunk/matrix theme.
  • Voxel Builder Class (ConstrutorVoxel):
    • A class to manage and add voxels to the scene.
    • add(x, y, z, cor): Adds a voxel with specified coordinates and color.
  • Construction Utilities:
    • preencherCaixa(x, y, z, w, h, d, cor): Fills a box-shaped area with voxels of the specified color.
  • Scene Generation Functions:
    • construirChao(largura, profundidade): Generates the ground and road.
    • construirPosto(x, y, z): Builds the gas station structure.
    • construirLoja(x, y, z): Creates the convenience store.
    • construirCarro(x, y, z, cor, direcao): Constructs futuristic cars.
    • construirPosteLuz(x, z): Adds streetlights to the scene.
  • Three.js Setup:
    • Scene:
      • const scene = new THREE.Scene();: Creates a new scene.
      • scene.background = new THREE.Color(0x000500);: Sets the background color to a dark greenish tone.
      • scene.fog = new THREE.FogExp2(0x000500, 0.015);: Adds exponential fog for depth.
    • Camera:
      • const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);: Creates a perspective camera.
      • camera.position.set(-30, 20, 40);: Sets the camera position.
      • camera.lookAt(0, 5, 0);: Sets the camera to look at a specific point.
    • Renderer:
      • const renderer = new THREE.WebGLRenderer({ antialias: true });: Creates a WebGL renderer with antialiasing.
      • renderer.setSize(window.innerWidth, window.innerHeight);: Sets the renderer size.
      • renderer.shadowMap.enabled = true;: Enables shadow mapping.
      • document.getElementById('canvas-container').appendChild(renderer.domElement);: Appends the renderer to the canvas container.
    • OrbitControls:
      • const controls = new THREE.OrbitControls(camera, renderer.domElement);: Creates orbit controls for camera interaction.
      • controls.enableDamping = true;: Enables damping for smooth camera movements.
      • controls.dampingFactor = 0.05;: Sets the damping factor.
      • controls.maxPolarAngle = Math.PI / 2 - 0.05;: Limits the camera's vertical orbit.
  • Lighting:
    • Ambient Light:
      • const luzAmbiente = new THREE.AmbientLight(0xAAAAAA, 1.5);: Creates ambient light to illuminate the scene.
      • scene.add(luzAmbiente);: Adds the ambient light to the scene.
    • Directional Light (Moonlight):
      • const luzDir = new THREE.DirectionalLight(0xaaccff, 0.5);: Creates a directional light.
      • luzDir.position.set(-20, 50, -20);: Sets the light position.
      • luzDir.castShadow = true;: Enables shadow casting.
      • scene.add(luzDir);: Adds the directional light to the scene.
    • Point Lights (Neons and Lamps):
      • An array of light configurations with positions, colors, intensities, and distances.
      • Iterates through the lights array to create and add THREE.PointLight instances to the scene.
  • Static Voxel Rendering:
    • const geometriaVoxel = new THREE.BoxGeometry(TAMANHO_VOXEL, TAMANHO_VOXEL, TAMANHO_VOXEL);: Creates a box geometry for voxels.
    • const materialVoxel = new THREE.MeshStandardMaterial({ ... });: Creates a material for voxels with roughness and metalness.
    • const malhaMundo = new THREE.InstancedMesh(geometriaVoxel, materialVoxel, construtor.voxels.length);: Creates an instanced mesh for efficient voxel rendering.
    • A loop iterates through the voxels to set their positions and colors using InstancedMesh.
  • Matrix Rain (Voxel Particles):
    • const contaChuva = 3000;: Number of rain particles.
    • const geomChuva = new THREE.BoxGeometry(0.15, 0.8, 0.15);: Geometry for rain particles.
    • const matChuva = new THREE.MeshBasicMaterial({ ... });: Material for rain particles with transparency.
    • const sistemaChuva = new THREE.InstancedMesh(geomChuva, matChuva, contaChuva);: Creates an instanced mesh for the rain system.
    • A loop generates the rain particles with random positions and stores their data for animation.
  • Animation Loop:
    • function animar(): The animation loop function.
    • requestAnimationFrame(animar);: Requests the next animation frame.
    • controls.update();: Updates the orbit controls.
    • Animates the rain particles by updating their positions each frame.
    • Simulates the flickering lights of the gas station by adjusting the intensity of the directional light.
    • renderer.render(scene, camera);: Renders the scene.
  • Event Listener for Window Resize:
    • Updates the camera aspect ratio and renderer size when the window is resized.

Creating the Matrix Rain Effect

To enhance the cyberpunk atmosphere, we'll add a Matrix-inspired rain effect using instanced meshes for performance. This effect involves creating numerous small voxel particles that fall from the sky. Implementing particle systems efficiently is a valuable skill in 3D graphics. Here’s how we create the rain effect:

 // --- CHUVA MATRIX (Partículas Voxel) ---
    const contaChuva = 3000;
    // Voxels menores para a chuva
    const geomChuva = new THREE.BoxGeometry(0.15, 0.8, 0.15); 
    const matChuva = new THREE.MeshBasicMaterial({ 
        color: 0x00ff00, 
        transparent: true, 
        opacity: 0.8 
    });
    const sistemaChuva = new THREE.InstancedMesh(geomChuva, matChuva, contaChuva);
    
    const chuvaDados = [];

    for(let i=0; i<contaChuva; i++) {
        let x = (Math.random() - 0.5) * 80;
        let y = Math.random() * 40;
        let z = (Math.random() - 0.5) * 80;
        
        dummy.position.set(x, y, z);
        dummy.updateMatrix();
        sistemaChuva.setMatrixAt(i, dummy.matrix);
        
        chuvaDados.push({
            x: x, y: y, z: z,
            velocidade: Math.random() * 0.5 + 0.2
        });
    }
    scene.add(sistemaChuva);


   // Animação chuva
        for(let i=0; i<contaChuva; i++) {
            let p = chuvaDados[i];
            p.y -= p.velocidade;

            // Resetar chuva
            if(p.y < 0) {
                p.y = 40;
                // Randomizar levemente a posição x/z ao respawnar para não ficar estático
                p.x = (Math.random() - 0.5) * 80; 
            }

            dummy.position.set(p.x, p.y, p.z);
            dummy.updateMatrix();
            sistemaChuva.setMatrixAt(i, dummy.matrix);
        }
        sistemaChuva.instanceMatrix.needsUpdate = true;
  • Define Rain Parameters:
    • contaChuva: Sets the number of rain particles (3000 in this case).
    • geomChuva: Creates a smaller box geometry for the rain particles (0.15 x 0.8 x 0.15 units).
    • matChuva: Creates a transparent, green material for the rain particles.
  • Create Instanced Mesh:
    • sistemaChuva: Creates an InstancedMesh to efficiently render multiple rain particles.
  • Initialize Rain Particle Data:
    • chuvaDados: An array to store the individual particle data (position and velocity).
    • A loop generates random positions for each rain particle and sets their initial matrix in the InstancedMesh.
  • Animate Rain:
    • In the animation loop (animar function):
      • Updates the position of each rain particle by decreasing its y coordinate based on its velocity.
      • Resets the particle's position to the top if it falls below the view, creating a continuous rain effect.
      • Updates the matrix of each particle in the InstancedMesh.
      • Marks the instance matrix as needing updates (sistemaChuva.instanceMatrix.needsUpdate = true;).

Animating the Scene

The animation loop is crucial for bringing the scene to life. We'll use requestAnimationFrame to create a smooth and efficient animation that updates the rain and simulates flickering lights. Understanding the animation loop and its role in rendering 3D scenes is essential for creating dynamic web applications. Let’s look at the animation process:

 let tempo = 0;
    function animar() {
        requestAnimationFrame(animar);
        controls.update();
        tempo += 0.01;

        // Animar chuva
        for(let i=0; i<contaChuva; i++) {
            let p = chuvaDados[i];
            p.y -= p.velocidade;

            // Resetar chuva
            if(p.y < 0) {
                p.y = 40;
                // Randomizar levemente a posição x/z ao respawnar para não ficar estático
                p.x = (Math.random() - 0.5) * 80; 
            }

            dummy.position.set(p.x, p.y, p.z);
            dummy.updateMatrix();
            sistemaChuva.setMatrixAt(i, dummy.matrix);
        }
        sistemaChuva.instanceMatrix.needsUpdate = true;
        
        // Piscar levemente a luz do posto para efeito cyberpunk
        if (Math.random() > 0.95) {
             luzDir.intensity = 0.5 + (Math.random() * 0.2);
        }

        renderer.render(scene, camera);
    }
  • Initialize Time:
    • let tempo = 0;: Initializes a variable tempo to keep track of the time for animation.
  • Animation Loop Function:
    • function animar() { ... }: Defines the animation loop function.
  • Request Animation Frame:
    • requestAnimationFrame(animar);: Schedules the next animation frame, ensuring smooth and efficient rendering.
  • Update Orbit Controls:
    • controls.update();: Updates the orbit controls to handle camera movements.
  • Increment Time:
    • tempo += 0.01;: Increments the tempo variable, which can be used for time-based animations.
  • Animate Rain:
    • Loops through each rain particle in chuvaDados:
      • Updates the y position of the particle based on its velocity.
      • If a particle falls below the view, reset its position to the top with a slightly randomized x and z position.
      • Updates the particle's matrix in the sistemaChuva instanced mesh.
    • Marks the instance matrix as needing updates (sistemaChuva.instanceMatrix.needsUpdate = true;).
  • Simulate Flickering Lights:
    • Conditionally adjusts the intensity of the directional light (luzDir) to simulate a flickering effect:
      • If a random number is greater than 0.95, the intensity is set to a random value between 0.5 and 0.7.
  • Render the Scene:
    • renderer.render(scene, camera);: Renders the 3D scene with the camera's perspective.
  • Start Animation:
    • animar();: Calls the animar function to start the animation loop.

Handling Window Resizing

To ensure the scene adapts to different screen sizes, we need to handle window resizing. This involves updating the camera's aspect ratio and the renderer's size. Implementing responsive design principles is crucial for modern web applications. Here’s how to handle window resizing:

 window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });
  • Add Event Listener:
    • window.addEventListener('resize', () => { ... });: Attaches an event listener to the window object to listen for the resize event.
  • Update Camera Aspect Ratio:
    • camera.aspect = window.innerWidth / window.innerHeight;: Updates the camera's aspect ratio to match the new window dimensions.
  • Update Projection Matrix:
    • camera.updateProjectionMatrix();: Applies the new aspect ratio to the camera's projection matrix.
  • Update Renderer Size:
    • renderer.setSize(window.innerWidth, window.innerHeight);: Resizes the renderer to match the new window dimensions.

Conclusion

Congratulations! You've successfully created a cyberpunk voxel matrix gas station using HTML, CSS, and Three.js. This project demonstrates the power of web technologies in building stunning 3D environments. Feel free to experiment with different colors, structures, and effects to further customize your scene. Explore more about Three.js and 3D graphics on the web at Three.js Official Documentation.