实现一个烟花效果
1. 首先创建一个烟花类,有烟花上升的效果,烟花爆炸的两种效果(爆炸型和球型)
2. 创建线的属性方法,因为上升效果和爆炸效果都会用到
3. 上升效果为了达到那种螺旋上升的效果可以通过sin函数实现一个点的偏移量
4. 爆炸效果则是将随机生成多条半径不同的线
5. 球形效果则是将规则的点和不规则的点结合起来实现的
6. 每个烟花爆炸消失后重新生成下一个烟花
7. 为了色彩值更弄,通过 EffectComposer\UnrealBloomPass添加辉光效果
/** * 添加烟花 */ function addFireworks() { // 烟花类 class Fireworks { constructor(props = {}) { // 烟花高度 this.height = props.height || Math.ceil(Math.random() * 8 + 5); // 烟花颜色 this.color = props.color || this.getRandomColor(); // 烟花的起始位置 this.startX = props.position && props.position[0] || Math.random() * 20 - 10; this.startY = -5; this.path = this.createPath(); // 烟花绽放结束函数 this.fireEnd = null; this.fire(); } /** * 生成随机颜色值 * @returns THREE.Color */ getRandomColor() { const [minHue, maxHue, minSaturation, maxSaturation, minLightness, maxLightness] = [0, 1, 0, 1, 0, 1]; // 生成随机色调 const hue = Math.random() * (maxHue - minHue) + minHue; // 生成随机饱和度 const saturation = Math.random() * (maxSaturation - minSaturation) + minSaturation; // 生成随机亮度 const lightness = Math.random() * (maxLightness - minLightness) + minLightness; // 使用 HSL 颜色空间创建颜色 const color = new THREE.Color(); color.setRGB(hue, saturation, lightness); return color; } /** * 生成烟花上升的轨迹,为了模拟烟花上升左右摆动效果,增加了sin函数插入部分偏移点 */ createPath() { const widthBeta = 0.01; // 烟花轨迹摆动的范围 const paths = []; // 轨迹点集 const nums = 60; // 轨迹点个数 const heigthtBeta = this.height / nums; // 烟花上升的幅度 for (let i = 0; i < nums; i++) { paths.push(new THREE.Vector3(this.startX + Math.sin(heigthtBeta * i * Math.PI * 14) * widthBeta, this.startY + heigthtBeta * i, 0)); } return paths; } /** * 爆炸效果 */ explode() { const random = Math.floor(Math.random() * 2); if (random === 0) { this.radialEffect(); } else { this.sphereEffect(); } } /** * 球形效果,为了模拟实际的点燃效果,首先生成一些规则的球型点,然后在球面上插入部分随机点 */ sphereEffect() { const vertex = ` attribute float aScale; attribute vec3 aRandom; uniform float uTime; uniform float uSize; void main() { vec4 modelPosition = modelMatrix * vec4(position, 1.0); modelPosition.xyz += 5.0*uTime; gl_Position = projectionMatrix * (viewMatrix * modelPosition); gl_PointSize = 3.0; } `; const frag = ` uniform vec3 uColor; uniform float uOpacity; void main() { float distanceTo = distance(gl_PointCoord , vec2(0.5)); // 必须有这段代码,不然辉光效果不会生效 if (distanceTo > 0.5) { discard; } float str = distanceTo * 2.0; str = 1.0 - str; str = pow(str,1.5); gl_FragColor = vec4(uColor,str * uOpacity); } `; const positions = []; const [centerX, centerY, centerZ] = [this.startX, this.startY + this.height, 0]; const radius = 2; // 生成规则的点 const widthSegments = 24; const heightSegments = 24; for (let i = 0; i < widthSegments; i++) { for (let j = 0; j < heightSegments; j++) { const y = radius * Math.sin(j / heightSegments * Math.PI * 2); const innerRadius = radius * Math.cos(j / heightSegments * Math.PI * 2); const x = innerRadius * Math.cos(i / widthSegments * Math.PI * 2); const z = innerRadius * Math.sin(i / widthSegments * Math.PI * 2); positions.push(centerX + x, centerY + y, centerZ + z); } } // 生成随机的点 const num = 360; for (let i = 0; i < num; i++) { const randomHAngle = Math.random() * Math.PI * 2 - Math.PI; const randomWAngle = Math.random() * Math.PI * 1; const y = radius * Math.sin(randomHAngle); const innerRadius = radius * Math.cos(randomHAngle); const x = innerRadius * Math.cos(randomWAngle); const z = innerRadius * Math.sin(randomWAngle); positions.push(centerX + x, centerY + y, centerZ + z); } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)); const material = new THREE.ShaderMaterial({ vertexShader: vertex, fragmentShader: frag, side: THREE.DoubleSide, uniforms: { uColor: { value: this.color }, uTime: { value: 0.01 }, uSize: { value: 5.0 }, uOpacity: { value: 1.0 } }, transparent: true, depthWrite: false // 解决透明的点和点相互压盖时没有透明效果的问题 }); const mesh = new THREE.Points(geometry, material); scene.add(mesh); // 添加定时函数,达到透明度逐渐降低,直至消失的效果 let i = 0; const interObj = setInterval(() => { material.uniforms.uOpacity.value -= 0.05; if (material.uniforms.uOpacity.value <= 0) { clearInterval(interObj); scene.remove(mesh); if (this.fireEnd) { this.fireEnd(); } } i++; }, 100) } /** * 射线效果,随机生成不同的线长实现 */ radialEffect() { function getRandom(src) { const radius = 4; return src + Math.random() * radius - radius / 2; } const nums = 180; const [centerX, centerY, centerZ] = [this.startX, this.startY + this.height, 0]; const positions = []; for (let i = 0; i < nums; i++) { positions.push( [ new THREE.Vector3(centerX, centerY, centerZ), new THREE.Vector3(getRandom(centerX), getRandom(centerY), getRandom(centerZ)), ] ); } positions.forEach((e, i) => { this.createLine(e, 3, 0.55, 1.5, null, 100, i === 0 ? () => { if (this.fireEnd) { this.fireEnd(); } } : null); }) } /** * 生成一条线,有箭头效果 * @param {*} points 线的关键点,在函数内部会根据这些点通过平滑函数插入多个点 * @param {*} lineWidth 线宽 * @param {*} lineLength 线长 * @param {*} uSpeed 线的移动速度 * @param {*} color 线颜色 * @param {*} pointNum 插入的点的个数 * @param {*} endCallback 线移动结束的回调函数 */ createLine(points, lineWidth, lineLength, uSpeed, color, pointNum, endCallback) { const vertex = ` attribute float aIndex; uniform float uTime; uniform float uNum; // 线上点个数 uniform float uWidth; // 线宽 uniform float uLength; // 线宽 uniform float uSpeed; // 飞线速度 varying float vIndex; // 内部点下标 void main() { vec4 viewPosition = viewMatrix * modelMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * viewPosition; vIndex = aIndex; // 通过时间点的增加作为点移动的下标,从末端的第一个点开始,已num点为一个轮回,往复执行运动 float num = uNum - mod(floor(uTime * uSpeed), uNum); // 只绘制部分点,多余的不绘制 if (aIndex + num >= uNum) { float size = (mod(aIndex + num, uNum) * (1.0 / uLength)) / uNum * uWidth; gl_PointSize = size; } } `; const frag = ` varying float vIndex; uniform float uTime; uniform float uLength; // 线宽 uniform float uNum; uniform float uSpeed; uniform vec3 uSColor; uniform vec3 uEColor; void main() { // 默认绘制的点是方形的,通过舍弃可以绘制成圆形 float distance = length(gl_PointCoord - vec2(0.5, 0.5)); if (distance > 0.5) { // discard; float glow = 1.0 - smoothstep(0.2, 0.5, distance); gl_FragColor = vec4(1.0, 1.0, 1.0, glow); } else { float num = uNum - mod(floor(uTime * uSpeed), uNum); // 根据点的下标计算渐变色值 vec3 color = mix(uSColor, uEColor, (num + vIndex - uNum) / uNum); // 越靠近末端透明度越大 float opacity = ((num + vIndex - uNum)) / uNum; // 根据长度计算显示点的个数,多余的透明显示 if (vIndex + num >= uNum && vIndex + num <= uNum * (1.0 + uLength)) { gl_FragColor = vec4(color, opacity); } else { gl_FragColor = vec4(color, 0); } } } `; const nums = pointNum || 500; const curve = new THREE.CatmullRomCurve3(points); const curveArr = curve.getPoints(nums); const flatArr = curveArr.map(e => e.toArray()); const lastArr = flatArr.flat(); const indexArr = [...Array(nums + 1).keys()]; const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(lastArr), 3)); geometry.setAttribute('aIndex', new THREE.BufferAttribute(new Float32Array(indexArr), 1)); // 创建一根曲线 const uniform = { uTime: { value: 0 }, uSColor: { value: color || new THREE.Color(this.color) }, uEColor: { value: new THREE.Color(0x000000) }, uWidth: { value: lineWidth || 6.0 }, uNum: { value: nums }, uSpeed: { value: uSpeed || 2 }, uLength: { value: lineLength || 0.25 } } const material = new THREE.ShaderMaterial({ vertexShader: vertex, fragmentShader: frag, side: THREE.DoubleSide, uniforms: uniform, transparent: true, depthWrite: false }); const mesh = new THREE.Points(geometry, material); scene.add(mesh); let i = 0; const interObj = setInterval(() => { material.uniforms.uTime.value = i; i++; if (uniform.uSpeed.value * i >= nums) { scene.remove(mesh); clearInterval(interObj); if (endCallback) { endCallback(); } } }, 20) } /** * 点燃烟花 */ fire() { this.createLine(this.path, 8, 0.45, 2, null, null, () => { this.explode(); }); } /** * 销毁 */ destory() { clearInterval(this.interObj); } } function callback() { const fireObj = new Fireworks(); fireObj.fireEnd = callback; } for (let i = 0; i < 5; i++) { setTimeout(() => { const fireObj = new Fireworks(); fireObj.fireEnd = callback; }, 500 * i) } }
/** * 添加相机等基础功能 */ function addEnvir(lightFlag = true, axFlag = true, gridFlag = false) { // 初始化相机 camera = new THREE.PerspectiveCamera(100, wWidth / wHeight, 0.01, 3000); camera.position.set(0, 10, 0); camera.lookAt(0, 0, 0); // 创建灯光 // 创建环境光 const ambientLight = new THREE.AmbientLight(0xf0f0f0, 1.0); ambientLight.position.set(0,0,0); scene.add(ambientLight); if (lightFlag) { // 创建点光源 const pointLight = new THREE.PointLight(0xffffff, 1); pointLight.decay = 0.0; pointLight.position.set(200, 200, 50); scene.add(pointLight); } // 添加辅助坐标系 if (axFlag) { const axesHelper = new THREE.AxesHelper(150); scene.add(axesHelper); } // 添加网格坐标 if (gridFlag) { const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x004444); scene.add(gridHelper); } // 创建渲染器 renderer = new THREE.WebGLRenderer({ antialias:true, logarithmicDepthBuffer: true }); renderer.setClearColor(0x000000, 1); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(wWidth, wHeight); //设置three.js渲染区域的尺寸(像素px) renderer.outputEncoding = THREE.sRGBEncoding; renderer.render(scene, camera); //执行渲染操作 // 创建后处理效果合成器 composer = new EffectComposer(renderer); // 添加渲染通道 const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); bloomPass.threshold = 0; bloomPass.strength = 3; bloomPass.radius = 0.5; composer.addPass(bloomPass); controls = new OrbitControls(camera, renderer.domElement); // 设置拖动范围 // controls.minPolarAngle = - Math.PI / 2; // controls.maxPolarAngle = Math.PI / 2 - Math.PI / 360; controls.addEventListener('change', () => { renderer.render(scene, camera); }) gui = new dat.GUI(); const clock = new THREE.Clock(); function render() { renderer.render(scene, camera); requestAnimationFrame(render); const elapsedTime = clock.getElapsedTime(); if (material && material.uniforms && material.uniforms.uTime) { material.uniforms.uTime.value = elapsedTime / 2; // material.uniforms.noiseScale.value = 10.0 + Math.sin(Date.now() * 0.001) * 5.0; } if (composer) { composer.render(); } } render(); document.getElementById('webgl').appendChild(renderer.domElement); }