新建会话
  • 网站
  • 文档
  • PPT
  • 表格
  • 深度研究
Kimi Code Kimi Claw
历史会话
  • 查看手机应用
  • 关于我们
    • 探索月之暗面
    • 开放平台
    • 社会招聘
    • 校园招聘
    • 用户协议
    • 隐私协议
  • Language
  • 用户反馈

三维大象牙膏实验

# 大象牙膏化学实验 3D 演示(Three.js) 请使用 Three.js 实现一个逼真的“大象牙膏”化学实验 3D 演示。所有代码(HTML、CSS、JavaScript)必须封装在一个独立的 HTML 文件中。 ## 场景设置 - **平面**:创建一个尺寸为 `1000x1000` 的灰色、光滑水平平面,能够接收阴影。 ## 三角烧瓶 ### 形状 - 在平面中心放置一个透明的玻璃三角烧瓶。 - 烧瓶轮廓要求清晰:圆柱形颈部、逐渐变宽的圆锥形瓶身、扁平底部。 - **禁止**使用简单的圆锥体代替。 ### 材质 - 高度透明的玻璃材质,具备以下属性: - 高透射率、低粗糙度、正确折射率 - 参数参考:`color: 0xffffff`、`transparent: true`、`opacity: 0.9`、`roughness: 0.95`、`metalness: 0.35`、`clearcoat: 1.0`、`clearcoatRoughness: 0.03`、`transmission: 0.95`、`ior: 1.5`、`side: THREE.DoubleSide` - 应能看到背景和透过液体的光线折射效果。 ### 液体 - 烧瓶内预装约 **1/3 高度** 的荧光粉色液体。 - 液体表面平整,与烧瓶内壁贴合。 - 液体建模应参考烧瓶的锥形部分,不得溢出烧瓶。 ### 朝向 - 三角烧瓶整体模型方向向上。 ## “大象牙膏”喷发效果模拟 ### 触发方式 - 页面底部(footer 区域)提供一个按钮,点击后开始喷发。 ### 泡沫形态与质感 - 泡沫由大量微小、半透明、能够相互融合的粒子组成,模拟真实泡沫的稠密、蓬松质感。 - 避免看起来像孤立的小球。 - 颜色为荧光粉色,带有一定亮度变化以增加层次感。 - 可使用简单的噪点纹理或程序化方式为泡沫粒子增加表面细节,使其更像多孔泡沫。 ### 喷射动态与流体模拟 - **初期喷发**:泡沫从烧瓶口猛烈向上喷出,形成持续上升的圆柱(紧密堆积的泡沫柱)。高度至少为三角烧瓶的 3 倍,泡沫柱接近最大高度时才开始分散。初始喷射速度和压力较大。 - **压力衰减**:喷射强度(速度和产生泡沫的速率)随时间逐渐减弱,导致泡沫柱高度和喷射距离减小,模拟化学反应物耗尽过程。 - **粒子运动**: - 模拟基本流体动力学影响,而非简单抛物线。 - 粒子间应有轻微排斥力以模拟泡沫膨胀感。 - 喷射主流周围存在随机的速度扰动。 - 整体受重力影响。 ### 泡沫与环境交互及变形 - **重力与堆积**:喷出的泡沫粒子受重力下落。接触平面或烧瓶外壁时,模拟受压变形效果(Y 轴压扁,X/Z 轴略微扩展)。 - **堆积形态**:落在平面和烧瓶上的泡沫逐渐堆积,形成一定厚度的覆盖层,不消失。堆积的泡沫同样具有变形和融合效果。 - **泡沫固定**:泡沫落到物体表面后,略微滑行一段距离便停止移动。 ### 液体减少 - 随着喷发,烧瓶内液体液面逐渐下降,演示液体减少效果。 - 注意:不是液体整体缩小,而是液体顶部表面逐渐下降。 ## 光照与渲染 ### 光源 - 一个主光源,产生清晰阴影。 - 一个环境光,提亮场景暗部,避免纯黑区域。 ### 阴影 - 启用渲染器的阴影贴图。 - 平面接收阴影,烧瓶和喷出的泡沫投射阴影。 ### 反射与折射 - 烧瓶材质展示环境的微弱反射以及光线透过液体和玻璃的折射。 ## 摄像机与控制 - 摄像机从 **斜 45 度角** 俯视场景中心(三角烧瓶所在位置)。 - 确保能清晰观察整个喷发过程和最终堆积效果。
液体喷射模拟与碰撞变形
用户需要一个使用 Three.js 实现的"大象牙膏"化学实验 3D 演示。这是一个复杂的交互式 3D 场景,需要在一个 HTML 文件中完成。
让我分析一下需求:
  1. 场景基础:
    • 1000x1000 的灰色平面
    • 阴影接收
  2. 三角烧瓶:
    • 不能用简单圆锥,需要圆柱颈部 + 圆锥瓶身 + 平底
    • 透明玻璃材质(具体参数已给出)
    • 内部有 1/3 高度的荧光粉色液体
    • 方向向上
  3. 喷发效果:
    • 底部按钮触发
    • 泡沫粒子系统:
      • 微小、半透明、融合感
      • 荧光粉色,有亮度变化
      • 初期猛烈向上喷发(高度为烧瓶 3 倍)
      • 压力衰减(速度和产生速率随时间降低)
      • 粒子间轻微排斥力
      • 随机速度扰动
      • 重力影响
    • 环境交互:
      • 接触平面或烧瓶时变形(Y 轴压扁,X/Z 扩展)
      • 堆积效果(不消失,形成覆盖层)
      • 略微滑行后停止
    • 液体减少(液面下降)
  4. 光照与渲染:
    • 主光源(阴影)
    • 环境光
    • 启用阴影贴图
    • 反射折射效果
  5. 摄像机:
    • 斜 45 度角俯视
技术实现考虑:
  • 使用 Three.js 的模块化导入
  • 烧瓶几何体:可以用 LatheGeometry(车削几何体)来创建轮廓旋转体,这样可以精确控制形状(圆柱颈部 + 圆锥过渡 + 平底)
  • 液体:同样用 LatheGeometry,但高度较低
  • 泡沫粒子:
    • 使用 InstancedMesh 高性能渲染大量粒子
    • 每个粒子是一个球体,但用半透明材质
    • 需要物理模拟:速度、重力、碰撞检测
    • 变形效果可以通过缩放矩阵实现
    • 堆积可以通过检测粒子与平面/烧瓶的碰撞,然后改变状态为"静态"
  • 液体减少:通过缩放液体的 Y 轴或调整几何体高度
实现步骤:
  1. 搭建 HTML 结构和基础 Three.js 场景
  2. 创建平面
  3. 创建烧瓶(LatheGeometry)
  4. 创建液体(LatheGeometry,内部)
  5. 设置光照
  6. 设置相机(45 度俯视)
  7. 创建泡沫粒子系统类
    • 位置、速度、状态(飞行中/已着陆)
    • 更新逻辑:运动、碰撞检测、变形
  8. 喷发逻辑:
    • 按钮触发
    • 随时间衰减的喷射力
    • 液体高度减少
  9. 渲染循环
关键难点:
  • 烧瓶形状:需要仔细设计 LatheGeometry 的点集
  • 泡沫融合感:通过大量半透明粒子叠加实现
  • 碰撞检测:烧瓶是复杂形状,可以做简化检测(圆柱体近似或距离检测)
  • 性能:粒子数量可能很大,需要用 InstancedMesh
让我开始编写代码:
烧瓶轮廓点设计:
  • 从底部中心开始 (0,0)
  • 底部半径 40,向上到高度 20(平底部分)
  • 然后向外倾斜到半径 80,高度 100(圆锥部分)
  • 然后向上圆柱形颈部,半径 15,高度 140(颈部)
  • 最后顶部开口
液体:
  • 在烧瓶内部,高度约为烧瓶的 1/3(约高度 47 左右)
  • 半径根据烧瓶形状对应
泡沫粒子:
  • 使用 InstancedMesh 渲染小球体
  • 数量:2000-3000 个
  • 材质:半透明,自发光(emissive)做荧光效果
  • 物理:
    • 发射位置:烧瓶口
    • 初始速度:向上 + 随机扩散
    • 喷射力随时间衰减
    • 重力加速度
    • 碰撞:
      • 与平面:Y=0,碰撞后变形并停止
      • 与烧瓶:距离检测,如果靠近烧瓶外壁,则沿法线滑下或停止
    • 变形:着陆后 scale.y 减小,scale.x/z 增加
液体减少:
  • 根据已发射的粒子数量或时间,线性减少液体 mesh 的 Y 轴缩放或顶点位置
代码结构:
  • 使用 import map 导入 three.js 模块
  • 类或函数组织
  • 窗口大小调整处理
让我写具体的实现代码。
HTML
预览
复制
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大象牙膏化学实验 3D 演示</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #1a1a1a;
        }
        
        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }
        
        #controls {
            position: absolute;
            bottom: 40px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 10;
        }
        
        button {
            padding: 15px 40px;
            font-size: 18px;
            background: linear-gradient(135deg, #ff006e 0%, #ff4d9e 100%);
            color: white;
            border: none;
            border-radius: 50px;
            cursor: pointer;
            box-shadow: 0 4px 20px rgba(255, 0, 110, 0.4);
            transition: all 0.3s ease;
            font-weight: bold;
            letter-spacing: 1px;
        }
        
        button:hover {
            transform: translateX(-50%) translateY(-2px);
            box-shadow: 0 6px 30px rgba(255, 0, 110, 0.6);
        }
        
        button:active {
            transform: translateX(-50%) translateY(0);
        }
        
        button:disabled {
            background: #666;
            cursor: not-allowed;
            box-shadow: none;
        }
        
        #info {
            position: absolute;
            top: 20px;
            left: 20px;
            color: rgba(255, 255, 255, 0.7);
            font-size: 14px;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="canvas-container"></div>
    <div id="info">
        <div>大象牙膏实验 (Elephant Toothpaste)</div>
        <div>Three.js 物理模拟</div>
    </div>
    <div id="controls">
        <button id="eruptBtn" onclick="startEruption()">开始喷发</button>
    </div>

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/[email protected]/build/three.module.js",
                "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

        // 场景设置
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x222222);
        scene.fog = new THREE.Fog(0x222222, 500, 2500);

        const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 5000);
        camera.position.set(400, 400, 400);
        camera.lookAt(0, 50, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.2;
        document.getElementById('canvas-container').appendChild(renderer.domElement);

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.target.set(0, 50, 0);
        controls.maxPolarAngle = Math.PI / 2 - 0.1;

        // 光照
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
        scene.add(ambientLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
        dirLight.position.set(200, 500, 200);
        dirLight.castShadow = true;
        dirLight.shadow.mapSize.width = 2048;
        dirLight.shadow.mapSize.height = 2048;
        dirLight.shadow.camera.near = 0.5;
        dirLight.shadow.camera.far = 2000;
        dirLight.shadow.camera.left = -500;
        dirLight.shadow.camera.right = 500;
        dirLight.shadow.camera.top = 500;
        dirLight.shadow.camera.bottom = -500;
        dirLight.shadow.bias = -0.001;
        scene.add(dirLight);

        const spotLight = new THREE.SpotLight(0xff69b4, 2);
        spotLight.position.set(-200, 300, -200);
        spotLight.angle = Math.PI / 4;
        spotLight.penumbra = 0.5;
        scene.add(spotLight);

        // 平面
        const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
        const planeMaterial = new THREE.MeshStandardMaterial({
            color: 0x444444,
            roughness: 0.2,
            metalness: 0.1
        });
        const plane = new THREE.Mesh(planeGeometry, planeMaterial);
        plane.rotation.x = -Math.PI / 2;
        plane.receiveShadow = true;
        scene.add(plane);

        // 创建烧瓶轮廓(车削几何体)
        function createFlaskProfile() {
            const points = [];
            // 底部中心
            points.push(new THREE.Vector2(0, 0));
            // 底部平坦部分
            points.push(new THREE.Vector2(40, 0));
            points.push(new THREE.Vector2(40, 10));
            // 圆锥部分(逐渐变宽)
            for (let i = 0; i <= 10; i++) {
                const t = i / 10;
                const x = 40 + (60 - 40) * t;
                const y = 10 + 90 * t;
                points.push(new THREE.Vector2(x, y));
            }
            // 肩部过渡
            points.push(new THREE.Vector2(62, 105));
            points.push(new THREE.Vector2(55, 110));
            // 圆柱颈部
            points.push(new THREE.Vector2(20, 115));
            points.push(new THREE.Vector2(20, 180));
            // 瓶口外沿
            points.push(new THREE.Vector2(25, 185));
            points.push(new THREE.Vector2(25, 190));
            // 瓶口内沿(厚度)
            points.push(new THREE.Vector2(18, 190));
            points.push(new THREE.Vector2(18, 115));
            // 内部圆锥
            for (let i = 10; i >= 0; i--) {
                const t = i / 10;
                const x = 37 + (55 - 37) * t;
                const y = 10 + 90 * t;
                points.push(new THREE.Vector2(x, y));
            }
            // 内部底部
            points.push(new THREE.Vector2(37, 10));
            points.push(new THREE.Vector2(0, 10));
            
            return points;
        }

        // 烧瓶玻璃材质
        const glassMaterial = new THREE.MeshPhysicalMaterial({
            color: 0xffffff,
            transparent: true,
            opacity: 0.3,
            roughness: 0.05,
            metalness: 0.35,
            clearcoat: 1.0,
            clearcoatRoughness: 0.03,
            transmission: 0.95,
            ior: 1.5,
            side: THREE.DoubleSide,
            thickness: 2.0,
            envMapIntensity: 1.0
        });

        const flaskGeometry = new THREE.LatheGeometry(createFlaskProfile(), 64);
        const flask = new THREE.Mesh(flaskGeometry, glassMaterial);
        flask.position.y = 0;
        flask.castShadow = true;
        flask.receiveShadow = true;
        scene.add(flask);

        // 液体
        function createLiquidProfile(level) {
            const points = [];
            const maxHeight = 60; // 液体高度约为烧瓶的1/3
            
            if (level <= 0) return points;
            
            const actualHeight = Math.min(level, maxHeight);
            
            // 从中心开始
            points.push(new THREE.Vector2(0, 10));
            // 底部
            points.push(new THREE.Vector2(37, 10));
            // 根据高度计算侧面
            for (let i = 0; i <= 10; i++) {
                const t = i / 10;
                const y = 10 + actualHeight * t;
                let x;
                if (y < 100) {
                    x = 37 + (55 - 37) * ((y - 10) / 90);
                } else {
                    x = 55 + (18 - 55) * ((y - 100) / 15);
                }
                points.push(new THREE.Vector2(x - 2, y)); // 稍微缩进一点,与玻璃有间隙
            }
            // 顶部表面
            points.push(new THREE.Vector2(0, 10 + actualHeight));
            
            return points;
        }

        let liquidLevel = 60; // 初始液位
        let liquidGeometry = new THREE.LatheGeometry(createLiquidProfile(liquidLevel), 64);
        const liquidMaterial = new THREE.MeshPhysicalMaterial({
            color: 0xff1493,
            emissive: 0xff006e,
            emissiveIntensity: 0.3,
            transparent: true,
            opacity: 0.8,
            roughness: 0.1,
            metalness: 0.1,
            transmission: 0.4,
            ior: 1.33,
            side: THREE.DoubleSide
        });
        const liquid = new THREE.Mesh(liquidGeometry, liquidMaterial);
        scene.add(liquid);

        // 泡沫粒子系统
        class FoamParticleSystem {
            constructor(maxParticles = 3000) {
                this.maxParticles = maxParticles;
                this.particles = [];
                this.activeCount = 0;
                
                // 几何体 - 使用二十面体增加细节
                const geometry = new THREE.IcosahedronGeometry(1, 1);
                
                // 材质 - 荧光粉色,半透明
                this.material = new THREE.MeshPhysicalMaterial({
                    color: 0xff69b4,
                    emissive: 0xff1493,
                    emissiveIntensity: 0.5,
                    transparent: true,
                    opacity: 0.6,
                    roughness: 0.8,
                    metalness: 0.1,
                    clearcoat: 0.3,
                    transmission: 0.2
                });

                // 使用 InstancedMesh 提高性能
                this.mesh = new THREE.InstancedMesh(geometry, this.material, maxParticles);
                this.mesh.castShadow = true;
                this.mesh.receiveShadow = true;
                this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
                scene.add(this.mesh);

                // 初始化粒子数据
                for (let i = 0; i < maxParticles; i++) {
                    this.particles.push({
                        position: new THREE.Vector3(),
                        velocity: new THREE.Vector3(),
                        scale: new THREE.Vector3(1, 1, 1),
                        active: false,
                        landed: false,
                        life: 0,
                        maxLife: 0,
                        deformation: 0
                    });
                }

                this.dummy = new THREE.Object3D();
            }

            spawnParticle(intensity) {
                if (this.activeCount >= this.maxParticles) return;

                const particle = this.particles[this.activeCount];
                particle.active = true;
                particle.landed = false;
                particle.life = 0;
                particle.maxLife = 300 + Math.random() * 200;
                particle.deformation = 0;

                // 从瓶口发射
                const angle = Math.random() * Math.PI * 2;
                const radius = Math.random() * 8;
                particle.position.set(
                    Math.cos(angle) * radius,
                    185,
                    Math.sin(angle) * radius
                );

                // 初始速度 - 猛烈向上喷发
                const speed = (15 + Math.random() * 10) * intensity;
                const spread = 0.3 * intensity;
                particle.velocity.set(
                    (Math.random() - 0.5) * spread * speed,
                    speed,
                    (Math.random() - 0.5) * spread * speed
                );

                // 随机大小
                const s = 3 + Math.random() * 4;
                particle.scale.set(s, s, s);

                this.activeCount++;
            }

            update(gravity, flaskRadius, flaskHeight) {
                let activeIndex = 0;

                for (let i = 0; i < this.activeCount; i++) {
                    const p = this.particles[i];
                    if (!p.active) continue;

                    if (!p.landed) {
                        // 物理更新
                        p.velocity.y -= gravity;
                        
                        // 空气阻力
                        p.velocity.multiplyScalar(0.98);
                        
                        // 粒子间轻微排斥(简化为随机扰动)
                        p.velocity.x += (Math.random() - 0.5) * 0.1;
                        p.velocity.z += (Math.random() - 0.5) * 0.1;

                        // 更新位置
                        p.position.add(p.velocity);

                        // 碰撞检测 - 地面
                        if (p.position.y <= p.scale.y) {
                            p.position.y = p.scale.y;
                            this.landParticle(p);
                        }

                        // 碰撞检测 - 烧瓶外壁(简化圆柱体检测)
                        const distFromCenter = Math.sqrt(p.position.x ** 2 + p.position.z ** 2);
                        if (p.position.y < flaskHeight && distFromCenter < flaskRadius + p.scale.x) {
                            // 沿着法线方向滑下或停止
                            const angle = Math.atan2(p.position.z, p.position.x);
                            p.position.x = Math.cos(angle) * (flaskRadius + p.scale.x);
                            p.position.z = Math.sin(angle) * (flaskRadius + p.scale.x);
                            
                            if (p.velocity.y < 0) {
                                this.landParticle(p);
                            }
                        }

                        // 生命值检查
                        p.life++;
                        if (p.life > p.maxLife) {
                            p.active = false;
                            continue;
                        }
                    } else {
                        // 已着陆粒子的轻微滑动
                        if (p.deformation < 1) {
                            p.deformation += 0.05;
                            // 变形:Y轴压扁,X/Z扩展
                            const flattenFactor = 0.3 + p.deformation * 0.4;
                            const spreadFactor = 1 + p.deformation * 0.3;
                            p.scale.y = p.scale.x * flattenFactor;
                            p.scale.x = p.scale.x * spreadFactor;
                            p.scale.z = p.scale.z * spreadFactor;
                        }
                    }

                    // 更新实例矩阵
                    this.dummy.position.copy(p.position);
                    this.dummy.scale.copy(p.scale);
                    this.dummy.rotation.set(
                        Math.random() * Math.PI,
                        Math.random() * Math.PI,
                        Math.random() * Math.PI
                    );
                    this.dummy.updateMatrix();
                    this.mesh.setMatrixAt(activeIndex, this.dummy.matrix);

                    // 更新颜色变化(亮度微调)
                    const brightness = 0.5 + Math.sin(p.life * 0.05) * 0.2;
                    this.material.emissiveIntensity = brightness;

                    activeIndex++;
                }

                this.mesh.count = activeIndex;
                this.mesh.instanceMatrix.needsUpdate = true;

                // 清理不活跃粒子(从尾部移除)
                while (this.activeCount > 0 && !this.particles[this.activeCount - 1].active) {
                    this.activeCount--;
                }
            }

            landParticle(p) {
                p.landed = true;
                p.velocity.set(0, 0, 0);
                // 初始变形
                p.scale.y = p.scale.x * 0.5;
                const spread = 1.4;
                p.scale.x *= spread;
                p.scale.z *= spread;
            }

            reset() {
                for (let i = 0; i < this.maxParticles; i++) {
                    this.particles[i].active = false;
                }
                this.activeCount = 0;
                this.mesh.count = 0;
            }
        }

        const foamSystem = new FoamParticleSystem(4000);

        // 喷发控制
        let isErupting = false;
        let eruptionTime = 0;
        const eruptionDuration = 300; // 帧数
        let spawnRate = 10;
        let intensity = 1.0;

        window.startEruption = function() {
            if (isErupting) return;
            isErupting = true;
            eruptionTime = 0;
            spawnRate = 15;
            intensity = 1.0;
            document.getElementById('eruptBtn').disabled = true;
            document.getElementById('eruptBtn').textContent = '喷发中...';
            
            // 重置液体
            liquidLevel = 60;
            updateLiquid();
        };

        function updateLiquid() {
            // 更新液体几何体
            liquid.geometry.dispose();
            liquid.geometry = new THREE.LatheGeometry(createLiquidProfile(liquidLevel), 64);
        }

        // 动画循环
        const clock = new THREE.Clock();
        const gravity = 0.15;

        function animate() {
            requestAnimationFrame(animate);
            
            const delta = clock.getDelta();

            if (isErupting) {
                eruptionTime++;
                
                // 压力衰减
                const progress = eruptionTime / eruptionDuration;
                intensity = Math.max(0.1, 1.0 - progress * 0.9);
                spawnRate = Math.max(1, 15 * intensity);
                
                // 生成粒子
                const spawnCount = Math.floor(spawnRate);
                for (let i = 0; i < spawnCount; i++) {
                    if (Math.random() < intensity) {
                        foamSystem.spawnParticle(intensity);
                    }
                }

                // 液体减少
                if (liquidLevel > 5 && eruptionTime % 5 === 0) {
                    liquidLevel -= 0.3;
                    updateLiquid();
                }

                // 结束喷发
                if (eruptionTime >= eruptionDuration && foamSystem.activeCount === 0) {
                    isErupting = false;
                    document.getElementById('eruptBtn').disabled = false;
                    document.getElementById('eruptBtn').textContent = '再次喷发';
                }
            }

            // 更新泡沫
            foamSystem.update(gravity, 65, 180);

            controls.update();
            renderer.render(scene, camera);
        }

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

        animate();
    </script>
</body>
</html>