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 methodcontrols.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 framethis.tick();// render a framethis.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 ratiocamera.aspect = container.clientWidth / container.clientHeight;// update the camera's frustumcamera.updateProjectionMatrix();// update the size of the renderer AND the canvasrenderer.setSize(container.clientWidth, container.clientHeight);// set the pixel ratio (for mobile devices)renderer.setPixelRatio(window.devicePixelRatio);};// set initial sizesetSize();window.addEventListener('resize', () => {// set the size again if a resize occurssetSize();// perform any custom actionsthis.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 elementconst container = document.querySelector('#scene-container');// create a new worldconst world = new World(container);// complete async tasksawait world.init();// start the animation loopworld.start();}main();
