three.js进阶之动画系统

news2024/11/26 13:24:21

我曾在three.js进阶之骨骼绑定文章中提到了AnimationMixer、AnimationAction等内容,其实这些应该属于Three.js的动画系统,本文就系统的介绍一下动画系统(Animation System)。

前言

一般情况下,我们很少会使用three.js的动画系统去手动创建动画——因为这真的很麻烦,更高效便捷的做法还是直接在建模软件如Blender中完成动画的制作,然后在three.js中进行播放。不过,学习了动画系统对我们还是会有帮助的,下面进入正文。

创建动画涉及三个概念:关键帧Keyframes,关键帧轨迹KeyframeTrack和动画剪辑AnimationClip

1 关键帧 Keyframes

在动画系统中最低级别的概念就是关键帧,每个关键帧由三个信息组成:时间、属性和值,举个栗子:

  • 在第0秒,position的取值为(0,0,0)
  • 在第3秒,scale的取值为(1,1,1)
  • 在第12秒,material.color为红色。

这三个关键帧分别描述特定时间的某些属性的值,不过关键帧并不指定任何特定对象。位置关键帧可用于为具有.location属性的任何对象设置动画,缩放关键帧可为具有.scale属性的任何对象设置动画,依此类推。但是,关键帧确实指定了数据类型。上面的positionscale关键帧指定矢量数据,而material.color关键帧指定颜色数据。目前,动画系统支持五种数据类型。
在这里插入图片描述
要创建动画,我们至少需要两个关键帧。最简单的例子是两个数字关键帧,例如,动画材质的不透明度(它的透明度/透视程度):

  • 在第0秒,material.opacity为0;
  • 在第3秒,material.opacity为1;

不透明度为0意味着完全不可见,不透明度为1意味着完全可见。当我们为某个对象设置了这两个关键帧后,它将会在3秒内逐渐出现。不管对象原本的透明度为多少,关键帧会覆盖其原本的值。

2 关键帧轨迹 KeyframeTrack

在Three.js中并没有表示单个关键帧的类,KeyframeTrack中包含两个数组——时间数组和取值数组,每一个关键帧就对应时间数组和取值数组中的一个值。另外,KeyframeTrack只是一个基类,不要直接使用KeyframeTrack,,针对前面提到的每种数据类型都有对应的子类,你需要根据取值的数据类型选择对应的子类:

  • BooleanKeyframeTrack
  • ColorKeyframeTrack
  • NumberKeyframeTrack
  • QuaternionKeyframeTrack
  • StringKeyframeTrack
  • VectorKeyframeTrack

2.1 NumberKeyframeTrack

使用前面的透明度关键帧的例子:

  • 在0秒时,material.opacity为0
  • 在1秒时,material.opacity为1
  • 在2秒时,material.opacity为0
  • 在3秒时,material.opacity为1
  • 在4秒时,material.opacity为0

由于透明度为数值型,因此可以使用NumberKeyframeTrack类来存储关键帧数据:

import { NumberKeyframeTrack } from "three";

const times = [0, 1, 2, 3, 4];
const values = [0, 1, 0, 1, 0];

const opacityKF = new NumberKeyframeTrack(".material.opacity", times, values);

说明:KeyframeTrack的构造函数为:

/**
 * KeyframeTrack构造函数
 * name: 关键帧轨道的名称
 * times: 关键帧时间数组,内部转换为Float32Array
 * values: 包含与时间数组相关的取值,内部转换为浮点32Array
 * interpolation: 要使用的插值类型,默认值为线性插值
 */
KeyframeTrack( name : String, times : Array, values : Array, interpolation : Constant )

2.2 VectorKeyframeTrack

由于NumberKeyframeTrack在每个时间点都只有一个数值类型的取值,因此times数组和values数组的长度是一样的,那么如果每一帧的数据是一个向量呢?应该如何构造values数组呢?我们使用下面的例子:

  • 在第0秒,position(0,0,0)
  • 在第3秒,position(2,2,2)
  • 在第6秒,position(0,0,0)
    这三个关键帧将使对象从场景的中心开始,在三秒内向右、向上和向前移动,然后反转方向并移动回中心。接下来,我们将使用这些关键帧创建矢量轨迹。
import { VectorKeyframeTrack } from "three";

const times = [0, 3, 6];
const values = [0, 0, 0, 2, 2, 2, 0, 0, 0];

const positionKF = new VectorKeyframeTrack(".position", times, values);

需要留意,由于每个时间点的position数据都是一个Vector3包含3个数值,而且这些数据是直接平铺开来的,因此values数组的长度是times数组的3倍,对应的映射关系为:

const times = [0, 3, 6];
const values = [
  0,
  0,
  0, // (x, y, z) at t = 0
  2,
  2,
  2, // (x, y, z) at t = 3
  0,
  0,
  0, // (x, y, z) at t = 6
];

3 动画剪辑 AnimationClip

如下图(动态效果可点击这里查看)中跳舞的模型动作非常复杂:双脚旋转,膝盖弯曲,手臂疯狂摆动,头部随着节拍点头。每个单独的动作都存储在一个单独的关键帧轨道中,因此有一个轨道控制舞者左脚的旋转,另一个轨道控制他的右脚的旋转,第三个轨道控制他的脖子的旋转,等等。
在这里插入图片描述

事实上,这个舞蹈动画是由53个关键帧轨道制成的,其中52个是控制舞者膝盖、肘部和脚踝等单个关节的四元数轨道。然后,有一个.location轨迹,可以在地板上来回移动图形。

这53个关键帧轨道结合在一起创建出的最终动画,我们称之为动画剪辑。因此,动画剪辑是附加到单个对象的任意数量关键帧的集合,表示剪辑的类是AnimationClip。动画剪辑可以循环播放,所以,虽然这个舞者的动画只有18秒长,但当它到达终点时,它开始下一轮的循环,因此看起来舞者似乎可以一直跳下去。

下面是AnimationClip的构造函数:

AnimationClip( name : String, duration : Number, tracks : Array )

从构造函数中能够看出动画剪辑存储三个信息:剪辑的名称、剪辑的长度和组成剪辑的轨道数组。如果我们将长度设置为-1,则轨道数组将用于计算长度。我们创建一个包含前面的单个位置轨迹的剪辑:

import { AnimationClip, VectorKeyframeTrack } from "three";

const times = [0, 3, 6];
const values = [0, 0, 0, 2, 2, 2, 0, 0, 0];

const positionKF = new VectorKeyframeTrack(".position", times, values);

// 当前只有一个关键帧轨道
const tracks = [positionKF];

// 将length设置为-1可以自动从tracks中计算长度,在本例中为6秒
const length = -1;

const clip = new AnimationClip("slowmove", length, tracks);

和关键帧一样,AnimationClip不会被附着到任何特定的对象上,那么应该如何将动画绑定到模型身上并且控制其进行播放呢?

4 动画混合器 AnimationMixer

为了让物体(如Mesh)接入动画系统并且能够动起来,我们需要将其和动画混合器AnimationMixer建立联系。场景中的每个动画对象都需要使用一个单独的混合器。混合器负责使模型按照动画剪辑的设定进行状态调整,如移动舞者的脚、手臂和臀部,或者是移动飞鸟的翅膀。

import { Mesh, AnimationMixer } from 'three';

// 创建一个静态的Mesh
const mesh = new Mesh();

// 通过将其连接到混合器,将其变为动画网格
const mixer = new AnimationMixer(mesh);

5 动画动作 AnimationAction

AnimationAction负责将动画对象连接到动画剪辑AnimationClip,同时也负责控制动画的暂停、播放、重置等操作。需要注意的是,我们不会直接创建action,而是借助AnimationMixer.clipAction()函数创建,这样能够有更好的性能,因为mixer会对action进行缓存。

看下面的例子:

import { AnimationClip, AnimationMixer } from "three";

const positionKF = new VectorKeyframeTrack(
  ".position",
  [0, 3, 6],
  [0, 0, 0, 2, 2, 2, 0, 0, 0]
);

const opacityKF = new NumberKeyframeTrack(
  ".material.opacity",
  [0, 1, 2, 3, 4, 5, 6],
  [0, 1, 0, 1, 0, 1, 0]
);

const moveBlinkClip = new AnimationClip("move-n-blink", -1, [
  positionKF,
  opacityKF,
]);

const mesh = new Mesh();

const mixer = new AnimationMixer(mesh);
const action = mixer.clipAction(moveBlinkClip);

5.1 多动作控制

假设我们有一个人的模型,并且这个模型可以走路、跑步和跳,每个动画都将在一个单独的剪辑中出现,每个剪辑必须连接到一个动作。因此,就像混合器和模型之间存在一对一的关系一样,动作和动画剪辑之间也存在一对一的关系:

const mixer = new AnimationMixer(humanModel);

const walkAction = mixer.clipAction(walkClip);
const runnAction = mixer.clipAction(runClip);
const jumpAction = mixer.clipAction(jumpClip);

下一步是选择要执行这些操作中的哪一个。你怎么做将取决于你正在建造什么样的场景。例如,如果是游戏,您将将这些操作连接到用户控件,这样当按下相应的按钮时,角色将行走、运行或跳跃。

参考资料

The three.js Animation System

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

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

相关文章

【学习视频】阅读开源工业软件和工业智能实战上线B站

图片来源:https://metrology.news/a-i-for-smarter-factories-the-world-of-industrial-artificial-intelligence/ 为了帮助大家做好工业软件以及用人工智能解决工业领域现实问题,我在B站上开了两个视频系列,一个是“一起来读开源工业软件”…

STM32 基础知识入门 (C语言基础巩固)

1、在不改变其他位的值的状况下,对某几个位进行设值 这个场景在单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作, 然后用|操作符设值。 比如我要改变 GPIOA 的 CRL 寄存器 bit6(第 6 位)的…

MiNiGPT4安装记录

装conda wget https://repo.anaconda.com/archive/Anaconda3-5.3.0-Linux-x86_64.sh chmod x Anaconda3-5.3.0-Linux-x86_64.sh ./Anaconda3-5.3.0-Linux-x86_64.sh export PATH~/anaconda3/bin:$PATH # 或者写到环境保护变量 # 不会弄看这吧 https://blog.csdn.net/wyf2017/a…

fork()创建进程原理

目录 一、写时复制技术写时复制的优点:vfork()和fork() 二、fork()原理初步再理解下页表与多进程在内存中的图像创建进程和创建线程的区别 三、fork()的具体过程 一、写时复制技术 fork()生成子进程时,只是把虚拟地址拷贝给子进程,也就是父进…

( 字符串) 205. 同构字符串 ——【Leetcode每日一题】

❓205. 同构字符串 难度:简单 给定两个字符串 s 和 t ,判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同…

网络基础:socket套接字

文章目录 1. 前导知识1.1 源MAC地址和目的MAC地址1.2 源IP地址和目的IP地址1.3 MAC地址和IP地址的配合1.4 源端口号和目的端口号1.5 Socket1.6 UCP协议和TCP协议1.7 网络字节序高低位高低地址大端和小端网络字节序常用转换函数 2. socket 网络编程2.1 socket 常见接口创建套接字…

ChatGPT最好用的连接-自动写文案-代码算法最佳选择

ChatGPT最好用的连接-自动写文案-代码算法最佳选择 最近测试了很多国内分享的ChatGPT,很多都是限制最多写200文字,超过200个文字就不显示了。或者有的写出的文章逻辑性不对,写的算法不能正常运行。 经过多天的搜索测试,最终确定…

某电商客户数据价值分析项目

目录 一、项目意义 二、项目流程 三、项目内容 1、导入数据 2、数据预处理 3、单变量分析 4、聚类分析—Kmeans算法 一、项目意义 客户价值分析就是一个客户分群问题,以客户为中心,从客户需求出发,搞清楚客户需要什么,他们…

Linux进程通信:进程组 会话

1. 进程组 (1)概念:一个或多个进程的集合,也称为“作业”。 (2)父进程创建子进程时,默认属于同一个进程组。进程组ID为组长进程ID。 (3)进程组中只要有一个进程存在&a…

unity中的Line Renderer

介绍 unity中的Line Renderer 方法 首先,Line Renderer 是 Unity 引擎中的一个组件,它可以生成直线、曲线等形状,并且在场景中呈现。通常情况下,Line Renderer 被用来实现轨迹、路径、线框渲染以及射线可视化等功能。 在使用 …

imx6ull开发板环境配置 - libusb、libudev、eudev交叉编译

目录 零、前言 一、libusb交叉编译 1.0 前言 1.1 交叉编译 二、usbutils交叉编译 2.0 前言 2.1 交叉编译 三、libudev交叉编译 3.0 前言 3.1 交叉编译 3.2 错误处理-没找到usbutils 3.3 错误处理-没找到pci.ids (pci.ids not found) 3.3.0 前…

【数据库】索引与事务

目录 1、索引 1.1、概念 1.2、索引的作用 1.3、 索引的缺点 1.4、数据库中实现索引的数据结构 1.4.1、B树/B-树 1.4.2、B树 1.4.3、回表 1.5、使用场景 1.6、索引的使用 1.6.1、查看索引 1.6.2、创建索引 1.6.3、 删除索引 1.7、索引的分类 2、事务 2.1、为什…

Arduino ESP8266基于ESPAsyncWebServer 网页GPIO控制

Arduino ESP8266基于ESPAsyncWebServer 网页GPIO控制 📍相关篇《Arduino ESP8266利用AJAX局部动态更新网页内容》 📺控制页面演示: 🌿在手机上可以通过接入ESP8266的WIFI,通过浏览器方位192.168.4.1进行网页页面操控引脚以及查看esp8266信息。 ✨本项目是基于github上…

[oeasy]python0143_主控程序_main

主控程序 回忆上次内容 上次把 apple.py 拆分成了 输入主函数 引用模块中变量的时候 要带上包(module)名 get_fruits.aget_fruits.b 最终 拆分代码 成功! 可以将程序 再拆分成 输入输出 然后 再由主函数调用吗?🤔 建立主控 新建一个 ma…

【Java笔试强训 10】

🎉🎉🎉点进来你就是我的人了博主主页:🙈🙈🙈戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔🤺🤺🤺 目录 一、选择题 二、编程题 🔥井字棋 …

大数据技术之大数据概论

第1章 大数据概念 大数据(Big Data): 指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产 大数据主要解决,海量数据的采集、存…

【吴恩达推荐】《ChatGPT Prompt Engineering for Developers》- 知识点目录

《ChatGPT Prompt Engineering for Developers》 1 Introduction 2 Guidelines Principle 1: Write clear and specific instructions Tactic 1: Use delimiters Tactic 3: “If-statement” Check whether conditions are satisfiedCheck assumptions required to do the …

RDD的Stage划分原理

1. 什么是RDD RDD(Resilient Distributed Dataset)叫做分布式数据集,是Spark 中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。在Spark 中,对数据的所有操作不外乎创建RDD、转化已有RDD 以…

JavaBeaneljstl

1.JavaBean 1.1 什么是JavaBean JavaBean 是一种JAVA语言写成的可重用组件。为写成JavaBean,类必须是具体的和公共的,并且具有无参数的构造器 简单一点:建一个类,给一个无参的构造方法. 它就是JavaBean,对应JavaBean来说&#x…

【C++】程序员的屠龙母鸡:二叉树进阶OJ题详解

不会自动生成,还是我自己写目录吧 -.- 文章目录 前言一、稍微简单一点的二叉树OJ题二、相对困难一点的二叉树OJ题总结 前言 在看这篇文章前希望大家是学过二叉树的,不然理解起来可能会比较费劲,但我会尽自己的努力让大家学会这些题&#xf…