1、WebGL與threeJS
WebGL是一種3D繪圖協(xié)議期虾,其允許JavaScript和OpenGL ES2.0結(jié)合在一起原朝,為H5 Canvas提供硬件3D加速渲染,可以借助系統(tǒng)顯卡在瀏覽器里更流暢地顯示3D場(chǎng)景和模型镶苞。Threejs是一款webGL框架喳坠,由于其易用性被廣泛應(yīng)用。Threejs在WebGL的api接口基礎(chǔ)上茂蚓,又進(jìn)行了一層封裝壕鹉。
WebGL原生的api是一種非常低層的接口剃幌,需要一些數(shù)學(xué)和圖形學(xué)的相關(guān)技術(shù)。其解決是如何在畫布上畫圖的問(wèn)題晾浴,怎么畫點(diǎn)负乡、線、面脊凰,怎么上色抖棘,怎么貼圖,怎么處理光線狸涌,視角轉(zhuǎn)動(dòng)之后怎么換算繪制等等切省。對(duì)于沒(méi)有相關(guān)基礎(chǔ)的人來(lái)說(shuō),入門很難帕胆,Three.js將入門的門檻降低了一大截朝捆,其解決底層的渲染細(xì)節(jié)和復(fù)雜的數(shù)據(jù)結(jié)構(gòu),將復(fù)雜的底層細(xì)節(jié)抽象出來(lái)懒豹,簡(jiǎn)化我們創(chuàng)建三維動(dòng)畫場(chǎng)景的過(guò)程芙盘。
2、ThreeJs核心概念
為快速入手歼捐,在使用threejs之前何陆,需要了解場(chǎng)景、照相機(jī)豹储、對(duì)象贷盲、光、渲染器等核心概念剥扣。
2.1巩剖、場(chǎng)景-Scene
場(chǎng)景是所有物體的容器,對(duì)應(yīng)著現(xiàn)實(shí)生活中三維世界钠怯,所有的可視化對(duì)象及相關(guān)的動(dòng)作均發(fā)生在場(chǎng)景中佳魔。
2.2、照相機(jī)-Camera
Camera是三維世界中觀察者晦炊,類似與眼睛鞠鲜。為了觀察這個(gè)世界,需要描述空間中的位置断国,three.js采用右手坐標(biāo)系
Threejs中的Camera有兩種贤姆,分別是正交投影相機(jī)THREE.OrthographicCamera和透視投影相機(jī)THREE.PerspectiveCamera
正交投影與透視投影的區(qū)別如上圖所示,左圖是正交投影稳衬,物體發(fā)出的光平行地投射到屏幕上霞捡,遠(yuǎn)近的方塊都是一樣大的;右圖是透視投影薄疚,近大遠(yuǎn)小碧信,符合我們平時(shí)看東西的感覺(jué)
2.3赊琳、對(duì)象-Objects
這是著名的斯坦福兔子,隨著三角形數(shù)量的增加砰碴,它的表面越來(lái)越平滑準(zhǔn)確躏筏。
在Three中,Mesh的構(gòu)造函數(shù)是這樣的:Mesh( geometry, material )呈枉。geometry是它的形狀寸士,material是它的材質(zhì)。不止是Mesh碴卧,創(chuàng)建很多物體都要用到這兩個(gè)屬性。下面我們來(lái)看看這兩個(gè)重要的屬性乃正。
Geometry--形狀住册,它通過(guò)存儲(chǔ)模型用到的點(diǎn)集和點(diǎn)間關(guān)系(哪些點(diǎn)構(gòu)成一個(gè)三角形)來(lái)達(dá)到描述物體形狀的目的。Three提供了立方體(其實(shí)是長(zhǎng)方體)瓮具、平面(其實(shí)是長(zhǎng)方形)荧飞、球體、圓形名党、圓柱叹阔、圓臺(tái)等6種基本形狀;你也可以通過(guò)自己定義每個(gè)點(diǎn)的位置來(lái)構(gòu)造形狀传睹;對(duì)于比較復(fù)雜的形狀耳幢,我們還可以通過(guò)外部的模型文件導(dǎo)入。
Material--材質(zhì)欧啤,材質(zhì)其實(shí)是物體表面除了形狀以為所有可視屬性的集合睛藻,例如色彩、紋理邢隧、光滑度店印、透明度、反射率倒慧、折射率按摘、發(fā)光度。Threejs里需要知道材質(zhì)(Material)纫谅、貼圖(Map)和紋理(Texture)的關(guān)系炫贤。材質(zhì)包括了貼圖以及其它。貼圖其實(shí)是‘貼’和‘圖’系宜,它包括了圖片和圖片應(yīng)當(dāng)貼到什么位置照激。紋理其實(shí)就是‘圖’。對(duì)于復(fù)雜的材質(zhì)盹牧,可以通過(guò)Threejs提供的貼圖和紋理api實(shí)現(xiàn)俩垃。同時(shí)励幼,Threejs提供了多種材質(zhì)可供選擇,能夠自由地選擇漫反射/鏡面反射等材質(zhì)口柳。
Points是另一種對(duì)象苹粟,其實(shí)就是一堆點(diǎn)的集合,它在之前很長(zhǎng)時(shí)間都被稱為ParticleSystem(粒子系統(tǒng))跃闹,而Three中的Points簡(jiǎn)單得多嵌削。因此最終這個(gè)類被命名為Points。
2.4望艺、光-Light
同現(xiàn)實(shí)世界一樣苛秕,我們要看到物體需要光,光影效果是讓畫面豐富的重要因素找默。Three提供了包括環(huán)境光AmbientLight艇劫、點(diǎn)光源PointLight、 聚光燈SpotLight惩激、方向光DirectionalLight店煞、半球光HemisphereLight等多種光源。只要在場(chǎng)景中添加需要的光源风钻,即可實(shí)現(xiàn)相應(yīng)得光效果顷蟀。
2.5、渲染器-Renderer
在場(chǎng)景中建立了各種物體骡技,也有了光鸣个,還有觀察物體的相機(jī),Renderer則負(fù)責(zé)將物體渲染到場(chǎng)景中布朦。Renderer綁定一個(gè)canvas對(duì)象毛萌,并可以設(shè)置大小,默認(rèn)背景顏色等屬性喝滞。
調(diào)用Renderer的render函數(shù)阁将,傳入scene和camera,就可以把圖像渲染到canvas中了右遭。
3做盅、vue結(jié)合three.js(準(zhǔn)備工作)
安裝three.js
安裝three-orbit-controls安裝軌道控件插件
安裝threebsp
安裝tweenjs/tween.js
npm install three three-orbit-controls threebsp tweenjs/tween.js -S
在需要用到的vue文件中導(dǎo)入
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可視化機(jī)房
目標(biāo)圖
3.1、初始化場(chǎng)景和相機(jī)
布置場(chǎng)景容器窘哈,之后所有我們創(chuàng)建的對(duì)象都會(huì)在scene場(chǎng)景對(duì)象中吹榴,相機(jī)的作用就相當(dāng)與人的眼睛。通過(guò)add()方法將創(chuàng)建的對(duì)象插入至場(chǎng)景中滚婉。
*// 獲取場(chǎng)景容器*
this.container = this.$refs.home*// 創(chuàng)建場(chǎng)景*
this.scene = new THREE.Scene()
let width = this.container.clientWidth*//窗口寬度*
let height = this.container.clientHeight*//窗口高度*
let k = width/height*//窗口寬高比*
let s = 200
*//創(chuàng)建相機(jī)對(duì)象*
this.camera = new THREE.OrthographicCamera(-s*k ,s*k ,s ,-s ,1 , 1000)
this.camera.position.set(80,100,200)*//設(shè)置相機(jī)位置*
this.camera.lookAt(this.scene.position)*//設(shè)置相機(jī)方向(指向場(chǎng)景對(duì)象)*
this.scene.add(this.camera)
3.2图筹、初始化渲染器
渲染場(chǎng)景容器,使場(chǎng)景呈現(xiàn)在頁(yè)面中,并且設(shè)置容器的大小登屬性
/*
- 創(chuàng)建渲染器對(duì)象*
*/
this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(width, height)*//設(shè)置渲染區(qū)域尺寸*
this.renderer.setClearColor(0x375e98,1)*//設(shè)置**渲染區(qū)域背景*
this.container.appendChild(this.renderer.domElement)*//home元素中插入canvas對(duì)象*
3.3远剩、構(gòu)建光系統(tǒng)
沒(méi)有光源的場(chǎng)景中是一片漆黑的扣溺,就算渲染器設(shè)置了渲染區(qū)域顏色,我們也看不見瓜晤,你可以理解為地球的夜晚中沒(méi)有光就會(huì)什么都看不見
點(diǎn)光源:THREE.PointLight()創(chuàng)建锥余,光線會(huì)從一個(gè)點(diǎn)向四周擴(kuò)散
環(huán)境光源:THREE.AmbientLight()環(huán)境光顏色與網(wǎng)格模型的顏色進(jìn)行RGB進(jìn)行乘法運(yùn)算,僅僅使用環(huán)境光的情況下痢掠,你會(huì)發(fā)現(xiàn)整個(gè)立方體沒(méi)有任何棱角感驱犹,這是因?yàn)榄h(huán)境光知識(shí)設(shè)置整個(gè)空間的明暗效果。如果需要立方體渲染要想有立體效果足画,需要使用具有方向性的點(diǎn)光源雄驹、平行光源等
*//點(diǎn)光源*
this.point1 = new THREE.PointLight(0xffffff)
this.point1.position.set(400,400,300)*//設(shè)置點(diǎn)光源位置*
this.scene.add(this.point1)
*//環(huán)境光源*
this.ambient = new THREE.AmbientLight(0x333333)
this.scene.add(this.ambient)
3.4、繪制機(jī)房地板
為了使機(jī)房的地板有自己的樣式淹辞,我找了一張地板紋理貼圖荠医,需要注意的是,紋理圖的尺寸都需要是寬和高都是2的冪桑涎,例如128x128、256*256等兼贡,這樣出來(lái)效果才會(huì)好攻冷。這也是3D軟件一般所要求的。另外紋理要能連續(xù)拼接不露破綻遍希,這樣才好
通過(guò)紋理貼圖加載器[TextureLoader]的load()方法加載一張圖片可以返回一個(gè)紋理對(duì)象Texture等曼,紋理對(duì)象Texture可以作為模型材質(zhì)顏色貼圖.map屬性的值。
//創(chuàng)建地板
let box = new THREE.BoxBufferGeometry(400,400,4)*//創(chuàng)建一個(gè)長(zhǎng)400凿蒜,寬400禁谦,厚度4的立方體*
let texture = new THREE.TextureLoader().load(require('../assets/textures/floor3.png'))*//加載紋理貼圖*
texture.wrapS = THREE.RepeatWrapping;*//水平方向紋理的包裹方式簡(jiǎn)單地重復(fù)到無(wú)窮大*
texture.wrapT = THREE.RepeatWrapping;*//垂直方向紋理的包裹方式簡(jiǎn)單地重復(fù)到無(wú)窮大*
texture.repeat.set( 12, 12 );*//水平、垂直重復(fù)次數(shù)*
let material = new THREE.MeshLambertMaterial({
map:texture *//材質(zhì)引用紋理貼圖*
})
this.mesh1 = new THREE.Mesh(box,material) *//創(chuàng)建網(wǎng)格對(duì)象*
this.mesh1.rotation.x = 0.5*Math.PI *//x軸旋轉(zhuǎn)*
this.scene.add(this.mesh1)
效果如下:
3.5废封、繪制機(jī)房過(guò)道邊墻
使為了減少重復(fù)代碼州泊,封裝了一個(gè)創(chuàng)建網(wǎng)格對(duì)象并插入場(chǎng)景方位的方法
*/**
* * ===========創(chuàng)建墻面*
* * width:寬度*
* * height:高度*
* * depth:深度*
* * angle:y軸旋轉(zhuǎn)角度*
* * material:材質(zhì)*
* * x:x坐標(biāo)位置*
* * y:y坐標(biāo)位置*
* * z:z坐標(biāo)位置*
* */*
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;*//-逆時(shí)針旋轉(zhuǎn),+順時(shí)針*
}
if (rotationY) {
wall.rotation.y += rotationY * Math.PI;*//-逆時(shí)針旋轉(zhuǎn),+順時(shí)針*
}
if (rotationZ) {
wall.rotation.z += rotationZ * Math.PI;*//-逆時(shí)針旋轉(zhuǎn),+順時(shí)針*
}
return wall
},
為創(chuàng)建玻璃效果,使用MeshStandardMaterial漂洋,PBR物理材質(zhì)遥皂,相比較高光Phong材質(zhì)可以更好的模擬金屬、玻璃等效果
并且為了讓玻璃拼接到墻內(nèi)去刽漂,threeBSP庫(kù)演训,可以將現(xiàn)有的模型組合出更多個(gè)性的模型來(lái)使用,它提供了3個(gè)方法來(lái)方便自由組合模型
- intersect(交集):使用該函數(shù)可以基于兩個(gè)現(xiàn)有幾何體的重合的部分定義此幾何體的形狀贝咙。
- union(并集):使用該函數(shù)可以將兩個(gè)幾何體聯(lián)合起來(lái)創(chuàng)建出一個(gè)新的幾何體样悟。
- subtract(差集):使用該函數(shù)可以在第一個(gè)幾何體中移除兩個(gè)幾何體重疊的部分來(lái)創(chuàng)建新的幾何體。
*//窗戶材質(zhì)*
let windowMaterial = new THREE.MeshStandardMaterial({
color:0x049ef4,*//注意:**transparent**必須設(shè)置為true,**opacity**的值才會(huì)生效*
opacity:0.5,
transparent:true,
})
*//創(chuàng)建邊墻*
let wall1 = this.createWall(400,60,6,-197,32,0,wallMaterial,0,0.5,0)
*//創(chuàng)建ThreeBSP對(duì)象*
let windowLeftBSP = new ThreeBSP(windowLeft)*//窗孔*
let wall1BSP = new ThreeBSP(wall1)
*//差集:新的模型會(huì)失去網(wǎng)格類型和網(wǎng)格材質(zhì),需要重新賦予----注意*
let resultwall1BSP = wall1BSP.subtract(windowLeftBSP)
*//更新網(wǎng)格對(duì)象*
let resultwall1 = resultwall1BSP.toMesh()
*//更新網(wǎng)格材質(zhì)*
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)
效果如下:
3.6窟她、繪制機(jī)房墻和綁定點(diǎn)擊事件
方法跟創(chuàng)建過(guò)道邊墻一樣陈症,利用threeBSP合成想要的墻體效果,但是為了使機(jī)房交互性更更強(qiáng)礁苗,添加了一個(gè)開門關(guān)門動(dòng)作爬凑。
由于在threeJS中,網(wǎng)格對(duì)象自身的旋轉(zhuǎn)试伙、平移都是針對(duì)于本身的中心為原點(diǎn)進(jìn)行的嘁信,所有使用Object3D()模型對(duì)象,它可以添加多個(gè)模型對(duì)象疏叨,要想門圍繞著自己的邊旋轉(zhuǎn)潘靖,就需要把門網(wǎng)格對(duì)象平移到Object3D()模型對(duì)象的中心,這樣開門的時(shí)候控制Object3D()模型對(duì)象繞y軸旋轉(zhuǎn)就可以實(shí)現(xiàn)開門關(guān)門了蚤蔓。
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/) = '房門'
實(shí)現(xiàn)方法是清楚了卦溢,可是如何給場(chǎng)景里的模型對(duì)象綁定點(diǎn)擊事件呢?這和普通的2D平面不一樣秀又,場(chǎng)景如何知道光標(biāo)位置以及作用的模型是哪個(gè)单寂?
光線投射(Raycaster):光線投射用于進(jìn)行鼠標(biāo)拾取(在三維空間中計(jì)算出鼠標(biāo)移過(guò)了什么物體)吐辙。
用戶點(diǎn)擊屏幕的時(shí)候宣决,threejs會(huì)根據(jù)視角從觸碰點(diǎn)會(huì)發(fā)射一條“激光”,激光掃到的所有記錄在數(shù)組里的對(duì)象昏苏,都被會(huì)捕捉到尊沸。
.setFromCamera(coords:Vector2,camera:Camera)
coords —— 在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中鼠標(biāo)的二維坐標(biāo) —— X分量與Y分量應(yīng)當(dāng)在-1到1之間。
camera —— 射線所來(lái)源的攝像機(jī)贤惯。
.intersectObjects(objects:Array,recursive:Boolean,optionalTarget:Array)
objects —— 檢測(cè)和射線相交的一組物體洼专。
recursive —— 若為true,則同時(shí)也會(huì)檢測(cè)所有物體的后代孵构。否則將只會(huì)檢測(cè)對(duì)象本身的相交部分屁商。默認(rèn)值為false。
optionalTarget —— (可選)(可選)設(shè)置結(jié)果的目標(biāo)數(shù)組颈墅。如果不設(shè)置這個(gè)值棒假,則一個(gè)新的Array會(huì)被實(shí)例化;如果設(shè)置了這個(gè)值精盅,則在每次調(diào)用之前必須清空這個(gè)數(shù)組(例如:array.length = 0;)帽哑。
檢測(cè)所有在射線與這些物體之間,包括或不包括后代的相交部分叹俏。返回結(jié)果時(shí)妻枕,相交部分將按距離進(jìn)行排序,最近的位于第一個(gè)),相交部分和.intersectObject所返回的格式是相同的屡谐。
*//給容器綁定點(diǎn)擊事件述么,以獲取光標(biāo)與容器的相對(duì)位置*
this.container.addEventListener( 'click', e => this.doorHandle(e), false );
this.raycaster = new THREE.Raycaster()
*//門點(diǎn)擊事件*
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
*// 通過(guò)鼠標(biāo)點(diǎn)的位置和當(dāng)前相機(jī)的矩陣計(jì)算出raycaster*
this.raycaster.setFromCamera( this.mouse, this.camera );
*// 獲取raycaster直線和所有模型相交的數(shù)組集合*
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
}
}
})
},
這樣一來(lái)點(diǎn)擊事件已經(jīng)綁定成功了,但突如其來(lái)的關(guān)門動(dòng)作顯得很生硬愕掏,這里還用到了tween.js度秘,借助tween.js快速創(chuàng)建補(bǔ)間動(dòng)畫,可以非常方便的控制機(jī)械饵撑、游戲角色運(yùn)動(dòng)
效果如圖:
3.7剑梳、繪制機(jī)房盆栽
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
},
效果如下:
3.8、繪制機(jī)柜
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
},
效果如下:
4滑潘、總結(jié)
前端開發(fā)要學(xué)習(xí)的地方還是太多了垢乙,多種技術(shù)交替穿插使用。webgl算是最難的一門技術(shù)之一语卤,難點(diǎn)不在于技術(shù)的難追逮,而在于靈活性大、涉及知識(shí)面廣粹舵。而three.js是我去年就知道的一個(gè)基于webgl的框架钮孵,3d機(jī)房是一個(gè)很好的學(xué)習(xí)過(guò)程,也很高興自己能在工作閑暇的時(shí)候接觸到three.js眼滤,也會(huì)繼續(xù)實(shí)現(xiàn)功能更多更復(fù)雜的3d機(jī)房巴席。