Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

599 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Niagara Falls 瀑布效果 - Three.js</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #0a0a15;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#canvas-container {
width: 100vw;
height: 100vh;
}
#stats {
position: fixed;
top: 20px;
left: 20px;
color: #fff;
background: rgba(0, 20, 40, 0.8);
padding: 15px 20px;
border-radius: 10px;
font-size: 14px;
line-height: 1.8;
backdrop-filter: blur(10px);
border: 1px solid rgba(100, 180, 255, 0.3);
box-shadow: 0 4px 20px rgba(0, 100, 200, 0.3);
z-index: 100;
}
#stats .label {
color: #88ccff;
font-weight: bold;
}
#stats .value {
color: #fff;
font-family: 'Courier New', monospace;
}
#title {
position: fixed;
top: 20px;
right: 20px;
color: #fff;
font-size: 18px;
font-weight: bold;
text-shadow: 0 0 20px rgba(100, 180, 255, 0.8);
z-index: 100;
}
.fps-good { color: #4eff4e !important; }
.fps-ok { color: #ffff4e !important; }
.fps-bad { color: #ff4e4e !important; }
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="stats">
<div><span class="label">FPS:</span> <span class="value" id="fps">--</span></div>
<div><span class="label">总粒子数:</span> <span class="value" id="particle-count">--</span></div>
<div><span class="label">瀑布粒子:</span> <span class="value" id="fall-count">--</span></div>
<div><span class="label">水花粒子:</span> <span class="value" id="splash-count">--</span></div>
<div><span class="label">帧时间:</span> <span class="value" id="frame-time">--</span> ms</div>
</div>
<div id="title">🌊 Niagara Falls 瀑布模拟</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 配置参数
const CONFIG = {
FALL_PARTICLES: 6000, // 瀑布粒子数
SPLASH_PARTICLES: 4000, // 水花粒子数
FALL_HEIGHT: 100, // 瀑布高度 (米)
FALL_WIDTH: 60, // 瀑布宽度 (米)
FALL_DEPTH: 5, // 瀑布厚度
GRAVITY: 9.8, // 重力加速度
GROUND_Y: -10, // 地面高度
SPAWN_RATE: 30, // 每帧生成粒子数
};
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a15);
scene.fog = new THREE.FogExp2(0x0a0a15, 0.008);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 30, 120);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.05;
controls.target.set(0, 20, 0);
// 创建粒子纹理
function createWaterTexture() {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// 径向渐变创建圆形水滴
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.3, 'rgba(200, 230, 255, 0.8)');
gradient.addColorStop(0.6, 'rgba(100, 180, 255, 0.4)');
gradient.addColorStop(1, 'rgba(0, 100, 200, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
const texture = new THREE.CanvasTexture(canvas);
return texture;
}
const waterTexture = createWaterTexture();
// 瀑布粒子类
class WaterfallParticles {
constructor(count) {
this.count = count;
this.geometry = new THREE.BufferGeometry();
// 位置数组
this.positions = new Float32Array(count * 3);
this.velocities = new Float32Array(count * 3);
this.life = new Float32Array(count);
this.colors = new Float32Array(count * 3);
this.sizes = new Float32Array(count);
// 初始化
for (let i = 0; i < count; i++) {
this.resetParticle(i, true);
}
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
// 创建材质
const material = new THREE.PointsMaterial({
size: 1,
map: waterTexture,
transparent: true,
opacity: 0.8,
vertexColors: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
});
this.mesh = new THREE.Points(this.geometry, material);
scene.add(this.mesh);
}
resetParticle(i, initial = false) {
const x = (Math.random() - 0.5) * CONFIG.FALL_WIDTH;
const z = (Math.random() - 0.5) * CONFIG.FALL_DEPTH;
const y = initial ? Math.random() * CONFIG.FALL_HEIGHT + CONFIG.GROUND_Y : CONFIG.FALL_HEIGHT;
this.positions[i * 3] = x;
this.positions[i * 3 + 1] = y;
this.positions[i * 3 + 2] = z;
// 初始速度 - 向下加速
this.velocities[i * 3] = (Math.random() - 0.5) * 2;
this.velocities[i * 3 + 1] = -Math.random() * 5 - 5;
this.velocities[i * 3 + 2] = (Math.random() - 0.5) * 2;
this.life[i] = 1.0;
// 大小变化
this.sizes[i] = Math.random() * 2 + 1;
this.updateColor(i, y);
}
updateColor(i, y) {
const height = y - CONFIG.GROUND_Y;
const ratio = Math.max(0, Math.min(1, height / CONFIG.FALL_HEIGHT));
// 颜色渐变:白(顶部) → 浅蓝(中部) → 深蓝(底部)
let r, g, b;
if (ratio > 0.6) {
// 顶部:白色带蓝
const t = (ratio - 0.6) / 0.4;
r = 0.9 + t * 0.1;
g = 0.95;
b = 1.0;
} else if (ratio > 0.3) {
// 中部:浅蓝色
const t = (ratio - 0.3) / 0.3;
r = 0.4 + t * 0.5;
g = 0.7 + t * 0.25;
b = 0.9 + t * 0.1;
} else {
// 底部:深蓝色
const t = ratio / 0.3;
r = 0.1 + t * 0.3;
g = 0.3 + t * 0.4;
b = 0.6 + t * 0.3;
}
this.colors[i * 3] = r;
this.colors[i * 3 + 1] = g;
this.colors[i * 3 + 2] = b;
}
update(deltaTime) {
const positions = this.positions;
const velocities = this.velocities;
const colors = this.colors;
for (let i = 0; i < this.count; i++) {
// 更新速度(重力)
velocities[i * 3 + 1] -= CONFIG.GRAVITY * deltaTime;
// 更新位置
positions[i * 3] += velocities[i * 3] * deltaTime;
positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime;
positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime;
// 更新颜色
this.updateColor(i, positions[i * 3 + 1]);
// 检查是否撞击地面
if (positions[i * 3 + 1] <= CONFIG.GROUND_Y) {
// 产生水花
splashParticles.createSplash(
positions[i * 3],
CONFIG.GROUND_Y,
positions[i * 3 + 2],
Math.abs(velocities[i * 3 + 1]) * 0.3
);
// 重置粒子
this.resetParticle(i);
}
// 随机扰动
velocities[i * 3] += (Math.random() - 0.5) * 2 * deltaTime;
velocities[i * 3 + 2] += (Math.random() - 0.5) * 2 * deltaTime;
}
this.geometry.attributes.position.needsUpdate = true;
this.geometry.attributes.color.needsUpdate = true;
}
}
// 水花粒子类
class SplashParticles {
constructor(count) {
this.count = count;
this.geometry = new THREE.BufferGeometry();
this.positions = new Float32Array(count * 3);
this.velocities = new Float32Array(count * 3);
this.life = new Float32Array(count);
this.maxLife = new Float32Array(count);
this.colors = new Float32Array(count * 3);
this.sizes = new Float32Array(count);
this.active = new Uint8Array(count);
// 初始化所有为非激活
for (let i = 0; i < count; i++) {
this.active[i] = 0;
this.positions[i * 3] = 0;
this.positions[i * 3 + 1] = -1000;
this.positions[i * 3 + 2] = 0;
}
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
const material = new THREE.PointsMaterial({
size: 1.5,
map: waterTexture,
transparent: true,
opacity: 0.9,
vertexColors: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
});
this.mesh = new THREE.Points(this.geometry, material);
scene.add(this.mesh);
this.nextIndex = 0;
}
createSplash(x, y, z, impactSpeed) {
const splashCount = Math.min(8, Math.floor(impactSpeed / 2) + 2);
for (let s = 0; s < splashCount; s++) {
let idx = this.nextIndex;
let attempts = 0;
// 寻找非激活粒子
while (this.active[idx] && attempts < this.count) {
idx = (idx + 1) % this.count;
attempts++;
}
if (attempts >= this.count) return; // 没有可用粒子
this.nextIndex = (idx + 1) % this.count;
// 设置初始位置
this.positions[idx * 3] = x + (Math.random() - 0.5) * 2;
this.positions[idx * 3 + 1] = y;
this.positions[idx * 3 + 2] = z + (Math.random() - 0.5) * 2;
// 向四周溅射的速度
const angle = Math.random() * Math.PI * 2;
const spread = Math.random() * 0.5 + 0.2;
const upwardSpeed = impactSpeed * (Math.random() * 0.4 + 0.3);
const horizontalSpeed = impactSpeed * spread;
this.velocities[idx * 3] = Math.cos(angle) * horizontalSpeed;
this.velocities[idx * 3 + 1] = upwardSpeed;
this.velocities[idx * 3 + 2] = Math.sin(angle) * horizontalSpeed;
// 生命周期
this.maxLife[idx] = Math.random() * 1.5 + 0.5;
this.life[idx] = this.maxLife[idx];
this.active[idx] = 1;
this.sizes[idx] = Math.random() * 1.5 + 0.8;
// 初始颜色 - 白色水花
this.colors[idx * 3] = 0.95;
this.colors[idx * 3 + 1] = 0.98;
this.colors[idx * 3 + 2] = 1.0;
}
}
update(deltaTime) {
const positions = this.positions;
const velocities = this.velocities;
const colors = this.colors;
let activeCount = 0;
for (let i = 0; i < this.count; i++) {
if (!this.active[i]) {
positions[i * 3 + 1] = -1000;
continue;
}
activeCount++;
// 更新生命
this.life[i] -= deltaTime;
if (this.life[i] <= 0) {
this.active[i] = 0;
positions[i * 3 + 1] = -1000;
continue;
}
// 重力
velocities[i * 3 + 1] -= CONFIG.GRAVITY * 2 * deltaTime;
// 空气阻力
velocities[i * 3] *= 0.98;
velocities[i * 3 + 2] *= 0.98;
// 更新位置
positions[i * 3] += velocities[i * 3] * deltaTime;
positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime;
positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime;
// 地面碰撞
if (positions[i * 3 + 1] < CONFIG.GROUND_Y) {
positions[i * 3 + 1] = CONFIG.GROUND_Y;
velocities[i * 3 + 1] *= -0.3;
velocities[i * 3] *= 0.7;
velocities[i * 3 + 2] *= 0.7;
}
// 更新颜色和大小
const lifeRatio = this.life[i] / this.maxLife[i];
colors[i * 3] = 0.9 * lifeRatio;
colors[i * 3 + 1] = 0.95 * lifeRatio;
colors[i * 3 + 2] = 1.0;
this.sizes[i] = (Math.random() * 0.5 + 0.5) * lifeRatio * 2;
}
this.geometry.attributes.position.needsUpdate = true;
this.geometry.attributes.color.needsUpdate = true;
this.geometry.attributes.size.needsUpdate = true;
return activeCount;
}
}
// 创建环境
function createEnvironment() {
// 地面
const groundGeometry = new THREE.PlaneGeometry(300, 300, 50, 50);
// 添加波浪
const positions = groundGeometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const z = positions[i + 1];
positions[i + 2] = Math.sin(x * 0.1) * Math.cos(z * 0.1) * 2;
}
groundGeometry.computeVertexNormals();
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x0d1a33,
roughness: 0.3,
metalness: 0.6,
emissive: 0x051020,
emissiveIntensity: 0.3
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = CONFIG.GROUND_Y - 0.5;
scene.add(ground);
// 水面反光层
const waterGeometry = new THREE.PlaneGeometry(200, 100);
const waterMaterial = new THREE.MeshBasicMaterial({
color: 0x1a3a5c,
transparent: true,
opacity: 0.4
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
water.position.y = CONFIG.GROUND_Y + 0.1;
scene.add(water);
// 瀑布源头的悬崖
const cliffGeometry = new THREE.BoxGeometry(CONFIG.FALL_WIDTH + 20, 10, 30);
const cliffMaterial = new THREE.MeshStandardMaterial({
color: 0x1a1a2e,
roughness: 0.9
});
const cliff = new THREE.Mesh(cliffGeometry, cliffMaterial);
cliff.position.set(0, CONFIG.FALL_HEIGHT + 5, -20);
scene.add(cliff);
// 添加一些雾效粒子
const fogParticles = new THREE.BufferGeometry();
const fogPositions = [];
const fogCount = 500;
for (let i = 0; i < fogCount; i++) {
fogPositions.push(
(Math.random() - 0.5) * 150,
Math.random() * 40 + CONFIG.GROUND_Y,
(Math.random() - 0.5) * 80 + 20
);
}
fogParticles.setAttribute('position', new THREE.Float32BufferAttribute(fogPositions, 3));
const fogMaterial = new THREE.PointsMaterial({
color: 0x88ccff,
size: 8,
transparent: true,
opacity: 0.15,
map: waterTexture,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const fog = new THREE.Points(fogParticles, fogMaterial);
scene.add(fog);
// 雾效动画
return { fog, fogPositions };
}
// 灯光
function setupLighting() {
const ambientLight = new THREE.AmbientLight(0x224466, 0.5);
scene.add(ambientLight);
const moonLight = new THREE.DirectionalLight(0x88ccff, 0.8);
moonLight.position.set(50, 100, 50);
scene.add(moonLight);
// 瀑布底部发光
const bottomLight = new THREE.PointLight(0x4488ff, 2, 100);
bottomLight.position.set(0, CONFIG.GROUND_Y + 10, 0);
scene.add(bottomLight);
// 顶部光源
const topLight = new THREE.PointLight(0xaaddff, 1, 80);
topLight.position.set(0, CONFIG.FALL_HEIGHT - 10, 0);
scene.add(topLight);
}
// 初始化
setupLighting();
const { fog, fogPositions } = createEnvironment();
const waterfallParticles = new WaterfallParticles(CONFIG.FALL_PARTICLES);
const splashParticles = new SplashParticles(CONFIG.SPLASH_PARTICLES);
// 性能统计
let lastTime = performance.now();
let frameCount = 0;
let fps = 0;
let lastFpsTime = lastTime;
function updateStats(frameTime, splashActive) {
const now = performance.now();
frameCount++;
if (now - lastFpsTime >= 1000) {
fps = Math.round(frameCount * 1000 / (now - lastFpsTime));
frameCount = 0;
lastFpsTime = now;
}
const fpsEl = document.getElementById('fps');
fpsEl.textContent = fps;
fpsEl.className = fps >= 50 ? 'fps-good' : fps >= 30 ? 'fps-ok' : 'fps-bad';
document.getElementById('particle-count').textContent =
(CONFIG.FALL_PARTICLES + splashActive).toLocaleString();
document.getElementById('fall-count').textContent = CONFIG.FALL_PARTICLES.toLocaleString();
document.getElementById('splash-count').textContent = splashActive.toLocaleString();
document.getElementById('frame-time').textContent = frameTime.toFixed(2);
}
// 动画循环
let fogTime = 0;
function animate() {
requestAnimationFrame(animate);
const currentTime = performance.now();
const deltaTime = Math.min((currentTime - lastTime) / 1000, 0.1);
lastTime = currentTime;
// 更新粒子
waterfallParticles.update(deltaTime);
const splashActive = splashParticles.update(deltaTime);
// 雾效动画
fogTime += deltaTime;
const positions = fog.geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
positions[i] += Math.sin(fogTime + i) * 0.1;
positions[i + 1] += Math.cos(fogTime * 0.5 + i) * 0.05;
}
fog.geometry.attributes.position.needsUpdate = true;
// 更新控制
controls.update();
// 渲染
renderer.render(scene, camera);
// 更新统计
updateStats(deltaTime * 1000, splashActive);
}
// 窗口调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 开始动画
animate();
</script>
</body>
</html>