1. Three.js 粒子动画与物理模拟

我们将创建一个具有物理模拟效果的粒子系统,包括重力、碰撞和交互功能。这个可视化效果将展示粒子在3D空间中的物理行为。

1.1. 思路分析

  1. 创建基础Three.js场景,包含相机、灯光和渲染器

  2. 实现粒子系统,使用自定义几何体或BufferGeometry

  3. 添加物理模拟(重力、碰撞检测、速度/加速度)

  4. 实现用户交互(鼠标拖动、点击生成粒子)

  5. 添加控制面板以调整物理参数

  6. 添加视觉反馈和性能显示
    下面是完整实现代码:

html

<!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://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lil-gui@0.17.0/dist/lil-gui.umd.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #0a0a1a;
            color: #e0e0ff;
            overflow: hidden;
            height: 100vh;
        }

        #container {
            position: relative;
            width: 100%;
            height: 100vh;
        }

        #info {
            position: absolute;
            top: 20px;
            left: 20px;
            z-index: 100;
            background: rgba(10, 10, 30, 0.7);
            padding: 15px;
            border-radius: 10px;
            max-width: 300px;
            backdrop-filter: blur(5px);
            border: 1px solid rgba(100, 150, 255, 0.3);
        }

        h1 {
            font-size: 24px;
            margin-bottom: 10px;
            background: linear-gradient(90deg, #4facfe, #00f2fe);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .subtitle {
            font-size: 14px;
            margin-bottom: 15px;
            color: #a0a0ff;
        }

        #stats {
            display: flex;
            justify-content: space-between;
            margin-top: 10px;
            font-size: 14px;
        }

        .stat-value {
            color: #00f2fe;
            font-weight: bold;
        }

        #controls {
            position: absolute;
            top: 20px;
            right: 20px;
            z-index: 100;
        }

        #instructions {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            text-align: center;
            background: rgba(10, 10, 30, 0.7);
            padding: 10px 15px;
            border-radius: 10px;
            font-size: 14px;
            backdrop-filter: blur(5px);
            border: 1px solid rgba(100, 150, 255, 0.3);
        }

        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <div id="container"></div>

    <div id="info">
        <h1>粒子物理模拟</h1>
        <p class="subtitle">Three.js粒子系统与物理引擎模拟</p>
        <p>体验粒子在重力、碰撞和相互作用下的行为。</p>
        <div id="stats">
            <div>粒子数量: <span class="stat-value" id="particleCount">0</span></div>
            <div>帧率: <span class="stat-value" id="fps">0</span> FPS</div>
            <div>物理计算: <span class="stat-value" id="physicsTime">0</span> ms</div>
        </div>
    </div>

    <div id="instructions">
        点击或拖动鼠标生成粒子 | 右键拖拽旋转视角 | 滚轮缩放
    </div>

    <script>
        // 全局变量
        let scene, camera, renderer, particles;
        let mouse = { x: 0, y: 0, isDown: false, isRightDown: false };
        let particleCount = 0;
        const maxParticles = 5000;
        let clock = new THREE.Clock();

        // 物理参数
        const physics = {
            gravity: 0.5,
            bounce: 0.8,
            friction: 0.99,
            attractionForce: 0.5,
            repulsionForce: 2.0,
            collisionDistance: 1.5,
            mouseInfluence: 10.0,
            timeScale: 1.0
        };

        // 粒子属性
        let particlePositions = [];
        let particleVelocities = [];
        let particleColors = [];
        let particleSizes = [];
        let particleGeometry, particleMaterial, particleSystem;

        // 初始化场景
        function init() {
            // 创建场景
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x0a0a1a);

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

            // 创建渲染器
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            document.getElementById('container').appendChild(renderer.domElement);

            // 添加光源
            const ambientLight = new THREE.AmbientLight(0x404080, 0.6);
            scene.add(ambientLight);

            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
            directionalLight.position.set(10, 20, 15);
            scene.add(directionalLight);

            // 创建粒子系统
            createParticleSystem();

            // 添加初始粒子
            for (let i = 0; i < 500; i++) {
                addParticle(
                    (Math.random() - 0.5) * 40,
                    (Math.random() - 0.5) * 40,
                    (Math.random() - 0.5) * 40
                );
            }

            // 添加轨道控制器
            const controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.05;
            controls.screenSpacePanning = false;
            controls.minDistance = 10;
            controls.maxDistance = 100;
            controls.maxPolarAngle = Math.PI / 2;

            // 添加GUI控制面板
            createGUI();

            // 事件监听
            setupEventListeners();

            // 开始动画
            animate();
        }

        // 创建粒子系统
        function createParticleSystem() {
            // 创建粒子几何体
            particleGeometry = new THREE.BufferGeometry();

            // 初始化粒子数据
            const positions = new Float32Array(maxParticles * 3);
            const colors = new Float32Array(maxParticles * 3);
            const sizes = new Float32Array(maxParticles);

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

            // 创建粒子材质
            particleMaterial = new THREE.PointsMaterial({
                size: 0.5,
                vertexColors: true,
                transparent: true,
                opacity: 0.8,
                sizeAttenuation: true
            });

            // 创建粒子系统
            particleSystem = new THREE.Points(particleGeometry, particleMaterial);
            scene.add(particleSystem);

            // 初始化粒子数组
            particlePositions = new Float32Array(maxParticles * 3);
            particleVelocities = new Array(maxParticles).fill().map(() => new THREE.Vector3());
            particleColors = new Float32Array(maxParticles * 3);
            particleSizes = new Float32Array(maxParticles);
        }

        // 添加一个粒子
        function addParticle(x, y, z) {
            if (particleCount >= maxParticles) return;

            const index = particleCount * 3;

            // 设置位置
            particlePositions[index] = x;
            particlePositions[index + 1] = y;
            particlePositions[index + 2] = z;

            // 设置随机速度
            particleVelocities[particleCount].set(
                (Math.random() - 0.5) * 2,
                (Math.random() - 0.5) * 2,
                (Math.random() - 0.5) * 2
            );

            // 设置随机颜色
            const color = new THREE.Color();
            color.setHSL(Math.random() * 0.2 + 0.5, 0.8, Math.random() * 0.3 + 0.5);

            particleColors[index] = color.r;
            particleColors[index + 1] = color.g;
            particleColors[index + 2] = color.b;

            // 设置随机大小
            particleSizes[particleCount] = Math.random() * 0.5 + 0.3;

            particleCount++;

            // 更新几何体属性
            updateParticleAttributes();
        }

        // 更新粒子属性
        function updateParticleAttributes() {
            particleGeometry.attributes.position.array.set(particlePositions);
            particleGeometry.attributes.color.array.set(particleColors);
            particleGeometry.attributes.size.array.set(particleSizes);

            particleGeometry.attributes.position.needsUpdate = true;
            particleGeometry.attributes.color.needsUpdate = true;
            particleGeometry.attributes.size.needsUpdate = true;

            // 更新统计信息
            document.getElementById('particleCount').textContent = particleCount;
        }

        // 物理模拟
        function simulatePhysics(deltaTime) {
            const startTime = performance.now();

            // 应用时间缩放
            deltaTime *= physics.timeScale;

            // 边界盒
            const boundary = 25;

            // 更新每个粒子
            for (let i = 0; i < particleCount; i++) {
                const index = i * 3;

                // 应用重力
                particleVelocities[i].y -= physics.gravity * deltaTime;

                // 应用摩擦力
                particleVelocities[i].multiplyScalar(physics.friction);

                // 更新位置
                particlePositions[index] += particleVelocities[i].x * deltaTime;
                particlePositions[index + 1] += particleVelocities[i].y * deltaTime;
                particlePositions[index + 2] += particleVelocities[i].z * deltaTime;

                // 边界碰撞检测
                if (particlePositions[index] < -boundary) {
                    particlePositions[index] = -boundary;
                    particleVelocities[i].x = -particleVelocities[i].x * physics.bounce;
                } else if (particlePositions[index] > boundary) {
                    particlePositions[index] = boundary;
                    particleVelocities[i].x = -particleVelocities[i].x * physics.bounce;
                }

                if (particlePositions[index + 1] < -boundary) {
                    particlePositions[index + 1] = -boundary;
                    particleVelocities[i].y = -particleVelocities[i].y * physics.bounce;
                } else if (particlePositions[index + 1] > boundary) {
                    particlePositions[index + 1] = boundary;
                    particleVelocities[i].y = -particleVelocities[i].y * physics.bounce;
                }

                if (particlePositions[index + 2] < -boundary) {
                    particlePositions[index + 2] = -boundary;
                    particleVelocities[i].z = -particleVelocities[i].z * physics.bounce;
                } else if (particlePositions[index + 2] > boundary) {
                    particlePositions[index + 2] = boundary;
                    particleVelocities[i].z = -particleVelocities[i].z * physics.bounce;
                }

                // 粒子间相互作用(简化的碰撞检测)
                for (let j = i + 1; j < particleCount; j++) {
                    const jIndex = j * 3;

                    const dx = particlePositions[index] - particlePositions[jIndex];
                    const dy = particlePositions[index + 1] - particlePositions[jIndex + 1];
                    const dz = particlePositions[index + 2] - particlePositions[jIndex + 2];

                    const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);

                    if (distance < physics.collisionDistance && distance > 0) {
                        // 简单碰撞响应
                        const force = (physics.collisionDistance - distance) / physics.collisionDistance;

                        const forceX = dx / distance * force * physics.repulsionForce * deltaTime;
                        const forceY = dy / distance * force * physics.repulsionForce * deltaTime;
                        const forceZ = dz / distance * force * physics.repulsionForce * deltaTime;

                        particleVelocities[i].x += forceX;
                        particleVelocities[i].y += forceY;
                        particleVelocities[i].z += forceZ;

                        particleVelocities[j].x -= forceX;
                        particleVelocities[j].y -= forceY;
                        particleVelocities[j].z -= forceZ;
                    }
                }

                // 鼠标交互
                if (mouse.isDown) {
                    // 将鼠标位置转换为3D空间
                    const mouseVector = new THREE.Vector3(
                        (mouse.x / window.innerWidth) * 2 - 1,
                        -(mouse.y / window.innerHeight) * 2 + 1,
                        0.5
                    );

                    mouseVector.unproject(camera);
                    mouseVector.sub(camera.position).normalize();

                    const distance = -camera.position.z / mouseVector.z;
                    const mousePos = camera.position.clone().add(mouseVector.multiplyScalar(distance));

                    const dx = mousePos.x - particlePositions[index];
                    const dy = mousePos.y - particlePositions[index + 1];
                    const dz = mousePos.z - particlePositions[index + 2];

                    const distanceToMouse = Math.sqrt(dx * dx + dy * dy + dz * dz);

                    if (distanceToMouse < 10) {
                        const force = (10 - distanceToMouse) / 10 * physics.mouseInfluence * deltaTime;

                        particleVelocities[i].x += dx / distanceToMouse * force;
                        particleVelocities[i].y += dy / distanceToMouse * force;
                        particleVelocities[i].z += dz / distanceToMouse * force;
                    }
                }
            }

            // 更新物理计算时间统计
            const physicsTime = performance.now() - startTime;
            document.getElementById('physicsTime').textContent = physicsTime.toFixed(2);
        }

        // 创建GUI控制面板
        function createGUI() {
            const gui = new lil.GUI({ autoPlace: false });
            document.getElementById('container').appendChild(gui.domElement);
            gui.domElement.style.position = 'absolute';
            gui.domElement.style.top = '20px';
            gui.domElement.style.right = '20px';

            // 物理参数控制
            const physicsFolder = gui.addFolder('物理参数');
            physicsFolder.add(physics, 'gravity', 0, 2, 0.1).name('重力');
            physicsFolder.add(physics, 'bounce', 0, 1, 0.05).name('弹性');
            physicsFolder.add(physics, 'friction', 0.9, 1, 0.001).name('摩擦力');
            physicsFolder.add(physics, 'repulsionForce', 0, 5, 0.1).name('排斥力');
            physicsFolder.add(physics, 'collisionDistance', 0.5, 3, 0.1).name('碰撞距离');
            physicsFolder.add(physics, 'mouseInfluence', 0, 20, 0.5).name('鼠标影响');
            physicsFolder.add(physics, 'timeScale', 0.1, 3, 0.1).name('时间缩放');
            physicsFolder.open();

            // 粒子控制
            const particleFolder = gui.addFolder('粒子控制');
            particleFolder.add({ addParticles: () => {
                for (let i = 0; i < 50; i++) {
                    addParticle(
                        (Math.random() - 0.5) * 20,
                        (Math.random() - 0.5) * 20,
                        (Math.random() - 0.5) * 20
                    );
                }
            } }, 'addParticles').name('添加粒子');

            particleFolder.add({ removeParticles: () => {
                if (particleCount > 0) {
                    particleCount = Math.max(0, particleCount - 100);
                    updateParticleAttributes();
                }
            } }, 'removeParticles').name('移除粒子');

            particleFolder.add({ resetParticles: () => {
                particleCount = 0;
                for (let i = 0; i < 500; i++) {
                    addParticle(
                        (Math.random() - 0.5) * 40,
                        (Math.random() - 0.5) * 40,
                        (Math.random() - 0.5) * 40
                    );
                }
            } }, 'resetParticles').name('重置粒子');
            particleFolder.open();

            // 视觉控制
            const visualFolder = gui.addFolder('视觉设置');
            visualFolder.add(particleMaterial, 'size', 0.1, 2, 0.1).name('粒子大小');
            visualFolder.add(particleMaterial, 'opacity', 0.1, 1, 0.05).name('透明度');
            visualFolder.open();
        }

        // 设置事件监听器
        function setupEventListeners() {
            // 鼠标移动事件
            renderer.domElement.addEventListener('mousemove', (event) => {
                mouse.x = event.clientX;
                mouse.y = event.clientY;
            });

            // 鼠标按下事件
            renderer.domElement.addEventListener('mousedown', (event) => {
                if (event.button === 0) { // 左键
                    mouse.isDown = true;

                    // 添加粒子
                    const mouseVector = new THREE.Vector3(
                        (event.clientX / window.innerWidth) * 2 - 1,
                        -(event.clientY / window.innerHeight) * 2 + 1,
                        0.5
                    );

                    mouseVector.unproject(camera);
                    mouseVector.sub(camera.position).normalize();

                    const distance = -camera.position.z / mouseVector.z;
                    const position = camera.position.clone().add(mouseVector.multiplyScalar(distance));

                    for (let i = 0; i < 10; i++) {
                        addParticle(
                            position.x + (Math.random() - 0.5) * 2,
                            position.y + (Math.random() - 0.5) * 2,
                            position.z + (Math.random() - 0.5) * 2
                        );
                    }
                } else if (event.button === 2) { // 右键
                    mouse.isRightDown = true;
                }
            });

            // 鼠标释放事件
            renderer.domElement.addEventListener('mouseup', (event) => {
                if (event.button === 0) {
                    mouse.isDown = false;
                } else if (event.button === 2) {
                    mouse.isRightDown = false;
                }
            });

            // 鼠标离开画布
            renderer.domElement.addEventListener('mouseleave', () => {
                mouse.isDown = false;
                mouse.isRightDown = false;
            });

            // 阻止右键菜单
            renderer.domElement.addEventListener('contextmenu', (event) => {
                event.preventDefault();
            });

            // 窗口调整大小
            window.addEventListener('resize', () => {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            });
        }

        // 帧率计算
        let frameCount = 0;
        let lastTime = performance.now();
        let fps = 0;

        function updateFPS() {
            frameCount++;
            const currentTime = performance.now();

            if (currentTime >= lastTime + 1000) {
                fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
                frameCount = 0;
                lastTime = currentTime;

                document.getElementById('fps').textContent = fps;
            }
        }

        // 动画循环
        function animate() {
            requestAnimationFrame(animate);

            const deltaTime = clock.getDelta();

            // 物理模拟
            simulatePhysics(deltaTime);

            // 更新粒子属性
            updateParticleAttributes();

            // 更新帧率
            updateFPS();

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

        // 初始化应用
        init();
    </script>
</body>
</html>

2. 功能说明

  1. 物理模拟效果

  2. 交互功能

  3. 控制面板

  4. 性能监控

这个示例展示了Three.js粒子系统与物理模拟的结合,通过调整参数可以观察到不同的粒子行为,适合学习和演示物理模拟原理。