vue+three.js的3D可视化机房

1、WebGL与threeJS

WebGL是一种3D绘图协议,其允许JavaScript和OpenGL ES2.0结合在一起,为H5 Canvas提供硬件3D加速渲染,可以借助系统显卡在浏览器里更流畅地显示3D场景和模型。Threejs是一款webGL框架,由于其易用性被广泛应用。Threejs在WebGL的api接口基础上,又进行了一层封装。

WebGL原生的api是一种非常低层的接口,需要一些数学和图形学的相关技术。其解决是如何在画布上画图的问题,怎么画点、线、面,怎么上色,怎么贴图,怎么处理光线,视角转动之后怎么换算绘制等等。对于没有相关基础的人来说,入门很难,Three.js将入门的门槛降低了一大截,其解决底层的渲染细节和复杂的数据结构,将复杂的底层细节抽象出来,简化我们创建三维动画场景的过程。

2、ThreeJs核心概念

为快速入手,在使用threejs之前,需要了解场景、照相机、对象、光、渲染器等核心概念。

2.1、场景-Scene

场景是所有物体的容器,对应着现实生活中三维世界,所有的可视化对象及相关的动作均发生在场景中。

2.2、照相机-Camera

Camera是三维世界中观察者,类似与眼睛。为了观察这个世界,需要描述空间中的位置,three.js采用右手坐标系

image2021-1-4_11-40-25.png

Threejs中的Camera有两种,分别是正交投影相机THREE.OrthographicCamera和透视投影相机THREE.PerspectiveCamera

image2021-1-4_11-40-31.png

正交投影与透视投影的区别如上图所示,左图是正交投影,物体发出的光平行地投射到屏幕上,远近的方块都是一样大的;右图是透视投影,近大远小,符合我们平时看东西的感觉

2.3、对象-Objects

这是著名的斯坦福兔子,随着三角形数量的增加,它的表面越来越平滑准确。

image2021-1-4_11-40-37.png

在Three中,Mesh的构造函数是这样的:Mesh( geometry, material )。geometry是它的形状,material是它的材质。不止是Mesh,创建很多物体都要用到这两个属性。下面我们来看看这两个重要的属性。

Geometry--形状,它通过存储模型用到的点集和点间关系(哪些点构成一个三角形)来达到描述物体形状的目的。Three提供了立方体(其实是长方体)、平面(其实是长方形)、球体、圆形、圆柱、圆台等6种基本形状;你也可以通过自己定义每个点的位置来构造形状;对于比较复杂的形状,我们还可以通过外部的模型文件导入。

Material--材质,材质其实是物体表面除了形状以为所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度。Threejs里需要知道材质(Material)、贴图(Map)和纹理(Texture)的关系。材质包括了贴图以及其它。贴图其实是‘贴’和‘图’,它包括了图片和图片应当贴到什么位置。纹理其实就是‘图’。对于复杂的材质,可以通过Threejs提供的贴图和纹理api实现。同时,Threejs提供了多种材质可供选择,能够自由地选择漫反射/镜面反射等材质。
Points是另一种对象,其实就是一堆点的集合,它在之前很长时间都被称为ParticleSystem(粒子系统),而Three中的Points简单得多。因此最终这个类被命名为Points。

2.4、光-Light

同现实世界一样,我们要看到物体需要光,光影效果是让画面丰富的重要因素。Three提供了包括环境光AmbientLight、点光源PointLight、 聚光灯SpotLight、方向光DirectionalLight、半球光HemisphereLight等多种光源。只要在场景中添加需要的光源,即可实现相应得光效果。

2.5、渲染器-Renderer

在场景中建立了各种物体,也有了光,还有观察物体的相机,Renderer则负责将物体渲染到场景中。Renderer绑定一个canvas对象,并可以设置大小,默认背景颜色等属性。
调用Renderer的render函数,传入scene和camera,就可以把图像渲染到canvas中了。

3、vue结合three.js(准备工作)

  1. 安装three.js

  2. 安装three-orbit-controls安装轨道控件插件

  3. 安装threebsp

  4. 安装tweenjs/tween.js

npm install three three-orbit-controls threebsp tweenjs/tween.js -S

在需要用到的vue文件中导入

import * as THREE from 'three'

import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'

import 'imports-loader?THREE=three!threebsp'

import TWEEN from '@tweenjs/tween.js'
  • ThreeJs制作3D可视化机房
    image2021-1-4_11-40-58.png

目标图

3.1、初始化场景和相机

布置场景容器,之后所有我们创建的对象都会在scene场景对象中,相机的作用就相当与人的眼睛。通过add()方法将创建的对象插入至场景中。

*// 获取场景容器*

this.container = this.$refs.home*// 创建场景*

this.scene = new THREE.Scene()

let width = this.container.clientWidth*//窗口宽度*

let height = this.container.clientHeight*//窗口高度*

let k = width/height*//窗口宽高比*

let s = 200 

*//创建相机对象*

this.camera = new THREE.OrthographicCamera(-s*k ,s*k ,s ,-s ,1 , 1000)

this.camera.position.set(80,100,200)*//设置相机位置*

this.camera.lookAt(this.scene.position)*//设置相机方向(指向场景对象)*

this.scene.add(this.camera)

3.2、初始化渲染器

渲染场景容器,使场景呈现在页面中,并且设置容器的大小登属性

/*

  • 创建渲染器对象*

*/

this.renderer = new THREE.WebGLRenderer()

this.renderer.setSize(width, height)*//设置渲染区域尺寸*

this.renderer.setClearColor(0x375e98,1)*//设置**渲染区域背景*

this.container.appendChild(this.renderer.domElement)*//home元素中插入canvas对象*

3.3、构建光系统

没有光源的场景中是一片漆黑的,就算渲染器设置了渲染区域颜色,我们也看不见,你可以理解为地球的夜晚中没有光就会什么都看不见

点光源:THREE.PointLight()创建,光线会从一个点向四周扩散

环境光源:THREE.AmbientLight()环境光颜色与网格模型的颜色进行RGB进行乘法运算,仅仅使用环境光的情况下,你会发现整个立方体没有任何棱角感,这是因为环境光知识设置整个空间的明暗效果。如果需要立方体渲染要想有立体效果,需要使用具有方向性的点光源、平行光源等

*//点光源*

this.point1 = new THREE.PointLight(0xffffff)

this.point1.position.set(400,400,300)*//设置点光源位置*

this.scene.add(this.point1)

*//环境光源*
 
this.ambient = new THREE.AmbientLight(0x333333)

this.scene.add(this.ambient)

3.4、绘制机房地板

为了使机房的地板有自己的样式,我找了一张地板纹理贴图,需要注意的是,纹理图的尺寸都需要是宽和高都是2的幂,例如128x128、256*256等,这样出来效果才会好。这也是3D软件一般所要求的。另外纹理要能连续拼接不露破绽,这样才好

image2021-1-4_11-41-27.png

通过纹理贴图加载器[TextureLoader]的load()方法加载一张图片可以返回一个纹理对象Texture,纹理对象Texture可以作为模型材质颜色贴图.map属性的值。

//创建地板

let box = new THREE.BoxBufferGeometry(400,400,4)*//创建一个长400,宽400,厚度4的立方体*

let texture = new THREE.TextureLoader().load(require('../assets/textures/floor3.png'))*//加载纹理贴图*

texture.wrapS = THREE.RepeatWrapping;*//水平方向纹理的包裹方式简单地重复到无穷大*

texture.wrapT = THREE.RepeatWrapping;*//垂直方向纹理的包裹方式简单地重复到无穷大*

texture.repeat.set( 12, 12 );*//水平、垂直重复次数*

let material = new THREE.MeshLambertMaterial({

   map:texture *//材质引用纹理贴图*

})

this.mesh1 = new THREE.Mesh(box,material) *//创建网格对象*

this.mesh1.rotation.x = 0.5*Math.PI *//x轴旋转*

this.scene.add(this.mesh1)

效果如下:

image2021-1-4_11-41-34.png

3.5、绘制机房过道边墙

使为了减少重复代码,封装了一个创建网格对象并插入场景方位的方法

*/**

*        *  ===========创建墙面*

*        *  width:宽度*

*        *  height:高度*

*        *  depth:深度*

*        *  angle:y轴旋转角度*

*        *  material:材质*

*        *  x:x坐标位置*

*        *  y:y坐标位置*

*        *  z:z坐标位置*

*    */*

createWall(width, height, depth, x, y, z,material, rotationX, rotationY, rotationZ){

      let geometry = new THREE.BoxGeometry(width,height,depth)

      let wall = new THREE.Mesh(geometry, material)

      wall.position.set(x,y,z)

      if (rotationX) {

        wall.rotation.x += rotationX * Math.PI;*//-逆时针旋转,+顺时针*

      }

      if (rotationY) {

        wall.rotation.y += rotationY * Math.PI;*//-逆时针旋转,+顺时针*

      }

      if (rotationZ) {

        wall.rotation.z += rotationZ * Math.PI;*//-逆时针旋转,+顺时针*

      }

    return wall

},

为创建玻璃效果,使用MeshStandardMaterial,PBR物理材质,相比较高光Phong材质可以更好的模拟金属、玻璃等效果

并且为了让玻璃拼接到墙内去,threeBSP库,可以将现有的模型组合出更多个性的模型来使用,它提供了3个方法来方便自由组合模型

  • intersect(交集):使用该函数可以基于两个现有几何体的重合的部分定义此几何体的形状。
  • union(并集):使用该函数可以将两个几何体联合起来创建出一个新的几何体。
  • subtract(差集):使用该函数可以在第一个几何体中移除两个几何体重叠的部分来创建新的几何体。
*//窗户材质*
let windowMaterial = new THREE.MeshStandardMaterial({

    color:0x049ef4,*//注意:**transparent**必须设置为true,**opacity**的值才会生效*

    opacity:0.5,

    transparent:true,

})

*//创建边墙*
let wall1 = this.createWall(400,60,6,-197,32,0,wallMaterial,0,0.5,0)

*//创建ThreeBSP对象*

let windowLeftBSP = new ThreeBSP(windowLeft)*//窗孔*

let wall1BSP = new ThreeBSP(wall1)

*//差集:新的模型会失去网格类型和网格材质,需要重新赋予----注意*

let resultwall1BSP = wall1BSP.subtract(windowLeftBSP)

*//更新网格对象*

let resultwall1 = resultwall1BSP.toMesh()

*//更新网格材质*

resultwall1.material = wallMaterial

*//玻璃窗*

let windowleftTrue = this.createWall(400,45,2,-197,32,0,windowMaterial,0,0.5,0)

this.scene.add(resultwall1)

this.scene.add(windowleftTrue)

效果如下:

image2021-1-4_11-41-45.png

3.6、绘制机房墙和绑定点击事件

方法跟创建过道边墙一样,利用threeBSP合成想要的墙体效果,但是为了使机房交互性更更强,添加了一个开门关门动作。

由于在threeJS中,网格对象自身的旋转、平移都是针对于本身的中心为原点进行的,所有使用Object3D()模型对象,它可以添加多个模型对象,要想门围绕着自己的边旋转,就需要把门网格对象平移到Object3D()模型对象的中心,这样开门的时候控制Object3D()模型对象绕y轴旋转就可以实现开门关门了。

this.door3D = new THREE.Object3D()

let doorGeometry = new THREE.BoxGeometry(28,40,2)

let doorTexture = new THREE.TextureLoader().load(require('../assets/textures/door.png'));

let doorMaterial = new THREE.MeshLambertMaterial({map:doorTexture,side:THREE.DoubleSide,transparent:true});

let doorTrue = new THREE.Mesh( doorGeometry, doorMaterial )

doorTrue.position.x = 14

[doorTrue.name](http://doortrue.name/) = '房门'

实现方法是清楚了,可是如何给场景里的模型对象绑定点击事件呢?这和普通的2D平面不一样,场景如何知道光标位置以及作用的模型是哪个?

光线投射(Raycaster):光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。

用户点击屏幕的时候,threejs会根据视角从触碰点会发射一条“激光”,激光扫到的所有记录在数组里的对象,都被会捕捉到。

.setFromCamera(coords:Vector2,camera:Camera)

coords —— 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。
camera —— 射线所来源的摄像机。

.intersectObjects(objects:Array,recursive:Boolean,optionalTarget:Array)

objects —— 检测和射线相交的一组物体。
recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为false。
optionalTarget —— (可选)(可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。

检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个),相交部分和.intersectObject所返回的格式是相同的。

*//给容器绑定点击事件,以获取光标与容器的相对位置*
this.container.addEventListener( 'click', e => this.doorHandle(e), false );

this.raycaster = new THREE.Raycaster()

*//门点击事件*

doorHandle(event){

   event.preventDefault();

   this.mouse.x = (event.offsetX/this.renderer.domElement.clientWidth) * 2 - 1

   this.mouse.y = -(event.offsetY/this.renderer.domElement.clientHeight) *2 + 1

      *// 通过鼠标点的位置和当前相机的矩阵计算出raycaster*

   this.raycaster.setFromCamera( this.mouse, this.camera );

      *// 获取raycaster直线和所有模型相交的数组集合*

   let intersects = this.raycaster.intersectObjects( this.scene.children,true );

   if (intersects.length > 0 && intersects[0].[object.name](http://object.name/) == '房门') {

      if (this.closeDoorFlag) {

        this.tweenTransform(this.door3D,500,0.4*Math.PI)

        this.closeDoorFlag = false

       } else {

           this.tweenTransform(this.door3D,500,0*Math.PI)

           this.closeDoorFlag = true

       }

  return

}

this.cabinetList3D.forEach(item => {

        if (item.children[1].children[0].name == intersects[0].[object.name](http://object.name/)) {

          if (item.children[1].closeFlag) {

            this.tweenTransform(item.children[1],500,0*Math.PI)

            item.children[1].closeFlag = false

          } else {

            this.tweenTransform(item.children[1],500,0.6*Math.PI)

            item.children[1].closeFlag = true

          }

        }

      })

    },

这样一来点击事件已经绑定成功了,但突如其来的关门动作显得很生硬,这里还用到了tween.js,借助tween.js快速创建补间动画,可以非常方便的控制机械、游戏角色运动

效果如图:

image2021-1-4_11-41-56.png

3.7、绘制机房盆栽

createPlant(x,y,z){

      let plant = new THREE.Object3D();

      let geometryplant = new THREE.CylinderBufferGeometry( 5, 3, 10, 22 );

      let materialplant = new THREE.MeshLambertMaterial( {color: 0x845527} );

      let cylinder = new THREE.Mesh( geometryplant, materialplant );

      cylinder.position.x = 0;

      cylinder.position.y = 4;

      cylinder.position.z = 0;

      plant.add( cylinder );

      let leafTexture = new THREE.TextureLoader().load(require('../assets/textures/plant1.png'));

      let leafMaterial = new THREE.MeshBasicMaterial({map:leafTexture,side:THREE.DoubleSide,transparent:true});

      let geom = new THREE.PlaneGeometry(12, 24);

      for(var i=0;i<4;i++){

        let leaf = new THREE.Mesh( geom, leafMaterial );

        leaf.position.y = 18;

        leaf.rotation.y = -Math.PI/(i+1);

        plant.add(leaf);

      }
      plant.position.x = x;

      plant.position.y = y;

      plant.position.z = z;

      return plant

    },

效果如下:

image2021-1-4_11-42-5.png

3.8、绘制机柜

createCabinet(x,z,id){

      let cabinet3D = new THREE.Object3D()

      let cabinetTexture = new THREE.TextureLoader().load(require('../assets/textures/jigui4.png'));

      cabinetTexture.wrapS = THREE.RepeatWrapping;

      cabinetTexture.wrapT = THREE.RepeatWrapping;

      cabinetTexture.repeat.set( 1, 1 );

      let cabinetMaterial = new THREE.MeshLambertMaterial(

        {

          map:cabinetTexture,

          side:THREE.DoubleSide,transparent:true,

        }

      );

      let outsideBox = new THREE.BoxGeometry(30,60,36)

      let outside = new THREE.Mesh(outsideBox,cabinetMaterial)

      let insideBox = new THREE.BoxGeometry(26,56,32)

      let inside = new THREE.Mesh(insideBox,cabinetMaterial)

      inside.position.x = 2

      let outsideBSP = new ThreeBSP(outside)

      let insideBSP = new ThreeBSP(inside)

      let cabinetBSP = outsideBSP.subtract(insideBSP)

      let cabinet = cabinetBSP.toMesh()

      cabinet.material = cabinetMaterial

      cabinet3D.add(cabinet)

      let cabDoor3D = new THREE.Object3D()

      let cabDoorTexture = new THREE.TextureLoader().load(require('../assets/textures/cabDoor3.jpg'));

      let cabDoorMaterial = new THREE.MeshLambertMaterial(

        {

          map:cabDoorTexture,

          side:THREE.DoubleSide,transparent:true,

        }

      );

      let cabDoorBox = new THREE.BoxGeometry(1,56,32)

      let cabDoor = new THREE.Mesh(cabDoorBox,cabDoorMaterial)

      [cabDoor.name](http://cabdoor.name/) = id

      cabDoor.position.z = 16

      cabDoor3D.add(cabDoor)

      cabDoor3D.closeFlag = false

      cabDoor3D.position.set(14,0,-16)

      cabinet3D.add(cabDoor3D)

      cabinet3D.position.set(x,32,z)

      return cabinet3D

    },

效果如下:

image2021-1-4_11-42-14.png

4、总结

前端开发要学习的地方还是太多了,多种技术交替穿插使用。webgl算是最难的一门技术之一,难点不在于技术的难,而在于灵活性大、涉及知识面广。而three.js是我去年就知道的一个基于webgl的框架,3d机房是一个很好的学习过程,也很高兴自己能在工作闲暇的时候接触到three.js,也会继续实现功能更多更复杂的3d机房。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容