vlambda博客
学习文章列表

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();