天然苏打水生产的原水抽取与三重除菌的3D模拟开发实战

发布于:2025-09-05 ⋅ 阅读:(18) ⋅ 点赞:(0)

天然苏打水生产的原水抽取与三重除菌的3D模拟开发实战(Vue 3 + Three.js)

本文面向工程与仿真开发者,完整讲解如何用 Vue 3 与 Three.js 实现“天然苏打水生产环节中原水抽取与三重除菌(膜过滤 + 臭氧 + UV)”的可交互 3D 模拟。文章将从工艺抽象、场景建模、粒子动画、交互控制、性能优化与工程化扩展等角度,拆解关键设计与实现细节。


摘要

  • 目标:搭建原水抽取 → 储水暂存 → 膜过滤(物理截留)→ 臭氧氧化(化学杀灭)→ UV 紫外(物理灭活)的三重除菌可视化 Demo,用以演示工艺逻辑与交互。
  • 技术栈:Vue 3(Composition API)+ Three.js(0.160.0)+ 原生 WebGL 渲染,无需打包器,使用 CDN 直接运行。
  • 亮点:
    • 设备实体化:井架、储水罐、膜过滤器、臭氧发生器、UV 腔室、联通管道等核心设备以简模还原。
    • 粒子场直观演示:水粒子、杂质、细菌、臭氧粒子以点云与 Mesh 模拟,动态呈现过滤、杀菌、灭活效果。
    • 一键聚焦视角与运行/灯控:操作栏控制动画开关、UV 开关,并快速聚焦到各工艺单元或全景。
    • 可扩展参数化:粒子数量、速度、区域阈值、可视透明度、设备尺寸比例等均可快速调整。

在这里插入图片描述

业务背景与工艺目标

天然苏打水生产的核心卫生控制环节,是在保持风味与矿化特征的前提下,确保微生物与颗粒杂质符合指标。典型的“三重除菌”路径包含:

  1. 膜过滤(过滤膜级别取决于工艺要求,常见微滤/超滤)
  2. 臭氧氧化(对细菌/病毒/有机物进行化学氧化)
  3. UV 紫外(对残余微生物实施光学灭活)

本文的 3D 模拟以“直观可交互”为首要目标,不进行流体力学与反应动力学的精细求解,而是以“区域概率规则 + 视觉粒子”来演示核心机理。


技术栈与运行环境

  • 前端框架:Vue 3(createApp + Composition API)
  • 3D 引擎:Three.js 0.160.0
  • 渲染与交互:WebGLRenderer、Points/PointsMaterial、Mesh/MeshStandardMaterial、灯光/相机控制
  • 运行方式:单 HTML 文件 + CDN。建议使用 VS Code Live Server 或任意静态服务器,也可直接浏览器打开

依赖通过 CDN 引入:

  • Vue:https://unpkg.com/vue@3/dist/vue.global.prod.js
  • Three.js:https://unpkg.com/three@0.160.0/build/three.min.js

系统总体设计

工艺流程(逻辑抽象)

原水抽取 → 储水缓冲 → 膜过滤(物理截留杂质)→ 臭氧氧化(化学杀灭细菌)→ UV 腔室(光学灭活残余)→ 下游(本 Demo 截止于 UV 出口)

  • 杂质(灰粒子):在膜过滤区间被截留(以概率“消失并回流/复位”模拟)
  • 细菌(红粒子):在臭氧区与 UV 区间被杀灭(以概率“消失并复位”模拟)
  • 水(蓝粒子):持续沿 x 方向“流动”,形成流态直觉
  • 抽水动画:井口水粒子沿 y 轴向上,模拟井泵抽取

3D 场景分层

  • 场景与灯光:深灰背景、环境光 + 平行光;启用阴影以增强工业质感
  • 主分组 group:包含各设备与粒子系统
  • 相机:透视相机,预设多个“聚焦点”便于跳转

交互控制

  • 启停动画:控制粒子推进、滤杀逻辑与视觉动效
  • UV 灯启停:控制 UV 点光源强度,并影响 UV 区的细菌灭活逻辑
  • 聚焦按钮:快速切换相机位置/视角至井架、储罐、膜过滤、臭氧、UV、全景

设备建模与坐标布局

为便于观感,沿 x 轴布置工艺单元,并搭配简化比例:

  • 井架与抽水单元:位于 x ≈ -30

    • 混凝土基础、四立柱 + 横梁、顶部泵与电机
    • 井口开孔与阀门、护栏、传感器
    • 井口水粒子以小球 Mesh 表示,持续向上运动并循环复位
  • 储水罐:位于 x ≈ -22

    • 半透明不锈钢圆筒体(可见 60% 水位)、加强环、人孔与把手
    • 进出水管、梯子、底部排水阀、外围护栏
  • 膜过滤器:位于 x ≈ -14

    • 水平放置的圆柱体(线框材质),体现“多通道/壳体”意象
    • 杂质灰点云在此区间(约 x ∈ [-18, -10])以概率被“截留”
  • 臭氧发生器:位于 x ≈ -6

    • 半透明青色立方体,内部携带蓝色点云(臭氧气泡/活性粒子)上下漂移
    • 细菌红点在此区域(x ∈ [-8, -4])以概率被“氧化消失”
  • UV 腔室:位于 x ≈ 0

    • 半透明圆柱壳体 + 内部蓝色灯管 + 可调蓝色点光源
    • UV 开启时,细菌红点在此区域(x ∈ [-2, 2])以概率被“灭活”
  • 联通管道:若干圆柱段连接各单元,水与细菌点云主要沿管路方向推进

提示:上述坐标区间与概率阈值均可在源码中快速修改,以匹配不同视觉节奏与演示长度。


粒子系统与“净化”机理映射

  • 水流粒子(蓝,Points)

    • 初始化在 x ∈ [-30, 10] 的带内,逐帧朝 +x 方向移动,到阈值后回到 -30 重新进入
    • 作用:体现连续流动,增强工艺连贯性
  • 杂质粒子(灰,Points)

    • 主要分布在储罐与膜过滤之间
    • 在膜过滤区间触发“过滤”:以小概率被移回上游(模拟被截留/回流)
  • 细菌粒子(红,Points)

    • 全线随水向前移动
    • 在臭氧区与 UV 区间触发“杀灭”:以小概率消失并从上游重新进入
  • 臭氧粒子(蓝,Points)

    • 作为臭氧发生器的“活性演示”,在局部坐标内上下漂移,提升动感与区域识别度

这种“概率移除 + 复位”是对实际过滤/杀菌过程的直观化模拟:并非精确动力学,而是事件驱动的可视表达。


动画主循环与交互逻辑

  • 动画驱动:requestAnimationFrame

  • 总开关:isAnimating

    • 控制井口抽水、水流推进、杂质过滤概率、细菌氧化/灭活概率、臭氧粒子漂移等
  • UV 灯:isLightOn

    • 控制 UV 点光源强度
    • 仅在开启时,UV 区间的细菌才会触发灭活概率
  • 聚焦函数:通过更新相机位置 camera.position.set(...)camera.lookAt(...) 实现一键切景


参数与可调项(建议)

根据你的演示需求与设备尺寸感受,优先调整以下“体验参数”:

  • 布局比例:储罐缩放系数、设备间距(便于在一个视域内观察全线)
  • 粒子数量:waterParticleCount / impurityCount / bacteriaCount / ozoneCount(性能与观感平衡)
  • 粒子速度:Δx / Δy(决定流速与驻留时间)
  • 区域阈值:膜过滤区、臭氧区、UV 区的 x 范围
  • 概率阈值:过滤/杀菌/灭活的概率(建议 1%–10% 之间调参寻找最佳演示节奏)
  • 材质明暗与透明度:金属感、半透明壳体、灯光强度,提升辨识度

从“可视”走向“可用”:工程化校准思路

如果你希望让 Demo 更贴近工程真实,可引入以下参数化与简单模型:

  • 臭氧接触时间与浓度(CT 概念):
    (\mathrm{CT} = C \times T)(mg·min/L),按不同菌种的 CT 要求,使用概率函数近似转化为“灭杀概率”
  • UV 剂量(Dose):
    (D = I \times t)(mJ/cm²),将 UV 区驻留时间与光强映射为灭活概率
  • 经验动力学(Chick–Watson 或对数衰减模型):
    (\log_{10}\left(\frac{N}{N_0}\right) = -k , C^n , t) 或 (N = N_0 e^{-kD})
    在粒子层面可用“存活概率”进行采样,逐步降低红粒子密度

这类标定不需要复杂 PDE/CFD,只需掌握关键工艺参数与经验常数,就能显著提升“可信度”。


性能优化建议

  • 使用 BufferGeometry + PointsMaterial 来渲染大规模粒子(已采用)
  • 控制粒子数量与大小,避免过度过亮导致过曝
  • 设备 Mesh 适度简化(例如线框/低面数)
  • 合理关闭阴影或降低分辨率,减少渲染压力
  • 使用“局部可见性”策略(切镜头时隐藏非焦点对象)进一步优化

可扩展方向

  • 工艺拓展:加入活性炭吸附、混合注气、接触罐、二氧化碳调配、后端灌装/检漏/灯检/装箱等环节
  • 调度与班次:在 GUI 加入“产量计数、合格率、驻留时间分布”等 KPI 看板
  • 交互增强:加入轨迹相机/轨道控制器(OrbitControls),支持拖拽缩放
  • 数据驱动:从后端/CSV 读取生产配方与节奏参数,实现“工单场景化播放”
  • 物理深化:分层耦合(离散事件 + 简化水力网络),对驻留时间与混合效率给出更合理的区间分布

运行与集成

  • 直接用浏览器打开 HTML 文件即可(建议 Chrome/Edge 最新版)
  • 推荐使用 VS Code + Live Server 或任何静态服务器,便于跨域/控制台调试
  • 确认你的网络可访问 CDN(unpkg)
  • 控制台包含初始化与状态切换日志,便于排障

常见问题与排查

  • 画面全黑或无物体:检查相机位置与 lookAt 指向、灯光是否添加到场景
  • 粒子不动:确认已点击“启动动画”,并观察控制台日志
  • UV 滅活无效果:确认 UV 已开启(按钮状态)且细菌进入 UV 区(概念 x ∈ [-2, 2])
  • 帧率偏低:降低粒子数量,关闭部分阴影,缩短视域或减少透明物体

结语

本文用一个工程友好的方式,演示了天然苏打水“原水抽取 + 三重除菌”的 3D 交互模拟:用简模呈现设备、用粒子表达机理、用概率规则模拟过滤/杀菌/灭活效果。它既适合作为销售/培训的直观演示,也能作为后续工程化仿真的“轻量前端壳”。你可以在此基础上,引入工艺参数与统计模型,让可视模拟逐步走向“能回答业务问题”的数字样机。

附录:完整源码

各位小伙伴可以复制下面的源码来研究学习或者二次开发。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D模拟天然苏打水生产线:原水抽取-三重除菌-罐装成瓶-成品质检-包装-码垛-入库</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
    <style>
        body { margin: 0; overflow: hidden; background: #fff; }
        #app { height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; }
        #scene-container { width: 100%; height: 80vh; }
        .controls { margin-top: 12px; font-size: 14px; display: flex; gap: 10px; }
        .controls button { padding: 8px 16px; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .controls button:hover { opacity: 0.8; }
        .btn-blue { background-color: blue; }
        .btn-purple { background-color: purple; }
        .btn-green { background-color: green; }
        .btn-yellow { background-color: orange; }
        .btn-red { background-color: red; }
    </style>
</head>
<body>
    <div id="app">
        <h1>3D模拟天然苏打水生产线:原水抽取-三重除菌</h1>
        <div id="scene-container"></div>
        <div class="controls">
            <button @click="toggleAnimation" class="btn-blue">{{ isAnimating ? '停止动画' : '启动动画' }}</button>
            <button @click="toggleLight" class="btn-purple">{{ isLightOn ? '关闭UV灯' : '开启UV灯' }}</button>
            <button @click="focusOnWellRig" class="btn-blue">聚焦井架</button>
            <button @click="focusOnTank" class="btn-blue">聚焦储水罐</button>
            <button @click="focusOnMembrane" class="btn-green">聚焦膜过滤</button>
            <button @click="focusOnOzone" class="btn-yellow">聚焦臭氧发生器</button>
            <button @click="focusOnUV" class="btn-purple">聚焦UV系统</button>
            <button @click="focusOnOverview" class="btn-red">聚焦全景</button>
        </div>
    </div>

    <script>
        const { createApp, ref, onMounted } = Vue;

        createApp({
            setup() {
                const isAnimating = ref(false);
                const isLightOn = ref(false);
                let camera; // 声明以便聚焦函数使用
                let wellWaterParticles = []; // 井架水粒子数组

                onMounted(() => {
                    console.log('开始初始化Three.js场景'); // 调试:场景初始化开始

                    let scene = new THREE.Scene();
                    scene.background = new THREE.Color(0x2d3748); // 深灰色背景

                    camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 400);
                    camera.position.set(-20, 10, 30); // 初始位置调整,概览包括井架
                    camera.lookAt(-20, 0, 0);

                    let renderer = new THREE.WebGLRenderer({ antialias: true });
                    renderer.setSize(window.innerWidth, window.innerHeight * 0.8);
                    renderer.shadowMap.enabled = true;
                    document.getElementById('scene-container').appendChild(renderer.domElement);
                    console.log('渲染器已附加到DOM'); // 调试:渲染器加载成功

                    // 灯光
                    scene.add(new THREE.AmbientLight(0xffffff, 1.2));
                    const dirLight = new THREE.DirectionalLight(0xffffff, 1);
                    dirLight.position.set(10, 20, 10);
                    dirLight.castShadow = true;
                    scene.add(dirLight);

                    // 主组
                    const group = new THREE.Group();

                    // 添加井架(最左边,x=-30)
                    const wellRigGroup = new THREE.Group();
                    wellRigGroup.position.set(-30, 0, 0); // 放置在最左

                    // 混凝土基础 (方形,尺寸约3x3米)
                    const baseGeometry = new THREE.BoxGeometry(6, 1, 6);
                    const baseMaterial = new THREE.MeshStandardMaterial({ color: 0xa9a9a9, roughness: 0.9, metalness: 0 });
                    const base = new THREE.Mesh(baseGeometry, baseMaterial);
                    base.position.set(0, -4.5, 0);
                    base.castShadow = true;
                    wellRigGroup.add(base);

                    // 井口 (圆形开口,直径约1米)
                    const wellHoleGeometry = new THREE.CylinderGeometry(1, 1, 0.2, 32);
                    const wellHoleMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 });
                    const wellHole = new THREE.Mesh(wellHoleGeometry, wellHoleMaterial);
                    wellHole.position.set(0, -4, 0);
                    wellHole.rotation.x = Math.PI / 2;
                    wellRigGroup.add(wellHole);

                    // 主框架支柱 (四个坚固支柱,银灰色不锈钢)
                    const pillarGeometry = new THREE.CylinderGeometry(0.3, 0.3, 8, 32);
                    const pillarMaterial = new THREE.MeshStandardMaterial({ color: 0xc0c0c0, roughness: 0.2, metalness: 1.0 }); // 银灰色,工业光泽
                    const pillarPositions = [[-1.5, 0, -1.5], [-1.5, 0, 1.5], [1.5, 0, -1.5], [1.5, 0, 1.5]];
                    pillarPositions.forEach(pos => {
                        const pillar = new THREE.Mesh(pillarGeometry, pillarMaterial);
                        pillar.position.set(...pos);
                        pillar.castShadow = true;
                        wellRigGroup.add(pillar);
                    });

                    // 横梁 (多个水平和交叉梁,支撑框架,使用相同银灰色材质)
                    const beamGeometry = new THREE.BoxGeometry(4, 0.2, 0.2);
                    const beamMaterial = new THREE.MeshStandardMaterial({ color: 0xc0c0c0, roughness: 0.2, metalness: 1.0 });
                    // 上部横梁
                    const upperBeam1 = new THREE.Mesh(beamGeometry, beamMaterial);
                    upperBeam1.position.set(0, 3.5, 0);
                    upperBeam1.rotation.y = Math.PI / 2;
                    upperBeam1.castShadow = true;
                    wellRigGroup.add(upperBeam1);
                    const upperBeam2 = new THREE.Mesh(beamGeometry, beamMaterial);
                    upperBeam2.position.set(0, 3.5, 0);
                    upperBeam2.castShadow = true;
                    wellRigGroup.add(upperBeam2);
                    // 中部横梁
                    const midBeam = new THREE.Mesh(beamGeometry, beamMaterial);
                    midBeam.position.set(0, 0, 0);
                    midBeam.castShadow = true;
                    wellRigGroup.add(midBeam);

                    // 泵头和电机 (顶部安装,灰色不锈钢)
                    const pumpGeometry = new THREE.BoxGeometry(2, 1.5, 2);
                    const pumpMaterial = new THREE.MeshStandardMaterial({ color: 0x808080, roughness: 0.5, metalness: 0.8 });
                    const pump = new THREE.Mesh(pumpGeometry, pumpMaterial);
                    pump.position.set(0, 2, 0);
                    pump.castShadow = true;
                    wellRigGroup.add(pump);
                    // 电机 (圆柱形)
                    const motorGeometry = new THREE.CylinderGeometry(0.8, 0.8, 1.5, 32);
                    const motor = new THREE.Mesh(motorGeometry, pumpMaterial);
                    motor.position.set(0, 3.2, 0);
                    motor.rotation.x = Math.PI / 2;
                    motor.castShadow = true;
                    wellRigGroup.add(motor);

                    // 管道连接 (从泵头向下连接到井口,并向外延伸短管道)
                    const pipeConnGeometry = new THREE.CylinderGeometry(0.4, 0.4, 4, 32);
                    const pipeConnMaterial = new THREE.MeshStandardMaterial({ color: 0x4d4d4d, roughness: 0.6, metalness: 0.8 });
                    const pipeConn = new THREE.Mesh(pipeConnGeometry, pipeConnMaterial);
                    pipeConn.position.set(0, -1, 0);
                    pipeConn.castShadow = true;
                    wellRigGroup.add(pipeConn);
                    // 水平短管道 (连接到主管道)
                    const horizPipeGeometry = new THREE.CylinderGeometry(0.4, 0.4, 3, 32);
                    const horizPipe = new THREE.Mesh(horizPipeGeometry, pipeConnMaterial);
                    horizPipe.position.set(2.5, -2, 0);
                    horizPipe.rotation.z = Math.PI / 2;
                    horizPipe.castShadow = true;
                    wellRigGroup.add(horizPipe);

                    // 阀门 (多个,表面细节)
                    const valveGeometry = new THREE.SphereGeometry(0.5, 32, 32);
                    const valveMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.4, metalness: 0.9 });
                    const valve1 = new THREE.Mesh(valveGeometry, valveMaterial);
                    valve1.position.set(2.5, -2, 0);
                    valve1.castShadow = true;
                    wellRigGroup.add(valve1);
                    const valve2 = new THREE.Mesh(valveGeometry, valveMaterial);
                    valve2.position.set(0, -3, 0);
                    valve2.scale.set(0.8, 0.8, 0.8);
                    valve2.castShadow = true;
                    wellRigGroup.add(valve2);

                    // 螺栓 (表面细节,小球模拟多个螺栓)
                    const boltGeometry = new THREE.SphereGeometry(0.1, 16, 16);
                    const boltMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.3, metalness: 1.0 });
                    const boltPositions = [
                        [-1.5, 3.5, -1.5], [1.5, 3.5, -1.5], [-1.5, 3.5, 1.5], [1.5, 3.5, 1.5], // 上梁
                        [-1.5, 0, -1.5], [1.5, 0, -1.5], [-1.5, 0, 1.5], [1.5, 0, 1.5] // 中梁
                    ];
                    boltPositions.forEach(pos => {
                        const bolt = new THREE.Mesh(boltGeometry, boltMaterial);
                        bolt.position.set(...pos);
                        bolt.castShadow = true;
                        wellRigGroup.add(bolt);
                    });

                    // 安全栅栏 (围绕基础,红色金属栅栏)
                    const fenceMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5, metalness: 0.5 });
                    const fencePostGeometry = new THREE.CylinderGeometry(0.1, 0.1, 2, 32);
                    const fencePosts = [
                        [-3, -3, -3], [3, -3, -3], [-3, -3, 3], [3, -3, 3]
                    ];
                    fencePosts.forEach(pos => {
                        const post = new THREE.Mesh(fencePostGeometry, fenceMaterial);
                        post.position.set(...pos);
                        post.castShadow = true;
                        wellRigGroup.add(post);
                    });
                    // 栅栏横杆 (线条连接)
                    const railGeometry = new THREE.BoxGeometry(6, 0.05, 0.05);
                    const railPositions = [[0, -3.5, -3], [0, -2.5, -3], [0, -3.5, 3], [0, -2.5, 3]];
                    railPositions.forEach(pos => {
                        const rail = new THREE.Mesh(railGeometry, fenceMaterial);
                        rail.position.set(...pos);
                        rail.castShadow = true;
                        wellRigGroup.add(rail);
                    });
                    const sideRailGeometry = new THREE.BoxGeometry(0.05, 0.05, 6);
                    const sideRailPositions = [[-3, -3.5, 0], [-3, -2.5, 0], [3, -3.5, 0], [3, -2.5, 0]];
                    sideRailPositions.forEach(pos => {
                        const rail = new THREE.Mesh(sideRailGeometry, fenceMaterial);
                        rail.position.set(...pos);
                        rail.castShadow = true;
                        wellRigGroup.add(rail);
                    });

                    // 监测设备 (水位传感器:细长杆子)
                    const sensorGeometry = new THREE.CylinderGeometry(0.05, 0.05, 5, 32);
                    const sensorMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00, roughness: 0.1, metalness: 0.5 }); // 绿色传感器
                    const sensor = new THREE.Mesh(sensorGeometry, sensorMaterial);
                    sensor.position.set(2, -1.5, 2);
                    sensor.castShadow = true;
                    wellRigGroup.add(sensor);
                    // 传感器头
                    const sensorHeadGeometry = new THREE.SphereGeometry(0.2, 32, 32);
                    const sensorHeadMesh = new THREE.Mesh(sensorHeadGeometry, sensorMaterial);
                    sensorHeadMesh.position.set(2, -4, 2);
                    sensorHeadMesh.castShadow = true;
                    wellRigGroup.add(sensorHeadMesh);

                    // 井架水粒子 (从井口向上流动到泵头,模拟抽水)
                    const wellParticleCount = 100;
                    const wellParticleGeometry = new THREE.SphereGeometry(0.1, 16, 16);
                    const wellParticleMaterial = new THREE.MeshStandardMaterial({ color: 0x00bfff, roughness: 0.9, metalness: 0 });
                    for (let i = 0; i < wellParticleCount; i++) {
                        const particle = new THREE.Mesh(wellParticleGeometry, wellParticleMaterial);
                        particle.position.set(Math.random() * 0.5 - 0.25, -4 + Math.random() * 8, Math.random() * 0.5 - 0.25);
                        particle.castShadow = true;
                        wellRigGroup.add(particle);
                        wellWaterParticles.push(particle);
                    }

                    group.add(wellRigGroup);
                    // 添加储水罐(在井架后面,x=-22,略微调整尺寸以匹配整体比例)
                    const tankGroup = new THREE.Group();
                    tankGroup.position.set(-22, 0, 0); // 放置在井架和膜过滤之间

                    // ===== 储水罐尺寸(保持文件2中尺寸,R=4, H=20,但整体缩放0.5以匹配场景比例) =====
                    const scaleFactor = 0.5; // 缩放以适应距离和视觉平衡
                    const R = 4 * scaleFactor;
                    const H = 20 * scaleFactor;

                    // 混凝土平台
                    const platform = new THREE.Mesh(
                        new THREE.PlaneGeometry(10 * scaleFactor, 10 * scaleFactor),
                        new THREE.MeshStandardMaterial({color:0xdddddd, roughness:0.8, metalness:0.2})
                    );
                    platform.rotation.x = -Math.PI/2;
                    platform.position.y = -H/2 - 2 * scaleFactor;
                    platform.receiveShadow = true;
                    tankGroup.add(platform);

                    // 1. 支腿 (调整位置到罐体边缘下方,更逼真支撑)
                    const legGeo = new THREE.CylinderGeometry(0.5 * scaleFactor, 0.5 * scaleFactor, 4.4 * scaleFactor, 32);
                    const legMat = new THREE.MeshStandardMaterial({color:0xc0c0c0, roughness:0.25, metalness:1});
                    const legPos = [
                        [-R, -H/2-0.2 * scaleFactor, -R],
                        [-R, -H/2-0.2 * scaleFactor,  R],
                        [ R, -H/2-0.2 * scaleFactor, -R],
                        [ R, -H/2-0.2 * scaleFactor,  R]
                    ];
                    legPos.forEach(p=>{
                        const m = new THREE.Mesh(legGeo, legMat);
                        m.position.set(...p); m.castShadow = true; tankGroup.add(m);
                    });

                    // 2. 半透明不锈钢罐体
                    const bodyMat = new THREE.MeshStandardMaterial({
                        color:0xd0d0d0,
                        roughness:0.08,
                        metalness:1,
                        transparent:true,
                        opacity:0.35
                    });
                    const body = new THREE.Mesh(
                        new THREE.CylinderGeometry(R, R, H, 96, 1, true),
                        bodyMat
                    );
                    body.castShadow = true;
                    tankGroup.add(body);

                    // 3. 加强焊缝环
                    const ringMat = new THREE.MeshStandardMaterial({color:0xa8a8a8, roughness:0.15, metalness:1});
                    [ H/3, 0, -H/3 ].forEach(y=>{
                        const ring = new THREE.Mesh(
                            new THREE.TorusGeometry(R, 0.12 * scaleFactor, 24, 96),
                            ringMat
                        );
                        ring.rotation.x = Math.PI/2;
                        ring.position.y = y;
                        tankGroup.add(ring);
                    });

                    // 4. 顶部人孔盖 + 手柄
                    const coverMat = new THREE.MeshStandardMaterial({color:0xb0b0b0, roughness:0.12, metalness:1});
                    const cover = new THREE.Mesh(new THREE.CylinderGeometry(R, R, 0.4 * scaleFactor, 96), coverMat);
                    cover.position.y = H/2 + 0.2 * scaleFactor; cover.castShadow = true; tankGroup.add(cover);
                    const handle = new THREE.Mesh(new THREE.BoxGeometry(1 * scaleFactor, 0.3 * scaleFactor, 1 * scaleFactor), coverMat);
                    handle.position.set(0, H/2 + 0.55 * scaleFactor, 0); handle.castShadow = true; tankGroup.add(handle);

                    // 5. 视窗
                    const viewMat = new THREE.MeshStandardMaterial({color:0xadd8e6, transparent:true, opacity:0.6});
                    const viewport = new THREE.Mesh(new THREE.CircleGeometry(0.6 * scaleFactor, 40), viewMat);
                    viewport.position.set(-R-0.02 * scaleFactor, 0, 0);
                    viewport.rotation.y = -Math.PI/2;
                    tankGroup.add(viewport);

                    // 6. 标签
                    const label = new THREE.Mesh(
                        new THREE.PlaneGeometry(2.8 * scaleFactor, 1.2 * scaleFactor),
                        new THREE.MeshBasicMaterial({color:0xffffff})
                    );
                    label.position.set(0, 0, R+0.02 * scaleFactor);
                    tankGroup.add(label);

                    // 7. 绝缘/加热环
                    const heaterMat = new THREE.MeshStandardMaterial({color:0x3a3a3a, roughness:0.85});
                    const heater = new THREE.Mesh(
                        new THREE.TorusGeometry(R+0.15 * scaleFactor, 0.15 * scaleFactor, 24, 72),
                        heaterMat
                    );
                    heater.rotation.x = Math.PI/2;
                    heater.position.y = -H/4;
                    tankGroup.add(heater);

                    // 8. 进出水管
                    const pipeMat = new THREE.MeshStandardMaterial({color:0x6e6e6e, roughness:0.35, metalness:0.9});
                    const pipe3 = new THREE.Mesh(new THREE.CylinderGeometry(0.25 * scaleFactor,0.25 * scaleFactor,4 * scaleFactor,32), pipeMat);
                    pipe3.position.set(R+2 * scaleFactor, -H/6, 0); pipe3.rotation.z = Math.PI/2; tankGroup.add(pipe3);
                    const pipe4 = new THREE.Mesh(new THREE.CylinderGeometry(0.25 * scaleFactor,0.25 * scaleFactor,5 * scaleFactor,32), pipeMat);
                    pipe4.position.set(-R-2.5 * scaleFactor, 0, 0); pipe4.rotation.z = Math.PI / 2; tankGroup.add(pipe4);

                    // 9. 外部梯子
                    const ladderMat = legMat;
                    const railGeo = new THREE.BoxGeometry(0.15 * scaleFactor, H+0.2 * scaleFactor, 0.15 * scaleFactor);
                    const railL = new THREE.Mesh(railGeo, ladderMat);
                    const railR = railL.clone();
                    railL.position.set(R+0.6 * scaleFactor, 0, -0.5 * scaleFactor);
                    railR.position.set(R+0.6 * scaleFactor, 0,  0.5 * scaleFactor);
                    tankGroup.add(railL, railR);
                    const rungGeo = new THREE.BoxGeometry(1.0 * scaleFactor, 0.1 * scaleFactor, 0.1 * scaleFactor);
                    for(let y=-H/2+0.6 * scaleFactor; y<=H/2-0.6 * scaleFactor; y+=0.6 * scaleFactor){
                        const rung = new THREE.Mesh(rungGeo, ladderMat);
                        rung.position.set(R+0.6 * scaleFactor, y, 0);
                        tankGroup.add(rung);
                    }

                    //10. 底部排水阀
                    const valve = new THREE.Mesh(
                        new THREE.CylinderGeometry(0.4 * scaleFactor,0.4 * scaleFactor,1.2 * scaleFactor,32),
                        coverMat
                    );
                    valve.position.set(0, -H/2-0.6 * scaleFactor, 0);
                    valve.rotation.x = Math.PI/2;
                    tankGroup.add(valve);

                    //11. 内部水柱 (固定水位60%)
                    const waterMat = new THREE.MeshStandardMaterial({
                        color:0x008cff,
                        transparent:true,
                        opacity:0.55
                    });
                    const h = H * (60/100);
                    const geo = new THREE.CylinderGeometry(R*0.97, R*0.97, h, 80);
                    const waterMesh = new THREE.Mesh(geo, waterMat);
                    waterMesh.position.y = -H/2 + h/2;
                    tankGroup.add(waterMesh);

//12. 护栏(重新设计为完整围栏,围绕罐体和支柱的外侧)
const railMat = new THREE.MeshStandardMaterial({color:0xff0000});
const postGeo = new THREE.CylinderGeometry(0.32 * scaleFactor,0.32 * scaleFactor,6.0 * scaleFactor,32);
const barGeo  = new THREE.BoxGeometry(10 * scaleFactor,0.14 * scaleFactor,0.14 * scaleFactor);
const sideGeo = new THREE.BoxGeometry(0.14 * scaleFactor,0.14 * scaleFactor,10 * scaleFactor);
const posts = [
    [- (R + 0.5 * scaleFactor) - 0.5, -H/2 + 1 * scaleFactor, - (R + 0.5 * scaleFactor)],  // 左前
    [- (R + 0.5 * scaleFactor) - 0.5, -H/2 + 1 * scaleFactor, (R + 0.5 * scaleFactor)],   // 左后
    [(R + 0.5 * scaleFactor) + 0.5, -H/2 + 1 * scaleFactor, - (R + 0.5 * scaleFactor)],   // 右前
    [(R + 0.5 * scaleFactor) + 0.5, -H/2 + 1 * scaleFactor, (R + 0.5 * scaleFactor)]     // 右后
];
posts.forEach(p=>{
    const post = new THREE.Mesh(postGeo, railMat);
    post.position.set(...p);
    tankGroup.add(post);
});
// 横梁 (前后)
const frontBackBars = [
    [0, -H/2 + 1.5 * scaleFactor, - (R + 0.5 * scaleFactor)],  // 前面横梁
    [0, -H/2 + 1.5 * scaleFactor, (R + 0.5 * scaleFactor)]    // 后面横梁
];
frontBackBars.forEach(pos => {
    const bar = new THREE.Mesh(barGeo, railMat);
    bar.position.set(...pos);
    tankGroup.add(bar);
});
// 侧梁 (左右) - 调整旋转使它们向内90度
const sideBars = [
    [- (R + 0.5 * scaleFactor) - 0.5, -H/2 + 1.5 * scaleFactor, 0],  // 左侧横梁
    [(R + 0.5 * scaleFactor) + 0.5, -H/2 + 1.5 * scaleFactor, 0]    // 右侧横梁
];
sideBars.forEach(pos => {
    const bar = new THREE.Mesh(sideGeo, railMat);
    bar.position.set(...pos);
    // 向内旋转90度(绕Y轴旋转90度,使横梁垂直于罐体轴向)
    bar.rotation.z = Math.PI / 2;
    tankGroup.add(bar);
});
                    group.add(tankGroup);

                    // 连接管道从井架到储水罐 (x从-30+2.5到-22)
                    const connectPipeToTankGeometry = new THREE.CylinderGeometry(0.4, 0.4, 6.5, 32); // 调整长度
                    const connectPipeToTank = new THREE.Mesh(connectPipeToTankGeometry, pipeConnMaterial);
                    connectPipeToTank.position.set(-26.25, -2, 0); // 中间位置
                    connectPipeToTank.rotation.z = Math.PI / 2;
                    connectPipeToTank.castShadow = true;
                    group.add(connectPipeToTank);

                    // 连接管道从储水罐到膜过滤 (x从-22到-18)
                    const connectPipeFromTankGeometry = new THREE.CylinderGeometry(0.4, 0.4, 4, 32); // 调整长度
                    const connectPipeFromTank = new THREE.Mesh(connectPipeFromTankGeometry, pipeConnMaterial);
                    connectPipeFromTank.position.set(-20, -2, 0); // 中间位置
                    connectPipeFromTank.rotation.z = Math.PI / 2;
                    connectPipeFromTank.castShadow = true;
                    group.add(connectPipeFromTank);

                    // 1. 膜过滤系统(x=-14,调整位置到出水管道结束)
                    const membraneGeometry = new THREE.CylinderGeometry(2, 2, 8, 32);
                    const membraneMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.7, roughness: 0.3, wireframe: true });
                    const membrane = new THREE.Mesh(membraneGeometry, membraneMaterial);
                    membrane.position.set(-14, 0, 0);
                    membrane.rotation.z = Math.PI / 2;
                    membrane.castShadow = true;
                    group.add(membrane);



                    // 杂质粒子(灰色)
                    const impurityCount = 100; // 减少数量优化性能
                    const impurityGeo = new THREE.BufferGeometry();
                    const impPositions = new Float32Array(impurityCount * 3);
                    for (let i = 0; i < impurityCount; i++) {
                        impPositions[i * 3] = -19 + Math.random() * 5;
                        impPositions[i * 3 + 1] = (Math.random() - 0.5) * 4;
                        impPositions[i * 3 + 2] = (Math.random() - 0.5) * 4;
                    }
                    impurityGeo.setAttribute('position', new THREE.BufferAttribute(impPositions, 3));
                    const impMaterial = new THREE.PointsMaterial({ color: 0x888888, size: 0.15, transparent: true });
                    let impurityParticles = new THREE.Points(impurityGeo, impMaterial);
                    group.add(impurityParticles);

                    // 2. 臭氧发生器(x=-6,缩短间距) - 半透明
                    const ozoneBoxGeometry = new THREE.BoxGeometry(4, 4, 4);
                    const ozoneMaterial = new THREE.MeshStandardMaterial({ 
                        color: 0x00ffff, 
                        metalness: 0.5, 
                        roughness: 0.4, 
                        transparent: true, 
                        opacity: 0.5 
                    });
                    const ozoneBox = new THREE.Mesh(ozoneBoxGeometry, ozoneMaterial);
                    ozoneBox.position.set(-6, 0, 0);
                    ozoneBox.castShadow = true;
                    group.add(ozoneBox);

                    // 臭氧粒子(蓝色)
                    const ozoneCount = 100;
                    const ozoneGeo = new THREE.BufferGeometry();
                    const ozonePositions = new Float32Array(ozoneCount * 3);
                    for (let i = 0; i < ozoneCount; i++) {
                        ozonePositions[i * 3] = (Math.random() - 0.5) * 4;
                        ozonePositions[i * 3 + 1] = (Math.random() - 0.5) * 4;
                        ozonePositions[i * 3 + 2] = (Math.random() - 0.5) * 4;
                    }
                    ozoneGeo.setAttribute('position', new THREE.BufferAttribute(ozonePositions, 3));
                    const ozoneMat = new THREE.PointsMaterial({ color: 0x00ffff, size: 0.1, transparent: true });
                    let ozoneParticles = new THREE.Points(ozoneGeo, ozoneMat);
                    ozoneParticles.visible = false;
                    ozoneBox.add(ozoneParticles);

                    // 3. UV 系统(x=0,缩短间距) - 腔室半透明
                    const chamberGeometry = new THREE.CylinderGeometry(2, 2, 10, 32);
                    const chamberMaterial = new THREE.MeshStandardMaterial({ 
                        color: 0xcccccc, 
                        metalness: 0.8, 
                        roughness: 0.2, 
                        transparent: true, 
                        opacity: 0.5 
                    });
                    const chamber = new THREE.Mesh(chamberGeometry, chamberMaterial);
                    chamber.position.set(0, 0, 0);
                    chamber.rotation.x = Math.PI / 2;
                    chamber.castShadow = true;
                    group.add(chamber);

                    const lampGeometry = new THREE.CylinderGeometry(0.2, 0.2, 8, 32);
                    const lampMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff });
                    const lamp = new THREE.Mesh(lampGeometry, lampMaterial);
                    lamp.position.set(0, 0, 0);
                    lamp.rotation.x = Math.PI / 2;
                    group.add(lamp);

                    let uvLight = new THREE.PointLight(0x0000ff, 0, 20);
                    uvLight.position.set(0, 0, 0);
                    group.add(uvLight);

                    // 水流粒子
                    const particleCount = 200;
                    const particleGeometry = new THREE.BufferGeometry();
                    const positions = new Float32Array(particleCount * 3);
                    for (let i = 0; i < particleCount; i++) {
                        positions[i * 3] = -30 + Math.random() * 40;
                        positions[i * 3 + 1] = (Math.random() - 0.5) * 2;
                        positions[i * 3 + 2] = (Math.random() - 0.5) * 2;
                    }
                    particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                    const particleMaterial = new THREE.PointsMaterial({ color: 0x00bfff, size: 0.1, transparent: true });
                    let waterParticles = new THREE.Points(particleGeometry, particleMaterial);
                    group.add(waterParticles);

                    // 细菌粒子(红色)
                    const bacteriaCount = 100;
                    const bacteriaGeo = new THREE.BufferGeometry();
                    const bacPositions = new Float32Array(bacteriaCount * 3);
                    for (let i = 0; i < bacteriaCount; i++) {
                        bacPositions[i * 3] = -30 + Math.random() * 40;
                        bacPositions[i * 3 + 1] = (Math.random() - 0.5) * 2;
                        bacPositions[i * 3 + 2] = (Math.random() - 0.5) * 2;
                    }
                    bacteriaGeo.setAttribute('position', new THREE.BufferAttribute(bacPositions, 3));
                    const bacMaterial = new THREE.PointsMaterial({ color: 0xff0000, size: 0.15, transparent: true });
                    let bacteriaParticles = new THREE.Points(bacteriaGeo, bacMaterial);
                    group.add(bacteriaParticles);

                    // 连接管道
                    const pipeGeometry = new THREE.CylinderGeometry(0.5, 0.5, 8, 32);
                    const pipeMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.6 });
                    const pipe1 = new THREE.Mesh(pipeGeometry, pipeMaterial);
                    pipe1.position.set(-10, 0, 0);
                    pipe1.rotation.z = Math.PI / 2;
                    pipe1.castShadow = true;
                    group.add(pipe1);

                    const pipe2Geometry = new THREE.CylinderGeometry(0.5, 0.5, 6, 32);
                    const pipe2 = new THREE.Mesh(pipe2Geometry, pipeMaterial);
                    pipe2.position.set(-3, 0, 0);
                    pipe2.rotation.z = Math.PI / 2;
                    pipe2.castShadow = true;
                    group.add(pipe2);

                    scene.add(group);

                    console.log('场景初始化完成,所有对象已添加'); // 调试:初始化结束

                    // 渲染循环
                    (function animate() {
                        requestAnimationFrame(animate);

                        if (isAnimating.value) {
                            // 井水粒子动画(模拟从井口向上流动到泵头)
                            wellWaterParticles.forEach(particle => {
                                particle.position.y += 0.1;
                                if (particle.position.y > 2) {
                                    particle.position.y = -4;
                                    particle.position.x = Math.random() * 0.5 - 0.25;
                                    particle.position.z = Math.random() * 0.5 - 0.25;
                                }
                            });

                            // 水流移动
                            const pos = waterParticles.geometry.attributes.position.array;
                            for (let i = 0; i < pos.length; i += 3) {
                                pos[i] += 0.05;
                                if (pos[i] > 5) pos[i] = -30;
                            }
                            waterParticles.geometry.attributes.position.needsUpdate = true;

                            // 细菌移动和“消灭”
                            const bacPos = bacteriaParticles.geometry.attributes.position.array;
                            for (let i = 0; i < bacPos.length; i += 3) {
                                bacPos[i] += 0.05;
                                if (bacPos[i] > 5) bacPos[i] = -30;

                                // 在臭氧或UV区“消灭”(随机重置位置模拟消失)
                                if ((bacPos[i] > -8 && bacPos[i] < -4) || (bacPos[i] > -2 && bacPos[i] < 2 && isLightOn.value)) {
                                    if (Math.random() < 0.05) bacPos[i] = -30; // 概率消失
                                }
                            }
                            bacteriaParticles.geometry.attributes.position.needsUpdate = true;

                            // 杂质移动和过滤
                            const impPos = impurityParticles.geometry.attributes.position.array;
                            for (let i = 0; i < impPos.length; i += 3) {
                                impPos[i] += 0.05;
                                if (impPos[i] > -8) impPos[i] = -25;

                                if (impPos[i] > -18 && impPos[i] < -10) {
                                    if (Math.random() < 0.05) impPos[i] = -25; // 概率消失
                                }
                            }
                            impurityParticles.geometry.attributes.position.needsUpdate = true;

                            // 臭氧粒子动画
                            const ozonePos = ozoneParticles.geometry.attributes.position.array;
                            for (let i = 0; i < ozonePos.length; i += 3) {
                                ozonePos[i + 1] += 0.01;
                                if (ozonePos[i + 1] > 2) ozonePos[i + 1] = -2;
                            }
                            ozoneParticles.geometry.attributes.position.needsUpdate = true;
                            ozoneParticles.visible = isAnimating.value;
                        }

                        uvLight.intensity = isLightOn.value ? 2 : 0;

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

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

                const toggleAnimation = () => {
                    isAnimating.value = !isAnimating.value;
                    console.log('动画状态切换为:', isAnimating.value);
                };

                const toggleLight = () => {
                    isLightOn.value = !isLightOn.value;
                    console.log('UV灯状态切换为:', isLightOn.value);
                };

                const focusOnWellRig = () => {
                    camera.position.set(-30, 5, 15);
                    camera.lookAt(-30, 0, 0);
                };

                const focusOnTank = () => {
                    camera.position.set(-22, 5, 15);
                    camera.lookAt(-22, 0, 0);
                };

                const focusOnMembrane = () => {
                    camera.position.set(-14, 5, 15);
                    camera.lookAt(-14, 0, 0);
                };

                const focusOnOzone = () => {
                    camera.position.set(-6, 5, 15);
                    camera.lookAt(-6, 0, 0);
                };

                const focusOnUV = () => {
                    camera.position.set(0, 5, 15);
                    camera.lookAt(0, 0, 0);
                };

                const focusOnOverview = () => {
                    camera.position.set(-15, 10, 40);
                    camera.lookAt(-15, 0, 0);
                };

                return { isAnimating, isLightOn, toggleAnimation, toggleLight, focusOnWellRig, focusOnTank, focusOnMembrane, focusOnOzone, focusOnUV, focusOnOverview };
            }
        }).mount('#app');
    </script>
<script>window.parent.postMessage({ action: "ready" }, "*"); 
 
window.console = new Proxy(console, {
  get(target, prop) {
    if (['log', 'warn', 'error'].includes(prop)) {
      return new Proxy(target[prop], {
        apply(fn, thisArg, args) {
          fn.apply(thisArg, args);
          window.parent.postMessage({ action: 'console', 
            type: prop, 
            args: args.map((arg) => {
              try {
                return JSON.stringify(arg).replace(/^["']|["']$/g, '');
              } catch (e) {
                return arg;
              }
            }) 
          }, '*');
        }
      });
    }
    return target[prop];
  }
});
</script><script>window.parent.postMessage({ action: "ready" }, "*"); 
 
window.console = new Proxy(console, {
  get(target, prop) {
    if (['log', 'warn', 'error'].includes(prop)) {
      return new Proxy(target[prop], {
        apply(fn, thisArg, args) {
          fn.apply(thisArg, args);
          window.parent.postMessage({ action: 'console', 
            type: prop, 
            args: args.map((arg) => {
              try {
                return JSON.stringify(arg).replace(/^["']|["']$/g, '');
              } catch (e) {
                return arg;
              }
            }) 
          }, '*');
        }
      });
    }
    return target[prop];
  }
});
</script><script>window.parent.postMessage({ action: "ready" }, "*"); 
 
window.console = new Proxy(console, {
  get(target, prop) {
    if (['log', 'warn', 'error'].includes(prop)) {
      return new Proxy(target[prop], {
        apply(fn, thisArg, args) {
          fn.apply(thisArg, args);
          window.parent.postMessage({ action: 'console', 
            type: prop, 
            args: args.map((arg) => {
              try {
                return JSON.stringify(arg).replace(/^["']|["']$/g, '');
              } catch (e) {
                return arg;
              }
            }) 
          }, '*');
        }
      });
    }
    return target[prop];
  }
});
</script></body>
</html>

网站公告

今日签到

点亮在社区的每一天
去签到