【钢铁侠劲舞团】通过钢铁侠的案例来深入 3D 模型使用
前言
上一篇文章一文带你悟道 Threejs 3D 模型开发通过三只飞鸟的案例系统的带大家串联了模型的基本使用,轻松几笔勾勒,就实现了一个不错的效果。
但如果你更深入的思考一下,就会想到很多盲点。
- 模型文件太大,如何优化模型来加快加载速度?
- 模型有大小吗?当然是有的,但大多模型并不是标准几何体,又该怎么计算模型的尺寸?
- 模型有大有小,如何保证模型显示的正合我意?
- 我们能不能对模型进行一些轻微的修改?
- ...
前段时间小包在sketchfab看到了一组特别炫酷的钢铁侠动作,念念不忘,本文就记录了小包与钢铁侠死死纠缠的故事,毕竟谁能拒绝会跳舞的钢铁侠那?
压缩模型
钢铁侠模型地址: 传送门
上一篇文章详细的讲解过 3D 模型的格式,推荐使用 GLTF 格式,并且最好是 glb 扩展格式,glb 把所有的文件都放置在一个二进制文件中,比较方便进行加载。
选择 glb 格式进行下载,讲实话,有点大,平均每个舞蹈为 9 MB 左右,最初的设想会加载多个舞蹈动作,累加起来加载速度不用测试肯定非常慢,所以我们首先要尝试降低模型的大小。
使用 gltf-pipeline 工具库来进行模型压缩。
npm install -g gltf-pipeline
该工具库基于谷歌推出的 Draco 算法,Draco 算法有两大优势: 通过 Edge breaker 3D 压缩算法改变了模型的网格数据的索引方法 通过减少顶点坐标、顶点纹理坐标等信息的位数,以减少数据的存储量
该工具库的具体使用参数大家可以百度,这里就直接上命令了,压缩并不会影响模型本身,会生成一个新的被压缩的模型。
gltf-pipeline -i ironman-dropkick.glb -o ironman-dropkick-zip.glb -d
以 ironman-dropkick 为例,压缩前 10.2MB,压缩后 3.3MB,压缩效果还是挺可观的。但这肯定不是该模型压缩的极限,还有一些更厉害的方法就等着各位巨佬们去探索了。
模型经过压缩后,模型的加载也需要进行一些改变,需要额外加载 DRACOLoader 及 dacro 算法。
// 部分代码逻辑请参考上一篇文章,不再重复赘述
async function loadIronman() {
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader(); // 加载 DRACOLoader
dracoLoader.setDecoderPath("/draco/"); // draco 算法
dracoLoader.preload(); // 预加载
loader.setDRACOLoader(dracoLoader);
const ironmanData = await loader.loadAsync(ironman_action_url);
const ironman = setupModel(ironmanData);
console.log(ironmanData);
return ironman;
}
然后就成功的导入模型了,我们来看看效果。
???钢铁侠发来疑问,我咋这么小???
这种现象很好解释,相机太远,模型太小,就相当于你在眺望远处的人一样,看上去都像一群小蚂蚁。
方案同样有两种,要么拉近相机,要么放大模型。相机拉近小包感觉还是比较难以完美实现的,设想一下现实中的案例,拍照时很难选取到一个合适的角度,需要反复调整。且各个模型的大小也不尽相似,掌握以相机为基准的模式还是有必要的。
因此小包选择固定相机,以相机为基准,根据数学计算来调整模型,使模型自适应。
模型自适应
包围盒
Three 提供了 Box3 方法,该方法可以在 3D 空间内创建一个包围盒,用来表示物体在世界坐标系的边界框。可以理解为模型周围包裹着一个长方体,既然能转换成长方体,那模型的长宽高就可以类比为包围盒的长宽高。(ps: 获取模型的大小或许是包围盒最简单的用法了)
const bbox = new THREE.Box3().setFromObject(object);
let mdlen = bbox.max.x - bbox.min.x; //边界的最小坐标值 边界的最大坐标值
let mdhei = bbox.max.y - bbox.min.y;
let mdwid = bbox.max.z - bbox.min.z;
bbox 中返回了 3D 空间 xyz 轴的两端坐标,可以发现,钢铁侠属实是有点小,当然也可能设计师是以 m 为尺度进行设计。
获取到模型大小后,剩下的问题就转换为数学问题。我们需要计算模型大小与相机之间距离的比例,然后来合理设定模型的放缩比例。
// props 是父组件传入的参数
let dist = Math.abs(camera.position.z - object.position.z - mdwid / 2);
let vFov = (camera.fov * Math.PI) / 180;
let vheight = 2 * Math.tan(vFov * 0.5) * dist;
let fraction = mdhei / vheight;
let finalHeight = props.height * fraction;
let finalWidth = (finalHeight * mdlen) / mdhei;
let sacle1 = props.width / finalWidth;
let sacle2 = props.height / finalHeight;
// 将模型适当缩小,为动画空出空间
sacle2 *= props.scale;
sacle1 *= props.scale;
if (sacle1 >= sacle2) {
object.scale.set(sacle2, sacle2, sacle2);
} else {
object.scale.set(sacle1, sacle1, sacle1);
}
结果模型还是很小,小包有点懵了,难道上面算法错了。小包尝试着翻转模型,果然模型的下面多一个底板,底板距离钢铁侠的距离很远造成包围盒高度太高。
这样有可能不够形象,Three 提供了 BoxHelper 方法,可以来绘制模型的包围盒。
let boxhelper = new THREE.BoxHelper(object, 0xbe1915); //外面红色框
scene.add(boxhelper);
通过 Boxhelper,可以比较明显地看到模型的外边界,这一大长条。😂
问题找到了,该如何解决这个问题那?想办法斩掉多余的部分,就在小包尝试修改模型时,发现了更奇怪的现象。
奇怪的模型
小包下载了大约 5-6 个模型,正常情况下包围盒应该能够包围模型,结果这个会打拳的钢铁侠非得跟别人不一样?
讲实话,他好酷啊 😂,这就是钢铁侠的孤傲吗?包围盒为什么包不住他啊,懵圈,有路过的大佬希望可以指点一下。
给模型动刀
除了孤傲的帝皇侠外,别的模型还算容易处理,只需要将下面多余的底座部分删除即可,因此我们直接借助 Three 官方提供的 editor 进行实现。
导入需要修改的模型,在 editor 中可以比较明显的包围盒,找到右侧 scene 部分,找到下面多余的物体,将其进行删除。
然后再导出为 glb 格式即可。
将导出的模型重新加载到程序中,四位钢铁侠就变得合理起来了,但新问题来了,你咋这么偏那?
本案例中共使用了四个钢铁侠动作,分别在四个 canvas 中实现,居中针对于各个画布
模型居中
设计师在设计模型时通常默认将模型默认放置在 (0,0,0) 位置,当我们放大模型后,模型的默认位置并没有发生变化,只占据了 3D 的上半部分,因此需要给模型设置一个居中效果。
计算出模型的中心,将中心的位置移到原点处,便可以实现居中效果。模型的中心可以通过包围盒计算得出。
function setMiddle(object) {
let box = new THREE.Box3().setFromObject(object); // 获取模型的包围盒
let mdlen = box.max.x - box.min.x; // 模型长度
let mdwid = box.max.z - box.min.z; // 模型宽度
let mdhei = box.max.y - box.min.y; // 模型高度
let x1 = box.min.x + mdlen / 2; // 模型中心点坐标X
let y1 = box.min.y + mdhei / 2; // 模型中心点坐标Y
let z1 = box.min.z + mdwid / 2; // 模型中心点坐标Z
object.position.set(-x1, -y1, -z1); // 将模型进行偏移
}
钢铁侠劲舞团
四位钢铁侠模型都已经导入了,下面咱们把他们的舞蹈启动起来,如何播放 GLTF 动画上一篇文章已经讲过了,就不重复描述了。
这里来重点提一下 clipAction 函数的作用,Three 的动画控制系统是特别完善的。
function setupModel(data) {
const model = data.scene.children[0];
const clip = data.animations[0];
const mixer = new AnimationMixer(model);
const action = mixer.clipAction(clip);
action.play();
model.tick = (delta) => mixer.update(delta);
return model;
}
clipAction 到底是做了什么那?
模型上的 animations 存储模型附带的动画效果,标准被称作动画剪辑——AnimationClip,每个动画剪辑都由多个关键帧 track 组成
AnimationAction 可以创建动画剪辑内的关键帧创建一个调度存储的控制功能。但为什么代码中并没有使用呐?
clipAction 本质就是运行了 AnimationAction,但该方法提供了缓存功能,能提供更好的性能。但需要注意,AnimationAction 虽然提供了动画的调度功能,但它并没有真正的开启权,它只提供开启信号,真正的开启由混合器启动。
下面展示了部分控制的参数及方法
- clampWhenFinished: 动画执行完是否停止
- loop: 动画循环次数
- timeScale: 动画播放速度,负值代表倒放
- play()/stop(): 动画开启与关闭信号
- reset(): 动画重置
设置完动画后,我们就可以借助一些公共的组件库,实现一下钢铁侠的轮番登场,最终结果就呈现出来了。
源码仓库
实现的过程有几分急迫,所以效果不算炫酷,小包会继续努力的
后语
我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。
一路加油,冲向未来!!!
转载自:https://juejin.cn/post/7171803353310035999