threejs将动态模型转化为动态粒子效果
之前看到人民网做的网站http://politics.people.com.cn/GB/8198/429373/index.html都是由粒子动画组成,当时私下模拟了一下,采用的方法是将模型通过低版本的Blender导出为threejs 所需要的json格式,然后绘制顶点。 后来发现生成的json格式太大,不太适合手机版观看,暂时搁浅。没想到有专门的代码可以将glb格式直接转为粒子的代码。
最终的形态:
项目的目录结构:
World/components/SparkleHorse/sparkeHorse.js
import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders/GLTFLoader.js';
import { convertMeshToPoints } from './utilities/convertMeshToPoints.js';
import { createSparkleMaterial } from './utilities/createSparkleMaterial.js';
import { createSizesAttribute } from './utilities/createSizesAttribute.js';
import { setupAnimation } from './utilities/setupAnimation.js';
async function createSparkleHorse() {
const loader = new GLTFLoader();
const data = await loader.loadAsync('./assets/models/Horse.glb');
const model = data.scene.children[0];
const clip = data.animations[0];
const material = createSparkleMaterial();
const sparkleHorse = convertMeshToPoints(model, material);
sparkleHorse.rotation.y = Math.PI / 2;
sparkleHorse.scale.multiplyScalar(0.03);
const sizeAttribute = createSizesAttribute(sparkleHorse.geometry);
sparkleHorse.geometry.setAttribute('size', sizeAttribute);
setupAnimation(sparkleHorse, clip, sizeAttribute);
return sparkleHorse;
}
export { createSparkleHorse };
World/components/SparkleHorse/utilities/convertMeshToPoints.js
import { Points } from 'https://unpkg.com/[email protected]/build/three.module.js';
function convertMeshToPoints(mesh, material) {
const points = new Points(mesh.geometry, material);
return points;
}
export { convertMeshToPoints };
World/components/SparkleHorse/utilities/createSizesAttribute.js
import { Float32BufferAttribute } from 'https://unpkg.com/[email protected]/build/three.module.js';
function createSizesAttribute(geometry) {
const positions = geometry.attributes.position;
const count = positions.count;
const sizes = [];
for (let i = 0; i < count; i++) {
sizes.push(1);
}
const sizeAttribute = new Float32BufferAttribute(sizes, 1);
return sizeAttribute;
}
export { createSizesAttribute };
World/components/SparkleHorse/utilities/createSparkleMaterial.js
import {
AdditiveBlending,
PointsMaterial,
TextureLoader,
} from 'https://unpkg.com/[email protected]/build/three.module.js';
function createSparkleMaterial() {
const map = new TextureLoader().load(
'./assets/textures/sprites/spark1.png',
);
const material = new PointsMaterial({
map,
// size: 0.75,
color: 'white',
morphTargets: true,
blending: AdditiveBlending,
depthTest: false,
// transparent: true,
vertexColors: true,
});
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
'uniform float size;',
'attribute float size;',
);
};
return material;
}
export { createSparkleMaterial };
World/components/SparkleHorse/utilities/setupAnimation.js
import { AnimationMixer } from 'https://unpkg.com/[email protected]/build/three.module.js';
function setupAnimation(model, clip, sizeAttribute) {
const mixer = new AnimationMixer(model);
const action = mixer.clipAction(clip);
action.play();
const minPointSize = 0.75;
let time = 0;
model.tick = (delta) => {
mixer.update(delta);
time += delta * 10;
for (let i = 0; i < sizeAttribute.count; i++) {
sizeAttribute.array[i] =
minPointSize + 0.5 * Math.sin(i + time);
}
sizeAttribute.needsUpdate = true;
};
}
export { setupAnimation };
World/components/camera.js
import { PerspectiveCamera } from 'https://unpkg.com/[email protected]/build/three.module.js';
function createCamera() {
const camera = new PerspectiveCamera(35, 1, 0.1, 100);
camera.position.set(7, 2, 10);
return camera;
}
export { createCamera };
World/components/lights.js
import { DirectionalLight, HemisphereLight } from 'https://unpkg.com/[email protected]/build/three.module.js';
function createLights() {
const ambientLight = new HemisphereLight(
'white',
'darkslategrey',
5,
);
const mainLight = new DirectionalLight('white', 4);
mainLight.position.set(10, 10, 10);
return { ambientLight, mainLight };
}
export { createLights };
World/components/scene.js
import { Color, Scene } from 'https://unpkg.com/[email protected]/build/three.module.js';
function createScene() {
const scene = new Scene();
// scene.background = new Color('skyblue');
return scene;
}
export { createScene };
World/ststems/controls.js
import { OrbitControls } from 'https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js';
function createControls(camera, canvas) {
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.target.y = 3;
// forward controls.update to our custom .tick method
controls.tick = (delta) => controls.update(delta);
return controls;
}
export { createControls };
World/systems/Loop.js
import { Clock } from 'https://unpkg.com/[email protected]/build/three.module.js';
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.updatables = [];
}
start() {
this.renderer.setAnimationLoop(() => {
// tell every animated object to tick forward one frame
this.tick();
// render a frame
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick() {
// only call the getDelta function once per frame!
const delta = clock.getDelta();
// console.log(
// `The last frame rendered in ${delta * 1000} milliseconds`,
// );
for (const object of this.updatables) {
object.tick(delta);
}
}
}
export { Loop };
World/systems/renderer.js
import { WebGLRenderer } from 'https://unpkg.com/[email protected]/build/three.module.js';
function createRenderer() {
const renderer = new WebGLRenderer({ antialias: true });
renderer.physicallyCorrectLights = true;
return renderer;
}
export { createRenderer };
World/systems/Resizer.js
class Resizer {
constructor(container, camera, renderer) {
const setSize = () => {
// Set the camera's aspect ratio
camera.aspect = container.clientWidth / container.clientHeight;
// update the camera's frustum
camera.updateProjectionMatrix();
// update the size of the renderer AND the canvas
renderer.setSize(container.clientWidth, container.clientHeight);
// set the pixel ratio (for mobile devices)
renderer.setPixelRatio(window.devicePixelRatio);
};
// set initial size
setSize();
window.addEventListener('resize', () => {
// set the size again if a resize occurs
setSize();
// perform any custom actions
this.onResize();
});
}
onResize() {}
}
export { Resizer };
World/World.js
import { createCamera } from './components/camera.js';
import { createScene } from './components/scene.js';
import { createSparkleHorse } from './components/SparkleHorse/sparkleHorse.js';
import { createControls } from './systems/controls.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';
let camera;
let controls;
let renderer;
let scene;
let loop;
class World {
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
controls = createControls(camera, renderer.domElement);
loop.updatables.push(controls);
const resizer = new Resizer(container, camera, renderer);
}
async init() {
const sparkleHorse = await createSparkleHorse();
loop.updatables.push(sparkleHorse);
scene.add(sparkleHorse);
}
render() {
renderer.render(scene, camera);
}
start() {
loop.start();
}
stop() {
loop.stop();
}
}
export { World };
main.js
import { World } from './World/World.js';
async function main() {
// Get a reference to the container element
const container = document.querySelector('#scene-container');
// create a new world
const world = new World(container);
// complete async tasks
await world.init();
// start the animation loop
world.start();
}
main();