(function() {
// Set our main variables
let scene,
renderer,
camera,
model, // Our character
neck, // Reference to the neck bone in the skeleton
waist, // Reference to the waist bone in the skeleton
possibleAnims, // Animations found in our file
mixer, // THREE.js animations mixer
idle, // Idle, the default state our character returns to
clock = new THREE.Clock(), // Used for anims, which run to a clock instead of frame rate
currentlyAnimating = false, // Used to check whether characters neck is being used in another anim
raycaster = new THREE.Raycaster(), // Used to detect the click on our character
loaderAnim = document.getElementById('js-loader');
})(); // Don't add anything below this line
// Add lights
let hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.61);
hemiLight.position.set(0, 50, 0);
// Add hemisphere light to scene
scene.add(hemiLight);
let d = 8.25;
let dirLight = new THREE.DirectionalLight(0xffffff, 0.54);
dirLight.position.set(-8, 12, 8);
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
dirLight.shadow.camera.near = 0.1;
dirLight.shadow.camera.far = 1500;
dirLight.shadow.camera.left = d * -1;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = d * -1;
// Add directional Light to scene
scene.add(dirLight);
环境光为强度 0.61 的白光,然后将其放置在中心点上方 50 单位。你也可以在后续尝试更改数值。
我根据个人感觉将定向光放置在一个适当的位置。随后,启用其投射阴影的能力并设置了阴影的分辨率。阴影的其余设置则与光的视场相关(译者注:定向光是使用正交摄像机计算阴影,参考 DirectionalLightShadow),这概念对我来说也有些模糊,但只要清晰知道:可通过调整变量 d 以确保阴影不被裁剪。
与此同时,在 init 函数内添加地板:
// Floor
let floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);
let floorMaterial = new THREE.MeshPhongMaterial({
color: 0xeeeeee,
shininess: 0,
});
let floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -0.5 * Math.PI; // This is 90 degrees by the way
floor.receiveShadow = true;
floor.position.y = -11;
scene.add(floor);
var loader = new THREE.GLTFLoader();
loader.load(
MODEL_PATH,
function(gltf) {
// A lot is going to happen here
},
undefined, // We don't need this function
function(error) {
console.error(error);
}
);
请注意注释“A lot is going to happen here”,这里是模型加载后会执行的地方。除非特别声明,否则接下来所有东西都放在该函数内。
model = gltf.scene;
let fileAnimations = gltf.animations;
scene.add(model);
至此,完整的 loader.load 函数如下:
loader.load(
MODEL_PATH,
function(gltf) {
// A lot is going to happen here
model = gltf.scene;
let fileAnimations = gltf.animations;
scene.add(model);
},
undefined, // We don't need this function
function(error) {
console.error(error);
}
);
let stacy_txt = new THREE.TextureLoader().load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/stacy.jpg');
stacy_txt.flipY = false; // we flip the texture so that its the right way up
const stacy_mtl = new THREE.MeshPhongMaterial({
map: stacy_txt,
color: 0xffffff,
skinning: true
});
// We've loaded this earlier
var loader = new THREE.GLTFLoader()
在地板代码下方(即 init() 函数的最后一行代码),添加一个圆符。这是一个很大但远离我们的 3D 球体,并使用 BasicMaterial 材质。该材质不具备先前使用的 PhongMaterial 材质所拥有的光泽和投射并接收阴影的特性。因此,它在该场景中能作为一个平面圆,很好地衬托着 Stacy。
let geometry = new THREE.SphereGeometry(8, 32, 32);
let material = new THREE.MeshBasicMaterial({ color: 0x9bffaf }); // 0xf2ce2e
let sphere = new THREE.Mesh(geometry, material);
sphere.position.z = -15;
sphere.position.y = -2.5;
sphere.position.x = -0.25;
scene.add(sphere);
可以改成你喜欢的颜色!
Part 4:赋予 Stacy 生气
在进入本节主题前,你可能注意到 Stacy 的加载需要一段时间。显然,白屏对用户并不友好。我曾提及到:在 HTML 中我们有一个 loading 元素被注释。现在回到那里取消这个注释。
<!-- The loading element overlays everything else until the model is loaded, at which point we remove this element from the DOM -->
<div class="loading" id="js-loader"><div class="loader"></div></div>
function getMouseDegrees(x, y, degreeLimit) {
let dx = 0,
dy = 0,
xdiff,
xPercentage,
ydiff,
yPercentage;
let w = { x: window.innerWidth, y: window.innerHeight };
// Left (Rotates neck left between 0 and -degreeLimit)
// 1. If cursor is in the left half of screen
if (x <= w.x / 2) {
// 2. Get the difference between middle of screen and cursor position
xdiff = w.x / 2 - x;
// 3. Find the percentage of that difference (percentage toward edge of screen)
xPercentage = (xdiff / (w.x / 2)) * 100;
// 4. Convert that to a percentage of the maximum rotation we allow for the neck
dx = ((degreeLimit * xPercentage) / 100) * -1; }
// Right (Rotates neck right between 0 and degreeLimit)
if (x >= w.x / 2) {
xdiff = x - w.x / 2;
xPercentage = (xdiff / (w.x / 2)) * 100;
dx = (degreeLimit * xPercentage) / 100;
}
// Up (Rotates neck up between 0 and -degreeLimit)
if (y <= w.y / 2) {
ydiff = w.y / 2 - y;
yPercentage = (ydiff / (w.y / 2)) * 100;
// Note that I cut degreeLimit in half when she looks up
dy = (((degreeLimit * 0.5) * yPercentage) / 100) * -1;
}
// Down (Rotates neck down between 0 and degreeLimit)
if (y >= w.y / 2) {
ydiff = y - w.y / 2;
yPercentage = (ydiff / (w.y / 2)) * 100;
dy = (degreeLimit * yPercentage) / 100;
}
return { x: dx, y: dy };
}
// Get a random animation, and play it
function playOnClick() {
let anim = Math.floor(Math.random() * possibleAnims.length) + 0;
playModifierAnimation(idle, 0.25, possibleAnims[anim], 0.25);
}
原文:How to Create an Interactive 3D Character with Three.js
在本长篇教程中,你将学会如何创建一个头部朝向鼠标和点击执行随机动画的交互式 3D 模型。
你是否曾经拥有一个展示职业生涯的个人网站,并且里面放着一张个人照片?最近我想更进一步,往里面添加一个完全交互式 3D 版本的自己,它能注视用户的光标。当然这还不够,你甚至可以点击“我”,然后我会作出动作进行响应。本篇教程将讲述如何基于名为 Stacy 的模型实现这件事。
以下就是体验案例(点击 Stacy,同时移动鼠标观察它的动作)。
由于是基于 Three.js 实现,我假设你已掌握了 JavaScript。
See the Pen Character Tutorial - Final by Kyle Wetton (@kylewetton) on CodePen.
该 模型 带有 10 个动画。而在本教程的最后一节,我将会阐述如何为模型添加多个动画。简言而之,模型是基于 Blender,动画是来自 Adobe 的免费动画网站——Mixamo。
Part 1:初始化项目 HTML、CSS
以下这个 pen(译者注:CodePen 的一个实例)包含了项目所有的 HTML 和 CSS。你可以 Fork 这个 pen 或从这里复制 HTML 和 CSS 到一个新项目。
See the Pen Character Tutorial - Blank by Kyle Wetton (@kylewetton) on CodePen.
HTML 含有一个加载动画(目前已注释,需要时再恢复)、一个包装(wrapper)div 和最重要的 canvas 标签。该 canvas 是 Three.js 拿来渲染场景的,另外 CSS 将其设为视口 100% 宽高大小。在 HTML 底部加载了两个依赖:Three.js 和 GLTFLoader(GLTF 是本教程引用的 3D 模型格式)。当然,这两个依赖都可作为 npm 模块使用。
CSS 含有一小部分“居中”样式,其余是 loading 动画。现在,你可以折叠 HTML 和 CSS 代码,我会在需要的时候再深入讲解。
Part 2:构建场景(Scene)
在 上一篇教程(译文:《【译】基于 Three.js 实现 3D 模型换肤》),我的做法是在用到全局变量时再回到文件顶部添加。而这次,我要把所有这些都预先定义,在需要时再讲解它们的作用。当然,每行都带有注释以满足你的好奇心。将这些全局变量放在一个函数内:
初始化 Three.js 的工作包含场景(scene)、渲染器(renderer)、摄像机(camera)、光(lights)和一个更新函数(每帧执行)。
以上这些工作都在
init()
函数内完成。在声明变量后(仍在函数作用域内)添加该初始化函数:在初始化函数内,先引用 canvas 元素和声明背景色(淡灰色)。需要注意的是,Three.js 不能使用字符串格式的颜色值,如
'#f1f1f1'
,而使用十六机制的整数,如0xf1f1f1
。接着,创建场景,并设置背景色和添加雾化效果。但在本教程中,你并不能看出有雾化效果,因为地板和背景色是一致的。若两者不一致,则能明显看到雾化的模糊效果。
接着是渲染器(renderer),向渲染器的构造函数传入 canvas 引用和其它可选项。这里唯一一个可选项是启用抗齿距。另外,启用了
shadowMap
,使得人物对象能投射阴影;基于设备设置了像素比,使得移动端的渲染效果更清晰,否则 canvas 会在高分度屏幕上呈现像素化。最后,将渲染器添加到document.body
(译者注:此行代码可省略)。这就完成了 Three.js 初始化工作的前两个。接下来是摄像机(camera)。创建一个透视摄像机,并设置其视场(field of view, fov)为 50,横纵向比例为视口宽高比,默认的前后边界裁剪区域。然后,将其往后 30 个单位和往下 3 个单位位移。后续你会明白为何这么做。这些参数都可以尝试更改,但建议目前就使用这些参数。
scene、renderer 和 camera 变量均已在项目顶部声明。
缺少光,摄像机就不能看到任何东西。那就现在创建两个光——环境光和定向光。然后,通过
scene.add(light)
将它们加到场景中。将光相关的代码放在摄像机下方,后面我会解释这具体做了什么:
环境光为强度 0.61 的白光,然后将其放置在中心点上方 50 单位。你也可以在后续尝试更改数值。
我根据个人感觉将定向光放置在一个适当的位置。随后,启用其投射阴影的能力并设置了阴影的分辨率。阴影的其余设置则与光的视场相关(译者注:定向光是使用正交摄像机计算阴影,参考 DirectionalLightShadow),这概念对我来说也有些模糊,但只要清晰知道:可通过调整变量
d
以确保阴影不被裁剪。与此同时,在 init 函数内添加地板:
首先,创建一个二维平面,它足够大:5000 个单位(确保无缝背景)。
然后创建一个材质(整篇教程中,我们只创建了两种不同的材质),并将它与几何图形结合为网格(mesh),最后将该网格添加到场景中。该网格足够大,被平放作为地面。网格的颜色是
0xeeeeee
,虽然比背景色稍暗,但在灯光的作用下,与不受灯光影响的背景融为一体。地板是由几何图形和材质结合而成的网格。通读一下我们刚添加的代码,我想你会发现一切都是不言自明。为了配合后续添加的人物模型,我们将地板向下移动 11 个单位。
这就是
init()
函数目前的内容。Three.js 应用一般都会依赖于一个每帧都会执行的更新函数,如果你有涉猎过 Unity,那么它与游戏引擎的工作方式类似。该函数需要放在 init() 函数后,而不是其内部。在更新函数内,renderer 会渲染摄像机下的场景,并立刻再次调用自身。
场景由此正式打开。canvas 目前看到的是亮灰色,实际是背景和地板。你可以更改地板的材质颜色为
0xff0000
进行测试,但记得改回来哦。我们将在下一节加载模型。在此之前,还需要为场景做一件事。canvas 作为一个 HMTL 元素,其 CSS 属性 width 和 height 均被设为 100%,这使得它能基于其容器良好地适配尺寸大小。但场景也需要同步调整大小以保持比例。因此,在调用 update 函数下方(非其定义内部)添加这个功能。其所做的事情是:不断检查 renderer 的尺寸是否与 canvas 相等,若不等则设置 renderer 的尺寸,最后返回布尔值变量
needResize
(译者注:建议通过监听 window resize 事件处理)。在 update 函数内找到这几行代码:
在这几行代码的上方,我们会调用该函数以检查是否需要调整大小,并在需要时更新摄像机的横纵向比例以适应新尺寸。
完整的 update 函数如下:
至此,我们整个项目如下。下一节是加载模型。
See the Pen Character Tutorial - Round 1 by Kyle Wetton (@kylewetton) on CodePen.
Part 3:添加模型
尽管场景目前十分空旷,但该有的配置都准备好了,如自适应大小、光和摄像机。现在就开始添加模型吧。
在 init() 函数顶部的 canvas 变量前引用模型。这是 GLTF 格式(.glb),尽管 Three.js 支持多种 3D 模型格式,但这是推荐的格式。我们将使用 GLTFLoader 加载模型。
在 init() 函数的 camera 下方,创建一个 loader:
然后使用该 loader 的 load 方法,它接受 4 个参数,分别是:模型路径、模型加载成功后的回调函数、模型加载中的回调函数、报错的回调函数。
请注意注释“A lot is going to happen here”,这里是模型加载后会执行的地方。除非特别声明,否则接下来所有东西都放在该函数内。
GLTF 文件本身(即传入该回调函数的形参
gltf
)由两部分组成,场景(gltf.scene,【译者注:即模型】)和动画(gltf.animations)。在该函数顶部引用它们,并将该模型添加到场景中:至此,完整的 loader.load 函数如下:
注意:
model
变量早已在项目顶部声明。现在你会看到场景中有一个小人物。
将模型添加到场景前,我们需要做几件事。
首先,使用模型的
traverse
方法遍历所有网格(mesh)以启用投射和接收阴影的能力。该操作需要在scene.add(model)
前完成。然后,将模型在原来大小的基础上放大 7 倍。该操作在
traverse
方法下方添加:最后,将模型向下移动 11 个单位,以保证它是站在地板上的。
完美,我们已成功加载模型。接着,我们加载并应用纹理。该模型带有纹理,并在 Blender 中已对模型进行贴图(map)。该过程被称为
UV mapping
。你可以下载该图片进行观察,如果你想尝试制作属于自己的模型,可以学习更多关于UV mapping
的知识。之前我们已声明 loader 变量;在该声明的上方创建一个新纹理和材质:
纹理不仅是一张图片的 URL,它要作为一个新纹理,需要通过 TextureLoader 加载。我们将其赋值给
stacy_txt
变量。在前面,我们已使用过材质。这个颜色为
0xeeeeee
的材质被用于地板。在这里,我们将为模型的材质使用一些新选项:1. 将stacy_txt
纹理赋值给map
属性;2. 将skinning
设置为true
,这对动画模型至关重要。最后将该材质赋值给stacy_mtl
。现在我们有了纹理材质。因为模型(gltf.scene)仅有一个对象,所以我们直接在
traverse
方法的阴影相关代码下方增添一行代码:就这样,模型就成为了一个可辨识的角色——Stacy。
不过她有点死气沉沉,下一节我们将处理动画。现在你已接触过几何体和材质,就让我们用这些所学到的知识让场景变得更有趣。
在地板代码下方(即 init() 函数的最后一行代码),添加一个圆符。这是一个很大但远离我们的 3D 球体,并使用 BasicMaterial 材质。该材质不具备先前使用的 PhongMaterial 材质所拥有的光泽和投射并接收阴影的特性。因此,它在该场景中能作为一个平面圆,很好地衬托着 Stacy。
可以改成你喜欢的颜色!
Part 4:赋予 Stacy 生气
在进入本节主题前,你可能注意到 Stacy 的加载需要一段时间。显然,白屏对用户并不友好。我曾提及到:在 HTML 中我们有一个 loading 元素被注释。现在回到那里取消这个注释。
再次回到 loader 函数。
一旦将 Stacy 添加至场景,就删除 loading 动画遮罩层。保存更改并刷新浏览器,在看到 Stacy 前会有一个加载动画。若模型已被缓存,则可能会因太快而看不到加载动画。
是时候进入模型动画了!
仍在 loader 函数,我们将创建一个 AnimationMixer,它是用于播放场景中特定对象动画的播放器。它看来有些陌生,也超出本教程的范围。若想了解更多,可阅读 Three.js 文档的 AnimationMixer。而本文并不要求你知道关于它的更多内容。
在删除 loading 动画下方添加这行代码,其中传入的参数是我们的模型:
注意 mixer 已在项目顶部声明。
在这行代码下方,我们创建 AnimationClip,并通过
fileAnimations
查找一个名为idle
(空闲)的动画。这个名字是在 Blender 中设置的。然后,使用 mixer 的 clipAction 方法,并传入
idleAnim
参数。我们将这个clipAction
命名为idle
。最后,调用
idle
的play
方法:其实这还不能让动画执行起来,我们还需要做一件事。为了让动画持续运行,mixer 需要不断更新。因此,我们需要让它在 update() 函数内进行更新。我们将它放在判断是否需要调整尺寸的代码上方:
mixer 的 update 方法以 clock(已在项目顶部定义)作为参数。因为是基于时间(增量)进行更新,所以动画并不会因帧率下降而变慢。如果是基于帧率执行动画,则动画的快慢取决于帧率的高低,这应该不是你想要的。
至此,Stacy 应该能快乐的摇摆着身体!真棒!这仅是加载模型内的 10 个动画之一,我们将很快实现点击 Stacy 随机播放一个动画的效果。但接下来,我们先让模型变得更生动:让她的头部和身体朝向光标。
Part 5:朝向光标
也许你不太了解 3D(大多数情况下甚至是 2D 动画),它其实是一个被网格(mesh)包裹着的骨架(skeleton)(即骨头数组)。更改骨头的位置、比例和旋转角度,就能以有趣的方式扭曲和移动网格。进入 Stacy 的骨架,找到脖子骨头和下脊柱骨头。以视口中点为基准,这两个骨头将朝向光标进行旋转。为了实现这一点,我们需要告诉当前的“空闲”动画忽略这两个骨头。现在就让我们开始实现吧。
还记得在模型方法 traverse 里运行这段代码
if (o.isMesh) { … set shadows ..}
的那部分吗?在该 traverse 方法内,我利用o.isBone
console 所有骨头,并找到脖子和脊柱(即名字)。对于你自己制作的角色,亦可通过该方式找到骨头的准确名字。实际输出了一堆骨头,但以下才是我们想要找到的(粘贴自我的 console):
现在我们知道了脊柱(从现在开始,我们称之为腰部)和脖子的名字。
在模型的 traverse 方法,将这两个骨头赋值给相应变量(已在项目顶部声明)。
现在,我们还需要做更多探究性工作。先前,我们创建了一个名为 idleAnim 的 AnimationClip,并将其放置在 mixer 播放。现在,我们想将脖子和腰部从这个动画中剥离,否则“空闲”动画将覆盖我们为模型创建的自定义动作。
因此,第一件需要做的是
console.log
idleAnim。它是一个对象,并带有一个名为tracks
的属性。该属性对应的值是一个长度为 156 的数组,其中,每 3 个子项代表一个骨头的动画。这 3 项分别表示骨头的位置、四元数(旋转)和比例。前三个子项是髋部位置、旋转和比例。我们要找的是这些(粘贴自我的 console):
…和这些:
因此,在动画中,我需要通过 splice 方法移除第
3,4,5
和12,13,14
个子项。然而,一旦移除
3,4,5
,脖子就变成了9,10,11
。这是需要注意的地方。现在就通过代码实现以上需求。在 loader 函数的 idleAnim 下方,添加以下几行代码:
我们会在后续对所有动画执行同样的操作。添加以上代码后,就意味着无论她执行何种动画,我们都拥有腰部和脖子的控制权,这使得我们能实时修改动画(为了让角色在玩空气吉时摇头,我花费了 3 小时)。
在项目底部,添加返回鼠标位置的事件。
接着,我们创建 moveJoint 函数。
moveJoint 函数接收 3 个参数,分别是:当前鼠标的位置,需要移动的关节和允许关节旋转的角度范围。
我们在该函数顶部定义了一个名为
degrees
的变量,该变量的值来自于返回对象为{x, y}
的getMouseDegrees
函数。然后,基于这个值对关节分别在 x、y 轴进行旋转。在实现
getMouseDegrees
前,我先讲解它的实现思路。图示如下:
尽管我很想详细讲解这个看起来比较复杂的函数,但我怕逐行讲解会十分无聊。所以如果你感兴趣,可以结合注释进行理解。
在项目底部添加该函数:
一旦完成该函数的定义,我们就能使用
moveJoint
。根据实际情况,我们将脖子的角度限值设为 50°,腰部的角度限值设为 30°。更新
mousemove
事件回调函数,以包含moveJoints
:现在,在视口范围内移动鼠标,Stacy 就会不断盯着光标!注意,“空闲”动画仍在同时执行,这是因为我们将脖子和脊柱骨头从中剥离,从而拥有了对它们的独立控制权。
这可能不是在科学上最准确的实现方式,但出来的效果却很有说服力。以上就是我们的进展,如果你遗漏了什么或者效果不一致,请仔细看看这个 pen。
See the Pen Character Tutorial - Round 2 by Kyle Wetton (@kylewetton) on CodePen.
Part 6:播放剩余动画
如前面提及,Stacy 的文件内实际上有 10 个动画,而我们仅用了其中一个。现在让我们回到 loader 函数,并找到这行代码。
在这行代码下方,我们获得除“空闲(idle)”外的 AnimationClip 列表(因为我们并不想在点击 Stacy 时随机播放的动画中包含“空闲”)。
接着,与“idle”相同,将所有这些 clip 转为 Three.js AnimationClip。同时,将脖子和脊柱骨头从中剔除。最后将这些 AnimationClip 赋值给
possibleAnims
(已在项目顶部定义)。现在,我们拥有了能播放动画的 clipAction 数组(点击 Stacy 时)。这里需要注意的是,我们并不能简单地为 Stacy 添加一个点击事件,毕竟她不是 DOM 的一部分。这里采用射线(raycasting)实现点击,即向指定方向发射激光束,然后返回被击中的对象集合。在该案例中,激光线是从摄像机射向光标。
在 mousemove 事件上方添加该函数:
我们添加了两个事件,分别对应 PC 和触屏。我们将 event 传入 raycast() 函数,并在触屏情况下,将 touch 参数设为 true。
在 raycast() 函数内,我们有一个
mouse
变量。若touch
为true
,mouse.x
和mouse.y
则被设为changedTouches[0]
的坐标,反之被设为鼠标的坐标。(译者注:WebGL,坐标轴的原点在画布中心,坐标轴的范围是 -1 至 1)。接着调用 raycaster (已在项目顶部声明为 new Raycaster 实例)的 setFromCamera 方法。这行代码表示光线从摄像机射向鼠标。
然后得到被射中的对象数组。若数组不为空,那么即可认为第一个子项就是被选中的对象。
如果选中对象的名字为
stacy
,那么会执行playOnClick()
。注意,我们同时也会判断currentlyAnimating
是否为false
,即当有动画正在执行(idle
除外)时,不会执行新动画。在
raycast
函数下方,定义playOnClick
函数。基于 possibleAnims 数组长度创建一个随机数,然后调用另一个函数 playModifierAnimation。该函数接收的参数有:idle(from,即从 idle 开始),从 idle 到新动画(possibleAnims[anim])的过渡时间;最后一个参数是从当前动画回到 idle 的过渡时间。在 playOnClick 函数下方,我们添加
playModifierAnimation
。该函数做的第一件事是 重置
to
动画,即将要播放的动画。同时,我们将其 播放次数 设为 1 次,因为一旦动画播放完成(也许我们之前已播放过),它需要重置后才能再次播放。然后,调用 play 方法。每个 clipAction 实例都有一个 crossFadeTo 方法,我们使用它来实现 from(idle) 到新动画的过渡,并且过渡时间为 fSpeed(即 from speed)。
至此,函数已有拥有了从 idle 过渡到新动画的能力。
接着,我们开启了一个定时器,用于将当前动画恢复到 from 动画(即 idle),同时将 currentlyAnimating 设置 false(这样就允许再次点击 Stacy)。setTimeout 的时间计算方法为:动画长度(* 1000 是因为过渡时间以秒而不是毫秒为单位)减去动画切入和切出的过渡时间(同样以秒为单位设置,所以需要 * 1000)来得到。
注意,脖子和脊柱骨头均不受动画控制,这使得我们能够在动画过程中旋转它们。
本教程到此已算结束,若遇到问题,请参考以下完整项目。
See the Pen Character Tutorial - Final by Kyle Wetton (@kylewetton) on CodePen.
如果你对模型和动画本身的工作感兴趣,那么我将在最后一节介绍一些基础知识,希望能拓展你的视野。
Part 7:创建一个模型文件(选读章节)
以下操作均基于最新稳定版 Blender 2.8。
在开始之前,请记住我曾经提到过的,尽管可以在 GLTF 文件(从 Blender 导出的格式)中包含纹理文件,但我遇到的问题是 Stacy 的纹理确实很暗。这与 GLTF 需要 sRGB 格式有关,尽管我尝试在 Photoshop 中进行转换,但这仍不起作用。在不能保证该文件格式的纹理质量下,我的做法是导出没有纹理的文件,然后再通过 Three.js 添加。除非你的项目非常复杂,否则我建议这样做。
不管怎样,一个 T 姿势的标准角色网格(mesh)就是我们在 Blender 起始点。之所以要让角色摆成 T 姿势,是因为 Mixamo 会基于此生成骨架,敬请期待。
然后以 FBX 格式导出模型。
然后可以离开 Blender 一阵子。
www.mixamo.com 网站提供了许多免费动画,可用于各种场景,而浏览者以独立游戏开发者居多。另外,该 Adobe 服务与 Adobe Fuse 关系密切,后者实际上是角色创建软件。该网站是免费使用的,但需要一个 Adobe 帐户(免费是指你不需要订阅 Creative Cloud)。因此,创建账号并登录。
你要做的第一件事是上传角色。这是我们从 Blender 导出的 FBX 文件。上传完成后,Mixamo 将自动启用 Auto-Rigger。
按照说明将标记放置在模型的关键位置上。一旦 Auto-Rigger 完成,你将会在面板上看到你的角色在运动。
Mixamo 已为你的模型创建骨架了,这就是本教程所谈及的骨架。
点击 “Next”,然后在左上方导航条中选择 “Animations”。让我们搜索 “idle” 动画作为开始,使用搜索框并输入 "idle"。本教程使用的是 “Happy idle”。
点击任意动画进行预览。当然该网站还有很多有趣的动画。对于本项目,结束动作与衔接动作的脚部位置尽可能相同,即其位置与空闲动画基本类似。因为结束姿势与下一个动画的开始姿势相似时,过渡会看起来更自然。
对 “idle” 动画感到满意后,请点击 “Downlod Character”。格式应为 FBX,并且 skin 应设置为 “With Skin”。其余设置保留为默认值。下载此文件,并保持 Mixamo 的打开状态。
返回到 Blender 中,将该文件导入到一个新空会话中(删除新会话附带的光源,摄像机和立方体)。
点击 play 按钮(如果未看到时间轴(timeline),将任意一个面板的 Editor Type 切换为 Timeline,若仍不懂,建议看看 Blender 的 界面介绍。
此时,若想重命名动画,则将 Editor Type 更改为 “Dope Sheet”,并将二级菜单设置为 “Action Editor”。
点击 “+ New” 旁的下拉框,选择从 Mixamo 得到动画。此时可以在输入框内重命名,我们将它改为 “idle”。
点击 “x” 可看到 “+ select” 标识
如果现在将该文件导出为 GLTF,那么在 gltf.animations 内就有一个名为 idle 的动画。记住,该文件同时拥有 gltf.animations 和 gltf.scene。
在导出之前,我们需要对角色对象进行重命名。设置如下所示。
请注意,在下方的子节点 stacy 是 JavaScript 中引用的对象名称。
现在我们仍不进行导出,相反,我将快速向你展示如何添加新动画。回到 Mixamo,我选择了 “Shake Fist”(挥拳)动画。下载此文件,我们仍保留皮肤,可能有人会说这次不需要保留皮肤。但我发现如果不保留皮肤会出现奇怪的状况。
将其导入 Blender。
此时,我们有两个 Stacy,一个叫 Armature,另一个是我们想保留的 Stacy。我们将删除 Armature,但首先要将其 Shake Fist 动画移至 Stacy。让我们回到 “Dope Sheet” > “Animation Editor”。
现在,你会看到在 idle 旁有一个新动画,让我们选择它,并将其重命名为 shakefist。
保持当前面板的 “Dope Sheet” > “Action Editor”,并将另一个未使用的面板(或拆分屏幕以创建一个新的面板。同样,阅读 Blender 界面介绍教程有助于理解这段话)设置 Editor Type 为非线性动画(NLA)。
点击 stacy,然后点击 idle 动画旁边的 “PUSH DOWN” 按钮。这样就能在已添加了 idle 动画基础上,创建新轨道以添加 shakefist 动画。
处理前,再次点击 stacy 名字:
回到 Animation Editor 面板,并从下拉列表中选择 “shaffist”。
最后,在 NLA 面板中点击 shaffist 旁边的 “Push Down” 按钮。
应该留下这些元素:
我们已经将动画从 Armature 转移到 Stacy,现在可以删除 Armature 了。
烦人的是,Armature 会将其子网格物体落到场景中,也将其删除。
重复以上步骤添加新动画(我相信做得越多,疑惑越少,效率越高)。
导出文件:
blender-17
这是使用新模型的 pen!(需要注意的是:Stacy 的缩放比例与之前有所不同,所以在该 pen 中进行了调整。尽管到现在我对那些经 Mixamo 添加骨架并从 Blender 导出的模型的缩放比例仍琢磨不透,但在 Three.js 中能轻易地解决这个问题)。
See the Pen Character Tutorial - Remix by Kyle Wetton (@kylewetton) on CodePen.
完!