我是如何用 Three.js 在三維世界建房子的(詳細(xì)教程)
這兩天用 Three.js 畫了一個(gè) 3D 的房子,放了一個(gè)床進(jìn)去,可以用鼠標(biāo)和鍵盤控制移動(dòng),有種 3D 游戲的即視感。
這篇文章就來講下實(shí)現(xiàn)原理。
代碼地址:https://github.com/QuarkGluonPlasma/threejs-exercize
思路分析
我們先不著急寫代碼,先來分析下思路。
這樣一個(gè)房子,其實(shí)也是由幾個(gè)幾何體堆起來的:
具體有這么些幾何體:
地板就是個(gè)平面,用 PlaneGeometry(平面幾何體) 就可以畫,貼上個(gè)紋理貼圖就行。
兩個(gè)側(cè)面的墻,是一個(gè)不規(guī)則的形狀,這個(gè)可以用 ExtrudeGeometry(擠壓幾何體),它支持用畫筆畫一個(gè) 2D 的路徑,然后加厚變成 3D 的。
同理,后面的墻也很簡(jiǎn)單,可以是 BoxGeometry(立方體)來畫,也可以是 ExtrudeGeometry(擠壓結(jié)合體)先畫個(gè)形狀,然后變成 3D 的。
前面的墻稍微復(fù)雜些,它也是不規(guī)則的,可以用 ExtrudeGeometry(擠壓幾何體)來畫出形狀,然后變成 3D 的,只不過它多了兩個(gè)洞,需要畫兩個(gè)洞加到形狀里面去。
門框、窗框也是形狀里扣個(gè)洞,用 ExtrudeGeometry 變成 3D 的。
那房頂呢?房頂也沒什么特殊的,只是立方體旋轉(zhuǎn)一定的角度就行,用 BoxGeometry(立方體) 就可以畫。
接下來,給墻和房頂、地板貼上不同的圖,設(shè)置好不同的位置,就可以組裝成一個(gè)房子了。
那么床呢?
Three.js 提供了很多的幾何體,可以畫一些簡(jiǎn)單的物體,但復(fù)雜的物體就很難畫出來了,這類物體一般會(huì)用專業(yè)的 3D 建模軟件來畫,導(dǎo)出 FPX 或者 OBJ 格式的文件由 Three.js 加載并渲染出來。
我們?cè)诰W(wǎng)上找一個(gè)床的 3D 模型,我找了一個(gè) FBX 格式的,然后用 Three.js 的 FBXLoader 加載就行。
還剩下一個(gè)草地,這個(gè)也是一個(gè)平面,用 PlaneGeometry(平面幾何體)畫,只不過就是長寬比較大,看不到盡頭而已。
看起來還有霧?
沒錯(cuò),確實(shí)設(shè)置了霧(Fog),Three.js 在場(chǎng)景中設(shè)置霧的效果,指定顏色和霧的遠(yuǎn)近范圍就行。為了有種模糊的感覺,我就在場(chǎng)景中加入了霧。
全部的物體都畫完了,接下來就可以在 3D 場(chǎng)景中漫游了,通過鼠標(biāo)和鍵盤可以改變方向和前后左右移動(dòng),這種交互使用 FirstPersonControls(第一人稱控制器) 來實(shí)現(xiàn)。
一般我們常用的是 OrbitsControls(軌道控制器),它支持圍繞物體轉(zhuǎn)動(dòng)相機(jī),就像衛(wèi)星一樣。但我們這里不是想繞著轉(zhuǎn),而是想鍵盤和鼠標(biāo)控制的前后左右的隨意移動(dòng)。
我們簡(jiǎn)單小結(jié)下:
Three.js 是在三維的坐標(biāo)系中添加各種物體,組裝成不同的 3D 場(chǎng)景。其中簡(jiǎn)單的物體可以畫,復(fù)雜的物體會(huì)用建模軟件畫,然后加載到場(chǎng)景中。我們可以用不同的控制器來控制相機(jī)移動(dòng),達(dá)到不同的交互效果,比如軌道控制器、第一人稱控制器等。
房子的墻、地板、房頂都可以用 BoxGeometry(立方體)、ExtrudeGeometry(擠壓幾何體)畫出來,但是床這種復(fù)雜的就不行了,會(huì)直接加載模型文件。
通過 FistPersonControls(第一人稱控制器)來控制交互,就能達(dá)到 3D 游戲的那種感覺。
思路理清了,接下來我們具體寫下代碼:
代碼實(shí)現(xiàn)
先畫草地,也就是一個(gè)大的平面,貼上草地的貼圖。
三維的物體(Mesh) 是由幾何體(Geometry),加上材質(zhì)(Material)構(gòu)成的。我們創(chuàng)建平面幾何體(PlaneGeometry),長和寬制定一個(gè)很大的值,比如 10000,然后加載草地的圖片作為紋理(Texture),構(gòu)成材質(zhì)。之后就可以創(chuàng)建出草地了。
- function createGrass() {
- const geometry = new THREE.PlaneGeometry( 10000, 10000);
- const texture = new THREE.TextureLoader().load('img/grass.jpg');
- texture.wrapS = THREE.RepeatWrapping;
- texture.wrapT = THREE.RepeatWrapping;
- texture.repeat.set( 100, 100 );
- const grassMaterial = new THREE.MeshBasicMaterial({map: texture});
- const grass = new THREE.Mesh( geometry, grassMaterial );
- grass.rotation.x = -0.5 * Math.PI;
- scene.add( grass );
- }
紋理貼圖要設(shè)置兩個(gè)方向都重復(fù),重復(fù)的次數(shù)是 100 次。
然后草地的平面要旋轉(zhuǎn)一下。
加點(diǎn)霧,讓天際模糊一些:
- scene.fog = new THREE.Fog(0xffffff, 10, 1500);
分別指定顏色為白色,霧的遠(yuǎn)近范圍為 10 到 1500。
接下來是創(chuàng)建房子,房子由地板、兩側(cè)的墻、前面的墻、后面的墻、門框窗框、房頂、床構(gòu)成,要分別創(chuàng)建每一部分,我們把它們放到單獨(dú)的 Group(分組)里。
- const house = new THREE.Group();
- function createHouse() {
- createFloor();
- const sideWall = createSideWall();
- const sideWall2 = createSideWall();
- sideWall2.position.z = 300;
- createFrontWall();
- createBackWall();
- const roof = createRoof();
- const roof2 = createRoof();
- roof2.rotation.x = Math.PI / 2;
- roof2.rotation.y = Math.PI / 4 * 0.6;
- roof2.position.y = 130;
- roof2.position.x = -50;
- roof2.position.z = 155;
- createWindow();
- createDoor();
- createBed();
- }
創(chuàng)建地板也是平面幾何體(PlaneGeometry),貼上木材的圖就行,然后設(shè)置下位置:
- function createFloor() {
- const geometry = new THREE.PlaneGeometry( 200, 300);
- const texture = new THREE.TextureLoader().load('img/wood.jpg');
- texture.wrapS = THREE.RepeatWrapping;
- texture.wrapT = THREE.RepeatWrapping;
- texture.repeat.set( 2, 2 );
- const material = new THREE.MeshBasicMaterial({map: texture});
- const floor = new THREE.Mesh( geometry, material );
- floor.rotation.x = -0.5 * Math.PI;
- floor.position.y = 1;
- floor.position.z = 150;
- house.add(floor);
- }
創(chuàng)建側(cè)面的墻,要用 ExtrudeGeometry(擠壓幾何體)來畫,也就是先畫出一個(gè) 2D 的形狀,然后擠壓成 3D。還要貼上墻的紋理貼圖。
- function createSideWall() {
- const shape = new THREE.Shape();
- shape.moveTo(-100, 0);
- shape.lineTo(100, 0);
- shape.lineTo(100,100);
- shape.lineTo(0,150);
- shape.lineTo(-100,100);
- shape.lineTo(-100,0);
- const extrudeGeometry = new THREE.ExtrudeGeometry( shape );
- const texture = new THREE.TextureLoader().load('./img/wall.jpg');
- texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
- texture.repeat.set( 0.01, 0.005 );
- var material = new THREE.MeshBasicMaterial( {map: texture} );
- const sideWall = new THREE.Mesh( extrudeGeometry, material ) ;
- house.add(sideWall);
- return sideWall;
- }
兩個(gè)側(cè)墻只是位置不同,修改下 z 軸位置就行:
- const sideWall = createSideWall();
- const sideWall2 = createSideWall();
- sideWall2.position.z = 300;
對(duì)了,如果對(duì)位置拿不準(zhǔn),可以在場(chǎng)景中加個(gè)坐標(biāo)系輔助工具(AxisHelper)。
- const axisHelper = new THREE.AxisHelper(2000);
- scene.add(axisHelper);
然后是后面的墻,這個(gè)形狀簡(jiǎn)單一些,就是個(gè)矩形:
- function createBackWall() {
- const shape = new THREE.Shape();
- shape.moveTo(-150, 0)
- shape.lineTo(150, 0)
- shape.lineTo(150,100)
- shape.lineTo(-150,100);
- const extrudeGeometry = new THREE.ExtrudeGeometry( shape )
- const texture = new THREE.TextureLoader().load('./img/wall.jpg');
- texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
- texture.repeat.set( 0.01, 0.005 );
- const material = new THREE.MeshBasicMaterial({map: texture});
- const backWall = new THREE.Mesh( extrudeGeometry, material) ;
- backWall.position.z = 150;
- backWall.position.x = -100;
- backWall.rotation.y = Math.PI * 0.5;
- house.add(backWall);
- }
接下來是前面的墻,這個(gè)除了要畫出形狀外,還要摳出兩個(gè)洞:
- function createFrontWall() {
- const shape = new THREE.Shape();
- shape.moveTo(-150, 0);
- shape.lineTo(150, 0);
- shape.lineTo(150,100);
- shape.lineTo(-150,100);
- shape.lineTo(-150,0);
- const window = new THREE.Path();
- window.moveTo(30,30)
- window.lineTo(80, 30)
- window.lineTo(80, 80)
- window.lineTo(30, 80);
- window.lineTo(30, 30);
- shape.holes.push(window);
- const door = new THREE.Path();
- door.moveTo(-30, 0)
- door.lineTo(-30, 80)
- door.lineTo(-80, 80)
- door.lineTo(-80, 0);
- door.lineTo(-30, 0);
- shape.holes.push(door);
- const extrudeGeometry = new THREE.ExtrudeGeometry( shape )
- const texture = new THREE.TextureLoader().load('./img/wall.jpg');
- texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
- texture.repeat.set( 0.01, 0.005 );
- const material = new THREE.MeshBasicMaterial({map: texture} );
- const frontWall = new THREE.Mesh( extrudeGeometry, material ) ;
- frontWall.position.z = 150;
- frontWall.position.x = 100;
- frontWall.rotation.y = Math.PI * 0.5;
- house.add(frontWall);
- }
只是形狀上多了兩個(gè)洞,畫起來復(fù)雜些,其余的紋理、材質(zhì),還有位置等設(shè)置方式都一樣。
門窗也是畫一個(gè)形狀,摳一個(gè)洞,然后加點(diǎn)厚度變成 3D 的:
- function createWindow() {
- const shape = new THREE.Shape();
- shape.moveTo(0, 0);
- shape.lineTo(0, 50)
- shape.lineTo(50,50)
- shape.lineTo(50,0);
- shape.lineTo(0, 0);
- const hole = new THREE.Path();
- hole.moveTo(5,5)
- hole.lineTo(5, 45)
- hole.lineTo(45, 45)
- hole.lineTo(45, 5);
- hole.lineTo(5, 5);
- shape.holes.push(hole);
- const extrudeGeometry = new THREE.ExtrudeGeometry(shape);
- var extrudeMaterial = new THREE.MeshBasicMaterial({ color: 'silver' });
- var window = new THREE.Mesh( extrudeGeometry, extrudeMaterial ) ;
- window.rotation.y = Math.PI / 2;
- window.position.y = 30;
- window.position.x = 100;
- window.position.z = 120;
- house.add(window);
- return window;
- }
顏色設(shè)置為銀白色。
門框也是一樣:
- function createDoor() {
- const shape = new THREE.Shape();
- shape.moveTo(0, 0);
- shape.lineTo(0, 80);
- shape.lineTo(50,80);
- shape.lineTo(50,0);
- shape.lineTo(0, 0);
- const hole = new THREE.Path();
- hole.moveTo(5,5);
- hole.lineTo(5, 75);
- hole.lineTo(45, 75);
- hole.lineTo(45, 5);
- hole.lineTo(5, 5);
- shape.holes.push(hole);
- const extrudeGeometry = new THREE.ExtrudeGeometry( shape );
- const material = new THREE.MeshBasicMaterial( { color: 'silver' } );
- const door = new THREE.Mesh( extrudeGeometry, material ) ;
- door.rotation.y = Math.PI / 2;
- door.position.y = 0;
- door.position.x = 100;
- door.position.z = 230;
- house.add(door);
- }
接下來是房頂,就是兩個(gè)立方體(BoxGeometry),做下旋轉(zhuǎn):
- const roof = createRoof();
- const roof2 = createRoof();
- roof2.rotation.x = Math.PI / 2;
- roof2.rotation.y = Math.PI / 4 * 0.6;
- roof2.position.y = 130;
- roof2.position.x = -50;
- roof2.position.z = 155;
房頂?shù)牧鶄€(gè)面的材質(zhì)不同,一個(gè)面放瓦片的貼圖,其余的面設(shè)置成灰色就行,模擬水泥的效果。其中,瓦片的紋理要做下旋轉(zhuǎn),設(shè)置下兩個(gè)方向的重復(fù)次數(shù)。
- function createRoof() {
- const geometry = new THREE.BoxGeometry( 120, 320, 10 );
- const texture = new THREE.TextureLoader().load('./img/tile.jpg');
- texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
- texture.repeat.set( 5, 1);
- texture.rotation = Math.PI / 2;
- const textureMaterial = new THREE.MeshBasicMaterial({ map: texture});
- const colorMaterial = new THREE.MeshBasicMaterial({ color: 'grey' });
- const materials = [
- colorMaterial,
- colorMaterial,
- colorMaterial,
- colorMaterial,
- colorMaterial,
- textureMaterial
- ];
- const roof = new THREE.Mesh( geometry, materials );
- house.add(roof);
- roof.rotation.x = Math.PI / 2;
- roof.rotation.y = - Math.PI / 4 * 0.6;
- roof.position.y = 130;
- roof.position.x = 50;
- roof.position.z = 155;
- return roof;
- }
接下來的床就簡(jiǎn)單了,因?yàn)椴挥米约寒?,直接加載一個(gè)已有的模型就行,這種復(fù)雜的模型一般都是專業(yè)建模軟件畫的。
- function createBed() {
- var loader = new THREE.FBXLoader();
- loader.load('./obj/bed.fbx', function ( object ) {
- object.position.x = 40;
- object.position.z = 80;
- object.position.y = 20;
- house.add( object );
- } );
- }
再就是燈光設(shè)置為環(huán)境光,也就是每個(gè)方向的光照強(qiáng)度都一樣。
- const light = new THREE.AmbientLight(0xCCCCCC);
- scene.add(light);
創(chuàng)建相機(jī),使用透視相機(jī),也就是近大遠(yuǎn)小的那種透視效果:
- const width = window.innerWidth;
- const height = window.innerHeight;
- const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
指定看的角度為 60 度,寬高比,遠(yuǎn)近范圍 0.1 到 1000。
創(chuàng)建渲染器,并用 requestAnimationFrame 一幀幀渲染就行了:
- const renderer = new THREE.WebGLRenderer();
- function render() {
- renderer.render(scene, camera);
- requestAnimationFrame(render)
- }
接下來還要支持在 3D 場(chǎng)景中漫游,這個(gè)也不用自己做,Three.js 貼心的提供了很多控制器,各自有不同的交互效果,其中有個(gè)第一人稱控制器(FirstPersonControls),就是玩游戲時(shí)那種交互,通過 W、S、A、D 鍵控制前后左右,通過鼠標(biāo)控制方向。
- const controls = new THREE.FirstPersonControls(camera);
- controls.lookSpeed = 0.05;
- controls.movementSpeed = 100;
- controls.lookVertical = false;
我們指定了轉(zhuǎn)換方向的速度 lookSpeed,移動(dòng)的速度 movementSpeed,禁止了縱向的轉(zhuǎn)動(dòng)。
然后每一幀都要更新一下看到的畫面,通過時(shí)鐘 Clock 獲取到過去了多久,然后更新下控制器。
- const clock = new THREE.Clock();
- function render() {
- const delta = clock.getDelta();
- controls.update(delta);
- renderer.render(scene, camera);
- requestAnimationFrame(render)
- }
總結(jié)
本文寫了 Three.js 畫 3D 房子的實(shí)現(xiàn)原理。
Three.js 通過場(chǎng)景 Scene 管理各種物體,物體之間可以分組。物體由幾何體(Geometry)和材質(zhì)(Material)兩部分構(gòu)成,房子就是由立方體(BoxGeometry)、擠壓幾何體(ExtrudeGeometry)等各種幾何體構(gòu)成的,設(shè)置不同的貼圖紋理,還有位置、旋轉(zhuǎn)角度。
其中比較特殊的是 ExtrudeGeometry(擠壓幾何體),它是通過在二維平面畫一個(gè)形狀,然后“擠壓”成 三維的形式,形狀中還可以扣個(gè)洞。
房子中放了一張床,這種復(fù)雜的物體用 Three.js 手畫就比較難了,這種一般都是由專業(yè)建模軟件,比如 blender 來畫好,然后用 Three.js 加載并渲染的。
視角的改變其實(shí)就是相機(jī)位置和朝向的改變,Three.js 提供了各種控制器,比如 OrbitsControls(軌道控制器)、FirstPersonControls(第一人稱控制器)等。
我們這里要的通過鍵盤控制前后左右,通過鼠標(biāo)控制轉(zhuǎn)向的交互就可以用 FirstPersonControls。
Three.js 還是挺好玩的,業(yè)務(wù)上可能主要用于可視化、游戲,但工作之余也可以用它來做些有趣的東西。