Three.js基础入门介绍——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 scenecamera.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基础入门介绍——Three.js学习四【模型导入】中有相对详细的介绍。

动画实现基本流程:

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

import { GLTFLoader } from "./three.js-master/examples/jsm/loaders/GLTFLoader.js";let model = null;	//用于模型移动
let clock = new THREE.Clock(); // 用于clock.getDelta()
let mixer; 
// 加载器加载完成后添加了动画设置
function loadModel() {// 加载模型并开启阴影和接受阴影const gltfLoader = new GLTFLoader();gltfLoader.setPath('./static/3dmod/gltf/').load('Soldier.glb', function (gltf) {gltf.scene.rotation.y = Math.PI;console.log("gltf", gltf)gltf.scene.scale.set(1, 1, 1)gltf.scene.traverse(function (object) {if (object.isMesh) {object.castShadow = true; //阴影object.receiveShadow = true; //接受别人投的阴影}});// 使用动画混合器及配置mixer = startAnimation(gltf.scene,gltf.animations,gltf.animations[1].name // animationName,这里是"Run");scene.add(gltf.scene);model = gltf.scene;}, function (res) {// console.log(res.total, res.loaded)});};/*** 启动特定网格对象的动画。在三维模型的动画数组中按名称查找动画* @param skinnedMesh {THREE.SkinnedMesh} 要设置动画的网格* @param animations {Array} 数组,包含此模型的所有动画* @param animationName {string} 要启动的动画的名称* @return {THREE.AnimationMixer} 要在渲染循环中使用的混合器*/
function startAnimation(skinnedMesh, animations, animationName) {const m_mixer = new THREE.AnimationMixer(skinnedMesh);          const clip = THREE.AnimationClip.findByName(animations, animationName);if (clip) {const action = m_mixer.clipAction(clip);action.play();}return m_mixer;
};function animate() {requestAnimationFrame(animate);// 更新动画帧if(mixer){mixer.update(clock.getDelta());}renderer.render(scene, camera);
};

添加路径和模型移动
路径:用到了Three.js提供的CatmullRomCurve3:使用Catmull-Rom算法, 从一系列的点创建一条平滑的三维样条曲线。
移动:在每一帧中按照一定步长更新模型位置。

let curve = null; // 存放路径对象
let progress = 0; // 物体运动时在运动路径的初始位置,范围0~1
const velocity = 0.002; // 影响运动速率的一个值,范围0~1,需要和渲染频率结合计算才能得到真正的速率function makeCurve() {//Create a closed wavey loopcurve = new THREE.CatmullRomCurve3([new THREE.Vector3(0, 0, 0),new THREE.Vector3(5, 0, 0),new THREE.Vector3(0, 0, 5)]);curve.curveType = "catmullrom";curve.closed = true;//设置是否闭环curve.tension = 0.5![请添加图片描述](https://img-blog.csdnimg.cn/12a2fa45062d44a58bb7cbf719e4b20f.gif)
; //设置线的张力,0为无弧度折线// 为曲线添加材质在场景中显示出来,不显示也不会影响运动轨迹,相当于一个Helperconst points = curve.getPoints(50);const geometry = new THREE.BufferGeometry().setFromPoints(points);const material = new THREE.LineBasicMaterial({ color: 0x000000 });// Create the final object to add to the sceneconst curveObject = new THREE.Line(geometry, material);scene.add(curveObject)
}// 物体沿线移动方法
function moveOnCurve() {if (curve == null || model == null) {console.log("Loading")} else {if (progress <= 1 - velocity) {const point = curve.getPointAt(progress); //获取样条曲线指定点坐标const pointBox = curve.getPointAt(progress + velocity); //获取样条曲线指定点坐标if (point && pointBox) {model.position.set(point.x, point.y, point.z);// model.lookAt(pointBox.x, pointBox.y, pointBox.z); //因为这个模型加载进来默认面部是正对Z轴负方向的,所以直接lookAt会导致出现倒着跑的现象,这里用重新设置朝向的方法来解决。var targetPos = pointBox   //目标位置点var offsetAngle = 0 //目标移动时的朝向偏移// //以下代码在多段路径时可重复执行var mtx = new THREE.Matrix4()  //创建一个4维矩阵// .lookAt ( eye : Vector3, target : Vector3, up : Vector3 ) : this,构造一个旋转矩阵,从eye 指向 target,由向量 up 定向。mtx.lookAt(model.position, targetPos, model.up) //设置朝向mtx.multiply(new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0, offsetAngle, 0)))var toRot = new THREE.Quaternion().setFromRotationMatrix(mtx)  //计算出需要进行旋转的四元数值model.quaternion.slerp(toRot, 0.2)}progress += velocity;} else {progress = 0;}}};
// 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";let scene, camera, renderer;let curve = null, model = null; let clock = new THREE.Clock();let mixer;let progress = 0; // 物体运动时在运动路径的初始位置,范围0~1const velocity = 0.002; // 影响运动速率的一个值,范围0~1,需要和渲染频率结合计算才能得到真正的速率// 渲染器开启阴影渲染: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 scenecamera.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 loadModel() {// 加载模型并开启阴影和接受阴影const gltfLoader = new GLTFLoader();gltfLoader.setPath('./static/3dmod/gltf/').load('Soldier.glb', function (gltf) {gltf.scene.rotation.y = Math.PI;console.log("gltf\ngltf", gltf)gltf.scene.scale.set(1, 1, 1)gltf.scene.traverse(function (object) {if (object.isMesh) {object.castShadow = true; //阴影object.receiveShadow = true; //接受别人投的阴影}});// 使用动画混合器及配置mixer = startAnimation(gltf.scene,gltf.animations,gltf.animations[1].name // animationName,这里是"Run");scene.add(gltf.scene);model = gltf.scene;}, function (res) {// console.log(res.total, res.loaded)});};function makeCurve() {// 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.// Create a closed wavey loopcurve = new THREE.CatmullRomCurve3([new THREE.Vector3(0, 0, 0),new THREE.Vector3(5, 0, 0),new THREE.Vector3(0, 0, 5)]);curve.curveType = "catmullrom";curve.closed = true;//设置是否闭环curve.tension = 0.5; //设置线的张力,0为无弧度折线// 为曲线添加材质在场景中显示出来,不添加到场景显示也不会影响运动轨迹,相当于一个Helperconst points = curve.getPoints(50);const geometry = new THREE.BufferGeometry().setFromPoints(points);const material = new THREE.LineBasicMaterial({ color: 0x000000 });// Create the final object to add to the sceneconst curveObject = new THREE.Line(geometry, material);scene.add(curveObject)};// 物体沿线移动方法function moveOnCurve() {if (curve == null || model == null) {console.log("Loading")} else {if (progress <= 1 - velocity) {const point = curve.getPointAt(progress); //获取样条曲线指定点坐标const pointBox = curve.getPointAt(progress + velocity); //获取样条曲线指定点坐标if (point && pointBox) {model.position.set(point.x, point.y, point.z);// model.lookAt(pointBox.x, pointBox.y, pointBox.z);//因为这个模型加载进来默认面部是正对Z轴负方向的,所以直接lookAt会导致出现倒着跑的现象,这里用重新设置朝向的方法来解决。var targetPos = pointBox   //目标位置点var offsetAngle = 0 //目标移动时的朝向偏移// //以下代码在多段路径时可重复执行var mtx = new THREE.Matrix4()  //创建一个4维矩阵// .lookAt ( eye : Vector3, target : Vector3, up : Vector3 ) : this,构造一个旋转矩阵,从eye 指向 target,由向量 up 定向。mtx.lookAt(model.position, targetPos, model.up) //设置朝向mtx.multiply(new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0, offsetAngle, 0)))var toRot = new THREE.Quaternion().setFromRotationMatrix(mtx)  //计算出需要进行旋转的四元数值model.quaternion.slerp(toRot, 0.2)}progress += velocity;} else {progress = 0;}}};/*** 启动特定网格对象的动画。在三维模型的动画数组中按名称查找动画* @param skinnedMesh {THREE.SkinnedMesh} 要设置动画的网格* @param animations {Array} 数组,包含此模型的所有动画* @param animationName {string} 要启动的动画的名称* @return {THREE.AnimationMixer} 要在渲染循环中使用的混合器*/function startAnimation(skinnedMesh, animations, animationName) {const m_mixer = new THREE.AnimationMixer(skinnedMesh);          const clip = THREE.AnimationClip.findByName(animations, animationName);if (clip) {const action = m_mixer.clipAction(clip);action.play();}return m_mixer;};function animate() {requestAnimationFrame(animate);// 更新动画帧if(mixer){mixer.update(clock.getDelta());}moveOnCurve();renderer.render(scene, camera);};init();loadModel();makeCurve();animate();</script>
</body></html>

实现效果
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/747067.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谁用过腾讯云轻量应用服务器2核2G3M配置,支持多少人在线?

腾讯云轻量应用服务器2核4G5M配置一年优惠价165元、252元15个月、三年756元&#xff0c;100%CPU性能&#xff0c;5M带宽下载速度640KB/秒&#xff0c;60GB SSD系统盘&#xff0c;月流量500GB&#xff0c;折合每天16.6GB流量&#xff0c;超出月流量包的流量按照0.8元每GB的价格支…

常用芯片学习——TP4057电源管理芯片

TP40578 500mA线性锂离子电池充电器 芯片介绍 TP4057是一款性能优异的单节锂离子电池恒流/恒压线性充电器。TP4057采用S0T23-6封装配合较少的外围原件使其非常适用于便携式产品&#xff0c;并且适合给USB电源以及适配器电源供电。 基于特殊的内部MOSFET架构以及防倒充电路&a…

Python实现一笔画游戏

Python实现一笔画游戏 关于一笔画介绍可参见“HTML5实现一笔画游戏”https://blog.csdn.net/cnds123/article/details/136669088 在Python中&#xff0c;Tkinter是一个广泛使用的标准GUI库&#xff0c;我们将使用它来实现这个游戏。 先给出效果图&#xff1a; 连接线段时&am…

城乡居民基本医疗信息管理系统|基于Springboot的城乡居民基本医疗信息管理系统设计与实现(源码+数据库+文档)

城乡居民基本医疗信息管理系统目录 目录 基于Springboot的城乡居民基本医疗信息管理系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、病例管理 2、医院资讯信息管理 3、医院资讯类型管理 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选…

微信小程序开发学习笔记《21》uni-app框架-楼层图片跳转

微信小程序开发学习笔记《21》uni-app框架-楼层图片跳转 博主正在学习微信小程序开发&#xff0c;希望记录自己学习过程同时与广大网友共同学习讨论。建议仔细阅读uni-app对应官方文档 一、创建新的分包goods_list 二、将请求到的楼层数据url调整为本地的 可以看到上图是请求…

关于固件的简单解释

我不喜欢等人也不喜欢被别人等——赤砂之蝎 简而言之 固件是什么 固件&#xff08;Firmware&#xff09;是一种软件类型&#xff0c;它是嵌入式系统中的一部分&#xff0c;通常存储在设备的非易失性存储器中&#xff0c;如闪存或ROM&#xff08;只读存储器&#xff09;。与操作…

libevent中bufferevent事件及常用的API函数

自带buffer的事件-bufferevent bufferevent实际上也是一个event&#xff0c;只不过比普通的event高级&#xff0c;他的内部有两个缓冲区&#xff0c;以及一个文件描述符&#xff08;网络套接字&#xff09;。一个网络套接字有读写两个缓冲区&#xff0c;bufferevent同样也带有…

探索仿函数(Functor):C++中的灵活函数对象

文章目录 一、仿函数定义及使用二、仿函数与函数指针的区别三、仿函数与算法的关系四、仿函数的实践用例 在C编程中&#xff0c;我们经常需要对数据进行排序、筛选或者其他操作。为了实现这些功能&#xff0c;C标准库提供了许多通用的算法和容器&#xff0c;而其中一个重要的概…

nut-ui组件库icon中使用阿里图标

1.需求 基本每个移动端组件库都有组件 icon组件 图标组件、 但是很多组件库中并找不到我们需要的图标 这时候 大家有可能会找图标库 最大众的就是iconfont的图标了 2.使用 有很多方式去使用这个东西 比如将再限链接中的css引入 在使用 直接下载图标 symbol 方式 等....…

【NR 定位】3GPP NR Positioning 5G定位标准解读(十三)-DL-AoD定位

前言 3GPP NR Positioning 5G定位标准&#xff1a;3GPP TS 38.305 V18 3GPP 标准网址&#xff1a;Directory Listing /ftp/ 【NR 定位】3GPP NR Positioning 5G定位标准解读&#xff08;一&#xff09;-CSDN博客 【NR 定位】3GPP NR Positioning 5G定位标准解读&#xff08;…

buuctf warmup 超详细

目录 1.代码审计&#xff1a; 2.逻辑分析 3.总结分析 4.分析记录 5.疑点解答 1.代码审计&#xff1a; <?phphighlight_file(__FILE__);class emmm //定义了一个类{public static function checkFile(&$page) 类里面又申明创建…

Android 架构师研发技术进阶之路:不同阶段需要掌握的那些技术及软技能

资深 而到了资深层次&#xff0c;技术栈已经不再是阻碍。能够从更高层面看待问题&#xff0c;理解整个系统的设计&#xff0c;作为系统架构师的角色存在。 1. 理解微服务、SOA思想&#xff0c;对于后端开发有一定涉猎。 2. 了解前端研发工具和思想&#xff0c;知道vue react…

centos破解root密码以及如何防止他人破解root密码

目录 破解root密码 服务器重启 1.再重启页面上下选择第一个按e进入内核编辑模式 2.找到linux16开头的一行&#xff0c;光标移动到最后添加 init/bin/sh Ctrlx 保存 3.进入单用户模式 4.重新挂在根分区 5.关闭selinux 6.更新密码 passwd 7.在根分区下面创建一个隐藏文件…

【C语言步行梯】一维数组、二维数组介绍与应用详谈

&#x1f3af;每日努力一点点&#xff0c;技术进步看得见 &#x1f3e0;专栏介绍&#xff1a;【C语言步行梯】专栏用于介绍C语言相关内容&#xff0c;每篇文章将通过图片代码片段网络相关题目的方式编写&#xff0c;欢迎订阅~~ 文章目录 为什么要有数组&#xff1f;一维数组数组…

uni-app微信小程序上拉加载,下拉刷新

pages.json配置官网链接 onPullDownRefresh、onReachBottom函数跟生命周期同级 data() {return {orderList:[],total: null, //总共多少条数据page: 1,pageSize: 10,} }, onLoad() {}, mounted(){this.getInfo() }, methods:{getInfo(){API.getListxxx().then(res > {const…

探索TikTok云手机在社交媒体营销的作用

近年来&#xff0c;TikTok作为全球短视频平台之一&#xff0c;其用户基数呈现持续增长的趋势。伴随社交媒体的蓬勃发展&#xff0c;企业和个人纷纷涌入TikTok平台&#xff0c;追求更广泛的曝光和用户互动。为满足这一需求&#xff0c;TikTok云手机应运而生。本文将深度剖析TikT…

蓝桥杯[OJ 1621]挑选子串-CPP-双指针

目录 一、题目描述&#xff1a; 二、整体思路&#xff1a; 三、代码&#xff1a; 一、题目描述&#xff1a; 二、整体思路&#xff1a; 要找子串&#xff0c;则必须找头找尾&#xff0c;找头可以遍历连续字串&#xff0c;找尾则是要从头的基础上往后遍历&#xff0c;可以设头…

【JS逆向学习】猿人学第六题 js混淆 回溯

逆向目标 网址&#xff1a;https://match.yuanrenxue.cn/match/6接口&#xff1a;https://match.yuanrenxue.cn/api/match/6参数&#xff1a;payload(m、q) 逆向过程 老规矩&#xff0c;先来分析网络请求&#xff0c;加密的地方一目了然&#xff0c;没什么可多说的&#xff…

【阿里云系列】-部署ACK集群的POD应用日志如何集成到日志服务(SLS)中

介绍 我们在实际部署应用到阿里云的ACK集群后&#xff0c;由于后期应用服务的持续维护诉求可能需要跟踪排查问题&#xff0c;此时就要具备将应用的历史日志存档便于后期排查问题 处理方式 为了解决以上的普遍需求&#xff0c;需要将ACK中的应用日志采集到SLS的Logstore中,然…

通付盾Web3专题 | SharkTeam:2023年加密货币犯罪分析报告

2023年&#xff0c;Web3行业共经历了940多起大大小小的安全事件&#xff0c;同比2022年增长了超过50%&#xff0c;损失金额达到17.9亿美元。其中&#xff0c;第三季度发生的安全事件最多&#xff08;360起&#xff09;&#xff0c;损失最大&#xff08;7.4亿美元&#xff09;&a…