学习threejs,使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、OutputPass渲染通道),实现迷人粒子系统
本文详细介绍如何基于threejs在三维场景中使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、OutputPass渲染通道),实现迷人粒子系统,亲测可用。希望能帮助到您。一起学习,加油!加油!
👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
一、🍀前言
本文详细介绍如何基于threejs在三维场景中使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、OutputPass渲染通道),实现迷人粒子系统,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️THREE.EffectComposer 后期处理
THREE.EffectComposer 用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。
1.1.1 ☘️代码示例
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 初始化 composer
const composer = new EffectComposer(renderer);
// 创建 RenderPass 并添加到 composer
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 添加其他后期处理通道(如模糊)
// composer.addPass(blurPass);
// 在动画循环中渲染
function animate() {
composer.render();
requestAnimationFrame(animate);
}
1.1.2 ☘️构造函数
EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget )
renderer – 用于渲染场景的渲染器。
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
1.1.3 ☘️属性
.passes : Array
一个用于表示后期处理过程链(包含顺序)的数组。
渲染通道:
BloomPass 该通道会使得明亮区域参入较暗的区域。模拟相机照到过多亮光的情形
DotScreenPass 将一层黑点贴到代表原始图片的屏幕上
FilmPass 通过扫描线和失真模拟电视屏幕
MaskPass 在当前图片上贴一层掩膜,后续通道只会影响被贴的区域
RenderPass 该通道在指定的场景和相机的基础上渲染出一个新的场景
SavePass 执行该通道时,它会将当前渲染步骤的结果复制一份,方便后面使用。这个通道实际应用中作用不大;
ShaderPass 使用该通道你可以传入一个自定义的着色器,用来生成高级的、自定义的后期处理通道
TexturePass 该通道可以将效果组合器的当前状态保存为一个纹理,然后可以在其他EffectCoposer对象中将该纹理作为输入参数
.readBuffer : WebGLRenderTarget
内部读缓冲区的引用。过程一般从该缓冲区读取先前的渲染结果。
.renderer : WebGLRenderer
内部渲染器的引用。
.renderToScreen : Boolean
最终过程是否被渲染到屏幕(默认帧缓冲区)。
.writeBuffer : WebGLRenderTarget
内部写缓冲区的引用。过程常将它们的渲染结果写入该缓冲区。
1.1.4 ☘️方法
.addPass ( pass : Pass ) : undefined
pass – 将被添加到过程链的过程
将传入的过程添加到过程链。
.dispose () : undefined
释放此实例分配的 GPU 相关资源。每当您的应用程序不再使用此实例时调用此方法。
.insertPass ( pass : Pass, index : Integer ) : undefined
pass – 将被插入到过程链的过程。
index – 定义过程链中过程应插入的位置。
将传入的过程插入到过程链中所给定的索引处。
.isLastEnabledPass ( passIndex : Integer ) : Boolean
passIndex – 被用于检查的过程
如果给定索引的过程在过程链中是最后一个启用的过程,则返回true。 由EffectComposer所使用,来决定哪一个过程应当被渲染到屏幕上。
.removePass ( pass : Pass ) : undefined
pass – 要从传递链中删除的传递。
从传递链中删除给定的传递。
.render ( deltaTime : Float ) : undefined
deltaTime – 增量时间值。
执行所有启用的后期处理过程,来产生最终的帧,
.reset ( renderTarget : WebGLRenderTarget ) : undefined
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
重置所有EffectComposer的内部状态。
.setPixelRatio ( pixelRatio : Float ) : undefined
pixelRatio – 设备像素比
设置设备的像素比。该值通常被用于HiDPI设备,以阻止模糊的输出。 因此,该方法语义类似于WebGLRenderer.setPixelRatio()。
.setSize ( width : Integer, height : Integer ) : undefined
width – EffectComposer的宽度。
height – EffectComposer的高度。
考虑设备像素比,重新设置内部渲染缓冲和过程的大小为(width, height)。 因此,该方法语义类似于WebGLRenderer.setSize()。
.swapBuffers () : undefined
交换内部的读/写缓冲。
1.2 ☘️THREE.RenderPass
THREE.RenderPass用于将场景渲染到中间缓冲区,为后续的后期处理效果(如模糊、色调调整等)提供基础。
1.2.1 ☘️构造函数
RenderPass(scene, camera, overrideMaterial, clearColor, clearAlpha)
- scene THREE.Scene 要渲染的 Three.js 场景对象。
- camera THREE.Camera 场景对应的相机(如 PerspectiveCamera)。
- overrideMaterial THREE.Material (可选) 覆盖场景中所有物体的材质(默认 null)。
- clearColor THREE.Color (可选) 渲染前清除画布的颜色(默认不主动清除)。
- clearAlpha number (可选) 清除画布的透明度(默认 0)。
1.2.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可跳过渲染。
.clear:boolean
渲染前是否清除画布(默认 true)。若需叠加多个 RenderPass,可设为 false。
.needsSwap:boolean
是否需要在渲染后交换缓冲区(通常保持默认 false)。
1.2.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
1.3 ☘️THREE.UnrealBloomPass
UnrealBloomPass 是 Three.js 中实现高质量泛光效果的后期处理通道,通过模拟类似 Unreal Engine 的泛光效果,为场景中的明亮区域添加柔和的光晕,提升视觉表现力。
1.3.1 ☘️构造函数
new UnrealBloomPass(resolution, strength, radius, threshold)
- resolution (Vector2): 泛光效果应用的场景分辨率,需与画布尺寸一致。
示例:new THREE.Vector2(window.innerWidth, window.innerHeight) - strength (Number): 泛光强度,默认值 1.0。值越大,光晕越明显。
- radius (Number): 模糊半径,默认值 0.4。值越大,光晕扩散范围越广。
- threshold (Number): 泛光阈值,默认值 0.85。仅对亮度高于此值的区域生效。
1.3.2 ☘️方法
- renderToScreen: 是否直接渲染到屏幕,默认为 false(需通过 EffectComposer 管理)。
- clearColor: 设置背景清除颜色,默认为透明。
1.4 ☘️THREE.OutputPass
OutputPass 是 Three.js 后期处理(Post-Processing)中的一个通道(Pass),用于控制最终渲染输出的颜色空间、色调映射(Tone Mapping)和抗锯齿等效果。它通常作为 EffectComposer 的最后一个通道,负责将处理后的图像输出到屏幕。
1.4.1 ☘️构造函数
new OutputPass(resolution)
resolution (Vector2): 可选参数,指定输出分辨率。默认值为 null,自动匹配画布尺寸。
1.4.2 ☘️属性
.output:WebGLRenderTarget
类型: WebGLRenderTarget
描述: 存储最终渲染结果的渲染目标对象。
.uniforms:Object
类型: Object
描述: 包含着色器统一变量(Uniforms)的对象,用于控制输出效果。常用属性:
toneMappingExposure (Number): 色调映射曝光值,默认 1.0。
toneMappingType (Number): 色调映射算法类型,默认 NoToneMapping。
.needsSwap:boolean
类型: Boolean
描述: 是否需要交换帧缓冲区,通常设为 true 以确保输出到屏幕。
1.4.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
.render(renderer, writeBuffer, readBuffer)
功能: 执行渲染操作(通常由 EffectComposer 自动调用)。
二、🍀使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、OutputPass渲染通道),实现迷人粒子系统
1. ☘️实现思路
本例子使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、OutputPass渲染通道)、THREE.BufferGeometry几何体、THREE.Points点对象等,实现迷人粒子系统。具体代码参考下面代码样例。
2. ☘️代码样例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Particle Shapes</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#000;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif}
#container{position:fixed;inset:0;background:linear-gradient(180deg,#000510 0%,#00081a 50%,#000c25 100%)}
.glow{position:fixed;inset:0;pointer-events:none;background:radial-gradient(circle at 50% 50%,rgba(0,80,180,0.02),rgba(20,0,100,0.03) 50%,transparent 75%);mix-blend-mode:screen;opacity:0.4}
#patternName{position:fixed;top:20px;left:50%;transform:translateX(-50%);
color:#fff;font-weight:300;letter-spacing:1px;font-size:18px;pointer-events:none;z-index:100;
opacity:0;transition:0.5s;text-align:center;background:rgba(0,0,0,0.5);padding:10px 20px;
border-radius:25px;text-shadow:0 0 5px #000;border:1px solid rgba(100,150,255,0.2);
white-space:nowrap;max-width:90%;overflow:hidden;text-overflow:ellipsis}
#ui-container{position:fixed;bottom:20px;left:0;width:100%;padding:0 20px;
display:flex;justify-content:space-between;align-items:flex-end;z-index:1000;pointer-events:none}
#shapeButton{padding:12px 24px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);
border-radius:12px;color:rgba(255,255,255,0.9);font-size:14px;letter-spacing:0.5px;cursor:pointer;
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 4px 12px rgba(0,0,0,0.2);
transition:0.3s;display:flex;align-items:center;gap:8px;pointer-events:auto}
#shapeButton:hover{background:rgba(255,255,255,0.15);border-color:rgba(255,255,255,0.3);transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,0.25)}
#shapeButton:active{transform:translateY(1px);box-shadow:0 2px 8px rgba(0,0,0,0.2)}
#shapeButton svg{width:18px;height:18px;fill:none;stroke:rgba(255,255,255,0.9);stroke-width:2}
#controlsPanel{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);
border-radius:12px;padding:15px;color:rgba(255,255,255,0.9);font-size:14px;
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 4px 12px rgba(0,0,0,0.2);
pointer-events:auto}
.control-option{margin-bottom:10px;display:flex;align-items:center;gap:10px}
.control-option:last-child{margin-bottom:0}
.toggle-switch{position:relative;display:inline-block;width:46px;height:24px}
.toggle-switch input{opacity:0;width:0;height:0}
.toggle-slider{position:absolute;inset:0;cursor:pointer;background:rgba(100,100,100,0.4);border-radius:24px;transition:0.3s}
.toggle-slider:before{content:"";position:absolute;width:18px;height:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:0.3s}
input:checked+.toggle-slider{background:rgba(100,150,255,0.6)}
input:checked+.toggle-slider:before{transform:translateX(22px)}
@media(max-width:768px){
#ui-container{flex-direction:column;align-items:center;gap:10px;bottom:15px}
#controlsPanel{flex-direction:row;justify-content:space-around;width:100%;padding:10px 15px;margin-bottom:10px}
.control-option{margin-right:10px}.control-option:last-child{margin-right:0}
#patternName{font-size:16px;padding:8px 16px;top:15px}
}
@media(max-width:480px){
#shapeButton{padding:10px 16px;font-size:13px;width:100%;justify-content:center}
#controlsPanel{padding:8px 12px}
.toggle-switch{width:40px;height:22px}
.toggle-slider:before{width:16px;height:16px}
input:checked+.toggle-slider:before{transform:translateX(18px)}
.control-option{gap:6px}.control-option label{font-size:12px}
}
</style>
<script type="importmap">
{
"imports":{
"three":"https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/":"https://unpkg.com/three@0.162.0/examples/jsm/"
}
}
</script>
<div id="container"></div>
<div class="glow"></div>
<div id="patternName">Supernova Spiral</div>
<div id="ui-container">
<div id="controlsPanel">
<div class="control-option">
<label class="toggle-switch"><input type="checkbox" id="autoRotateToggle" checked><span class="toggle-slider"></span></label>
<label for="autoRotateToggle">Auto Rotate</label>
</div>
<div class="control-option">
<label class="toggle-switch"><input type="checkbox" id="animateToggle" checked><span class="toggle-slider"></span></label>
<label for="animateToggle">Animate</label>
</div>
</div>
<button id="shapeButton">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path></svg>
Change Shape
</button>
</div>
<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js';
import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
let scene,camera,renderer,particles,stars,composer,controls;
let time=0,current=0,isTrans=false,prog=0;
let autoRot=true,animOn=true;
const N=20000,NSTAR=6000,SPEED=0.02;
const names=['Supernova Spiral','Quantum Lattice','Stellar Torus','Celestial Helix'];
function rand(min,max){return Math.random()*(max-min)+min;}
function patSpiral(i,n){const t=i/n,arms=5,arm=i%arms,θ=3*2*Math.PI*Math.pow(t,0.7)+arm*2*Math.PI/arms,r=t*50+Math.pow(t,2)*10,z=Math.cos(t*6*Math.PI)*5*t;return new THREE.Vector3(Math.cos(θ)*r,Math.sin(θ)*r,z);}
const Φ=(1+Math.sqrt(5))/2,R=28;
const NODES=[[-1,Φ,0],[1,Φ,0],[-1,-Φ,0],[1,-Φ,0],[0,-1,Φ],[0,1,Φ],[0,-1,-Φ],[0,1,-Φ],[Φ,0,-1],[Φ,0,1],[-Φ,0,-1],[-Φ,0,1]]
.map(v=>{const l=Math.hypot(...v);return new THREE.Vector3(v[0]/l*R,v[1]/l*R,v[2]/l*R);});
const ELEN=4*R/Math.sqrt(10+2*Math.sqrt(5)),EDGES=[];
for(let a=0;a<12;a++)for(let b=a+1;b<12;b++)
if(Math.abs(NODES[a].distanceTo(NODES[b])-ELEN)<1e-3)EDGES.push([a,b]);
function patLattice(i,n){
const quota=Math.floor(n*0.5);
if(i<quota){
const node=i%NODES.length,r=Math.cbrt(Math.random())*6,u=Math.random(),v=Math.random();
const θ=2*Math.PI*u,φ=Math.acos(2*v-1);
const off=new THREE.Vector3(r*Math.sin(φ)*Math.cos(θ),r*Math.sin(φ)*Math.sin(θ),r*Math.cos(φ));
return NODES[node].clone().add(off);
}
const perEdge=Math.max(1,Math.floor((n-quota)/EDGES.length)),loc=i-quota,eIdx=Math.floor(loc/perEdge)%EDGES.length,τ=(loc%perEdge)/perEdge;
const [ai,bi]=EDGES[eIdx],A=NODES[ai],B=NODES[bi],mid=A.clone().add(B).multiplyScalar(0.5);
const dir=B.clone().sub(A).normalize(),perp=new THREE.Vector3().crossVectors(dir,new THREE.Vector3(0,1,0)).normalize();
mid.add(perp.multiplyScalar(4));
const p=new THREE.Vector3(
(1-τ)*(1-τ)*A.x+2*(1-τ)*τ*mid.x+τ*τ*B.x,
(1-τ)*(1-τ)*A.y+2*(1-τ)*τ*mid.y+τ*τ*B.y,
(1-τ)*(1-τ)*A.z+2*(1-τ)*τ*mid.z+τ*τ*B.z
);
p.add(new THREE.Vector3(Math.random()-0.5,Math.random()-0.5,Math.random()-0.5));
return p;
}
const SIN45=Math.SQRT1_2,COS45=Math.SQRT1_2;
function patTorus(i,n){
const MAJ=Math.floor(Math.sqrt(n)),MIN=Math.floor(n/MAJ);
const u=(i%MAJ)/MAJ*2*Math.PI,v=Math.floor(i/MAJ)/MIN*2*Math.PI;
const Rmaj=40,Rmin=10;
const x=(Rmaj+Rmin*Math.cos(v))*Math.cos(u);
const y=Rmin*Math.sin(v);
const z=(Rmaj+Rmin*Math.cos(v))*Math.sin(u);
const y2=y*COS45 - z*SIN45;
const z2=y*SIN45 + z*COS45;
const breath=0.6*Math.sin(v*3+u*2);
return new THREE.Vector3(
(x+breath)*(1+0.02*(Math.random()-0.5)),
(y2)*(1+0.02*(Math.random()-0.5)),
(z2+breath)*(1+0.02*(Math.random()-0.5))
);
}
function patHelix(i,n){
const hel=i%2,r=35,turns=5,height=80,half=n/2,t=(i%half)/half,θ=t*turns*2*Math.PI,y=(t-0.5)*height,φ=hel*Math.PI;
const x=Math.cos(θ+φ)*r,z=Math.sin(θ+φ)*r;
if(i%20===0){
const bt=(i%200)/200,b=Math.sin(bt*2*Math.PI);
return new THREE.Vector3(Math.cos(θ)*r*(1-b)+Math.cos(θ+Math.PI)*r*b,y,Math.sin(θ)*r*(1-b)+Math.sin(θ+Math.PI)*r*b);
}
if(i%10===0){
const or=r+10+Math.random()*15,oy=y+(Math.random()-0.5)*10;
return new THREE.Vector3(Math.cos(θ+Math.random())*or,oy,Math.sin(θ+Math.random())*or);
}
const rv=1+0.2*Math.sin(θ*3),j=0.8;
return new THREE.Vector3(x*rv+(Math.random()-0.5)*j,y+(Math.random()-0.5)*j,z*rv+(Math.random()-0.5)*j);
}
const patterns=[patSpiral,patLattice,patTorus,patHelix];
const palettes=[
[0xff3300,0xff6600,0xff9900,0xffcc00,0xffff00],
[0x6600cc,0x9900ff,0xcc00ff,0x6600ff,0x330099],
[0x007777,0x00a999,0x00d5bb,0x33ffdd,0x88fff1],
[0x9900ff,0x6600ff,0x0066ff,0x00ccff,0x9966ff]
].map(arr=>arr.map(c=>new THREE.Color(c)));
window.onload=init;
function init(){
scene=new THREE.Scene();
camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.1,1000);
camera.position.z=100;
renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.getElementById('container').appendChild(renderer.domElement);
controls=new OrbitControls(camera,renderer.domElement);
controls.enableDamping=true;controls.dampingFactor=0.1;controls.rotateSpeed=0.5;controls.zoomSpeed=0.7;
controls.minDistance=30;controls.maxDistance=200;controls.enablePan=false;controls.autoRotate=autoRot;
composer=new EffectComposer(renderer);
composer.addPass(new RenderPass(scene,camera));
composer.addPass(new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight),0.35,0.4,0.9));
composer.addPass(new OutputPass());
makeStars();makeParticles();
addEventListener('resize',resize);
document.getElementById('shapeButton').addEventListener('click',next);
document.getElementById('shapeButton').addEventListener('touchend',e=>{e.preventDefault();next();});
document.getElementById('autoRotateToggle').addEventListener('change',e=>{autoRot=e.target.checked;controls.autoRotate=autoRot;});
document.getElementById('animateToggle').addEventListener('change',e=>{animOn=e.target.checked;});
banner(names[0],true);
animate();
}
function starTex(){const c=document.createElement('canvas');c.width=c.height=32;const ctx=c.getContext('2d'),g=ctx.createRadialGradient(16,16,0,16,16,16);g.addColorStop(0,'#fff');g.addColorStop(0.1,'rgba(255,255,255,0.8)');g.addColorStop(0.25,'rgba(128,128,255,0.5)');g.addColorStop(0.5,'rgba(64,64,200,0.3)');g.addColorStop(1,'rgba(0,0,64,0)');ctx.fillStyle=g;ctx.fillRect(0,0,32,32);ctx.strokeStyle='rgba(255,255,255,0.8)';ctx.beginPath();ctx.moveTo(16,8);ctx.lineTo(16,24);ctx.moveTo(8,16);ctx.lineTo(24,16);ctx.stroke();return new THREE.CanvasTexture(c);}
function makeStars(){const g=new THREE.BufferGeometry(),p=new Float32Array(NSTAR*3),c=new Float32Array(NSTAR*3),s=new Float32Array(NSTAR);
for(let i=0;i<NSTAR;i++){
const R=800,φ=Math.acos(2*Math.random()-1),θ=Math.random()*2*Math.PI;
p[i*3]=R*Math.sin(φ)*Math.cos(θ);p[i*3+1]=R*Math.sin(φ)*Math.sin(θ);p[i*3+2]=R*Math.cos(φ);
const r=Math.random();let Rcol,Gcol,Bcol;
if(r<0.5){Rcol=Gcol=Bcol=0.8+Math.random()*0.2;s[i]=0.5+Math.random()*0.5;}
else if(r<0.85){Rcol=0.8+Math.random()*0.2;Gcol=0.6+Math.random()*0.3;Bcol=0.4+Math.random()*0.2;s[i]=0.6+Math.random()*0.6;}
else if(r<0.98){Rcol=0.4+Math.random()*0.2;Gcol=0.6+Math.random()*0.2;Bcol=0.8+Math.random()*0.2;s[i]=0.7+Math.random()*0.9;}
else{Rcol=0.8+Math.random()*0.2;Gcol=0.2+Math.random()*0.2;Bcol=0.2+Math.random()*0.2;s[i]=0.7+Math.random()*0.9;}
c[i*3]=Rcol;c[i*3+1]=Gcol;c[i*3+2]=Bcol;
}
g.setAttribute('position',new THREE.BufferAttribute(p,3));
g.setAttribute('color',new THREE.BufferAttribute(c,3));
g.setAttribute('size',new THREE.BufferAttribute(s,1));
const m=new THREE.PointsMaterial({size:1.5,map:starTex(),vertexColors:true,transparent:true,blending:THREE.AdditiveBlending,depthWrite:false});
stars=new THREE.Points(g,m);scene.add(stars);
}
function dotTex(){const c=document.createElement('canvas');c.width=c.height=64;const ctx=c.getContext('2d'),g=ctx.createRadialGradient(32,32,0,32,32,32);g.addColorStop(0,'#fff');g.addColorStop(0.2,'rgba(255,255,255,0.9)');g.addColorStop(0.4,'rgba(200,200,255,0.5)');g.addColorStop(0.8,'rgba(100,100,200,0.2)');g.addColorStop(1,'rgba(0,0,64,0)');ctx.fillStyle=g;ctx.fillRect(0,0,64,64);return new THREE.CanvasTexture(c);}
let geoPart;
function makeParticles(){
geoPart=new THREE.BufferGeometry();
const p=new Float32Array(N*3),c=new Float32Array(N*3),s=new Float32Array(N);
for(let i=0;i<N;i++){
const v=patterns[current](i,N);
p[i*3]=v.x;p[i*3+1]=v.y;p[i*3+2]=v.z;
let idx=Math.floor(Math.random()*palettes[current].length),b=0.8+Math.random()*0.4;
if(current===0){const u=i/N;idx=Math.min(Math.floor((1-Math.pow(u,0.5))*palettes[current].length),palettes[current].length-1);}
const col=palettes[current][idx];c[i*3]=col.r*b;c[i*3+1]=col.g*b;c[i*3+2]=col.b*b;
s[i]=0.8+Math.random()*1.8;
}
geoPart.setAttribute('position',new THREE.BufferAttribute(p,3));
geoPart.setAttribute('color',new THREE.BufferAttribute(c,3));
geoPart.setAttribute('size',new THREE.BufferAttribute(s,1));
geoPart.userData.currentColors=new Float32Array(c);
const mat=new THREE.PointsMaterial({size:2.5,map:dotTex(),vertexColors:true,transparent:true,blending:THREE.AdditiveBlending,depthWrite:false});
particles=new THREE.Points(geoPart,mat);scene.add(particles);
}
function banner(msg,inst=false){const el=document.getElementById('patternName');el.textContent=msg;el.style.opacity='1';if(!inst)setTimeout(()=>el.style.opacity='0',2500);}
function next(){if(isTrans)finish();const nxt=(current+1)%patterns.length;start(nxt);banner(names[nxt]);}
function start(nxt){
isTrans=true;prog=0;
const fromP=new Float32Array(geoPart.attributes.position.array),
fromC=geoPart.userData.currentColors,
fromS=new Float32Array(geoPart.attributes.size.array),
toP=new Float32Array(fromP.length),toC=new Float32Array(fromC.length),toS=new Float32Array(fromS.length);
for(let i=0;i<N;i++){
const v=patterns[nxt](i,N);toP[i*3]=v.x;toP[i*3+1]=v.y;toP[i*3+2]=v.z;
let idx=Math.floor(Math.random()*palettes[nxt].length),b=0.8+Math.random()*0.4;
if(nxt===0){const u=i/N;idx=Math.min(Math.floor((1-Math.pow(u,0.5))*palettes[nxt].length),palettes[nxt].length-1);}
const col=palettes[nxt][idx];toC[i*3]=col.r*b;toC[i*3+1]=col.g*b;toC[i*3+2]=col.b*b;
toS[i]=0.8+Math.random()*1.8;
}
particles.userData={fromP,toP,fromC,toC,fromS,toS,target:nxt};
}
function finish(){
const d=particles.userData;
geoPart.attributes.position.array.set(d.toP);
geoPart.attributes.color.array.set(d.toC);
geoPart.attributes.size.array.set(d.toS);
geoPart.attributes.position.needsUpdate=geoPart.attributes.color.needsUpdate=geoPart.attributes.size.needsUpdate=true;
geoPart.userData.currentColors=new Float32Array(d.toC);
current=d.target;isTrans=false;prog=0;
}
function resize(){camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);composer.setSize(innerWidth,innerHeight);}
function animate(){requestAnimationFrame(animate);time+=0.01;controls.update();
if(isTrans){
prog+=SPEED;
if(prog>=1){finish();}
else{
const e=prog<0.5?4*prog*prog*prog:1-Math.pow(-2*prog+2,3)/2,d=particles.userData,pos=geoPart.attributes.position.array,col=geoPart.attributes.color.array,size=geoPart.attributes.size.array;
for(let i=0;i<pos.length;i++){pos[i]=d.fromP[i]*(1-e)+d.toP[i]*e;col[i]=d.fromC[i]*(1-e)+d.toC[i]*e;}
for(let i=0;i<size.length;i++){size[i]=d.fromS[i]*(1-e)+d.toS[i]*e;}
geoPart.attributes.position.needsUpdate=geoPart.attributes.color.needsUpdate=geoPart.attributes.size.needsUpdate=true;
}
}else if(animOn){liveAnim();}
if(stars){stars.rotation.y+=0.0001;stars.rotation.x+=0.00005;}
composer.render();
}
function liveAnim(){
const pos=geoPart.attributes.position.array;
switch(current){
case 0:
for(let i=0;i<N;i++){
const x=i*3,y=x+1,z=x+2,X=pos[x],Y=pos[y],Z=pos[z],d=Math.hypot(X,Y);
if(d>0.1){
const a=0.005*(1-Math.min(d/50,0.8)),c=Math.cos(a),s=Math.sin(a);
pos[x]=X*c-Y*s;pos[y]=X*s+Y*c;
}
pos[z]=Z+Math.sin(time+d*0.1)*0.2;
}
break;
case 1:
for(let i=0;i<N;i++){const p=i*3;pos[p]+=Math.sin(time+i*0.2)*0.002;pos[p+1]+=Math.cos(time*0.7+i*0.3)*0.002;pos[p+2]+=Math.sin(time*0.4+i*0.5)*0.002;}
break;
case 2:
const rot=0.01,cs=Math.cos(rot),sn=Math.sin(rot);
for(let i=0;i<N;i++){
const x=i*3,y=x+1,z=x+2,X=pos[x],Z=pos[z];
pos[x]=X*cs-Z*sn;pos[z]=X*sn+Z*cs;
pos[y]+=Math.sin(time*0.5+X*0.02+Z*0.02)*0.02;
}
break;
case 3:
for(let i=0;i<N;i++){
const x=i*3,y=x+1,z=x+2,X=pos[x],Y=pos[y],Z=pos[z],bridge=i%20===0,orb=!bridge&&i%10===0;
if(!bridge&&!orb){
pos[y]+=0.05;if(pos[y]>40)pos[y]=-40;
const dir=i%2?1:-1,a=0.003*dir,cs=Math.cos(a),sn=Math.sin(a);
pos[x]=X*cs-Z*sn;pos[z]=X*sn+Z*cs;
}else if(bridge){
const w=Math.sin(time*2+Y*0.1)*0.3;pos[x]+=w;pos[z]+=w;
}else{
const a=0.01*(1+Math.sin(Y*0.05)),cs=Math.cos(a),sn=Math.sin(a),nx=X*cs-Z*sn,nz=X*sn+Z*cs;
pos[x]=nx;pos[z]=nz;pos[y]+=Math.sin(time+X*0.01)*0.05;
}
}
break;
}
geoPart.attributes.position.needsUpdate=true;
}
</script>
</html>
效果如下
更多推荐
所有评论(0)