Three.js不同模型在不同轨道上的动画和移动

目录

  • 效果描述
  • 实现流程
    • 基本流程
    • 工程文件
    • 搭建场景
    • 添加模型和播放动画
    • 添加路径和模型移动
    • 完整代码和实现效果

效果描述

在场景中放置多个轨道路线,并在轨道上放置模型,在模型动画播放的同时让模型沿着预定轨迹移动。

实现流程

基本流程

1、搭建场景
2、添加模型和播放动画
3、添加路径和模型移动

工程文件

工程文件结构如下图:
static:存放静态资源文件
three.js-master:为官网下载的代码包,包含所有需要用到的资源包,链接:https://github.com/mrdoob/three.js/archive/master.zip
index.html:页面代码
在这里插入图片描述
模型使用的是官方示例中的Soldier模型,文件位置:three.js-master\examples\models\gltf\Soldier.glb
为了方便操作我们将文件拷出来放在上图static\3dmod\gltf文件夹下,static与three.js-master同级

index.html单页代码组成

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
	<title>My first three.js app</title>
	<style>
		body {
			margin: 0;
		}
	</style>
</head>

<body>
	<script type="importmap">
			{
				"imports": {
					"three": "./three.js-master/build/three.module.js"
				}
			}
		</script>
	<script type="module">
		// 下文JS代码位置
		// ...
	</script>
</body>

</html>


参照官网例子:https://threejs.org/examples/#webgl_animation_skinning_blending中的场景和模型

搭建场景

搭建场景环境

import * as THREE from "three";
import { OrbitControls } from "./three.js-master/examples/jsm/controls/OrbitControls.js";

let scene, camera, renderer;

// 渲染器开启阴影渲染:renderer.shadowMapEnabled = true;
// 灯光需要开启“引起阴影”:light.castShadow = true;
// 物体需要开启“引起阴影”和“接收阴影”:mesh.castShadow = mesh.receiveShadow = true;

function init() {
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
	renderer = new THREE.WebGLRenderer();
	// position and point the camera to the center of the scene
	camera.position.set(5, 5, 5);
	camera.lookAt(scene.position);
	
	// 增加坐标系红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
	// 添加坐标系到场景中
	const axes = new THREE.AxesHelper(20);
	scene.add(axes);
	
	// 调整背景颜色,边界雾化
	scene.background = new THREE.Color(0xa0a0a0);
	scene.fog = new THREE.Fog(0xa0a0a0, 10, 30);
	
	// 半球形光源
	const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
	hemiLight.position.set(0, 10, 0);
	scene.add(hemiLight);
	
	// 创建一个虚拟的球形网格 Mesh 的辅助对象来模拟 半球形光源 HemisphereLight.
	const hemiLighthelper = new THREE.HemisphereLightHelper(hemiLight, 5);
	scene.add(hemiLighthelper);
	
	// 地面
	const mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
	mesh.rotation.x = - Math.PI / 2;
	mesh.receiveShadow = true;
	scene.add(mesh);
	
	// 平行光
	const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
	directionalLight.castShadow = true;
	directionalLight.shadow.camera.near = 0.5;
	directionalLight.shadow.camera.far = 50;
	directionalLight.shadow.camera.left = -10;
	directionalLight.shadow.camera.right = 10;
	directionalLight.shadow.camera.top = 10;
	directionalLight.shadow.camera.bottom = -10;
	directionalLight.position.set(0, 5, 5);
	scene.add(directionalLight);
	
	// 用于模拟场景中平行光 DirectionalLight 的辅助对象. 其中包含了表示光位置的平面和表示光方向的线段.
	const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
	scene.add(directionalLightHelper);
	
	renderer.shadowMap.enabled = true;
	renderer.setSize(window.innerWidth, window.innerHeight);
	document.body.appendChild(renderer.domElement);
	
	// 控制器
	const controls = new OrbitControls(camera, renderer.domElement);
}
// 渲染
function animate() {
	requestAnimationFrame(animate);
	renderer.render(scene, camera);
};


添加模型和播放动画

导入模型,在《Three.js学习四——模型导入》中有相对详细的介绍。

动画实现基本流程:

1、使用加载器导入模型后,在加载成功后调用的函数中设置动画
2、新建一个AnimationMixer(动画混合器)
3、获取AnimationClip(动画)实例列表
4、设置播放动画,并在每一帧中更新mixer

因为需要用到多个模型,所以将不同模型的配置单独拿出来写

import { GLTFLoader } from "./three.js-master/examples/jsm/loaders/GLTFLoader.js";
import * as SkeletonUtils from "./three.js-master/examples/jsm/utils/SkeletonUtils.js";
let clock = new THREE.Clock();
let mods = [];
let mixers = [];
let numLoadedModels = 0;
let MODELS = [
	{ name: "Soldier" },
];
// 定义要放置在场景中的模型实例、它们的位置、比例以及必须播放的动画。
let UNITS = [
      {
		modelName: "Soldier", // 使用文件models/gltf/Soldier.glb中的3D模型
		meshName: "Scene", // 要设置动画的网格的名称,这个值在模型加载后的对象中找到
		position: { x: 0, y: 0, z: 0 }, // 在场景中放置单元的位置
		scale: 1, // 单位的缩放比例。1.0表示:使用原始尺寸,0.1表示“小10倍”等。
		animationName: "Run", // 要运行的动画的名称
	},
	{
		modelName: "Soldier", // 使用文件models/gltf/Soldier.glb中的3D模型
		meshName: "Scene", // 要设置动画的网格的名称,这个值在模型加载后的对象中找到
		position: { x: 0, y: 0, z: 0 }, // 在场景中放置单元的位置
		scale: 1, // 单位的缩放比例。1.0表示:使用原始尺寸,0.1表示“小10倍”等。
		animationName: "Walk", // 要运行的动画的名称
	},
	{
		modelName: "Soldier", // 使用文件models/gltf/Soldier.glb中的3D模型
		meshName: "Scene", // 要设置动画的网格的名称,这个值在模型加载后的对象中找到
		position: { x: 0, y: 0, z: 0 }, // 在场景中放置单元的位置
		scale: 1, // 单位的缩放比例。1.0表示:使用原始尺寸,0.1表示“小10倍”等。
		animationName: "Run", // 要运行的动画的名称
	},
];
/**
 * 函数用于启动队列中下一个模型的加载过程。加载过程为异步的:它发生在“后台”。
 * 所以这里不一次加载所有模型。我们加载一个,等待完成,然后加载下一个。
 * 当所有模型都被加载时,我们调用loadUnits()。
 */
function loadModels() {
	// 在预定模型(需要加载的)MODELS数组中拿模型对象(只包含少部分信息用作加载相关)
	for (let i = 0; i < MODELS.length; ++i) {
		const m = MODELS[i];
		// 加载模型,加载成功后对模型进行复制,具体的类别个数在UNITS单元组中
		loadGltfModel(m, function () {
			++numLoadedModels;
			// 所有模型加载完成后开始进行对应的模型单元初始化(复制)
			if (numLoadedModels === MODELS.length) {
				console.log("All models loaded, time to instantiate units...");
				//
				instantiateUnits();
			}
		});
	}
};
/**
 * 从GLTF文件加载三维模型。使用GLTFLoader。
 * @param model {object} 模型配置,模型数组中的一项。它将在函数内部更新
 * @param onLoaded {function} 加载模型时将调用的回调函数
 */
function loadGltfModel(model, onLoaded) {
	const loader = new GLTFLoader(); //加载器
	const modelName =
		"./static/3dmod/gltf/" + model.name + ".glb";

	loader.load(modelName, function (gltf) {
		const scene = gltf.scene;
		model.animations = gltf.animations;
		model.scene = scene;
		// 启用阴影
		gltf.scene.traverse(function (object) {
			if (object.isMesh) {
				object.castShadow = true;
			}
		});
		console.log("Done loading model", model.name);
		onLoaded();
	});
};
/**
 * 按名称查找模型对象
 * @param name
 * @returns {object|null}
 */
function getModelByName(name) {
	for (let i = 0; i < MODELS.length; ++i) {
		if (MODELS[i].name === name) {
			return MODELS[i];
		}
	}
	return null;
};
/**
 * 查看UNITS配置,克隆必要的3D模型场景,在场景中放置电枢和网格,并启动必要的动画
 */
function instantiateUnits() {
	let numSuccess = 0;

	for (let i = 0; i < UNITS.length; ++i) {
		const u = UNITS[i];
		const model = getModelByName(u.modelName);

		if (model) {
			const clonedScene = SkeletonUtils.clone(model.scene);
			if (clonedScene) {
				// 当THREE.Scene 场景已正确克隆时,找到一个模型网格并为其启动动画
				// 根据meshName拿到模型网格
				const clonedMesh = clonedScene.getObjectByName(u.meshName);
				if (clonedMesh) {
					const mixer = startAnimation(
						clonedMesh,
						model.animations,
						u.animationName
					);
					// 将动画混合器保存在列表中,在动画循环中需要使用
					mixers.push(mixer);
					numSuccess++;
				}
				// 不同的模型可以具有不同的电枢和网格配置,我们无法为单个网格对象设置位置、比例或旋转。
				// 所以,我们将其设置为整个克隆场景,然后将整个场景添加到游戏世界
				// 注意:如果GLTF文件的场景中有灯光或其他项目,这可能会对其产生影响
				mods.push(clonedScene);
				scene.add(clonedScene);
				if (u.position) {
					clonedScene.position.set(
						u.position.x,
						u.position.y,
						u.position.z
					);
				}
				if (u.scale) {
					clonedScene.scale.set(u.scale, u.scale, u.scale);
				}
			}
		} else {
			console.error("Can not find model", u.modelName);
		}
	}
	console.log(`Successfully instantiated ${numSuccess} units`);
};
/**
 * 启动特定网格对象的动画。在三维模型的动画数组中按名称查找动画
 * @param skinnedMesh {THREE.SkinnedMesh} 要设置动画的网格
 * @param animations {Array} 数组,包含此模型的所有动画
 * @param animationName {string} 要启动的动画的名称
 * @return {THREE.AnimationMixer} 要在渲染循环中使用的混合器
 */
function startAnimation(skinnedMesh, animations, animationName) {
	const mixer = new THREE.AnimationMixer(skinnedMesh);          
	const clip = THREE.AnimationClip.findByName(animations, animationName);
	if (clip) {
		const action = mixer.clipAction(clip);
		action.play();
	}
	return mixer;
};
function animate() {
	const UpdateDelta = clock.getDelta();
    //执行渲染操作
    renderer.render(scene, camera); 
	// 更新动画帧
    moveOnCurve();
    for (let i = 0; i < mixers.length; ++i) {
        mixers[i].update(UpdateDelta);
    }
    requestAnimationFrame(animate);
};


添加路径和模型移动

路径:用到了Three.js提供的CatmullRomCurve3:使用Catmull-Rom算法, 从一系列的点创建一条平滑的三维样条曲线。
移动:在每一帧中按照一定步长更新模型位置。
因为设置加载了三个模型,这里直接和模型一一对应起来进行路径生成

let curves = [];
const velocity = 0.002; // 影响运动速率的一个值,范围0~1,需要和渲染频率结合计算才能得到真正的速率
let progress = [1, 0.3, 0.6];// 物体运动时在运动路径的初始位置,范围0~1

function makeCurve() {
	// 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
	// 创建闭合的曲线
    const curve = [
        [new THREE.Vector3(0, 2, 0),new THREE.Vector3(-5, 2, 2),new THREE.Vector3(2, 2, -5)],
        [new THREE.Vector3(2, 0, 2),new THREE.Vector3(5, 0, 2),new THREE.Vector3(2, 0, 5)],
        [new THREE.Vector3(0, 0, -2),new THREE.Vector3(5, 0, -2),new THREE.Vector3(0, 0, 3)]
    ]
    curve.forEach((item, i) => {
        const cur = new THREE.CatmullRomCurve3(item);
        cur.curveType = "catmullrom";
        cur.closed = true;//设置是否闭环
        cur.tension = 0.5; //设置线的张力,0为无弧度折线
        curves.push(cur);
        // 为曲线添加材质在场景中显示出来,不添加到场景显示也不会影响运动轨迹,相当于一个Helper
        const points = cur.getPoints(50);
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        const material = new THREE.LineBasicMaterial({ color: 0x000000 });

        const curveObject = new THREE.Line(geometry, material);
        scene.add(curveObject);
    });	
};

// 物体沿线移动方法
function moveOnCurve() {
    let velocity = 0.001;
    let move = function (i, velocity, obj) {
	    if (progress[i] <= 1 - velocity) {
           const point = curves[i].getPointAt(progress[i]); //获取样条曲线指定点坐标,作为相机的位置
           const pointBox = curves[i].getPointAt(progress[i] + velocity); //获取样条曲线指定点坐标
           if (point.x && pointBox.x && mods !== []) {
               obj.position.set(point.x, point.y, point.z);
               // obj.lookAt(pointBox.x, pointBox.y, pointBox.z);

               let targetPos = pointBox   //目标位置点
			let offsetAngle = 0 //目标移动时的朝向偏移
			// //以下代码在多段路径时可重复执行
			let mtx = new THREE.Matrix4()  //创建一个4维矩阵
			// .lookAt ( eye : Vector3, target : Vector3, up : Vector3 ) : this,构造一个旋转矩阵,从eye 指向 target,由向量 up 定向。
			mtx.lookAt(obj.position, targetPos, obj.up) //设置朝向
			mtx.multiply(new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0, offsetAngle, 0)))
			let toRot = new THREE.Quaternion().setFromRotationMatrix(mtx)  //计算出需要进行旋转的四元数值
			obj.quaternion.slerp(toRot, 0.2)
           }
          progress[i] += velocity;
       } else {
           progress[i] = 0;
       }
   };
    mods.forEach((item, i) => {
        move(i, velocity, item);
    });
};
// moveOnCurve()需要在渲染中一直调用更新,以达到物体移动效果
function animate() {
	requestAnimationFrame(animate);
	moveOnCurve();
	renderer.render(scene, camera);
};


完整代码和实现效果

完整代码

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
	<title>My first three.js app</title>
	<style>
		body {
			margin: 0;
		}
	</style>
</head>

<body>
	<script type="importmap">
			{
				"imports": {
					"three": "./three.js-master/build/three.module.js"
				}
			}
		</script>
	<script type="module">
		import * as THREE from "three";
		import { OrbitControls } from "./three.js-master/examples/jsm/controls/OrbitControls.js";
		import { GLTFLoader } from "./three.js-master/examples/jsm/loaders/GLTFLoader.js";
        import * as SkeletonUtils from "./three.js-master/examples/jsm/utils/SkeletonUtils.js";

		let scene, camera, renderer;
		let curves = [];
        const velocity = 0.002; // 影响运动速率的一个值,范围0~1,需要和渲染频率结合计算才能得到真正的速率
        let progress = [1, 0.3, 0.6];// 物体运动时在运动路径的初始位置,范围0~1
        
        let clock = new THREE.Clock();
        let mods = [];
        let mixers = [];
        let numLoadedModels = 0;
        let MODELS = [
			{ name: "Soldier" },
		];

		// 定义要放置在场景中的模型实例、它们的位置、比例以及必须播放的动画。
		let UNITS = [
        {
				modelName: "Soldier", // 使用文件models/gltf/Soldier.glb中的3D模型
				meshName: "Scene", // 要设置动画的网格的名称,这个值在模型加载后的对象中找到
				position: { x: 0, y: 0, z: 0 }, // 在场景中放置单元的位置
				scale: 1, // 单位的缩放比例。1.0表示:使用原始尺寸,0.1表示“小10倍”等。
				animationName: "Run", // 要运行的动画的名称
			},
			{
				modelName: "Soldier", // 使用文件models/gltf/Soldier.glb中的3D模型
				meshName: "Scene", // 要设置动画的网格的名称,这个值在模型加载后的对象中找到
				position: { x: 0, y: 0, z: 0 }, // 在场景中放置单元的位置
				scale: 1, // 单位的缩放比例。1.0表示:使用原始尺寸,0.1表示“小10倍”等。
				animationName: "Walk", // 要运行的动画的名称
			},
			{
				modelName: "Soldier", // 使用文件models/gltf/Soldier.glb中的3D模型
				meshName: "Scene", // 要设置动画的网格的名称,这个值在模型加载后的对象中找到
				position: { x: 0, y: 0, z: 0 }, // 在场景中放置单元的位置
				scale: 1, // 单位的缩放比例。1.0表示:使用原始尺寸,0.1表示“小10倍”等。
				animationName: "Run", // 要运行的动画的名称
			},
		];

		// 渲染器开启阴影渲染:renderer.shadowMapEnabled = true;
		// 灯光需要开启“引起阴影”:light.castShadow = true;
		// 物体需要开启“引起阴影”和“接收阴影”:mesh.castShadow = mesh.receiveShadow = true;

		function init() {
			scene = new THREE.Scene();
			camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
			renderer = new THREE.WebGLRenderer();
			// position and point the camera to the center of the scene
			camera.position.set(5, 5, 5);
			camera.lookAt(scene.position);

			// 增加坐标系红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
			// 添加坐标系到场景中
			const axes = new THREE.AxesHelper(20);
			scene.add(axes);

			// 调整背景颜色,边界雾化
			scene.background = new THREE.Color(0xa0a0a0);
			scene.fog = new THREE.Fog(0xa0a0a0, 10, 30);

			// 半球形光源
			const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
			hemiLight.position.set(0, 10, 0);
			scene.add(hemiLight);

			// 创建一个虚拟的球形网格 Mesh 的辅助对象来模拟 半球形光源 HemisphereLight.
			const hemiLighthelper = new THREE.HemisphereLightHelper(hemiLight, 5);
			scene.add(hemiLighthelper);

			// 地面
			const mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
			mesh.rotation.x = - Math.PI / 2;
			mesh.receiveShadow = true;
			scene.add(mesh);

			// 平行光
			const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
			directionalLight.castShadow = true;
			directionalLight.shadow.camera.near = 0.5;
			directionalLight.shadow.camera.far = 50;
			directionalLight.shadow.camera.left = -10;
			directionalLight.shadow.camera.right = 10;
			directionalLight.shadow.camera.top = 10;
			directionalLight.shadow.camera.bottom = -10;
			directionalLight.position.set(0, 5, 5);
			scene.add(directionalLight);

			// 用于模拟场景中平行光 DirectionalLight 的辅助对象. 其中包含了表示光位置的平面和表示光方向的线段.
			const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
			scene.add(directionalLightHelper);

			renderer.shadowMap.enabled = true;
			renderer.setSize(window.innerWidth, window.innerHeight);
			document.body.appendChild(renderer.domElement);

			// 控制器
			const controls = new OrbitControls(camera, renderer.domElement);
		};

		function makeCurve() {
			// 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
			// 创建闭合的曲线
            const curve = [
                [new THREE.Vector3(0, 2, 0),new THREE.Vector3(-5, 2, 2),new THREE.Vector3(2, 2, -5)],
                [new THREE.Vector3(2, 0, 2),new THREE.Vector3(5, 0, 2),new THREE.Vector3(2, 0, 5)],
                [new THREE.Vector3(0, 0, -2),new THREE.Vector3(5, 0, -2),new THREE.Vector3(0, 0, 3)]
            ]
            curve.forEach((item, i) => {
                const cur = new THREE.CatmullRomCurve3(item);
                cur.curveType = "catmullrom";
                cur.closed = true;//设置是否闭环
                cur.tension = 0.5; //设置线的张力,0为无弧度折线
                curves.push(cur);
                // 为曲线添加材质在场景中显示出来,不添加到场景显示也不会影响运动轨迹,相当于一个Helper
                const points = cur.getPoints(50);
                const geometry = new THREE.BufferGeometry().setFromPoints(points);
                const material = new THREE.LineBasicMaterial({ color: 0x000000 });

                const curveObject = new THREE.Line(geometry, material);
                scene.add(curveObject);
            });	
		};

		// 物体沿线移动方法
		function moveOnCurve() {
            let velocity = 0.001;
            let move = function (i, velocity, obj) {
                if (progress[i] <= 1 - velocity) {
                    const point = curves[i].getPointAt(progress[i]); //获取样条曲线指定点坐标,作为相机的位置
                    const pointBox = curves[i].getPointAt(progress[i] + velocity); //获取样条曲线指定点坐标
                    if (point.x && pointBox.x && mods !== []) {
                        obj.position.set(point.x, point.y, point.z);
                        // obj.lookAt(pointBox.x, pointBox.y, pointBox.z);

                        let targetPos = pointBox   //目标位置点
						let offsetAngle = 0 //目标移动时的朝向偏移
						// //以下代码在多段路径时可重复执行
						let mtx = new THREE.Matrix4()  //创建一个4维矩阵
						// .lookAt ( eye : Vector3, target : Vector3, up : Vector3 ) : this,构造一个旋转矩阵,从eye 指向 target,由向量 up 定向。
						mtx.lookAt(obj.position, targetPos, obj.up) //设置朝向
						mtx.multiply(new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0, offsetAngle, 0)))
						let toRot = new THREE.Quaternion().setFromRotationMatrix(mtx)  //计算出需要进行旋转的四元数值
						obj.quaternion.slerp(toRot, 0.2)
                    }
                    progress[i] += velocity;
                } else {
                    progress[i] = 0;
                }
            };
            mods.forEach((item, i) => {
                move(i, velocity, item);
            });

		};

        /**
        * 函数用于启动队列中下一个模型的加载过程。加载过程为异步的:它发生在“后台”。
        * 所以这里不一次加载所有模型。我们加载一个,等待完成,然后加载下一个。
        * 当所有模型都被加载时,我们调用loadUnits()。
        */
		function loadModels() {
			// 在预定模型(需要加载的)MODELS数组中拿模型对象(只包含少部分信息用作加载相关)
			for (let i = 0; i < MODELS.length; ++i) {
				const m = MODELS[i];
				// 加载模型,加载成功后对模型进行复制,具体的类别个数在UNITS单元组中
				loadGltfModel(m, function () {
					++numLoadedModels;
					// 所有模型加载完成后开始进行对应的模型单元初始化(复制)
					if (numLoadedModels === MODELS.length) {
						console.log("All models loaded, time to instantiate units...");
						//
						instantiateUnits();
					}
				});
			}
		};
		/**
		 * 从GLTF文件加载三维模型。使用GLTFLoader。
		 * @param model {object} 模型配置,模型数组中的一项。它将在函数内部更新
		 * @param onLoaded {function} 加载模型时将调用的回调函数
		 */
		function loadGltfModel(model, onLoaded) {
			const loader = new GLTFLoader(); //加载器
			const modelName =
				"./static/3dmod/gltf/" + model.name + ".glb";

			loader.load(modelName, function (gltf) {
				const scene = gltf.scene;
				model.animations = gltf.animations;
				model.scene = scene;
				// 启用阴影
				gltf.scene.traverse(function (object) {
					if (object.isMesh) {
						object.castShadow = true;
					}
				});
				console.log("Done loading model", model.name);
				onLoaded();
			});
		};
		/**
		 * 按名称查找模型对象
		 * @param name
		 * @returns {object|null}
		 */
		function getModelByName(name) {
			for (let i = 0; i < MODELS.length; ++i) {
				if (MODELS[i].name === name) {
					return MODELS[i];
				}
			}
			return null;
		};
		/**
		 * 查看UNITS配置,克隆必要的3D模型场景,在场景中放置电枢和网格,并启动必要的动画
		 */
		function instantiateUnits() {
			let numSuccess = 0;

			for (let i = 0; i < UNITS.length; ++i) {
				const u = UNITS[i];
				const model = getModelByName(u.modelName);

				if (model) {
					const clonedScene = SkeletonUtils.clone(model.scene);
					if (clonedScene) {
						// 当THREE.Scene 场景已正确克隆时,找到一个模型网格并为其启动动画
						// 根据meshName拿到模型网格
						const clonedMesh = clonedScene.getObjectByName(u.meshName);
						if (clonedMesh) {
							const mixer = startAnimation(
								clonedMesh,
								model.animations,
								u.animationName
							);
							// 将动画混合器保存在列表中,在动画循环中需要使用
							mixers.push(mixer);
							numSuccess++;
						}
						// 不同的模型可以具有不同的电枢和网格配置,我们无法为单个网格对象设置位置、比例或旋转。
						// 所以,我们将其设置为整个克隆场景,然后将整个场景添加到游戏世界
						// 注意:如果GLTF文件的场景中有灯光或其他项目,这可能会对其产生影响
						mods.push(clonedScene);
						scene.add(clonedScene);
						if (u.position) {
							clonedScene.position.set(
								u.position.x,
								u.position.y,
								u.position.z
							);
						}
						if (u.scale) {
							clonedScene.scale.set(u.scale, u.scale, u.scale);
						}
					}
				} else {
					console.error("Can not find model", u.modelName);
				}
			}
			console.log(`Successfully instantiated ${numSuccess} units`);
		};
		/**
		 * 启动特定网格对象的动画。在三维模型的动画数组中按名称查找动画
		 * @param skinnedMesh {THREE.SkinnedMesh} 要设置动画的网格
		 * @param animations {Array} 数组,包含此模型的所有动画
		 * @param animationName {string} 要启动的动画的名称
		 * @return {THREE.AnimationMixer} 要在渲染循环中使用的混合器
		 */
		function startAnimation(skinnedMesh, animations, animationName) {
			const mixer = new THREE.AnimationMixer(skinnedMesh);          
			const clip = THREE.AnimationClip.findByName(animations, animationName);
			if (clip) {
				const action = mixer.clipAction(clip);
				action.play();
			}
			return mixer;
		};

		function animate() {
            const UpdateDelta = clock.getDelta();
            //执行渲染操作
            renderer.render(scene, camera); 
			// 更新动画帧
            moveOnCurve();
            for (let i = 0; i < mixers.length; ++i) {
                mixers[i].update(UpdateDelta);
            }
            requestAnimationFrame(animate);
		};

		init();
		loadModels()
		makeCurve();
		animate();
	</script>
</body>

</html>

实现效果

你可能感兴趣的:(Three.js,javascript,前端,html,three.js,动画)