1. Three.js 烟花、雨雪、火焰等特效实现

在 Three.js 中实现烟花、雨雪、火焰等特效,通常需要使用粒子系统(Particle System)。下面我将分别介绍这些特效的实现思路和关键代码。

1.1. 烟花特效

1.1.1. 实现思路

使用粒子系统模拟烟花爆炸效果,分为发射、爆炸两个阶段。

javascript

import * as THREE from 'three';

class Firework {
    constructor(scene) {
        this.scene = scene;
        this.particles = [];
        this.gravity = 0.01;
    }

    // 创建单个粒子
    createParticle(position, color, velocity, size = 2, lifetime = 100) {
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array([0, 0, 0]);
        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

        const material = new THREE.PointsMaterial({
            color: color,
            size: size,
            transparent: true,
            blending: THREE.AdditiveBlending
        });

        const particle = new THREE.Points(geometry, material);
        particle.position.copy(position);
        particle.userData = {
            velocity: velocity,
            lifetime: lifetime,
            age: 0
        };

        this.scene.add(particle);
        this.particles.push(particle);
    }

    // 发射烟花
    launch(position) {
        const baseColor = new THREE.Color(
            Math.random() * 0.5 + 0.5,
            Math.random() * 0.5 + 0.5,
            Math.random() * 0.5 + 0.5
        );

        // 发射轨迹粒子
        for (let i = 0; i < 30; i++) {
            const velocity = new THREE.Vector3(
                (Math.random() - 0.5) * 0.2,
                Math.random() * 0.5 + 0.5,
                (Math.random() - 0.5) * 0.2
            );
            this.createParticle(
                position.clone(),
                baseColor,
                velocity,
                1.5,
                30
            );
        }

        // 延迟爆炸
        setTimeout(() => {
            this.explode(position, baseColor);
        }, 1000);
    }

    // 爆炸效果
    explode(position, baseColor) {
        const particleCount = 100;

        for (let i = 0; i < particleCount; i++) {
            // 随机方向
            const phi = Math.random() * Math.PI * 2;
            const theta = Math.random() * Math.PI;
            const radius = Math.random() * 0.5 + 0.1;

            const velocity = new THREE.Vector3(
                Math.sin(theta) * Math.cos(phi) * radius,
                Math.sin(theta) * Math.sin(phi) * radius,
                Math.cos(theta) * radius
            );

            // 颜色变化
            const color = baseColor.clone();
            color.offsetHSL(Math.random() * 0.2 - 0.1, 0, 0);

            this.createParticle(
                position.clone(),
                color,
                velocity,
                Math.random() * 3 + 1,
                60
            );
        }
    }

    // 更新粒子
    update() {
        for (let i = this.particles.length - 1; i >= 0; i--) {
            const particle = this.particles[i];
            particle.userData.age++;
            particle.userData.velocity.y -= this.gravity;
            particle.position.add(particle.userData.velocity);

            // 粒子大小随生命周期减小
            particle.material.size *= 0.98;
            particle.material.opacity *= 0.97;

            if (particle.userData.age > particle.userData.lifetime) {
                this.scene.remove(particle);
                particle.geometry.dispose();
                particle.material.dispose();
                this.particles.splice(i, 1);
            }
        }
    }
}

// 使用示例
const firework = new Firework(scene);
firework.launch(new THREE.Vector3(0, 0, 0));

// 在动画循环中
function animate() {
    firework.update();
    requestAnimationFrame(animate);
}

1.2. 雨雪特效

javascript

class WeatherEffect {
    constructor(scene, type = 'snow', count = 1000) {
        this.scene = scene;
        this.type = type;
        this.particles = null;
        this.count = count;
        this.init();
    }

    init() {
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(this.count * 3);
        const velocities = [];
        const sizes = new Float32Array(this.count);

        // 初始化粒子位置和速度
        for (let i = 0; i < this.count; 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;

            if (this.type === 'snow') {
                velocities.push({
                    x: (Math.random() - 0.5) * 0.1,
                    y: Math.random() * 0.5 + 0.2,
                    z: (Math.random() - 0.5) * 0.1
                });
                sizes[i] = Math.random() * 2 + 1;
            } else if (this.type === 'rain') {
                velocities.push({
                    x: (Math.random() - 0.5) * 0.2,
                    y: Math.random() * 2 + 3,
                    z: (Math.random() - 0.5) * 0.2
                });
                sizes[i] = Math.random() * 0.5 + 0.3;
            }
        }

        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

        const material = new THREE.PointsMaterial({
            color: this.type === 'snow' ? 0xFFFFFF : 0x8888FF,
            size: this.type === 'snow' ? 2 : 0.5,
            transparent: true,
            opacity: this.type === 'snow' ? 0.8 : 0.6,
            blending: THREE.AdditiveBlending
        });

        this.particles = new THREE.Points(geometry, material);
        this.particles.userData.velocities = velocities;
        this.scene.add(this.particles);
    }

    update() {
        const positions = this.particles.geometry.attributes.position.array;
        const velocities = this.particles.userData.velocities;

        for (let i = 0; i < this.count; i++) {
            const i3 = i * 3;

            positions[i3] += velocities[i].x;
            positions[i3 + 1] += velocities[i].y;
            positions[i3 + 2] += velocities[i].z;

            // 边界重置
            if (positions[i3 + 1] < -10) {
                positions[i3] = (Math.random() - 0.5) * 100;
                positions[i3 + 1] = 50;
                positions[i3 + 2] = (Math.random() - 0.5) * 100;
            }
        }

        this.particles.geometry.attributes.position.needsUpdate = true;
    }
}

// 使用示例
const snow = new WeatherEffect(scene, 'snow', 2000);
const rain = new WeatherEffect(scene, 'rain', 3000);

function animate() {
    snow.update();
    rain.update();
}

1.3. 火焰特效

javascript

class FireEffect {
    constructor(scene, position, size = 1) {
        this.scene = scene;
        this.position = position;
        this.size = size;
        this.particles = [];
        this.init();
    }

    init() {
        const particleCount = 100;
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(particleCount * 3);
        const colors = new Float32Array(particleCount * 3);
        const sizes = new Float32Array(particleCount);

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

            // 初始位置在火焰底部
            const radius = Math.random() * 0.5 * this.size;
            const angle = Math.random() * Math.PI * 2;
            positions[i3] = Math.cos(angle) * radius;
            positions[i3 + 1] = 0;
            positions[i3 + 2] = Math.sin(angle) * radius;

            // 颜色从黄色到红色
            const color = new THREE.Color();
            const hue = 0.1 + Math.random() * 0.1;
            const saturation = 0.8 + Math.random() * 0.2;
            const lightness = 0.5;
            color.setHSL(hue, saturation, lightness);

            colors[i3] = color.r;
            colors[i3 + 1] = color.g;
            colors[i3 + 2] = color.b;

            sizes[i] = Math.random() * 2 + 1;
        }

        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
        geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

        const textureLoader = new THREE.TextureLoader();
        const sprite = textureLoader.load('https://threejs.org/examples/textures/sprites/disc.png');

        const material = new THREE.PointsMaterial({
            size: 3,
            map: sprite,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            transparent: true,
            vertexColors: true
        });

        this.points = new THREE.Points(geometry, material);
        this.points.position.copy(this.position);
        this.scene.add(this.points);

        // 存储粒子数据
        this.particlesData = [];
        for (let i = 0; i < particleCount; i++) {
            this.particlesData.push({
                velocity: new THREE.Vector3(
                    (Math.random() - 0.5) * 0.1,
                    Math.random() * 0.2 + 0.1,
                    (Math.random() - 0.5) * 0.1
                ),
                life: Math.random() * 1 + 0.5,
            });
        }
    }

    update() {
        const positions = this.points.geometry.attributes.position.array;
        const colors = this.points.geometry.attributes.color.array;
        const sizes = this.points.geometry.attributes.size.array;

        for (let i = 0; i < this.particlesData.length; i++) {
            const i3 = i * 3;
            const particle = this.particlesData[i];

            // 更新位置
            positions[i3] += particle.velocity.x;
            positions[i3 + 1] += particle.velocity.y;
            positions[i3 + 2] += particle.velocity.z;

            // 减小生命周期
            particle.life -= 0.01;

            // 重置死亡的粒子
            if (particle.life <= 0 || positions[i3 + 1] > 3 * this.size) {
                positions[i3] = (Math.random() - 0.5) * this.size * 0.5;
                positions[i3 + 1] = 0;
                positions[i3 + 2] = (Math.random() - 0.5) * this.size * 0.5;

                particle.velocity.set(
                    (Math.random() - 0.5) * 0.1,
                    Math.random() * 0.2 + 0.1,
                    (Math.random() - 0.5) * 0.1
                );
                particle.life = Math.random() * 1 + 0.5;

                // 重置颜色
                const hue = 0.05 + Math.random() * 0.1;
                const color = new THREE.Color();
                color.setHSL(hue, 1, 0.5);
                colors[i3] = color.r;
                colors[i3 + 1] = color.g;
                colors[i3 + 2] = color.b;
            }

            // 更新颜色(从黄到红到透明)
            const age = 1 - particle.life;
            colors[i3 + 1] *= 0.95; // 减少绿色分量
            colors[i3] = Math.min(1, colors[i3] * 1.02); // 增加红色分量

            // 更新大小
            sizes[i] = Math.max(0, sizes[i] * 0.98);
        }

        this.points.geometry.attributes.position.needsUpdate = true;
        this.points.geometry.attributes.color.needsUpdate = true;
        this.points.geometry.attributes.size.needsUpdate = true;

        // 添加随机抖动
        this.points.rotation.y += 0.001;
    }
}

// 使用示例
const fire = new FireEffect(scene, new THREE.Vector3(0, 0, 0), 2);

function animate() {
    fire.update();
}

1.4. 综合优化建议

1.4.1. 性能优化

javascript

// 1. 使用 InstancedMesh 优化大量粒子
const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const instancedMesh = new THREE.InstancedMesh(particleGeometry, material, 1000);

// 2. 使用着色器材质(ShaderMaterial)获得更好性能
const vertexShader = `
    attribute float size;
    attribute vec3 color;
    varying vec3 vColor;

    void main() {
        vColor = color;
        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        gl_PointSize = size * (300.0 / -mvPosition.z);
        gl_Position = projectionMatrix * mvPosition;
    }
`;

const fragmentShader = `
    varying vec3 vColor;

    void main() {
        gl_FragColor = vec4(vColor, 1.0);
    }
`;

1.4.2. 交互功能扩展

javascript

// 添加鼠标交互
function addInteraction() {
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    window.addEventListener('click', (event) => {
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        raycaster.setFromCamera(mouse, camera);

        // 在点击位置创建特效
        const intersects = raycaster.intersectObjects(groundMesh);
        if (intersects.length > 0) {
            firework.launch(intersects[0].point);
        }
    });
}

这些特效可以根据需要进行组合和调整,通过调整粒子数量、速度、颜色和生命周期等参数,可以创建出各种不同的视觉效果。记得在 Three.js 项目中合理管理资源,及时清理不再使用的几何体和材质。