前言:对于使用类似于 cesium 和 three.js 这种高度封装的 WebGL 库,其 api 非常多且每个名字取得非常长,使用场景也非常广泛,在实践中按需学习才是进步最快的方式。没有项目或者需求牵引,哪怕一时学会了也会很快忘记或者记得没那么深刻。建议的学习方式是从头到尾过一遍所有概念、功能和api,形成一个初步印象,然后在实际使用中不断地去查阅和熟悉,附一个官网:threejs.org/ ,Three.js中文网:www.webgl3d.cn/ 。
下面以我们某项目中部分片段为例,理一下我们学习 three.js 的方法。
一、项目需求
该项目是想做一个机器修理车间的数字孪生系统,在大屏上用作展示和动态监控,分为 2D 部分和 3D 部分。
(一)2D 部分
- 需求1:展示一张待维修机器的平面图,平面图上会有各个关键部位的标记和高亮展示;
- 需求2:以轮播的方式展示当前维修车间的工作人员,包括工号、姓名、年龄、工龄、擅长内容等信息;
- 需求3:以轮播的方式展示当前维修车间内所有送过来的机器的信息,点击某台机器,可以在 2D 的视角上看到该机器的平面图以及待维修内容。
(二)3D 部分
- 需求1:在轮播信息板块点击某台机器,可以在 2D 的视角上看到该机器的平面图以及待维修内容,在 3D 视角上同步展示该型号机器的模型;
- 需求2:可以对该机器进行拆分,便于维修人员观看其内部结构,辅助其维修;
- 需求3:可以对该机器进行透视,便于维修人员观看其内部结构,辅助其维修;
- 需求4:可以对该机器进行放大缩小,任意角度旋转观看,辅助维修人员维修。
- 需求5:自由发挥,完成其他视觉效果,这里想到的是粒子化处理,自动动画,自动旋转展示等。
二、需求分析及资料查阅
(一)2D 部分
需求分析:这一块的目的就是展示和好看,所以需要各种好看的切图来堆砌,这部分主要靠 UI 来实现,前端程序员来还原,唯一一个轮播的部分用到 Swiper 库。
import { Swiper, SwiperSlide } from 'swiper/vue';
import { Autoplay } from 'swiper';
import 'swiper/css';
const onSwiper = swiper => {
swiper.el.onmouseover = function () {
swiper.autoplay.stop();
};
swiper.el.onmouseleave = function () {
swiper.autoplay.start();
};
};
<swiper
:modules="[Autoplay]"
:loop="true"
:slides-per-view="10"
:space-between="10"
direction="vertical"
:speed="1500"
@swiper="onSwiper"
:autoplay="{
delay: 100,
}"
>
<swiper-slide v-for="(item, index) in uavList" :key="item" @click="detail(item, index)">
<div class="uav_list" :class="{ navigationItemSel: curSelectNavigation === index }">
<div class="uav_list_id">
<img :src="item.image" alt="" />
<div>
<p style="font-family: BigYoungMediumGB20">{{ item.name }}</p>
<span>{{ item?.id }}</span>
</div>
</div>
<p class="show">{{ item?.lng?.toFixed(2) || '暂无' }}</p>
<p class="show">{{ item?.lat?.toFixed(2) || '暂无' }}</p>
<p class="show">{{ item?.height }}</p>
<p class="show" :class="EStatus[item.type].class">{{ EStatus[item.type].text }}</p>
</div>
</swiper-slide>
</swiper>
(二)3D 部分
需求分析:首先要完善三要素(上篇文章已经讲过)-->然后要导入模型-->然后要对模型播放动画、做拆解和还原、自动旋转,大概就这些,所以去查阅文档,发现需要用到 gltf 模型加载器
GLTFLoader.js,于是开始学习。
① 学习导入模型
第一步 引入
GLTFLoader.js在three.js官方文件的 examples/jsm/子文件loaders/ 目录下,可以找到一个文件
GLTFLoader.js,这个文件就是three.js的一个扩展库,专门用来加载gltf格式模型加载器。
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
第二步 gltf加载器
new GLTFLoader()执行
new GLTFLoader()就可以实例化一个gltf的加载器对象。
// 创建GLTF加载器对象
const loader = new GLTFLoader();
第三步 gltf加载器方法
.load()通过gltf加载器方法
.load()就可以加载外部的gltf模型。执行方法.load()会返回一个gltf对象,作为参数2函数的参数,改gltf对象可以包含模型、动画等信息。
loader.load( 'gltf模型.gltf', gltf => {
// 返回的场景对象gltf.scene插入到threejs场景中
scene.add( gltf.scene );
})
② 学习播放模型动画
在
three.js中动画也是很重要的一环。在创建模型时,一般都会创建模型的动画用于在开发中使用。AnimationMixer动画混合器是用于场景中特定对象的动画的播放器。当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。rootObject 混合器播放的动画所属的对象,常用参数和属性如下:
.time全局的混合器时间。.clipAction(AnimationClip)返回所传入的剪辑参数的AnimationAction对象。AnimationAction用来调度存储在AnimationClip中的动画。AnimationClip动画剪辑,是一个可重用的关键帧轨道集,它代表动画。.getRoot()返回混合器的根对象。.update()推进混合器时间并更新动画。在渲染函数中调用更新动画。
第一步 创建全局参数初始化动画相关对象
let actions = [] // 所有的动画数组
let mixer = null // AnimationMixer 对象
第二步 解析挂载并执行动画
import {AnimationMixer} from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const gltfLoader = new GLTFLoader();
gltfLoader.load(uri, glb => {
mixer = new AnimationMixer(glb.scene);//准备场景的动画混合器
//在该模型上使用动画混合器挂在所有动画
const clips = glb.animations;
clips.forEach(item => {
actions.push(mixer.clipAction(item));
});
//播放动画或者暂停
actions.forEach(item => {
if (item.isRunning()) {
item.stop();
} else {
item.play();
}
});
③ 学习拆解、还原、透视、线框化模型
思考一下,为什么会把这几个需求写在一起,因为本质上它们无非都是对模型的 child 进行操作而已,比如拆解和还原就是把模型的每个 child 拿到,改变下位置;再比如透视就是把模型的每个child 拿到,改变下透明度;再比如线框化就是把模型的每个child 拿到,改变下材质。因此我们自然而然地联想到需要一个方法来遍历模型,拿到它的每一个 child。
递归遍历方法
.traverse(),three.js 层级模型就是一个树结构,可以通过递归遍历的算法去遍历 three.js 一个模型对象的所有后代。 为了更顺滑一点,我们需要使用动画,也就是前几篇文章提到的 gsap 库。
// 拆解和还原模型
const disassemble = (mesh: Object3D<Event> | Group) => {
if (isFlat.value) {
mesh.traverse((child: any) => {
if (child.isMesh) {
gsap.to(child.position, {
x: child.oldPosition.x,
y: child.oldPosition.y,
z: child.oldPosition.z,
duration: 1,
});
}
});
isFlat.value = false;
} else {
mesh.traverse((child: any) => {
if (child.isMesh) {
child.oldPosition = child.position.clone();
gsap.to(child.position, EModel[child.name].animation);
}
});
isFlat.value = true;
}
};
// 透视和还原模型
const transparency = (mesh: Object3D<Event> | Group) => {
mesh.traverse(child => {
if (child.material) {
if (!child.material.transparent) child.material.transparent = true;
if (child.material.opacity === 1) {
gsap.to(child.material, {
opacity: 0.3,
duration: 1,
});
} else {
gsap.to(child.material, {
opacity: 1,
duration: 1,
});
}
}
});
};
// 线框化和还原模型
const wireFrame = () => {
planeModel.traverse(child => {
if (child.isMesh) {
if (state.wireFrame) {
child.material.wireframe = false;
} else {
child.material.wireframe = true;
}
}
});
state.wireFrame = !state.wireFrame;
};
④ 学习手动和自动旋转、缩放模型
所谓模型缩放与旋转,在 three.js 中,是使用
轨道控制器(OrbitControls)使得相机围绕目标进行轨道运动实现的。OrbitControls 本质上就是改变相机的参数,比如相机的位置属性,改变相机位置也可以改变相机拍照场景中模型的角度,实现模型的360度旋转预览效果,改变透视投影相机距离模型的距离,就可以改变相机能看到的视野范围。
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
renderer.render(scene, camera); //执行渲染操作
console.log('camera.position',camera.position);// 浏览器控制台查看相机位置变化
});
OrbitControls 会有很多属性和方法,比如
autoRotate:Boolean,将其设为 true,以自动围绕目标旋转。请注意,如果它被启用,你必须在你的动画循环里调用 update()。同时设置autoRotateSpeed:Float,当 autoRotate 为 true 时,围绕目标旋转的速度将有多快,默认值为2.0,相当于在60fps时每旋转一周需要30秒。同样,如果它被启用,你必须在你的动画循环里调用update()。
const displayModel = () => {
controls.autoRotate = true;
controls.autoRotateSpeed = 3;
!camera.basePosition && (camera.basePosition = camera.position.clone());
!planeModel.baseRotation && (planeModel.baseRotation = planeModel.rotation.clone());
if (state.realism) {
gsap.to(camera.position, {
x: 0.15,
y: 0.15,
z: 0.15,
duration: 1,
});
gsap.to(planeModel.rotation, {
y: 0.8,
duration: 1,
});
} else {
gsap.to(camera.position, {
x: camera.basePosition.x,
y: camera.basePosition.y,
z: camera.basePosition.z,
duration: 1,
});
gsap.to(planeModel.rotation, {
y: planeModel.baseRotation.y,
duration: 1,
});
}
};
// 渲染函数
function render() {
renderer.render(scene, camera);//执行渲染操作
controls.update();//更新控制器
mixer && mixer.update(clock.getDelta());//更新模型动画的状态,如果不放动画可以不要这一行
window.requestAnimationFrame(render)//直接这样写也可以 requestAnimationFrame(render)
}
render();