从零开始学习three.js(20):three.js实现天气与时间动态效果(白天,黑夜,下雨,下雪)

发布于:2025-05-17 ⋅ 阅读:(15) ⋅ 点赞:(0)

基于Three.js的天气与时间动态效果实现

本文将通过代码解析,介绍如何使用Three.js实现动态天气(下雨、下雪)和时间(白天、黑夜)切换效果。完整代码基于一个交互式天气模拟项目,支持粒子密度、速度和环境亮度的实时调整。


一、场景初始化

Three.js场景的基础搭建是项目的核心,包含相机、渲染器、光照和地面模型:

let scene, camera, renderer, controls;

function initScene() {
    // 创建场景
    scene = new THREE.Scene();

    // 创建相机(透视相机)
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 10, 20);

    // 创建WebGL渲染器
    const container = document.getElementById("sceneContainer");
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    container.appendChild(renderer.domElement);

    // 添加轨道控制器
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    // 添加环境光与平行光
    ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    scene.add(ambientLight, directionalLight);

    // 创建地面
    const ground = new THREE.Mesh(
        new THREE.PlaneGeometry(100, 100),
        new THREE.MeshPhongMaterial({ color: 0xcccccc })
    );
    ground.rotation.x = -Math.PI / 2;
    scene.add(ground);

    // 启动动画循环
    animate();
}

二、天气效果实现

1. 下雨效果

通过粒子系统模拟雨滴下落:

function setupRain() {
    // 创建包含位置和速度数据的几何体
    const rainGeometry = new THREE.BufferGeometry();
    const positions = new Float32Array(particleCount * 3);
    const velocities = new Float32Array(particleCount * 3);

    // 初始化粒子位置和速度
    for (let i = 0; i < particleCount; i++) {
        positions[i*3] = (Math.random() - 0.5) * 100;   // X轴随机位置
        positions[i*3 + 1] = Math.random() * 50;        // Y轴初始高度
        positions[i*3 + 2] = (Math.random() - 0.5) * 100;// Z轴随机位置
        velocities[i*3 + 1] = -0.2 * particleSpeed;     // 垂直下落速度
    }

    // 创建粒子材质
    const rainMaterial = new THREE.PointsMaterial({
        color: 0xadd8e6,
        size: 0.1,
        transparent: true
    });

    // 生成粒子系统
    rainMesh = new THREE.Points(rainGeometry, rainMaterial);
    scene.add(rainMesh);
}

2. 下雪效果

雪花通过随机水平位移和大小变化增强真实感:

function setupSnow() {
    const snowGeometry = new THREE.BufferGeometry();
    const sizes = new Float32Array(particleCount);

    // 为雪花添加水平运动和随机大小
    for (let i = 0; i < particleCount; i++) {
        velocities[i*3] = (Math.random() - 0.5) * 0.05 * particleSpeed; // X轴漂移
        velocities[i*3 + 2] = (Math.random() - 0.5) * 0.05 * particleSpeed; // Z轴漂移
        sizes[i] = 0.1 + Math.random() * 0.2; // 随机大小
    }

    snowGeometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));
    // 其余逻辑与下雨类似...
}

三、时间切换实现

1. 白天模式

通过调整背景渐变和光照参数实现:

function setDayTime() {
    // 修改天空背景
    sceneContainer.style.background = "linear-gradient(to bottom, #87CEEB, #E0F7FA)";
    
    // 设置光照参数
    ambientLight.color.set(0xffffff);
    ambientLight.intensity = lightIntensity;
    directionalLight.color.set(0xffffff);
    directionalLight.intensity = 1;
}

2. 夜晚模式

降低光照强度并修改光源颜色:

function setNightTime() {
    sceneContainer.style.background = "linear-gradient(to bottom, #0A192F, #112240)";
    
    ambientLight.color.set(0x404040);
    ambientLight.intensity = lightIntensity * 0.3;
    directionalLight.color.set(0x8080ff); // 添加冷色调月光
    directionalLight.intensity = 0.5;
}

3. 多云模型

降低光照强度模拟多云天气

function setupCloudy() {
   // 降低光照强度模拟多云天气
    ambientLight.intensity = lightIntensity * 0.7;
    directionalLight.intensity = 0.7;
}

四、动态效果更新

在动画循环中持续更新粒子位置:

function updateParticles() {
    if (isRaining) {
        const positions = rainMesh.geometry.attributes.position.array;
        for (let i = 0; i < positions.length; i += 3) {
            positions[i + 1] += velocities[i + 1]; // Y轴位置更新
            if (positions[i + 1] < 0) resetParticlePosition(i); // 重置超出范围的粒子
        }
        rainMesh.geometry.attributes.position.needsUpdate = true;
    }
    // 雪花更新逻辑类似...
}

五、用户交互实现

通过事件监听实现参数调整:

// 密度滑块控制
densitySlider.addEventListener("input", (e) => {
    particleCount = parseInt(e.target.value);
    updateParticleDensity(); // 重新生成粒子系统
});

// 重置按钮
resetBtn.addEventListener("click", () => {
    particleCount = 5000;
    setupRain(); // 重置为初始状态
    setDayTime();
});

六、整体代码,可直接复制运行,修改orbitControl.js地址即可运行

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js天气效果演示</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet" />
    <!--可用的three.js地址-->
    <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/three.js/110/three.js">  </script>
    <!--可用的orbitContorl.js地址-->
    <script src="./orbitControl.js"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: "#165DFF",
                        secondary: "#6B7280",
                        accent: "#3B82F6",
                        dark: "#1F2937",
                        light: "#F9FAFB",
                    },
                    fontFamily: {
                        inter: ["Inter", "sans-serif"],
                    },
                },
            },
        };
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
        .content-auto {
          content-visibility: auto;
        }
        .text-shadow {
          text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        .bg-blur {
          backdrop-filter: blur(8px);
        }
        .transition-all-300 {
          transition: all 300ms ease-in-out;
        }
        .weather-card-hover {
          @apply hover:shadow-lg hover:-translate-y-1 transition-all duration-300;
        }
      }
    </style>
</head>

<body class="font-inter bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen flex flex-col overflow-x-hidden">
    <!-- 顶部导航 -->
    <header class="bg-white/80 bg-blur shadow-sm fixed w-full z-50 transition-all duration-300">
        <div class="container mx-auto px-4 py-3 flex justify-between items-center">
            <div class="flex items-center space-x-2">
                <i class="fa-solid fa-cloud-sun-rain text-primary text-2xl"></i>
                <h1 class="text-xl font-bold text-dark">天气模拟器</h1>
            </div>
            <button class="md:hidden text-dark text-xl">
                <i class="fa-solid fa-bars"></i>
            </button>
        </div>
    </header>

    <!-- 主要内容 -->
    <main class="flex-grow pt-16 flex flex-col md:flex-row">
        <!-- 控制面板 -->
        <aside
            class="w-full md:w-80 bg-white/80 bg-blur shadow-md p-4 md:sticky md:top-20 md:h-[calc(100vh-5rem)] overflow-y-auto">
            <h2 class="text-xl font-semibold mb-4 text-dark border-b pb-2">
                场景控制
            </h2>

            <div class="space-y-6">
                <!-- 天气选择 -->
                <div>
                    <h3 class="font-medium text-dark/90 mb-3">天气效果</h3>
                    <div class="grid grid-cols-2 gap-3">
                        <button id="rainBtn"
                            class="weather-card-hover bg-white border border-gray-200 rounded-lg p-3 flex flex-col items-center">
                            <i class="fa-solid fa-cloud-rain text-primary text-2xl mb-2"></i>
                            <span class="text-sm">下雨</span>
                        </button>
                        <button id="snowBtn"
                            class="weather-card-hover bg-white border border-gray-200 rounded-lg p-3 flex flex-col items-center">
                            <i class="fa-solid fa-snowflake text-accent text-2xl mb-2"></i>
                            <span class="text-sm">下雪</span>
                        </button>
                        <button id="clearBtn"
                            class="weather-card-hover bg-white border border-gray-200 rounded-lg p-3 flex flex-col items-center">
                            <i class="fa-solid fa-sun text-yellow-500 text-2xl mb-2"></i>
                            <span class="text-sm">晴天</span>
                        </button>
                        <button id="cloudyBtn"
                            class="weather-card-hover bg-white border border-gray-200 rounded-lg p-3 flex flex-col items-center">
                            <i class="fa-solid fa-cloud text-gray-400 text-2xl mb-2"></i>
                            <span class="text-sm">多云</span>
                        </button>
                    </div>
                </div>

                <!-- 时间选择 -->
                <div>
                    <h3 class="font-medium text-dark/90 mb-3">时间</h3>
                    <div class="grid grid-cols-2 gap-3">
                        <button id="dayBtn"
                            class="weather-card-hover bg-white border border-gray-200 rounded-lg p-3 flex flex-col items-center">
                            <i class="fa-solid fa-sun text-yellow-500 text-2xl mb-2"></i>
                            <span class="text-sm">白天</span>
                        </button>
                        <button id="nightBtn"
                            class="weather-card-hover bg-white border border-gray-200 rounded-lg p-3 flex flex-col items-center">
                            <i class="fa-solid fa-moon text-blue-700 text-2xl mb-2"></i>
                            <span class="text-sm">夜晚</span>
                        </button>
                    </div>
                </div>

                <!-- 粒子密度控制 -->
                <div>
                    <h3 class="font-medium text-dark/90 mb-2">粒子密度</h3>
                    <div class="flex items-center space-x-3">
                        <i class="fa-solid fa-minus text-gray-400"></i>
                        <input type="range" id="densitySlider" min="1000" max="20000" value="5000"
                            class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary" />
                        <i class="fa-solid fa-plus text-gray-400"></i>
                    </div>
                    <p class="text-xs text-gray-500 mt-1">
                        当前: <span id="densityValue">5000</span>
                    </p>
                </div>

                <!-- 粒子速度控制 -->
                <div>
                    <h3 class="font-medium text-dark/90 mb-2">粒子速度</h3>
                    <div class="flex items-center space-x-3">
                        <i class="fa-solid fa-turtle text-gray-400"></i>
                        <input type="range" id="speedSlider" min="1" max="20" value="10"
                            class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary" />
                        <i class="fa-solid fa-rabbit text-gray-400"></i>
                    </div>
                    <p class="text-xs text-gray-500 mt-1">
                        当前: <span id="speedValue">10</span>
                    </p>
                </div>

                <!-- 环境光强度 -->
                <div>
                    <h3 class="font-medium text-dark/90 mb-2">环境亮度</h3>
                    <div class="flex items-center space-x-3">
                        <i class="fa-solid fa-moon text-gray-400"></i>
                        <input type="range" id="lightSlider" min="0" max="1" step="0.01" value="0.5"
                            class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary" />
                        <i class="fa-solid fa-sun text-gray-400"></i>
                    </div>
                    <p class="text-xs text-gray-500 mt-1">
                        当前: <span id="lightValue">0.5</span>
                    </p>
                </div>

                <!-- 重置按钮 -->
                <button id="resetBtn"
                    class="w-full bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-lg transition-all duration-300 flex items-center justify-center space-x-2">
                    <i class="fa-solid fa-refresh"></i>
                    <span>重置场景</span>
                </button>
            </div>
        </aside>

        <!-- 3D场景容器 -->
        <div class="flex-grow relative">
            <div id="sceneContainer" class="w-full h-full bg-gradient-to-b from-blue-400 to-blue-600"></div>

            <!-- 信息卡片 -->
            <div class="absolute top-4 left-4 bg-white/80 bg-blur rounded-lg shadow-md p-4 max-w-md">
                <h3 class="text-lg font-semibold text-dark mb-2">Three.js天气模拟</h3>
                <p class="text-sm text-dark/80">
                    使用Three.js实现的交互式天气模拟系统,支持下雨、下雪等天气效果,以及白天黑夜的时间变化。
                </p>
                <div class="mt-3 flex items-center text-xs text-dark/60">
                    <i class="fa-solid fa-info-circle mr-1"></i>
                    <span>拖动鼠标旋转视角,滚轮缩放场景</span>
                </div>
            </div>

            <!-- 天气状态显示 -->
            <div id="weatherStatus"
                class="absolute top-4 right-4 bg-white/80 bg-blur rounded-lg shadow-md p-4 flex items-center">
                <i id="weatherIcon" class="fa-solid fa-cloud-sun-rain text-primary text-3xl mr-3"></i>
                <div>
                    <h3 id="weatherType" class="text-lg font-semibold text-dark">
                        下雨
                    </h3>
                    <p id="timeOfDay" class="text-sm text-dark/80">白天</p>
                </div>
            </div>
        </div>
    </main>

    <!-- 页脚 -->
    <footer class="bg-dark text-white/80 py-4">
        <div class="container mx-auto px-4 text-center text-sm">
            <p>© 2025 天气模拟器 | 使用 Three.js 构建</p>
            <div class="mt-2 flex justify-center space-x-4">
                <a href="#" class="hover:text-white transition-colors"><i class="fa-brands fa-github"></i></a>
                <a href="#" class="hover:text-white transition-colors"><i class="fa-brands fa-twitter"></i></a>
                <a href="#" class="hover:text-white transition-colors"><i class="fa-brands fa-linkedin"></i></a>
            </div>
        </div>
    </footer>

    <script>
        // 场景初始化
        let scene, camera, renderer, controls;
        let rainMesh, snowMesh, ambientLight, directionalLight;
        let isRaining = false,
            isSnowing = false;
        let currentTime = "day";
        let particleCount = 5000;
        let particleSpeed = 10;
        let lightIntensity = 0.5;

        // 初始化Three.js场景
        function initScene() {
            // 创建场景
            scene = new THREE.Scene();

            // 创建相机
            camera = new THREE.PerspectiveCamera(
                75,
                window.innerWidth / window.innerHeight,
                0.1,
                1000
            );
            camera.position.set(0, 10, 20);

            // 创建渲染器
            const container = document.getElementById("sceneContainer");
            renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
            renderer.setSize(container.clientWidth, container.clientHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            container.appendChild(renderer.domElement);

            // 添加轨道控制器
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.1;
            controls.rotateSpeed = 0.5;
            controls.zoomSpeed = 0.8;

            // 添加环境光
            ambientLight = new THREE.AmbientLight(0xffffff, lightIntensity);
            scene.add(ambientLight);

            // 添加平行光(太阳光/月光)
            directionalLight = new THREE.DirectionalLight(0xffffff, 1);
            directionalLight.position.set(1, 2, 1);
            directionalLight.castShadow = true;
            scene.add(directionalLight);

            // 添加地面
            const groundGeometry = new THREE.PlaneGeometry(100, 100);
            const groundMaterial = new THREE.MeshPhongMaterial({ color: 0xcccccc });
            const ground = new THREE.Mesh(groundGeometry, groundMaterial);
            ground.rotation.x = -Math.PI / 2;
            ground.receiveShadow = true;
            scene.add(ground);

            // 添加一些简单的建筑模型
            // addBuildings();

            // 初始设置为下雨和白天
            setupRain();
            setDayTime();

            // 渲染循环
            function animate() {
                requestAnimationFrame(animate);

                // 更新控制器
                controls.update();

                // 更新粒子系统
                updateParticles();

                // 渲染场景
                renderer.render(scene, camera);
            }

            animate();

            // 监听窗口大小变化
            window.addEventListener("resize", onWindowResize);
        }

        // 窗口大小变化处理
        function onWindowResize() {
            const container = document.getElementById("sceneContainer");
            camera.aspect = container.clientWidth / container.clientHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(container.clientWidth, container.clientHeight);
        }

        // 添加简单的建筑模型
        function addBuildings() {
            const buildingPositions = [
                [-15, 0, -15],
                [0, 0, -15],
                [15, 0, -15],
                [-15, 0, 0],
                [0, 0, 0],
                [15, 0, 0],
                [-15, 0, 15],
                [0, 0, 15],
                [15, 0, 15],
            ];

            const buildingHeights = [5, 7, 4, 6, 10, 8, 4, 9, 6];

            const buildingColors = [
                0x8b4513, 0xa0522d, 0xd2691e, 0xcd853f, 0xf4a460, 0xdaa520, 0xb8860b,
                0xbc8f8f, 0xf5deb3,
            ];

            buildingPositions.forEach((pos, index) => {
                const geometry = new THREE.BoxGeometry(3, buildingHeights[index], 3);
                const material = new THREE.MeshPhongMaterial({
                    color: buildingColors[index],
                });
                const building = new THREE.Mesh(geometry, material);
                building.position.set(pos[0], buildingHeights[index] / 2, pos[1]);
                building.castShadow = true;
                building.receiveShadow = true;
                scene.add(building);

                // 添加屋顶
                const roofGeometry = new THREE.ConeGeometry(2.5, 3, 4);
                const roofMaterial = new THREE.MeshPhongMaterial({ color: 0x8b0000 });
                const roof = new THREE.Mesh(roofGeometry, roofMaterial);
                roof.position.set(pos[0], buildingHeights[index] + 1.5, pos[1]);
                roof.castShadow = true;
                scene.add(roof);
            });
        }

        // 设置下雨效果
        function setupRain() {
            // 移除现有的雨雪效果
            if (rainMesh) scene.remove(rainMesh);
            if (snowMesh) scene.remove(snowMesh);

            isRaining = true;
            isSnowing = false;

            // 更新UI
            document.getElementById("weatherType").textContent = "下雨";
            document.getElementById("weatherIcon").className =
                "fa-solid fa-cloud-rain text-primary text-3xl mr-3";

            // 创建雨滴几何体
            const rainGeometry = new THREE.BufferGeometry();
            const rainCount = particleCount;
            // 随机位置和速度
            const positions = new Float32Array(rainCount * 3);
            const velocities = new Float32Array(rainCount * 3);

            for (let i = 0; i < rainCount; i++) {
                const i3 = i * 3;

                // 随机位置
                positions[i3] = (Math.random() - 0.5) * 100;
                positions[i3 + 1] = Math.random() * 50;
                positions[i3 + 2] = (Math.random() - 0.5) * 100;

                // 随机速度
                velocities[i3] = 0;
                velocities[i3 + 1] = -(0.1 + Math.random() * 0.2) * particleSpeed;
                velocities[i3 + 2] = 0;
            }

            rainGeometry.setAttribute(
                "position",
                new THREE.BufferAttribute(positions, 3)
            );
            rainGeometry.setAttribute(
                "velocity",
                new THREE.BufferAttribute(velocities, 3)
            );

            // 创建雨滴材质
            const rainMaterial = new THREE.PointsMaterial({
                color: 0xadd8e6,
                size: 0.1,
                transparent: true,
                opacity: 0.8,
            });

            // 创建雨滴粒子系统
            rainMesh = new THREE.Points(rainGeometry, rainMaterial);
            scene.add(rainMesh);
        }

        // 设置下雪效果
        function setupSnow() {
            // 移除现有的雨雪效果
            if (rainMesh) scene.remove(rainMesh);
            if (snowMesh) scene.remove(snowMesh);

            isRaining = false;
            isSnowing = true;

            // 更新UI
            document.getElementById("weatherType").textContent = "下雪";
            document.getElementById("weatherIcon").className =
                "fa-solid fa-snowflake text-accent text-3xl mr-3";

            // 创建雪花几何体
            const snowGeometry = new THREE.BufferGeometry();
            const snowCount = particleCount;

            const positions = new Float32Array(snowCount * 3);
            const velocities = new Float32Array(snowCount * 3);
            const sizes = new Float32Array(snowCount);

            for (let i = 0; i < snowCount; i++) {
                const i3 = i * 3;

                // 随机位置
                positions[i3] = (Math.random() - 0.5) * 100;
                positions[i3 + 1] = Math.random() * 50;
                positions[i3 + 2] = (Math.random() - 0.5) * 100;

                // 随机速度
                velocities[i3] = (Math.random() - 0.5) * 0.05 * particleSpeed;
                velocities[i3 + 1] = -(0.05 + Math.random() * 0.1) * particleSpeed;
                velocities[i3 + 2] = (Math.random() - 0.5) * 0.05 * particleSpeed;

                // 随机大小
                sizes[i] = 0.1 + Math.random() * 0.2;
            }

            snowGeometry.setAttribute(
                "position",
                new THREE.BufferAttribute(positions, 3)
            );
            snowGeometry.setAttribute(
                "velocity",
                new THREE.BufferAttribute(velocities, 3)
            );
            snowGeometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));

            // 创建雪花材质
            const snowMaterial = new THREE.PointsMaterial({
                color: 0xffffff,
                size: 0.1,
                transparent: true,
                opacity: 0.9,
            });

            // 创建雪花粒子系统
            snowMesh = new THREE.Points(snowGeometry, snowMaterial);
            scene.add(snowMesh);
        }

        // 设置晴天效果
        function setupClear() {
            // 移除现有的雨雪效果
            if (rainMesh) scene.remove(rainMesh);
            if (snowMesh) scene.remove(snowMesh);

            isRaining = false;
            isSnowing = false;

            // 更新UI
            document.getElementById("weatherType").textContent = "晴天";
            document.getElementById("weatherIcon").className =
                "fa-solid fa-sun text-yellow-500 text-3xl mr-3";
        }

        // 设置多云效果
        function setupCloudy() {
            // 移除现有的雨雪效果
            if (rainMesh) scene.remove(rainMesh);
            if (snowMesh) scene.remove(snowMesh);

            isRaining = false;
            isSnowing = false;

            // 更新UI
            document.getElementById("weatherType").textContent = "多云";
            document.getElementById("weatherIcon").className =
                "fa-solid fa-cloud text-gray-400 text-3xl mr-3";

            // 降低光照强度模拟多云天气
            ambientLight.intensity = lightIntensity * 0.7;
            directionalLight.intensity = 0.7;
        }

        // 设置白天
        function setDayTime() {
            currentTime = "day";
            document.getElementById("timeOfDay").textContent = "白天";

            // 更新天空颜色
            const container = document.getElementById("sceneContainer");
            container.style.background =
                "linear-gradient(to bottom, #87CEEB, #E0F7FA)";

            // 更新光照
            ambientLight.color.set(0xffffff);
            ambientLight.intensity = lightIntensity;

            directionalLight.color.set(0xffffff);
            directionalLight.intensity = 1;
            directionalLight.position.set(1, 2, 1);
        }

        // 设置夜晚
        function setNightTime() {
            currentTime = "night";
            document.getElementById("timeOfDay").textContent = "夜晚";

            // 更新天空颜色
            const container = document.getElementById("sceneContainer");
            container.style.background =
                "linear-gradient(to bottom, #0A192F, #112240)";

            // 更新光照
            ambientLight.color.set(0x404040);
            ambientLight.intensity = lightIntensity * 0.3;

            directionalLight.color.set(0x8080ff);
            directionalLight.intensity = 0.5;
            directionalLight.position.set(-1, 2, -1);
        }

        // 更新粒子系统
        function updateParticles() {
            if (isRaining && rainMesh) {
                const positions = rainMesh.geometry.attributes.position.array;
                const velocities = rainMesh.geometry.attributes.velocity.array;

                for (let i = 0; i < positions.length; i += 3) {
                    // 更新位置
                    positions[i + 1] += velocities[i + 1];

                    // 如果雨滴落到地面,重置位置
                    if (positions[i + 1] < 0) {
                        positions[i] = (Math.random() - 0.5) * 100;
                        positions[i + 1] = 50;
                        positions[i + 2] = (Math.random() - 0.5) * 100;
                    }
                }

                rainMesh.geometry.attributes.position.needsUpdate = true;
            }

            if (isSnowing && snowMesh) {
                const positions = snowMesh.geometry.attributes.position.array;
                const velocities = snowMesh.geometry.attributes.velocity.array;

                for (let i = 0; i < positions.length; i += 3) {
                    // 更新位置
                    positions[i] += velocities[i];
                    positions[i + 1] += velocities[i + 1];
                    positions[i + 2] += velocities[i + 2];

                    // 如果雪花落到地面,重置位置
                    if (positions[i + 1] < 0) {
                        positions[i] = (Math.random() - 0.5) * 100;
                        positions[i + 1] = 50;
                        positions[i + 2] = (Math.random() - 0.5) * 100;
                    }
                }

                snowMesh.geometry.attributes.position.needsUpdate = true;
            }
        }

        // 更新粒子密度
        function updateParticleDensity() {
            if (isRaining) {
                setupRain();
            } else if (isSnowing) {
                setupSnow();
            }
        }

        // 初始化事件监听
        function initEventListeners() {
            // 天气按钮
            document.getElementById("rainBtn").addEventListener("click", () => {
                setupRain();
                document
                    .querySelectorAll("#rainBtn, #snowBtn, #clearBtn, #cloudyBtn")
                    .forEach((btn) => {
                        btn.classList.remove("ring-2", "ring-primary");
                    });
                document
                    .getElementById("rainBtn")
                    .classList.add("ring-2", "ring-primary");
            });

            document.getElementById("snowBtn").addEventListener("click", () => {
                setupSnow();
                document
                    .querySelectorAll("#rainBtn, #snowBtn, #clearBtn, #cloudyBtn")
                    .forEach((btn) => {
                        btn.classList.remove("ring-2", "ring-primary");
                    });
                document
                    .getElementById("snowBtn")
                    .classList.add("ring-2", "ring-primary");
            });

            document.getElementById("clearBtn").addEventListener("click", () => {
                setupClear();
                document
                    .querySelectorAll("#rainBtn, #snowBtn, #clearBtn, #cloudyBtn")
                    .forEach((btn) => {
                        btn.classList.remove("ring-2", "ring-primary");
                    });
                document
                    .getElementById("clearBtn")
                    .classList.add("ring-2", "ring-primary");

                // 恢复光照
                ambientLight.intensity = lightIntensity;
                directionalLight.intensity = 1;
            });

            document.getElementById("cloudyBtn").addEventListener("click", () => {
                setupCloudy();
                document
                    .querySelectorAll("#rainBtn, #snowBtn, #clearBtn, #cloudyBtn")
                    .forEach((btn) => {
                        btn.classList.remove("ring-2", "ring-primary");
                    });
                document
                    .getElementById("cloudyBtn")
                    .classList.add("ring-2", "ring-primary");
            });

            // 时间按钮
            document.getElementById("dayBtn").addEventListener("click", () => {
                setDayTime();
                document.querySelectorAll("#dayBtn, #nightBtn").forEach((btn) => {
                    btn.classList.remove("ring-2", "ring-primary");
                });
                document
                    .getElementById("dayBtn")
                    .classList.add("ring-2", "ring-primary");
            });

            document.getElementById("nightBtn").addEventListener("click", () => {
                setNightTime();
                document.querySelectorAll("#dayBtn, #nightBtn").forEach((btn) => {
                    btn.classList.remove("ring-2", "ring-primary");
                });
                document
                    .getElementById("nightBtn")
                    .classList.add("ring-2", "ring-primary");
            });

            // 滑块控制
            const densitySlider = document.getElementById("densitySlider");
            const densityValue = document.getElementById("densityValue");
            densitySlider.addEventListener("input", (e) => {
                particleCount = parseInt(e.target.value);
                densityValue.textContent = particleCount;
                updateParticleDensity();
            });

            const speedSlider = document.getElementById("speedSlider");
            const speedValue = document.getElementById("speedValue");
            speedSlider.addEventListener("input", (e) => {
                particleSpeed = parseInt(e.target.value);
                speedValue.textContent = particleSpeed;
                
                // 更新雨滴速度
                if (isRaining && rainMesh) {
                    const velocities = rainMesh.geometry.attributes.velocity.array;
                    for (let i = 0; i < velocities.length; i += 3) {
                        velocities[i + 1] = -(0.1 + Math.random() * 0.2) * particleSpeed;
                    }
                    rainMesh.geometry.attributes.velocity.needsUpdate = true;
                }
                
                // 更新雪花速度
                if (isSnowing && snowMesh) {
                    const velocities = snowMesh.geometry.attributes.velocity.array;
                    for (let i = 0; i < velocities.length; i += 3) {
                        velocities[i] = (Math.random() - 0.5) * 0.05 * particleSpeed;
                        velocities[i + 1] = -(0.05 + Math.random() * 0.1) * particleSpeed;
                        velocities[i + 2] = (Math.random() - 0.5) * 0.05 * particleSpeed;
                    }
                    snowMesh.geometry.attributes.velocity.needsUpdate = true;
                }
            });

            const lightSlider = document.getElementById("lightSlider");
            const lightValue = document.getElementById("lightValue");
            lightSlider.addEventListener("input", (e) => {
                lightIntensity = parseFloat(e.target.value);
                lightValue.textContent = lightIntensity.toFixed(2);

                // 更新光照
                if (currentTime === "day") {
                    ambientLight.intensity = lightIntensity;
                    directionalLight.intensity = 1;
                } else {
                    ambientLight.intensity = lightIntensity * 0.3;
                    directionalLight.intensity = 0.5;
                }

                // 如果是多云天气,调整光照
                if (
                    document.getElementById("cloudyBtn").classList.contains("ring-2")
                ) {
                    ambientLight.intensity = lightIntensity * 0.7;
                    directionalLight.intensity = 0.7;
                }
            });

            // 重置按钮
            document.getElementById("resetBtn").addEventListener("click", () => {
                // 重置所有参数
                particleCount = 5000;
                particleSpeed = 10;
                lightIntensity = 0.5;

                // 更新滑块
                densitySlider.value = particleCount;
                densityValue.textContent = particleCount;
                speedSlider.value = particleSpeed;
                speedValue.textContent = particleSpeed;
                lightSlider.value = lightIntensity;
                lightValue.textContent = lightIntensity.toFixed(2);

                // 重置场景
                setupRain();
                setDayTime();

                // 更新按钮状态
                document
                    .querySelectorAll("#rainBtn, #snowBtn, #clearBtn, #cloudyBtn")
                    .forEach((btn) => {
                        btn.classList.remove("ring-2", "ring-primary");
                    });
                document
                    .getElementById("rainBtn")
                    .classList.add("ring-2", "ring-primary");

                document.querySelectorAll("#dayBtn, #nightBtn").forEach((btn) => {
                    btn.classList.remove("ring-2", "ring-primary");
                });
                document
                    .getElementById("dayBtn")
                    .classList.add("ring-2", "ring-primary");
            });

            // 初始化按钮状态
            document
                .getElementById("rainBtn")
                .classList.add("ring-2", "ring-primary");
            document
                .getElementById("dayBtn")
                .classList.add("ring-2", "ring-primary");
        }

        // 初始化页面
        window.addEventListener("DOMContentLoaded", () => {
            initScene();
            initEventListeners();
        });
    </script>
</body>

</html>

七、总结

本项目通过Three.js实现了以下特性:

  1. 粒子系统动态控制:通过BufferGeometry高效管理大量粒子
  2. 光照体系:环境光与平行光配合实现昼夜变化
  3. 交互设计:参数实时调整带来灵活体验

网站公告

今日签到

点亮在社区的每一天
去签到