111 lines
2.8 KiB
TypeScript
111 lines
2.8 KiB
TypeScript
import * as THREE from "three";
|
|
|
|
export interface ExplodedPart {
|
|
object: THREE.Object3D;
|
|
originalPosition: THREE.Vector3;
|
|
targetPosition: THREE.Vector3;
|
|
}
|
|
|
|
interface ExplodedModelOptions {
|
|
distance?: number;
|
|
speed?: number;
|
|
}
|
|
|
|
const _center = new THREE.Vector3();
|
|
const _direction = new THREE.Vector3();
|
|
|
|
export class ExplodedModel {
|
|
private readonly parts: ExplodedPart[] = [];
|
|
private readonly distance: number;
|
|
private readonly speed: number;
|
|
private progress = 0;
|
|
private targetProgress = 0;
|
|
|
|
constructor(model: THREE.Object3D, options: ExplodedModelOptions = {}) {
|
|
this.distance = options.distance ?? 1.2;
|
|
this.speed = options.speed ?? 6;
|
|
this.parts = this.createParts(model);
|
|
}
|
|
|
|
setSplit(split: boolean): void {
|
|
this.targetProgress = split ? 1 : 0;
|
|
}
|
|
|
|
getParts(): readonly ExplodedPart[] {
|
|
return this.parts;
|
|
}
|
|
|
|
update(delta: number): void {
|
|
const diff = this.targetProgress - this.progress;
|
|
if (Math.abs(diff) < 0.001) {
|
|
this.progress = this.targetProgress;
|
|
} else {
|
|
this.progress += diff * Math.min(delta * this.speed, 1);
|
|
}
|
|
|
|
this.parts.forEach((part) => {
|
|
part.object.position.lerpVectors(
|
|
part.originalPosition,
|
|
part.targetPosition,
|
|
this.progress,
|
|
);
|
|
});
|
|
}
|
|
|
|
private createParts(model: THREE.Object3D): ExplodedPart[] {
|
|
const root =
|
|
model.children.length === 1 && model.children[0]
|
|
? model.children[0]
|
|
: model;
|
|
const directChildren = root.children.filter((child) => hasMesh(child));
|
|
const sourceObjects =
|
|
directChildren.length > 1 ? directChildren : getMeshes(root);
|
|
|
|
if (sourceObjects.length === 0) return [];
|
|
|
|
_center.set(0, 0, 0);
|
|
sourceObjects.forEach((object) => _center.add(object.position));
|
|
_center.divideScalar(sourceObjects.length);
|
|
|
|
return sourceObjects.map((object, index) => {
|
|
const originalPosition = object.position.clone();
|
|
_direction.subVectors(originalPosition, _center);
|
|
|
|
if (_direction.lengthSq() < 0.0001) {
|
|
const angle = (index / sourceObjects.length) * Math.PI * 2;
|
|
_direction.set(Math.cos(angle), 0.25, Math.sin(angle));
|
|
}
|
|
|
|
_direction.normalize();
|
|
|
|
return {
|
|
object,
|
|
originalPosition,
|
|
targetPosition: originalPosition
|
|
.clone()
|
|
.addScaledVector(_direction, this.distance),
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
function hasMesh(object: THREE.Object3D): boolean {
|
|
let found = false;
|
|
object.traverse((child) => {
|
|
if (child instanceof THREE.Mesh) {
|
|
found = true;
|
|
}
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function getMeshes(object: THREE.Object3D): THREE.Object3D[] {
|
|
const meshes: THREE.Object3D[] = [];
|
|
object.traverse((child) => {
|
|
if (child instanceof THREE.Mesh) {
|
|
meshes.push(child);
|
|
}
|
|
});
|
|
return meshes;
|
|
}
|