关于单个模型用粒子组成可参考使用粒子效果动画组成模型[自定义shader实现]
本文介绍多模型切换技巧
思路
- 获取需要切换的几个模型的数据就是每个点的坐标在
geometry.attributes.position 中array 即时构成mesh的数据。 例:
const targetMesh = g.scene.getObjectByProperty("type", "Mesh");
if (!targetMesh) throw new Error("获取目标物体失败");
const { array} = targetMesh.geometry.attributes.position;
- 将数据保存到一个列表中供切换数据使用
- 切换模型数据实现思路
粒子的动画应该使用自定义shader实现,如果直接更新attributes 几千个点还好,数量达到几万就卡顿严重,无法使用,上述案例使用9万多点,运行时无丝毫卡顿不影响页面其他功能,实为上策。 为了让shader计算每个点在某一时刻应该在什么位置需要提供几个属性参与计算:变换前的位置,变换后应该的位置,变换的时长,当前的时间,如果需要颜色渐变可以额外提供之前和之后的颜色,这些数据通过geometry.attributes 传递给shader,在自定义shader中设置对应属性接收、计算。 例:
this.particlesGeometry = new BufferGeometry();
this.color = new Float32Array(this.numberOfPoints * 3);
this.particlesGeometry.setAttribute("color", new BufferAttribute(this.color, 3));
this.particles = new Points(this.particlesGeometry, PointsShaderMaterial);
每次切换模型数据 将原先的属性替换为新的模型数据 然后告诉shader需要更新这一属性,这样shader才会接受到新的有效的数据
this.particlesGeometry.attributes.color.needsUpdate = true;
shader中的计算和控制粒子变换的代码请看下面代码区域
顶点着色器
varying vec3 vColor;
uniform float time;
uniform float size;
attribute vec3 color;
attribute vec3 oldColor;
attribute vec3 toPositions;
attribute vec3 oldPositions;
attribute float toPositionsDuration;
void main() {
vec3 dispatchPos = toPositions;
vColor = color;
float percent = time / toPositionsDuration;
if(percent <= 1.) {
dispatchPos.x = oldPositions.x + percent * (toPositions.x - oldPositions.x);
dispatchPos.y = oldPositions.y + percent * (toPositions.y - oldPositions.y);
dispatchPos.z = oldPositions.z + percent * (toPositions.z - oldPositions.z);
vColor.x = oldColor.x + percent * (color.x - oldColor.x);
vColor.y = oldColor.y + percent * (color.y - oldColor.y);
vColor.z = oldColor.z + percent * (color.z - oldColor.z);
}
vec4 viewPosition = modelViewMatrix * vec4(dispatchPos, 1.0);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = size;
gl_PointSize *= (120. / -(modelViewMatrix * vec4(dispatchPos, 1.0)).z);
}
片元着色器
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.);
}
粒子控制器
import { BufferAttribute, BufferGeometry, Color, Material, Points, TextureLoader } from "three";
import starMap from "../assets/img/star_09.png";
import { PointsShaderMaterial } from "./shader/points.material";
export interface IColor {
left: THREE.Color;
right: THREE.Color;
dividingLine: number;
}
interface IPointsModelParams {
positions: ArrayLike<number>;
duration: { min: number; max: number };
box3: THREE.Box3;
color: IColor;
}
export class PointsControlClass {
numberOfPoints: number;
particles: Points<BufferGeometry, Material>;
particlesGeometry: BufferGeometry;
toPositions: Float32Array;
toPositionsDuration: Float32Array;
oldPositions: Float32Array;
color: Float32Array;
defaultPointsColor = { left: new Color("#fff"), right: new Color("#51f"), dividingLine: 0.5 };
modelsList: { [key: number]: IPointsModelParams; current: number; length: number } = { current: -1, length: 0 };
oldColor: Float32Array;
positions: Float32Array;
constructor(numberOfPoints: number) {
this.numberOfPoints = numberOfPoints;
this.particlesGeometry = new BufferGeometry();
this.positions = new Float32Array(numberOfPoints * 3);
this.oldPositions = new Float32Array(numberOfPoints * 3);
this.toPositions = new Float32Array(this.numberOfPoints * 3);
this.toPositionsDuration = new Float32Array(this.numberOfPoints);
this.color = new Float32Array(this.numberOfPoints * 3);
this.oldColor = new Float32Array(this.numberOfPoints * 3);
this.particlesGeometry.setAttribute("color", new BufferAttribute(this.color, 3));
this.particlesGeometry.setAttribute("oldColor", new BufferAttribute(this.oldColor, 3));
this.particlesGeometry.setAttribute("position", new BufferAttribute(this.positions, 3));
this.particlesGeometry.setAttribute("oldPositions", new BufferAttribute(this.oldPositions, 3));
this.particlesGeometry.setAttribute("toPositions", new BufferAttribute(this.toPositions, 3));
this.particlesGeometry.setAttribute("toPositionsDuration", new BufferAttribute(this.toPositionsDuration, 1));
const textureLoader = new TextureLoader();
PointsShaderMaterial.uniforms.textureMap = {
value: textureLoader.load(starMap.src),
};
this.particles = new Points(this.particlesGeometry, PointsShaderMaterial);
this.init();
}
toIndex(index: number) {
const { positions, color, box3, duration } = this.modelsList[index];
const { length } = positions;
const pointCount = length / 3;
const { dividingLine, left, right } = color;
const { abs } = Math;
const {
min: { x: minX },
max: { y: maxX },
} = box3;
const disColor = {
r: left.r - right.r,
g: left.g - right.g,
b: left.b - right.b,
};
const width = abs(box3.min.x - box3.max.x);
const dividingLineValue = box3.min.x + width * dividingLine;
console.log(`小:${box3.min.x} 大:${box3.max.x} 宽${width},分界线:${dividingLineValue}`);
for (let i = 0, realCount = 0; i < this.numberOfPoints; i++, realCount++) {
realCount = realCount % pointCount;
const i3 = i * 3;
const r3 = realCount * 3;
this.oldPositions[i3] = this.toPositions[i3];
this.oldPositions[i3 + 1] = this.toPositions[i3 + 1];
this.oldPositions[i3 + 2] = this.toPositions[i3 + 2];
const x = positions[r3];
this.toPositions[i3] = x;
this.toPositions[i3 + 1] = positions[r3 + 1];
this.toPositions[i3 + 2] = positions[r3 + 2];
const percent = abs(
x <= dividingLineValue
? ((minX - x) / (dividingLineValue - minX)) * dividingLine * dividingLine
: ((x - dividingLineValue) / (maxX - dividingLineValue)) * (1 - dividingLine) + dividingLine
);
this.oldColor[i3] = this.color[i3];
this.oldColor[i3 + 1] = this.color[i3 + 1];
this.oldColor[i3 + 2] = this.color[i3 + 2];
const r = left.r - percent * disColor.r;
const g = left.g - percent * disColor.g;
const b = left.b - percent * disColor.b;
this.color[i3] = r;
this.color[i3 + 1] = g;
this.color[i3 + 2] = b;
const useDuration = duration.min + Math.random() * (duration.max - duration.min);
this.toPositionsDuration[i] = useDuration;
}
}
append(params: IPointsModelParams, index: number) {
if (this.modelsList[index]) {
console.log("已经有这项了");
}
this.modelsList[index] = params;
this.modelsList.length = Object.keys(this.modelsList).length - 2;
}
next() {
const current = this.modelsList.current;
const toIndex = current + 1;
if (toIndex < this.modelsList.length && toIndex >= 0) {
this.toIndex(toIndex);
this.updateAttributes();
this.modelsList.current++;
return true;
}
return false;
}
prev() {
const current = this.modelsList.current;
const toIndex = current - 1;
if (toIndex < this.modelsList.length && toIndex >= 0) {
this.toIndex(toIndex);
this.updateAttributes();
this.modelsList.current--;
return true;
}
return false;
}
updateAttributes() {
this.particlesGeometry.attributes.color.needsUpdate = true;
this.particlesGeometry.attributes.oldColor.needsUpdate = true;
this.particlesGeometry.attributes.toPositions.needsUpdate = true;
this.particlesGeometry.attributes.oldPositions.needsUpdate = true;
this.particlesGeometry.attributes.toPositionsDuration.needsUpdate = true;
}
update(time: number) {
PointsShaderMaterial.uniforms.time.value = time;
}
init(range: number = 1000) {
for (let i = 0; i < this.numberOfPoints; i++) {
const i3 = i * 3;
const x = (0.5 - Math.random()) * range;
const y = (0.5 - Math.random()) * range;
const z = (0.5 - Math.random()) * range;
this.toPositions[i3] = x;
this.toPositions[i3 + 1] = y;
this.toPositions[i3 + 2] = z;
const c = Math.random();
this.color[i3] = c;
this.color[i3 + 1] = c;
this.color[i3 + 2] = c;
}
this.particlesGeometry.attributes.toPositions.needsUpdate = true;
}
}
自定义shader
import vertexShader from "./points.vt.glsl";
import fragmentShader from "./points.fm.glsl";
import { AdditiveBlending,ShaderMaterial } from "three";
export const PointsShaderMaterial = new ShaderMaterial({
uniforms: {
time: { value: 0 },
size: { value: 8 },
},
blending: AdditiveBlending,
transparent: true,
vertexShader,
fragmentShader,
alphaTest: 0.001,
depthWrite: false,
});
初始化控制器
const PointsControl = new PointsControlClass(90686);
scene.add(PointsControl.particles);
加载模型 填充数据
td.loadGltf(url).then((g) => {
const targetMesh = g.scene.getObjectByProperty("type", "Mesh") as Mesh;
if (!targetMesh) throw new Error("获取目标物体失败");
const { array: positions} = targetMesh.geometry.attributes.position;
targetMesh.geometry.computeBoundingBox();
const color = { left: new Color("#f00"), right: new Color("#5f1"), dividingLine: 0.65 };
const params = {
duration: { min: 1000, max: 3000 },
color,
positions,
box3: targetMesh.geometry.boundingBox!,
};
PointsControl.append(params, 0);
});
|