a
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user