diff --git a/src/composes/three/particles.ts b/src/composes/three/particles.ts index 73182ed..fa81636 100644 --- a/src/composes/three/particles.ts +++ b/src/composes/three/particles.ts @@ -30,6 +30,29 @@ export function useParticleSystem() { return texture } + /** + * 创建火焰纹理 + */ + function createFlameTexture() { + const canvas = document.createElement('canvas') + canvas.width = 64 + canvas.height = 64 + const ctx = canvas.getContext('2d')! + + const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32) + gradient.addColorStop(0, 'rgba(255, 255, 255, 1)') + gradient.addColorStop(0.3, 'rgba(255, 200, 0, 0.8)') + gradient.addColorStop(0.6, 'rgba(255, 100, 0, 0.4)') + gradient.addColorStop(1, 'rgba(255, 0, 0, 0)') + + ctx.fillStyle = gradient + ctx.fillRect(0, 0, 64, 64) + + const texture = new THREE.Texture(canvas) + texture.needsUpdate = true + return texture + } + /** * 创建粒子 */ @@ -60,7 +83,7 @@ export function useParticleSystem() { const velocity = options.velocity || new THREE.Vector3( (Math.random() - 0.5) * 0.05, - 0.1 + Math.random() * 0.1, + 0.03 + Math.random() * 0.03, (Math.random() - 0.5) * 0.05, ) @@ -101,20 +124,34 @@ export function useParticleSystem() { const texture = createSmokeTexture() const particles: Particle[] = [] let emissionCounter = 0 + let startupTime = 0 // 启动时间计数器(秒) + const maxStartupTime = 3 // 3秒达到最大强度 function emit() { - for (let i = 0; i < emissionRate; i++) { + // 根据启动时间计算当前发射强度(0 到 1) + const intensity = Math.min(startupTime / maxStartupTime, 1) + const currentRate = Math.floor(emissionRate * intensity) + + for (let i = 0; i < currentRate; i++) { const particlePos = position.clone() particlePos.x += (Math.random() - 0.5) * 0.3 particlePos.z += (Math.random() - 0.5) * 0.3 - const particle = createParticle(texture, particlePos) + // 粒子大小也随启动时间渐变 + const particle = createParticle(texture, particlePos, { + size: 0.3 * intensity + 0.2, // 从 0.2 渐变到 0.5 + }) scene.add(particle.sprite) particles.push(particle) } } function update() { + // 更新启动时间(每帧约 1/60 秒) + if (startupTime < maxStartupTime) { + startupTime += 1 / 60 + } + // 更新现有粒子 for (let i = particles.length - 1; i >= 0; i--) { const particle = particles[i] @@ -153,10 +190,100 @@ export function useParticleSystem() { } } + /** + * 创建火焰发射器 + */ + function createFlameEmitter( + scene: THREE.Scene, + position: THREE.Vector3, + emissionRate = 8, + ) { + const texture = createFlameTexture() + const particles: Particle[] = [] + let emissionCounter = 0 + let startupTime = 0 // 启动时间计数器(秒) + const maxStartupTime = 3 // 3秒达到最大强度 + + function emit() { + // 根据启动时间计算当前发射强度(0 到 1) + const intensity = Math.min(startupTime / maxStartupTime, 1) + const currentRate = Math.floor(emissionRate * intensity) + + for (let i = 0; i < currentRate; i++) { + const particlePos = position.clone() + particlePos.x += (Math.random() - 0.5) * 0.2 * intensity // 扩散范围也渐变 + particlePos.z += (Math.random() - 0.5) * 0.2 * intensity + + // 火焰颜色随机(黄色到橙红色) + const colorVariation = Math.random() + const color = colorVariation > 0.5 ? 0xFFAA00 : 0xFF4400 + + const particle = createParticle(texture, particlePos, { + color, + opacity: 0.6 + 0.2 * intensity, // 透明度从 0.6 到 0.8 + size: 0.15 + 0.15 * intensity, // 大小从 0.15 渐变到 0.3 + velocity: new THREE.Vector3( + (Math.random() - 0.5) * 0.03, + (0.05 + Math.random() * 0.05) * intensity, // 速度也渐变,降低到原来的1/3 + (Math.random() - 0.5) * 0.03, + ), + maxAge: 50, + }) + scene.add(particle.sprite) + particles.push(particle) + } + } + + function update() { + // 更新启动时间(每帧约 1/60 秒) + if (startupTime < maxStartupTime) { + startupTime += 1 / 60 + } + + // 更新现有粒子 + for (let i = particles.length - 1; i >= 0; i--) { + const particle = particles[i] + if (!particle) + continue + + const shouldRemove = updateParticle(particle) + + if (shouldRemove) { + scene.remove(particle.sprite) + particle.sprite.material.dispose() + particles.splice(i, 1) + } + } + + // 控制发射频率 + emissionCounter++ + if (emissionCounter >= 1) { + emit() + emissionCounter = 0 + } + } + + function stop() { + particles.forEach((particle) => { + scene.remove(particle.sprite) + particle.sprite.material.dispose() + }) + particles.length = 0 + } + + return { + update, + stop, + particles, + } + } + return { createSmokeTexture, + createFlameTexture, createParticle, updateParticle, createEmitter, + createFlameEmitter, } } diff --git a/src/views/three/12/index.vue b/src/views/three/12/index.vue index cab7597..f29d57d 100644 --- a/src/views/three/12/index.vue +++ b/src/views/three/12/index.vue @@ -33,6 +33,14 @@ let outlinePass: any let particleEmitter: any const isEmitting = ref(false) +// 火焰发射器 +let flameEmitter: any +const isFlaming = ref(false) +const flameEmitters: any[] = [] // 所有火焰发射器数组 +const burningMeshes = new Set() // 已着火的网格集合 +let spreadTimer = 0 // 蔓延计时器 +const spreadInterval = 2 // 每2秒蔓延一次 + // 地板流动效果 let flowingGround: any @@ -105,7 +113,7 @@ function init() { shadowMap: true, shadowMapType: THREE.PCFSoftShadowMap, toneMapping: THREE.ACESFilmicToneMapping, - toneMappingExposure: 1.2, + toneMappingExposure: 0.8, }) // 创建控制器 @@ -219,6 +227,25 @@ function loadModel(particleUtils: any) { child.userData.canAlert = true child.userData.status = 'normal' } + + // 找到 pingt01_0019_pCylinder10001 添加火焰效果 + if (child.name === 'pingt01_0019_pCylinder10001') { + const box = new THREE.Box3().setFromObject(child) + const center = box.getCenter(new THREE.Vector3()) + const topPosition = new THREE.Vector3(center.x, box.max.y, center.z) + + flameEmitter = particleUtils.createFlameEmitter(scene, topPosition, 8) + flameEmitters.push({ emitter: flameEmitter, mesh: child }) + burningMeshes.add(child) + isFlaming.value = true // 默认开启火焰效果 + + // 给着火物体添加发光效果 + if (child.material) { + const material = child.material as THREE.MeshStandardMaterial + material.emissive = new THREE.Color(0xFF4400) + material.emissiveIntensity = 0.5 + } + } } }) @@ -226,6 +253,47 @@ function loadModel(particleUtils: any) { }) } +// 火焰蔓延函数 +function spreadFire() { + if (!model) + return + + const particleUtils = useParticleSystem() + const spreadDistance = 3 // 蔓延距离阈值 + + // 遍历所有已着火的物体 + burningMeshes.forEach((burningMesh) => { + const burningBox = new THREE.Box3().setFromObject(burningMesh) + const burningCenter = burningBox.getCenter(new THREE.Vector3()) + + // 遍历模型中的所有网格 + model.traverse((child) => { + if (child instanceof THREE.Mesh && !burningMeshes.has(child)) { + // 计算与着火物体的距离 + const childBox = new THREE.Box3().setFromObject(child) + const childCenter = childBox.getCenter(new THREE.Vector3()) + const distance = burningCenter.distanceTo(childCenter) + + // 如果距离小于阈值,点燃该物体 + if (distance < spreadDistance) { + const topPosition = new THREE.Vector3(childCenter.x, childBox.max.y, childCenter.z) + const newFlameEmitter = particleUtils.createFlameEmitter(scene, topPosition, 6) + + flameEmitters.push({ emitter: newFlameEmitter, mesh: child }) + burningMeshes.add(child) + + // 给新着火物体添加发光效果 + if (child.material) { + const material = child.material as THREE.MeshStandardMaterial + material.emissive = new THREE.Color(0xFF4400) + material.emissiveIntensity = 0.5 + } + } + } + }) + }) +} + function animate() { animationId = requestAnimationFrame(animate) controls.update() @@ -242,6 +310,24 @@ function animate() { particleEmitter.update() } + // 更新火焰粒子系统 + if (isFlaming.value) { + // 更新所有火焰发射器 + flameEmitters.forEach(({ emitter }) => { + emitter.update() + }) + + // 火焰蔓延逻辑 + if (model) { + spreadTimer += 1 / 60 // 每帧增加约1/60秒 + + if (spreadTimer >= spreadInterval) { + spreadTimer = 0 + spreadFire() + } + } + } + // 更新告警闪烁效果 if (isAlerting.value && alertMesh) { alertTime += 0.05 @@ -400,6 +486,53 @@ function toggleParticles() { } } +function toggleFlame() { + isFlaming.value = !isFlaming.value + + if (!isFlaming.value) { + // 停止所有火焰发射器 + flameEmitters.forEach(({ emitter, mesh }) => { + emitter.stop() + + // 移除发光效果 + if (mesh.material) { + const material = mesh.material as THREE.MeshStandardMaterial + material.emissive = new THREE.Color(0x000000) + material.emissiveIntensity = 0 + } + }) + + // 清空数组和集合 + flameEmitters.length = 0 + burningMeshes.clear() + spreadTimer = 0 + } + else { + // 重新点燃初始物体 + if (model) { + const particleUtils = useParticleSystem() + model.traverse((child) => { + if (child instanceof THREE.Mesh && child.name === 'pingt01_0019_pCylinder10001') { + const box = new THREE.Box3().setFromObject(child) + const center = box.getCenter(new THREE.Vector3()) + const topPosition = new THREE.Vector3(center.x, box.max.y, center.z) + + const newFlameEmitter = particleUtils.createFlameEmitter(scene, topPosition, 8) + flameEmitters.push({ emitter: newFlameEmitter, mesh: child }) + burningMeshes.add(child) + + // 添加发光效果 + if (child.material) { + const material = child.material as THREE.MeshStandardMaterial + material.emissive = new THREE.Color(0xFF4400) + material.emissiveIntensity = 0.5 + } + } + }) + } + } +} + function closeDialog() { showDialog.value = false } @@ -425,6 +558,13 @@ onUnmounted(() => { if (particleEmitter) { particleEmitter.stop() } + // 停止所有火焰发射器 + flameEmitters.forEach(({ emitter }) => { + emitter.stop() + }) + flameEmitters.length = 0 + burningMeshes.clear() + renderer.dispose() composer.dispose() }) @@ -519,6 +659,9 @@ onUnmounted(() => { +
@@ -808,6 +951,15 @@ onUnmounted(() => { transform: translateY(0); } +.flame-btn { + background: linear-gradient(135deg, #FF6600 0%, #FF0000 100%); + box-shadow: 0 2px 8px rgba(255, 100, 0, 0.3); +} + +.flame-btn:hover { + box-shadow: 0 4px 12px rgba(255, 100, 0, 0.5); +} + .info-item { background: rgba(0, 255, 255, 0.1); color: #AAAAAA;