前言:
随这5g时代的到来,虚拟现实,物联网,智慧城市,人工智能,智慧家具,自动驾驶等离我们的平民百姓的生活越来越近。对前端而言在上面的多个领域都可以占有一席之地,个人猜想前端webGL人员需求在未来肯定有一段供不应求的时机。趁此机会去学习了一下three.js(选择它作为学习的库主要是因为它文档齐全有中文版的,需求又不依赖gis,如果有需要依赖gis为背景的项目可以参考cesium) 并做了个demo。这篇文章纯粹是为方便以后自己巩固知识点,和再次加深对three理解。如果也可以帮助到他人那就更棒了~
three配套插件
就像react拥有react-router,redux等一样,three也有自己的一些插件可以让我们在开发时大大的提高自己的开发效率。以下是我本次用到且觉很方便的一些插件:
- three-orbitcontrols(threejs本身就是一个3d引擎,而这个插件可以使threejs开发的场景实现自由拖动,缩放,角度控制等功能可以说是有threejs的地方就会有它)
- @tweenjs/tween.js (一个简单的补间动画库,有点类似css3的transition属性,在做一些过滤效果的时候非常方便)
- stats-js (监控three性能插件,如果对性能方面没有要求可以忽略)
- Earcut (因为three提供的几何体并不能满足我们平时的开发,所有我们要自定义一些几何体这时你就会发现贴图效果跟自己想的有很大差距,这是你就要自定义面和uv映射,而它就可以帮我们快速的构建几何面)
整体效果
gif图太大不能上传就只能看图片 。
万事开头难
此次开发难处比比皆是,刚开始就被设计所绊脚了。因为缺少设计人员只能跟另外一个前端同事两个人瞎琢磨。那种看着自己做得东西丑,但是你去修改一下发挥自己审美就变更丑的感觉真的是无力啊。最后我们放弃了那该死的美观(深深发现设计师的重要性,好想回到设计师的怀抱 😉 ),
因为是和另外一个同事一起开发,又是个单页面,想到代码提交冲突肯定少不了,于是我们就把项目拆分成了几个模块,项目入口主文件main.js, 外部模型文件,静态文件,工具文件。大家可以专注自己的模块减少代码冲突,还使得项目层次性更高,维护成本降低。
艰辛过程1
3d效果只要模型做得好,在加一些炫酷得动画效果。那这个东西看起来这么都不会太差。
由于没有会建模的同事,模型只有我们自己一点一点的画,复杂模型只有去网上找(有时候我们只想要一个简单的模型外观可以就行,但是网上基本上都是细节处理很好的模型体积过大,最终导致项目大QAQ)。
自己构建模型遇见一个很严重的问题,那就是利用挤压几何体(ExtrudeGeometry)建模时贴图总是达不到理想效果.如图:
右边贴图明显的不符合我们最开始的初衷,而导致这样的原因就是挤压几何体的uv映射规则。
何为uv映射? 它和我们生活中的水转印技术有点类似,如图:uv映射就像水中的花印,我们设置花印的大小,方向。具体可以参考https://www.cnblogs.com/yanan-boke/p/7815018.html
知道了原因我们就重新定义一下几何体的uv映射
/**
*
* @param geometry THREE.geometry THREE几何体
*/
reMapUv(geometry) {
let faces = geometry.faces;
geometry.faceVertexUvs[0] = []; // 清空几何体uv映射
geometry.faces.forEach(function(face, index) {
var components = ['x', 'y', 'z'];
var v1 = geometry.vertices[face.a];
var v2 = geometry.vertices[face.b];
var v3 = geometry.vertices[face.c];
let distanceZ = 0;
// 若三个点都在xy轴上,就把这个三个点构成的图形称为二维图形,否则为三维图形
if(v1.z == v2.z && v1.z == v3.z) {
distanceZ = 0;
} else {
distanceZ = v1.z || v2.z || v3.z;
}
let g = [];
// 判断是否为二维图形
if( !distanceZ) {
// 若为二维图形, 直接使用各个顶点的坐标
g.push(
new THREE.Vector2(v1.x, v1.y),
new THREE.Vector2(v2.x, v2.y),
new THREE.Vector2(v3.x, v3.y)
)
} else {
// 若为三维图形, 则使用同一平面的点的距离作为uv的x轴,异面点的距离作为y轴;
let distanceX = 0; // x轴
let distanceY = 0; // y轴
// 找出二维坐标
if(v1.z === v2.z ) {
distanceX = Math.sqrt(Math.pow((v1.x-v2.x),2)+Math.pow((v1.y-v2.y),2));
distanceY = Math.abs(v3.z - v1.z);
} else if(v1.z === v3.z) {
distanceX = Math.sqrt(Math.pow((v1.x-v3.x),2)+Math.pow((v1.y-v3.y),2));
distanceY = Math.abs(v2.z - v1.z);
} else {
distanceX = Math.sqrt(Math.pow((v3.x-v2.x),2)+Math.pow((v3.y-v2.y),2));
distanceY = Math.abs(v1.z - v2.z);
}
if(index % 2 === 0){
g.push(
new THREE.Vector2(0, 0),
new THREE.Vector2(distanceX, 0),
new THREE.Vector2(0, distanceY)
)
} else {
g.push(
new THREE.Vector2(distanceX, 0),
new THREE.Vector2(distanceX, distanceY),
new THREE.Vector2(0, distanceY)
)
}
}
geometry.faceVertexUvs[0].push(g);
});
}
在为几何体使用材质之前调用一下该方法就行了,效果如图:
艰辛过程2
为了场景看起来不那么空旷,我们决定在周围随机创建一些白模(偷了个懒,白模类型我们就用了一种 😂)听起来这个需求一点都不复杂甚至还有点简单,随着手指在键盘上不停的飞舞,代码敲好了,看看效果还不错,但看了一下fps居然下降了6-10。对于处女座的我来说肯定不行,所以就寻找优化方法,去百度,去社区论坛(不得不说程序员真的是很有奉献精神,乐于助人,在社区提出了问题一会儿就会有人来帮你,真的是很感动 😋)。最后得知原因是在创建白模的时候由于数量多场景里面有太多的mesh,而这些mesh会给渲染带来负担,所以最终我们合并全部的白模几何体,最终放到一个mesh里面。代码如下
/**
* 随机生成简单模型
* @param num number 白模数量
*/
randomBuild( num ) {
let demoBuild = new THREE.BoxBufferGeometry(20, 45, 20);
let geometers = [];
// 创建白模
for( let i=0; i<num; i++ ) {
let x,z;
if(i<num/4){
x = 750+(Math.random()-.5)*350
z = 1800*(Math.random()-.5)
}else if(i<num/2){
x = -760+(Math.random()-.5)*340
z = 1800*(Math.random()-.5)
}else if(i<num*3/4){
z = -600+(Math.random()-.5)*500
x = 1800*(Math.random()-.5)
}else{
z = 600+(Math.random()-.5)*500
x = 1800*(Math.random()-.5)
}
let s = THREE.Math.randFloat(0.6, 1.4);
let cloneDemo = demoBuild.clone();
cloneDemo.scale( 1, s, 1 );
cloneDemo.translate(x, 45*s/2, z);
geometers.push(cloneDemo);
}
// 合并为一个geometry对象
let geometry = BufferGeometryUtils.mergeBufferGeometries( geometers );
let mesh = new THREE.Mesh(geometry, new THREE.MeshToonMaterial({color: '#525352',transparent:true,opacity:.8}));
this.diffModel.random = mesh;
this.scene.add(this.diffModel.random);
}
艰辛过程3
最后就是一些动画效果。本身这个项目对动画得需求量就不是很大,就要一个涟漪扩散,和一个指定路线运动效果就可以了。这两个动画效果并不难,放在requestAnimationFrame里面就行了,唯一有点困难的就是涟漪效果得波浪渐变。 但之所以被这个问题困扰了很久是以为我一直很坚信THREE里面有一个属性就能实现这个效果(可能受到了echart的影响),翻了THREE的API几天都没有找到最终我放弃了。效果图吧。提一下核心思想: 其实中间的波浪图形是一个圆形只不过我用canvas画了一个渐变圆作为纹理,然后利用TWEEN循环扩大该几何体就行了。核心代码如下
/**
* 创建CanvasTexture纹理
* @param {string rgb类型} color 颜色
*/
createCanvasTexture(color){
let canvas = document.createElement("canvas");
canvas.width = 56;
canvas.height = 56;
let context = canvas.getContext("2d");
var gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
gradient.addColorStop(0, 'rgba(' + color + ')');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
return canvas;
}
// 波浪线图形
let textureWave = new THREE.CanvasTexture(createCanvasTexture('224,42,39,1'));
let materialWave = new THREE.MeshBasicMaterial({map: textureWave, side: THREE.DoubleSide});
let waveLine = new THREE.CircleBufferGeometry(4, 32);
let waveMesh = new THREE.Mesh(waveLine, materialWave);
总结
1.其实关于three的知识点还有很多像粒子系统,贴图map,three坐标系等等。之所有没有提似是因为网上这类似的总结很多很多,而且有些写的真的不错这里就几个我个人认为的难点。
2.其实three的社区真的很棒有问题直接去发帖求助,里面有几个大佬超级热情。(强力推荐)
3.开始做项目之前一定要把目录结构给分化清楚,整体流程在脑壳里面过一遍有一个大概思路,正所谓磨刀不误砍柴工,前面的思考会给你接下来的工作节省时间和减少麻烦。
鸡汤
其实做3d之前内心很没有底气的,以为之前根本就没有学过图形,而且涉及到空间知识。但人想要进步总是要一步一步跨出自己的舒适圈,大家都在进步你一直停留在原地那你也是退步了。在自己工作累了,遇见不开心的事了,我总用这句话来激励自己 “每一个优秀的人,都有一段沉默的时光。那一段时光,是付出了很多努力,忍受了很多的孤独和寂寞,不抱怨不诉苦,只有自己知道。而当日后说起时,连自己都能被感动的日子”。 愿你我皆能成为自己想成为的人。