基于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实现了以下特性:
- 粒子系统动态控制:通过BufferGeometry高效管理大量粒子
- 光照体系:环境光与平行光配合实现昼夜变化
- 交互设计:参数实时调整带来灵活体验