1. 开始
1.1 开使用threejs写一个最简洁的demo
function init() { // **写任何3d界面初始化必备的东西**
**// renderer 渲染器**
var renderer = new THREE.WebGLRenderer({
canvas: document.getElementById(“test-overview”)
});
renderer.setClearColor(0x000000); // 设置场景舞台背景色
**// scene 场景舞台,可以看做是最大的group,特殊的group**
var scene = new THREE.Scene();
**// camera 相机**
var camera = new THREE.PerspectiveCamera(45, 4 / 3, 1, 1000);
camera.position.set(0, 0, 5);
scene.add(camera);
** // 创建一个模型, 模型(Mesh)=几何体(XXGeometry)+材质(XXMaterial),材质可以理解为游戏中的英雄皮肤,你可以切换皮肤,但模型的几何体不变**
var cube = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
new THREE.MeshBasicMaterial({ // 基础材质,不受光照影响
color: 0xff000 // 材质颜色
}));
scene.add(cube); // 最后将模型添加到场景当中
render(scene, camera)
}
funciton render(scene, camera){
var myReq = requestAnimationFrame(render); // 相当于setInterval 不停地render渲染, 渲染消耗性能,在不必要的情况下可以暂停渲染,暂停之后它就相当于静态图片
renderer.render(scene, camera);
}
1.2 threejs常用对象或对象属性
1.2.1 Scene,Group
scene 是场景,group是组,他们都是装Mesh模型的箱子,容器, 不同的是:
scene一般只会有一个,他就好比树状结构的根节点,group可以有好几个,group还可以装group;
group 一般只会装模型, 而sence一般用来装group,mesh模型,camera相机, light灯光,就好比拍电影会用的道具,模型
var scene = new THREE.Scene()
var group = new THREE.Group()
- 他俩常用api属性:
增加.add(xx), 删除.remove(xx), 清空所有clear(),
是否可见.visible = 布尔值
名称.name = ”xx” // 这个一般是group用,给组命个名,用到时好找,比如要给哪个组加特效
1.2.2 Light
没有光,就算模型加到场景当中也看不见,一片黑,但是呢,模型的亮度不一定是光越强越亮,这跟模型材质的反光度也有关
- 常用的光的类型:
- 环境光
var light = new THREE.AmbientLight(光的颜色); // 环境光一个就够了
light.intensity = 1 // 光的强度
sence.add(light) // 记得加入场景
- 点光源
var light = new THREE.PointLight(颜色, 强度, 距离); // 点光源可以多弄几个照射
环境光不需要调整位置他没有XXXHelper,点光源可以,将光添加到PointLightHelper就看到一个菱形物体了,你可以像调整模型一样调整光的位置
var pointLightHelper = new THREE.PointLightHelper(light, 10);
scene.add(pointLightHelper)
1.2.3 Camera
就算有光,没有眼睛照样一片黑
- 常用camera类型:
- 透视相机,就跟人眼看到的世界一样效果,远小近大
var width = document.getElementById("overview-container").clientWidth // canvas的宽
var height= document.getElementById("overview-container").clientHeight // canvas 的高
var camera = new THREE.PerspectiveCamera(45, width/height, 0.5, 6000) // 参数基本不用调,详情看文档
- 正交相机, 没有远小近大,看到都就是物体本身大小,使用场景比如选矿的俯视图
var k = width / height; //窗口宽高比
var s = 450; //三维场景显示范围控制系数,系数越大,显示的范围越大
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
- 常用属性:
camera.lookAt(xx.poation) // 相机聚焦于那个点,即屏幕中心对准的三维空间中的哪个点
1.2.4 WebGLRender
有上述几个对象加到场景当中,我们就可以将模型渲染出来了,他就好比是一支笔开始画画了, 光告诉他颜色该怎么画,摄像机告诉他从哪个角度画
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }) // 创建render
renderer.setSize(document.getElementById("overview-container").clientWidth, document.getElementById("overview-container").clientHeight)
renderer.setClearColor(0xeeeeee, 0.0)
实例化后renderer会有一个domElement属性,值是一个canvas的dom元素,因为threejs就是靠canvas画出来的场景,把这个canvas塞到vue的div中就行了
let webglOutput = document.getElementById(“xx-container”);
webglOutput.appendChild(renderer.domElement)
然后定时器不停地调用,写一个render方法, 如果你只调用一次,那就是绘制了一张图片相当于,不停的用render方法,才会不停地绘制
function render() {
requestAnimationFrame(render)
camera.updateProjectionMatrix(); // 摄像机也要不停地更新
renderer.render(场景, 摄像机)
}
1.2.5 Controls
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 引文件
想要鼠标控制三维场景,那么就需要用到控制器, 控制器是我们一般只会用到OrbitControls,就是鼠标左键旋转场景,右键平移场景,滚轮放大缩小.
还有其他类型的控制器比如射击类游戏,他镜头的中心点始终就是你鼠标的位置;
var controls = new OrbitControls(camera, renderer.domElement)
- 常用api属性:
controls.addEventListener("start", () => { // 监听鼠标触发的事件})
controls.addEventListener("end", () => { // 监听鼠标触发结束的事件})
controls.minDistance = 100 // 最近视距 // 滚轮放大的数值
controls.maxDistance = 1500 //最远视距 // 滚轮缩小的数值
controls.rotateSpeed = 0.7 // 左键旋转的速度
controls.enableDamping = true // 是否开启阻尼效果, 展示单个模型可以用到,惯性漂移
1.2.6 XXGeometry
创建各种类型的几何体,虽然大部分模型都是ui给的,但是我们要加些效果的话,还是要手动写,这样性能会更好!
var geometry= new THREE.PlaneGeometry(1, 1) // 平面形状, 就是地板,只有一个面,要么是正方形,要么长方形, 常见就是作为场景的舞台地板,或者作为文字logo的面板
var geometry = new THREE.BoxGeometry(1,1,1) // 几何体,长方体,正方体
var geometry = new THREE.CylinderGeometry(1,1,1,1, 布尔值) // 圆柱体, 布尔值表示是否渲染上下两个面, 比如选矿中箭头旋转动画,表示机器在运转
var geometry = new THREE.EdgesGeometry(geometry) // 边框模型, 只渲染边, 比如鼠标移到某个物体上给他加个边框
var shape = new THREE.Shape();
var geometry = new THREE.ShapeGeometry() // 配合THREE.Shape绘制任意几边形,类似于钢笔工具, 比如选矿中每个区域地面标记的颜色
var geometry = new THREE.TubeGeometry() // 管道几何体,就是类似下水管道,比如炼钢钢水从管道中倒出;但是管道比较多的情况下,自己画管道太麻烦了,直接修改ui给的管道中的材质,也可以做流动动画,他会将管道分为几个部分,让他告诉你每个管道的.name就行了
1.2.7 XXMaterial
虽然threejs自带了很多类型的材质,但我们就用最基础的就好了,因为模型一般是ui给的,材质也在里面,我们自己也不会经常去画模型.
var material= new THREE.MeshBasicMaterial()
- 材质的属性:
我们做的更多的是修改材质的属性, 一下列举常用的一些属性:
new THREE.MeshBasicMaterial({
color: “#fff”, //材质的颜色,好比衣服的颜色
transparent: true, //配合opcity使用
opacity: 1 // 0~1之间,设置材质透明度,即模型的透明度
side: THREE.DoubleSide // 决定渲染那个面,比如PlaneGeometry他有上下两个面,默认全部渲染,你也可以之渲染上面,或者只渲染下面,没有渲染的面从那个方向看,就是透明的
metalness: // 金属度,0~1之间调整, 解决ui给的模型较暗问题
map: texture //纹理
})
1.2.8 TextureLoader
纹理是材质的很重要组成部分,如果没有纹理就好像你的衣服没有装饰,就是纯白色,纯黑色,很单调
加载纹理,纹理可是是图片,也可以是canavas或者视频都可以,这里以图片举例
var textureLoader = new THREE.TextureLoader();
var texture = textureLoader.load(path); // 图片路径
texture 常用api属性:
// 设置阵列模式 RepeatWrapping, 纹理一般是一张图片,你需要铺满整个面,那么你就需要设置他以何种方式铺满,大部分只要按下面这两句复制就行了
texture.wrapS = THREE.RepeatWr<u>a</u>pping
texture.wrapT = THREE.RepeatWrapping
texture.repeat.x = 2 // 纹理在X轴方向重复多少份,相同的还有y轴和z轴
texture.offset.x = 2 // 纹理在X轴方向挪动多少单位,你想让他对齐的位置是哪里,相同的还有y轴和z轴
texture.needsUpdate = true; 如果你的纹理是动态的,比如canvas动画,vedio视频,你需要在render里面持续设置, 因为render是在定时器中执行的,只设置一次是无效的!
1.2.9 Mesh
var mesh = new THREE.Mesh(几何体,材质) // 最常用的模型,你用loader加载ui给的模型就是mesh对象
- 常用api属性:
缩放mesh.scale.set(x,y,z) ,旋转mesh.rotation.set(x,y,z),位置mesh.position.set(x,y,z) ,
mesh.clone() // 克隆模型,复制同一个引用,节约内存
遍历mesh,因为ui给的mesh可能是好几个mesh组成的整体,比如你要给这个模型透明,你需要遍历整个mesh的材质才能实现
mesh.traverse(function (child) {
if (child.isMesh) { // 如果是mesh类型,怎么怎么样
....
}
})
1.2.10 GLTFLoader
加载ui给的模型文件,还有其他XXloader,我们基本不会用,就gltfLoader够了
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' //gltfloader需要单独引文件, 我上面写的THREE.xxx都是在three包里面,这个是单独分开的
gltfloader 用来加载.gltf格式的文件,这个文件就是ui给你的模型文件,如果不是这个格式让他转一下,或者你自己转一下
var gltfLoader = new GLTFLoader() // 这个一般情况下只要在new一次就够了
gltfLoader.load(
“模型文件路径”,
(gltf) => {
// 一般是gltf.sence,这个就是一个Mesh对象,加载完之后通过group.add(gltf.sence)添加到场景中
},
)
1.2.11 AnimationMixer
除了你自己可以做一些简单的缩放平移旋转动画, 但是比较复杂的动画是ui给你画的,需要你手动播放还是停止
定义AnimationMixer对象
var animate = new THREE.AnimationMixer(mesh模型) // 看业务,如果你复制的多个模型共用一个动画,那你就new一个,如果比如6个磁选机,可能某一个磁选机暂停了动画,但其他不暂停,那就要针对每个磁选机模型都要new一个AnimationMixer了
因为一个模型可能有多个动画片段,好比打游戏按Q是出拳,按W是踢腿,目前我遇到的都是一个模型就一个动画片段;所以直接获取第一个片段;如果多个片段就遍历
var clip = gltf.animations[0];
var animationAction = animate.clipAction( clip )
clipAction的常用的一些属性:
animationAction.clampWhenFinished = true; // 是否最后一帧暂停
animationAction.loop = THREE.LoopOnce; // 不循环播放
animationAction.time = 0; // 动画开始播放时间
clip.duration = 0; // 动画过程时间
animationAction.play(); // 动画开始执行,你可以把animationAction存起来,在合适的时机去play(),比如设备故障了你就不执行.stop()
1.3 在vue中使用threejs
cnpm i three --save 安装依赖
import * as THREE from 'three' // 引入threejs对象,你也可以按需引入,这里方便就全引入了
在template中定义一个dom, <div id="test-overview"></div>
在mounted中初始化, 这里可以看出created和mounted区别, 你需要等待vue的dom加载完成才能去画3d
mounted(){
执行1.1中的init方法
}
最简单的大致是这四个步骤.实际以这个为拓展;
下面看实际项目,虽然3d在项目之内,但写好一个3d文件代码量多,完全可以看做一个单独的项目;
我的项目结构一般这样:
├── overview
│ ├── components // 大屏的组件,比如两边侧边栏信息,3dcanvas
│ └── 3d // 3d相关组件
│ └──XX3D.vue // 承载threejs的容器,threejs生成的canvas插入到其中
│ └── images //图片资源,比如纹理会用到的图片
│ └── utils //我一般分为三个文件
│ └──model-animate.js // 专门写动画
│ └──model-common.js // 专门写初始化的一些操作,比如场景,相机,render渲染
│ └──model-loader.js // 专门加载模型的,大部分是ui给的gltf或glb的模型,也有自己写的Mesh,比如文字
│ └──MoveUtil.js // 调试模型位置角度的工具logo,纹理等等
│ ├── Overview.vue // 最大的父组件,路由引用的这个文件
1.4 Vue中优化threejs加载
如果用最简单的方式写threejs有一堆问题,你需要考虑模型加载速度问题
1.4.1 使用keep-alive缓存
大部分情况下,我们最好将初始化后的threejs 场景缓存下来,不要每次都初始化一下,如果每次都初始化,如果内存清理不干净,vue单页面路由反复跳转会导致内存一直增长.
这里先使用keep-alive 将组件缓存,我是直接将Overview 缓存了,缓存了父组件子组件也是会被缓存的;
activated(){
执行1.1中的init方法
} // 使用activated钩子代替mounted钩子;
tip: 如果一个组件使用了activated钩子,并且这个组件在父组件是被异步引用的,那么这个组件第一次初始化是不会执行activeted钩子,异步引用组件好处父组件加载就是快那么一点点,同步相反;
1.4.2 模型预加载
如果你的页面一进来不是3d概览页面,那最好不过了,第一次与后续加载都会很快; 如果不是的话,并且你模型大小综合一两百兆第一次加载就会比较迟钝, 但是后续加载还是很快;
比如在main.js中 引入model-loader
import { prevLoadModels } from "@/views/overview/utils/model-loader";
const modelPathArr = [
'static/three-models/管道.gltf',
'static/three-models/地形.gltf',
'static/three-models/碎矿.gltf',
...
]
modelPathArr.forEach(path => { // 遍历模型文件路径,一个个后台偷偷加载,并存到内存中,这里我直接挂在到window对象上,因为他肯定是一直存在的;
prevLoadModels(path)
})
1.4.3 模型按需加载
如果一个模型很大,就把ui拆分成好几个模型,就跟加载游戏场景一样,先把主要的场景加载完,一些小模型,细枝末节的后加载;
1.4.4 相同模型克隆
为了提高threejs 渲染效率,长得一摸一样的模型可以使用Mesh.clone的方法, 比如选矿的皮带
1.4.5 相同材质克隆(有缺点)
当你需要替换一个模型的材质时候,你不需要每次都new XXMaterial(), 这样可以优化渲染性能;但是不好的是同一个材质对象是引用类型,一个材质得变化会导致其他所有材质都变化;有时候我并不想这样; 比如选矿分成三个区域,选矿磨矿,当我的视角切换到选矿时候,我希望自己的区域内的皮带不透明另外两个区域透明(透明就是改变材质的.opacity属性), 但是改变了一个所有的都透明了(因为皮带Mesh.clone的时候材质都是用的同一份),这时候我应该new三个材质,分别替换每个区域皮带的材质,相对的这样性能就下降了些
1.5 更好的使用threejs,经验总结
1.5.1 善用Mesh模型的add方法
Mesh的add就跟group的add一样,只不过group是看不见容器,Mesh是看的见的, 如果A模型.add(B模型),那么这两个模型就可以是一个组,A模型放大,B模型会跟着放大,A模型移动,B模型会跟着移动,这样的好处就是就好比写vue组件,你将重复的代码抽成一个组件,这里不是为了方便引用而是方便调试,因为模型位置,大小经常会随业务变化而变化;
1.5.2 善用group和group.name
这比如选矿中有三个区,选矿,磨矿,尾矿,显而易见我应该给他们分成三个组,分别再设置组的name属性, 然后使用上面1.4.1的逻辑模型嵌套模型
总结就是 场景(sence) => 组(group) => 父模型(Mesh) => 子模型(Mesh)....
不仅是为了后面好写代码,而且你点开sence属性的时候逻辑也清晰明了;
1.5.3 修改模型的metalness和加多个灯光
初遇threejs 有一大堆格式的模型文件.fbx, .glb, .gltf, .... 我们就用gltf模型. 但是导入进入明明加了灯光,亮度也调的挺高的,可模型就是偏暗, 这时候要么你修改材质的metalness (金属度,我喜欢理解为反光程度) 0~1 之间调整; 我的loader方法里都是加了initEmissive方法的, 默认会设置为.1, 这时候大部分模型应该是亮的, 如果只有个别模型很暗,联系ui修改
mesh.traverse(function (child) {
if (child.isMesh) {
child.material.metalness= metalness;
}
})
灯光的话一般加4个就够了,分别是三角形摆放3个灯, 然后正中心的上方放个灯,组成金字塔形状;
最后还有一个环境光,他是一个无处不在的光,他不是很强.加了更好
1.5.4 理解摄像机和控制器的关系
常用的camera 对象就是 PerspectiveCamera (透视摄像机), 另外一个只在选矿中用了一下 OrthographicCamera(正投影摄像机,可以用俯视图扁平化查看,类似于流程图一样);
摄像机你也可以当做是一个模型,每new 一个Camera 就是创建一个眼睛, 在threejs中我们不需要两只眼睛,一个就够了; 初始化的时候你需为摄像机设置.postion.set(x,y,z); 同时你还需要设置一下.lookAt(xx.postion) 一般是场景的中心,当然你也可以看向别处;设置了眼睛看相的位置,那么屏幕中点就对准你看的那个位置
在写摄像机角度切换的动画时, 你会发现就算你设置了相机的postion,rotation的起始位置,终点位置,实际效果还是有偏差,这是因为你在调整摄像机时,摄像机的lookAt的点已经变了,你还需要调整control.target的起始位置和中心位置;,因为你在鼠标调整视角时,lookAt也跟着变了,变成了control的target坐标
1.5.5 判断模型有没有加载完成
虽然模型gltfLoader有加载的进度回调函数,但是没啥用,因为我们模型分开加载的,那么,你可以定义一个变量,每加载完成一个模型变量++,在定义一个模型总数的常量,如果到了总数就发送事件;
1.5.6 其他文件更好的调用render
现在的utils文件大致分为三个model_common, model_animate, model_loader, 只有common里面有定时器不断地render, 如果其他文件也要调用render ,建议各自在文件里写个方法,然后export他,然后common里面引入,放到render里面;这样三个文件尽量只做跟自己相关的事
1.5.7 vue文件跟js文件通信
- js文件中:
var vm; // vue对象
export function getVue(that) {
vm = that;
}
2.在Overview.vue 引入他,并把this,即当前对象传递过去即可;
1.6 threejs一些效果(具体业务具体发挥)
1.6.1 使用CSS2DObject 将html与3d场景结合
一般是用来做模型上面的标签,点击事件也好处理,直接是用vue那一套,如果是3d场景中做点击事件,点击某个模型触发xxx代码贼多!
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; // 引入他
比如我创建一个html的dom元素
方法1: let domContainer = document.createElement(“div”) // 相当于html文件中直接写<div></div>,因为在js中,就需要函数式编程;
方法2: let domContainer = document.getElementById(“#xxx”) // 如果你已经在vue文件中创建了这个元素,并给他加个id=xxx,那么你可以直接获取到这个元素
以上简单的html你可以用方法1,复杂的推荐2;
let label = new CSS2DObject(domContainer); // label也有postion,rotation等属性
mesh.add(label) // 最后就跟添加模型一样添加label;
1.6.2 使用TWEENJS做动画
cnpm i @tweenjs/tween.js --save // 安装依赖
import TWEEN from"@tweenjs/tween.js" // 引入他
function xxx( mesh ){
let startY = mesh.position.clone().y; // 人生建议,先把起始位置通过clone的方式复制一份,不然某些情况下他会出现叠加态势的变化
var tween = new TWEEN.Tween({ // 动画开始的数值,可以是postion,也可以是rotation,也可是scale,总之是数字的值都可以
oGun_y: startY, // 氧枪y轴位置,
});
tween.to({ // 动画结束数值
oGun_y: getRatioPosition(endY), // 氧枪y轴位置,
}, 1000);
tween.onUpdate(function (object) { // 动画更新钩子,意思就是每次更新tween将下一次的运算结果覆盖上一个结果,实现一点点的更新,你用定时器也能实现类似简单的效果
oGun_model.position.y = object.oGun_y;
})
tween.onComplete(function () { // 动画执行完成时候的钩子,你可以在一个动画结束后执行另一个动画
})
tween.easing(TWEEN.Easing.Quartic.Out); // 设置动画执行的贝赛尔曲线
tween.delay(1000) // 动画延迟1秒执行
tween.start(); // 动画执行
}
更多参考链接:
中文文档
http://www.yanhuangxueyuan.com/threejs/docs/index.html#api/zh/renderers/WebGLRenderer
threejs 官方demo,每个知识点都有
https://threejs.org/examples/#webgl_animation_keyframes // 网络有时候慢,需要翻墙
可应用与大多数项目的特效:
镜头切换效果
天空盒背景
模型阴影效果
鼠标判断是否选中取场景中的模型;
精灵图做模型标签
Echart图表映射到3d场景中,并可以实时更新变化
模型外发光
模型沿自定义路径移动
粒子成像
物理引擎模拟碎石掉落
Shader高级渲染
模型拆散自动组装