天然苏打水生产的原水抽取与三重除菌的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 开关,并快速聚焦到各工艺单元或全景。
- 可扩展参数化:粒子数量、速度、区域阈值、可视透明度、设备尺寸比例等均可快速调整。
业务背景与工艺目标
天然苏打水生产的核心卫生控制环节,是在保持风味与矿化特征的前提下,确保微生物与颗粒杂质符合指标。典型的“三重除菌”路径包含:
- 膜过滤(过滤膜级别取决于工艺要求,常见微滤/超滤)
- 臭氧氧化(对细菌/病毒/有机物进行化学氧化)
- 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>