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); }