真实烟花效果代码

创建于:2025年1月29日

使用 O1 回答 Chat01

问题

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>3D烟花特效</title> <style> body { margin: 0; overflow: hidden; background: #000; }
text
canvas { display: block; } </style>
</head> <body> <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/LuminosityHighPassShader.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/CopyShader.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/EffectComposer.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/BloomPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/ShaderPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/RenderPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/UnrealBloomPass.js"></script> <script> // 轨迹粒子几何体(全局共用) const trailGeometry = new THREE.SphereGeometry(0.2, 8, 8); const trailMaterial = new THREE.MeshBasicMaterial({ color: 0xFFA500, // 橙色轨迹 transparent: true, opacity: 0.7 });
text
// 场景初始化 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); camera.position.z = 50; // 在场景初始化后添加后期处理 const composer = new THREE.EffectComposer(renderer); const renderPass = new THREE.RenderPass(scene, camera); const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, // 强度 2, // 减小泛光半径 0.85 // 阈值 ); bloomPass.threshold = 0.1; // 增加泛光阈值,减少过度发光 bloomPass.strength = 0.5; // 减小泛光强度 bloomPass.radius = 0.5; composer.addPass(renderPass); composer.addPass(bloomPass); // 烟花粒子类 class Firework { constructor() { this.particles = []; // 增加更丰富的颜色变化 this.color = new THREE.Color().setHSL( Math.random(), // 色相 0.8 + Math.random() * 0.2, // 提高饱和度 0.4 + Math.random() * 0.3 // 降低亮度范围 ); this.exploded = false; this.dead = false; // 增加更多形状变化 this.baseSize = Math.random() * 1 + 0.5; this.explosionCount = Math.floor(Math.random() * 5) + 1; // 最多三次爆炸 this.explosionPattern = Math.floor(Math.random() * 5); // 0-4种爆炸模式 this.dragCoefficient = 0.97 + Math.random() * 0.03; // 空气阻力系数 // 使用 BufferGeometry 和 Points 来管理粒子 const geometry = new THREE.BufferGeometry(); const positions = []; const colors = []; const sizes = []; this.particleCount = 200; for (let i = 0; i < this.particleCount; i++) { positions.push(0, 0, 0); // 初始位置 colors.push(this.color.r, this.color.g, this.color.b); sizes.push(this.baseSize); } geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute('customColor', new THREE.Float32BufferAttribute(colors, 3)); geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1)); // 添加粒子速度数组 this.velocities = []; for (let i = 0; i < this.particleCount; i++) { this.velocities.push(new THREE.Vector3()); } const material = new THREE.ShaderMaterial({ uniforms: { color: { value: new THREE.Color(0xffffff) }, pointTexture: { value: new THREE.TextureLoader().load('https://threejs.org/examples/textures/sprites/disc.png') } }, vertexShader: ` attribute float size; attribute vec3 customColor; varying vec3 vColor; varying float life; void main() { life = 1.0 - (position.y + 30.0) / 60.0; // 基于高度的生命周期 vColor = customColor * (1.0 + 2.0 * life); // 颜色增强 vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = size * (300.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` uniform vec3 color; uniform sampler2D pointTexture; varying vec3 vColor; void main() { gl_FragColor = vec4(color * vColor, 1.0); gl_FragColor = gl_FragColor * texture2D(pointTexture, gl_PointCoord); if (gl_FragColor.a < 0.1) discard; } `, blending: THREE.AdditiveBlending, depthTest: false, transparent: true }); this.particles = new THREE.Points(geometry, material); // 减小发射范围 const randomX = (Math.random() - 0.5) * window.innerWidth * 0.8; // 使用屏幕宽度的80%作为范围 const randomZ = (Math.random() - 0.5) * 40; this.particles.position.set(randomX, -30, randomZ); // 调整发射速度,目标点更分散 // 在空间中随机选择爆炸点 // 确保目标点在视野范围内 const targetX = randomX; // 保持水平位置不变 const targetZ = randomZ; const targetY = 15 + Math.random() * 10; // 控制爆炸高度 // 计算从初始位置到目标位置的方向 const directionX = targetX - randomX; const directionZ = targetZ - randomZ; const directionY = targetY - (-30); // 标准化方向并设置速度 const length = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ); // 随机化速度大小 const speedMultiplier = 1.2 + Math.random() * 0.3; // 调整速度范围 this.velocity = new THREE.Vector3( directionX / length * speedMultiplier, directionY / length * speedMultiplier, directionZ / length * speedMultiplier ); // 存储目标位置用于判断爆炸 this.targetPosition = new THREE.Vector3(targetX, targetY, targetZ); // 设置允许的爆炸距离误差 this.explosionTolerance = 5; // 初始化每个粒子的速度 for (let i = 0; i < this.particleCount; i++) { this.velocities[i].set( (Math.random() - 0.5) * 1, Math.random() * 2 + 2, (Math.random() - 0.5) * 1 ); } scene.add(this.particles); } explode(phase = 0) { // 使用 Points 系统进行爆炸 const geometry = this.particles.geometry; const positions = geometry.attributes.position.array; const sizes = geometry.attributes.size.array; // 获取当前烟花的位置作为爆炸中心点 const centerX = this.particles.position.x; const centerY = this.particles.position.y; const centerZ = this.particles.position.z; for (let i = 0; i < this.particleCount; i++) { // 设置粒子的初始位置为爆炸中心 positions[i * 3 + 0] = 0; positions[i * 3 + 1] = 0; positions[i * 3 + 2] = 0; // 为每个粒子设置向外扩散的速度 const phi = Math.random() * Math.PI * 2; const theta = Math.random() * Math.PI; const speed = 2 + Math.random() * 3; // 减小爆炸速度 this.velocities[i].set( Math.sin(theta) * Math.cos(phi) * speed, Math.cos(theta) * speed, Math.sin(theta) * Math.sin(phi) * speed ); sizes[i] = Math.random() * 3 + 2; // 减小粒子尺寸 // 更新颜色渐变 const colors = this.particles.geometry.attributes.customColor.array; const baseIndex = i * 3; const currentColor = new THREE.Color( colors[baseIndex], colors[baseIndex + 1], colors[baseIndex + 2] ); const targetColor = new THREE.Color().setHSL( Math.random(), 0.8 + Math.random() * 0.2, 0.4 + Math.random() * 0.3 // 降低爆炸粒子亮度 ); const lerpedColor = currentColor.lerp(targetColor, 0.3); // 减小颜色变化强度 colors[baseIndex] = lerpedColor.r; colors[baseIndex + 1] = lerpedColor.g; colors[baseIndex + 2] = lerpedColor.b; } geometry.attributes.position.needsUpdate = true; geometry.attributes.customColor.needsUpdate = true; geometry.attributes.size.needsUpdate = true; this.exploded = true; // 添加连锁爆炸效果 if (phase === 0 && this.explosionCount > 1) { setTimeout(() => this.explode(1), 400 + Math.random() * 200); } // 修正轨迹材质创建方式 if(phase === 0) { this.trailParticles = new THREE.Group(); const trailColor = this.color.clone().multiplyScalar(1.5); for(let i=0; i<20; i++) { const material = trailMaterial.clone(); material.color = trailColor; // 正确设置材质颜色的方式 material.transparent = true; material.opacity = 0.7; const trail = new THREE.Mesh(trailGeometry, material); trail.position.copy(this.particles.position); this.trailParticles.add(trail); } scene.add(this.trailParticles); } } update() { if (!this.exploded) { this.particles.position.add(this.velocity); this.velocity.y -= 0.015; // 增强上升轨迹的视觉效果 this.particles.scale.set( 1 + Math.sin(Date.now() * 0.01) * 0.3, 1 + Math.cos(Date.now() * 0.01) * 0.3, 1 ); // 检查是否需要爆炸 // 在以下情况爆炸: // 1. 接近目标位置 // 2. 超出视野范围 // 3. 上升速度过小 if ( this.particles.position.distanceTo(this.targetPosition) < this.explosionTolerance || Math.abs(this.particles.position.x) > 50 || Math.abs(this.particles.position.z) > 50 || this.particles.position.y > 25 || // 增加最大高度 this.velocity.y < 0.3 ) { this.explode(); } } else { let allDead = true; const positions = this.particles.geometry.attributes.position.array; for (let i = 0; i < this.particleCount; i++) { // 根据速度更新粒子位置 positions[i * 3 + 0] += this.velocities[i].x * 0.016; positions[i * 3 + 1] += this.velocities[i].y * 0.016; positions[i * 3 + 2] += this.velocities[i].z * 0.016; // 更新速度(重力作用) this.velocities[i].y -= 0.02; // 减小重力影响 // 添加阻力 this.velocities[i].multiplyScalar(this.dragCoefficient); // 应用空气阻力公式 // 检查粒子是否仍在可见范围内 const speed = this.velocities[i].length(); if (speed > 0.1) { // 当粒子速度足够小时才消失 allDead = false; } } this.particles.geometry.attributes.position.needsUpdate = true; this.dead = allDead; // 粒子逐渐变小 const sizes = this.particles.geometry.attributes.size.array; for (let i = 0; i < this.particleCount; i++) { sizes[i] *= 0.99; } this.particles.geometry.attributes.size.needsUpdate = true; } // 添加轨迹粒子自动清理 if(this.trailParticles && this.trailParticles.children.length > 0) { this.trailParticles.children.forEach(particle => { particle.material.opacity *= 0.9; if(particle.material.opacity < 0.05) { this.trailParticles.remove(particle); } }); } } } const fireworks = []; let lastFireworkTime = 0; // 添加一个变量来记录上次创建烟花的时间 // 性能优化 const performanceConfig = { maxFireworks: window.innerWidth < 768 ? 2 : 4, // 减少同时存在的烟花数量 particleMultiplier: window.devicePixelRatio > 1 ? 0.7 : 1, bloomStrength: window.devicePixelRatio > 1 ? 0.8 : 1.0 // 降低泛光强度 }; // 动画循环 function animate() { requestAnimationFrame(animate); const currentTime = Date.now(); // 移除最大烟花数量限制,改为固定时间间隔创建新烟花 if (currentTime - lastFireworkTime > 400) { // 每800毫秒发射一个新烟花 // 每次发射2-30个烟花 const count = Math.floor(Math.random() * 2) + 30; for (let i = 0; i < count; i++) { fireworks.push(new Firework()); } lastFireworkTime = currentTime; } // 更新和清理烟花 for (let i = fireworks.length - 1; i >= 0; i--) { fireworks[i].update(); if (fireworks[i].dead) { scene.remove(fireworks[i].particles); fireworks.splice(i, 1); } } composer.render(); } // 窗口大小调整 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }); animate(); </script>
</body> </html> 对比与真实的烟花秀,当前代码实现的效果并不真实。请优化一下。

思考

Enhancing realism

OK, let’s think. The user aims to refine the 3D fireworks effect with ThreeJS, focusing on more realistic speed, random patterns, and variations in explosion shapes, intensities, and timings.

Setting off

Setting off fireworks involves waiting for rockets to rise to a height or ping randomly, then igniting an explosion with a sphere-shaped spread. Sub-explosions and random color patterns enhance the effect.

Tweaking the intervals

Increasing the interval to between 2000 and 3000 ms or using random spots helps! Adding a slight horizontal velocity makes the rocket's trajectory more realistic.

Adding finishing touches

I'm integrating a bloom effect and color transitions to capture the fading sparks from launch to explosion. Advanced particle systems on the GPU will elevate the overall visual appeal.

Thought about improving firework realism,持续 17 秒

回答

下面给出一段相对更贴近真实烟花效果的示例代码,重点在于:

  1. 真实烟花节奏

    • 真实烟花不会像机枪一样连续发射,一般会有一段间隔,并且间隔还带有一定随机性。
    • 在同一个大烟花秀里,也会有时“集中爆发”,有时零星绽放。
  2. 更自然的上升轨迹

    • 真实烟花在上升时速度并非恒定,而且大多会带有轻微弧度或者抖动,表现出空气阻力和风的影响。
    • 上升末期速度会衰减,几乎垂直后开始爆炸,而非瞬间到达指定点爆炸。
  3. 爆炸形状与层次

    • 真实烟花往往不是一次爆炸就结束,可能会有多层次的爆炸,或呈现球形、环形、星形、柳絮状等。
    • 用不同的算法去生成不同的爆炸分布,可以让场景更加丰富。
  4. 渐变、闪烁与重力衰减

    • 烟花星点(颗粒)会随着时间快速衰减亮度和大小,有时带有闪烁效果。
    • 重力会让爆炸后的颗粒向下坠落,并逐渐熄灭。
  5. 发射尾迹

    • 火箭上升过程通常带有火光尾迹,颜色往往较单一(偏白黄),亮度随时间衰减;在爆炸时才会呈现主色。

下面这段示例代码针对这些方面都做了一些改进,你可以根据需要再做进一步微调(如减少粒子数量、减小 bloom 强度、改变爆炸形状等):

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>更真实的3D烟花</title> <style> body { margin: 0; overflow: hidden; background: #000; } canvas { display: block; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/LuminosityHighPassShader.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/CopyShader.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/EffectComposer.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/BloomPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/ShaderPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/RenderPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/UnrealBloomPass.js"></script> <script> /** * ------------------------------------------- * 场景、摄像机、渲染器和后期处理初始化 * ------------------------------------------- */ const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 2000 ); camera.position.set(0, 20, 100); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 后期处理 const composer = new THREE.EffectComposer(renderer); const renderPass = new THREE.RenderPass(scene, camera); composer.addPass(renderPass); const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, // 适当控制爆炸的炫光强度 1.0, 0.85 ); composer.addPass(bloomPass); /** * ------------------------------------------- * 烟花类:包括上升(带尾迹)和爆炸 * ------------------------------------------- */ class Firework { constructor() { this.isExploded = false; this.dead = false; // 烟花上升阶段:创造一个简单的“火箭”点 this.rocketGeo = new THREE.SphereGeometry(0.4, 8, 8); this.rocketMat = new THREE.MeshBasicMaterial({ color: 0xffffff }); this.rocket = new THREE.Mesh(this.rocketGeo, this.rocketMat); // 初始位置在底部(随机X和Z),Y稍低于视线 this.rocket.position.set( (Math.random() - 0.5) * 60, -30, (Math.random() - 0.5) * 60 ); scene.add(this.rocket); // 目标爆炸高度 this.targetY = 30 + Math.random() * 20; // 火箭上升速度(略带横向抖动,模拟风) this.velocity = new THREE.Vector3( (Math.random() - 0.5) * 0.2, // 偏小的水平抖动 0.8 + Math.random() * 0.5, // 主向上速度 (Math.random() - 0.5) * 0.2 ); // 尾迹粒子,用 Points 来渲染 // --------------------------------------- const trailCount = 40; const positions = new Float32Array(trailCount * 3); const colors = new Float32Array(trailCount * 3); for (let i = 0; i < trailCount; i++) { // 先都放在 (0,0,0),等会儿动态更新 positions[i * 3] = 0; positions[i * 3 + 1] = 0; positions[i * 3 + 2] = 0; // 尾迹大多是偏白偏黄 const color = new THREE.Color(1, 0.8, 0.4); colors[i * 3] = color.r; colors[i * 3 + 1] = color.g; colors[i * 3 + 2] = color.b; } const trailGeometry = new THREE.BufferGeometry(); trailGeometry.setAttribute( "position", new THREE.BufferAttribute(positions, 3) ); trailGeometry.setAttribute( "color", new THREE.BufferAttribute(colors, 3) ); const trailMaterial = new THREE.PointsMaterial({ size: 0.8, vertexColors: true, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true }); this.trail = new THREE.Points(trailGeometry, trailMaterial); scene.add(this.trail); // 用来存储每个尾迹粒子坐标的队列 this.trailPositions = []; this.trailLength = trailCount; // 爆炸阶段:预先创建爆炸粒子系统,但先隐藏 // --------------------------------------- this.explosionCount = 120 + Math.floor(Math.random() * 60); const ePositions = new Float32Array(this.explosionCount * 3); const eColors = new Float32Array(this.explosionCount * 3); const eSizes = new Float32Array(this.explosionCount); this.velocities = []; // 随机主色 this.fireworkColor = new THREE.Color().setHSL( Math.random(), 0.7 + Math.random() * 0.3, 0.5 ); for (let i = 0; i < this.explosionCount; i++) { // 初始都在 (0,0,0),稍后爆炸时再加上爆心坐标 ePositions[i * 3] = 0; ePositions[i * 3 + 1] = 0; ePositions[i * 3 + 2] = 0; // 爆炸的粒子颜色会在主色基础上稍有随机 const colorVariation = this.fireworkColor.clone().offsetHSL( (Math.random() - 0.5) * 0.1, (Math.random() - 0.5) * 0.2, (Math.random() - 0.5) * 0.1 ); eColors[i * 3] = colorVariation.r; eColors[i * 3 + 1] = colorVariation.g; eColors[i * 3 + 2] = colorVariation.b; // 粒子大小 eSizes[i] = 12 + Math.random() * 8; // 粒子速度(爆炸形态:均匀球面 + 少量的扁平或环形变化) // 先来个球面分布 const theta = Math.random() * Math.PI * 2; const phi = Math.random() * Math.PI; const r = 15 + Math.random() * 10; // 爆炸半径 // 也可以额外判断,做一些环形或“柳絮状”分布 let sx = r * Math.sin(phi) * Math.cos(theta); let sy = r * Math.cos(phi); let sz = r * Math.sin(phi) * Math.sin(theta); // 加一点“扁平”的概率:让y方向更扁 if (Math.random() < 0.2) { sy *= 0.4; } // 记录速度向量 this.velocities.push(new THREE.Vector3(sx, sy, sz)); } const explosionGeometry = new THREE.BufferGeometry(); explosionGeometry.setAttribute( "position", new THREE.BufferAttribute(ePositions, 3) ); explosionGeometry.setAttribute( "color", new THREE.BufferAttribute(eColors, 3) ); explosionGeometry.setAttribute( "size", new THREE.BufferAttribute(eSizes, 1) ); // 使用 ShaderMaterial 来控制粒子大小衰减和纹理 const explosionMaterial = new THREE.ShaderMaterial({ uniforms: { pointTexture: { value: new THREE.TextureLoader().load( "https://threejs.org/examples/textures/sprites/disc.png" ) }, uTime: { value: 0 }, }, vertexShader: ` attribute float size; varying vec3 vColor; uniform float uTime; void main() { vColor = color; // 让粒子随时间变小 // 爆炸后 t 越大,粒子越小 float t = uTime * 0.6; float s = max(size - t*18.0, 0.0); vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = s * (300.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` varying vec3 vColor; uniform sampler2D pointTexture; void main() { gl_FragColor = vec4(vColor, 1.0); gl_FragColor *= texture2D(pointTexture, gl_PointCoord); // 让边缘更柔和 if (gl_FragColor.a < 0.1) discard; } `, blending: THREE.AdditiveBlending, depthTest: false, transparent: true, vertexColors: true }); this.explosion = new THREE.Points(explosionGeometry, explosionMaterial); this.explosion.visible = false; // 初始隐藏,等到爆炸时再显示 scene.add(this.explosion); // 时间 this.clock = new THREE.Clock(); this.timeOffset = Math.random() * 1000; // 让每朵烟花有点时间差 } update(delta) { if (!this.isExploded) { // 上升阶段 this.rocket.position.add(this.velocity); // 模拟空气阻力和重力对火箭的影响 this.velocity.y -= 0.005; this.velocity.multiplyScalar(0.995); // 微弱衰减 // 上升尾迹:记录历史位置,用于绘制 this.recordTrail(); if (this.rocket.position.y >= this.targetY || this.velocity.y < 0) { // 到达预定高度或速度衰减后爆炸 this.explode(); } } else { // 爆炸阶段 const time = this.clock.getElapsedTime() + this.timeOffset; this.explosion.material.uniforms.uTime.value = time; // 让粒子向外扩散 + 重力 const positions = this.explosion.geometry.attributes.position.array; for (let i = 0; i < this.velocities.length; i++) { const idx = i * 3; // velocities[i] 本身是爆心坐标系的“初速度”, // 每帧要叠加到 positions 上 positions[idx] += this.velocities[i].x * delta; positions[idx + 1] += this.velocities[i].y * delta; positions[idx + 2] += this.velocities[i].z * delta; // 模拟重力 this.velocities[i].y -= 0.08 * delta; // 空气阻力 this.velocities[i].multiplyScalar(0.98); } this.explosion.geometry.attributes.position.needsUpdate = true; // 判断是否所有粒子已经衰减得几乎不可见,则回收 if (time > 3.5) { // 粒子大约 3-4 秒后就可以消失 this.dead = true; } } } recordTrail() { // 只记录固定长度的尾迹 this.trailPositions.push(this.rocket.position.clone()); if (this.trailPositions.length > this.trailLength) { this.trailPositions.shift(); // 移除最早的 } // 更新 trail 的 geometry const positions = this.trail.geometry.attributes.position.array; for (let i = 0; i < this.trailPositions.length; i++) { const p = this.trailPositions[i]; positions[i * 3] = p.x - this.rocket.position.x; positions[i * 3 + 1] = p.y - this.rocket.position.y; positions[i * 3 + 2] = p.z - this.rocket.position.z; } // 若不足 trailLength,则其余位置填充在末尾 for (let i = this.trailPositions.length; i < this.trailLength; i++) { positions[i * 3] = 9999; // 放到很远处,看不见就行 positions[i * 3 + 1] = 9999; positions[i * 3 + 2] = 9999; } this.trail.geometry.attributes.position.needsUpdate = true; // 随时间衰减尾迹整体不透明度 this.trail.material.opacity = 0.7 * (this.velocity.y + 1.0); } explode() { // 移除火箭模型 scene.remove(this.rocket); scene.remove(this.trail); // 显示爆炸粒子 this.explosion.visible = true; // 将爆炸粒子整体移到火箭消失的位置 this.explosion.position.copy(this.rocket.position); this.isExploded = true; this.clock.start(); } } /** * ------------------------------------------- * 全局控制:每隔一段时间随机发射烟花 * ------------------------------------------- */ const fireworks = []; let lastSpawnTime = 0; function spawnFireworks(time) { // 真实烟花:发射频率不会太快,可以在0.5秒到3秒之间随机 const interval = 1000 + Math.random() * 2000; if (time - lastSpawnTime > interval) { // 一次可以发射1~3个 const count = 1 + Math.floor(Math.random() * 3); for (let i = 0; i < count; i++) { fireworks.push(new Firework()); } lastSpawnTime = time; } } /** * ------------------------------------------- * 动画循环 * ------------------------------------------- */ const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); // 距离上次帧的时间 // 检查是否需要新发射烟花 const currentTime = performance.now(); spawnFireworks(currentTime); // 更新每个烟花 for (let i = fireworks.length - 1; i >= 0; i--) { fireworks[i].update(delta); if (fireworks[i].dead) { // 移除 scene.remove(fireworks[i].explosion); fireworks.splice(i, 1); } } composer.render(); } animate(); /** * ------------------------------------------- * 窗口自适应 * ------------------------------------------- */ window.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }); </script> </body> </html>

主要改进思路说明

  1. 上升阶段与爆炸阶段分离

    • 使用一个简单的球体或 Points 充当“火箭”,负责上升轨迹及尾迹;
    • 一旦到达目标高度或速度衰减至一定程度,就移除火箭本体并显示“爆炸”粒子。
  2. 火箭尾迹

    • 用一个 Points 管理尾迹粒子,记录火箭过去的若干位置,并让这些粒子保持在相对于火箭的偏移量上,看起来就像一条拖尾。
    • 尾迹颜色一般偏白黄,可稍微加透明度衰减,看起来更像真实火光。
  3. 爆炸形状

    • 用随机球面分布(或加入一些倾斜、扁平、环形等变体)生成大量粒子的初速度。
    • 让粒子在“爆心”位置作球形扩散,并带有重力下坠和空气阻力衰减。
    • 爆炸持续时间常常只有几秒,过后就逐渐消失。
  4. 随机发射频率

    • 给一次发射设定一定随机间隔,这样既能模拟真实烟花的节奏,也防止屏幕上烟花密集到失真。
    • 有时可以一次发射多枚(1~3枚或更多),就像烟花秀高峰时会同时升起多个火箭。
  5. 后期泛光 (Bloom) 调整

    • 使用较适中的 bloomStrengththresholdradius 参数,既能让烟花看起来炫目,又不至于过曝。
    • 设备性能不好时,可以进一步降低粒子数量和泛光强度。

通过以上改动,可以让整体观感与真实的烟花秀更为接近。你还可以进一步对色彩、粒子数量、速度衰减系数、爆炸形状等作深度微调,或利用音频(模拟爆炸声、尾迹声)等增强沉浸感。祝你创作愉快!

分享这个问答