效果预览:
模型粒子化切换动画
模型粒子化
直接遍历模型的geometry.attributes.position,不过为了实现模型粒子动画,切换的模型粒子数要和原本模型的粒子数相同,我的解决办法是补齐粒子数少的模型。还要给每一个粒子添加一个随机延迟,不然粒子同步运动效果不好看
constructor({ origin, target, allTime, delayPct, easing }) {
allTime = allTime || 5000;
delayPct = delayPct || 0;
easing = easing || TWEEN.Easing.Sinusoidal.InOut;
let position1 = origin.geometry.attributes.position;
let position2 = target.geometry.attributes.position;
const bool = position1.count < position2.count;
if (bool) {
position1 = position2;
position2 = origin.geometry.attributes.position;
}
let x2, y2, z2;
const vertices1 = [],
vertices2 = [],
delays = [];
for (let i = 0; i < position1.count; i++) {
const x1 = position1.getX(i);
const y1 = position1.getY(i);
const z1 = position1.getZ(i);
vertices1.push(x1, y1, z1);
const j = i % position2.count;
x2 = position2.getX(j);
y2 = position2.getY(j);
z2 = position2.getZ(j);
vertices2.push(x2, y2, z2);
delays.push(Math.random() * delayPct);
}
const geometry = new THREE.BufferGeometry();
let position_v = vertices1;
let target_v = vertices2;
if (bool) {
position_v = vertices2;
target_v = vertices1;
}
geometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(position_v, 3)
);
geometry.setAttribute(
"target",
new THREE.Float32BufferAttribute(target_v, 3)
);
geometry.setAttribute("delay", new THREE.Float32BufferAttribute(delays, 1));
geometry.setDrawRange(0, position1.count);
const material = this.getMaterial({
color: 0xffffff,
size: 1,
});
this.points = new THREE.Points(geometry, material);
this.animationInit(1 + delayPct, allTime, easing, this.endCallback);
}
顶点着色器实现动画
传统办法是使用cpu计算每一个点的位置并在requestAnimationFrame中更新,这种方式完全没有利用到gpu的计算优势,这种简单且大量的计算用顶点着色器是很简单的,我们只要一个进程百分比的uniforms就能实现切换动画,问题的关键就变为了找到进程百分比和位置的对应关系,然后通过改变进程百分比就实现了动画。我这里为了动画效果还加了一个周期扰动,让动画不那么死板
getMaterial(options) {
options = Object.assign(
{
color: 0xffffff,
size: 1,
map: new THREE.TextureLoader().load(
`${VITE_PUBLIC_PATH || "/public/"}textures/sprites/circle.png`
),
},
options
);
this.uniforms = {
color: { value: new THREE.Color(options.color) },
size: { value: options.size },
map: { value: options.map },
percent: { value: 0 },
};
const material = new THREE.ShaderMaterial({
// transparent: true,
depthTest: true,
depthWrite: true,
// blending: THREE.AdditiveBlending,
uniforms: this.uniforms,
vertexShader: `
attribute float delay;
attribute vec3 target;
uniform float percent;
uniform float size;
void main() {
float p = clamp(percent - delay,0.0,1.0); //进程百分比
float p2 = (0.5 - abs(p-0.5))*6.2831; //进程百分比相联系的扰动参数
vec3 translate = vec3(sin(p2),sin(p2*1.4),p2*cos(p2*1.6)*0.3); //扰动
vec3 _position = mix( position,target,p); //根据进程百分比插值计算当前位置
vec4 mvPosition = modelViewMatrix * vec4(_position+translate*0.1, 1.0);
gl_Position = projectionMatrix * mvPosition;
gl_PointSize = sqrt(30.0 / -mvPosition.z)*size;
}
`,
fragmentShader: `
uniform vec3 color;
uniform sampler2D map;
void main() {
gl_FragColor = vec4(color, 1.0);
// gl_FragColor = gl_FragColor * texture2D( map, gl_PointCoord );
}
`,
});
// gui.add(material.uniforms.percent, "value", 0, 2, 0.01);
return material;
}
更新进程百分比我用了tween,方便实现缓动,由慢到快再又快到慢的缓动可让动画表现更符合直觉
/**
* 初始化动画,设置对象消失的百分比、持续时间和缓动函数。
* @param {number} percent - 对象消失的百分比。
* @param {number} duration - 动画的持续时间,默认为 2000 毫秒。
* @param {Function} easing - 动画的缓动函数,默认为 TWEEN.Easing.Sinusoidal.InOut。
* @returns {TWEEN} - 返回一个Tween对象,用于控制动画。
*/
animationInit(
percent,
duration = 2000,
easing = TWEEN.Easing.Sinusoidal.InOut,
endCallback = () => {}
) {
if (this.action) {
this.action = null;
}
this.action = new TWEEN.Tween(this.uniforms.percent)
.to({ value: percent }, duration)
.easing(easing)
.onUpdate((obj) => {})
.onComplete((res) => {
cancelAnimationFrame(this.animateRequestID);
this.animateRequestID = null;
endCallback();
})
.onStop(() => {
cancelAnimationFrame(this.animateRequestID);
this.animateRequestID = null;
endCallback();
});
this.animateRequestID = null;
// this.startAnimation();
}
// 开始动画
startAnimation() {
if (this.animateRequestID) return;
this.action.start();
const animate = () => {
this.animateRequestID = requestAnimationFrame(animate);
TWEEN.update();
};
animate();
}
// 停止动画
stopAnimation() {
if (!this.animateRequestID) return;
this.action.stop();
}