当前位置: 首页 > ds >正文

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

天然苏打水生产的原水抽取与三重除菌的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>
http://www.xdnf.cn/news/20069.html

相关文章:

  • AI大模型对决:谁是最强智能?
  • MySQL 清空表实战:TRUNCATE 与 DELETE 的核心差异与正确用法
  • 小白成长之路-develops -jenkins部署lnmp平台
  • 淘宝京东拼多多爬虫实战:反爬对抗、避坑技巧与数据安全要点
  • EDVAC:现代计算机体系的奠基之作
  • JMeter下载安装及使用入门
  • MySQL 行转列 (Pivot) 的 N 种实现方式:静态、动态与 GROUP_CONCAT 详解
  • linux0.12 head.s代码解析
  • Langchain4j 整合MongoDB 实现会话持久化存储详解
  • Day34 UDP套接字编程 可靠文件传输与实时双向聊天系统
  • HTML5圣诞网站源码
  • Python基础(①①Ctypes)
  • Web安全——JWT
  • 厦门创客匠人靠谱嘛?从内容交付能力看其核心优势
  • el-tree 点击父节点无效,只能选中子节点
  • [BUUCTF-OGeek2019]babyrop详解(包含思考过程)
  • C++:类和对象(上)
  • 微软rStar2-Agent:新的GRPO-RoC算法让14B模型在复杂推理时超越了前沿大模型
  • 卷积操作原来分3种
  • 2025年工科生转型必考的十大高含金量证书!
  • 腾讯云建站多少钱?2025年最新价格曝光,0基础也能做出专业网站?实测真假
  • flutter专栏--深入剖析你的第一个flutter应用
  • 从一次Crash分析Chromium/360浏览器的悬空指针检测机制:raw_ref与BackupRefPtr揭秘
  • 留学第一天,语言不通怎么办?同声传译工具推荐来了
  • 常用假设检验方法及 Python 实现
  • 亚马逊云代理商:配置安全组规则步骤
  • kafka Partition(分区)详解
  • nestjs 阿里云服务端签名
  • 深度学习篇---SGD+Momentum优化器
  • Photoshop - Photoshop 触控手势