什么是元宇宙的房子,送一套給你
這兩天用 Three.js 畫了一個 3D 的房子,放了一個床進去,可以用鼠標(biāo)和鍵盤控制移動,有種 3D 游戲的即視感。
這篇文章就來講下實現(xiàn)原理。
代碼地址:https://github.com/QuarkGluonPlasma/threejs-exercize
思路分析
我們先不著急寫代碼,先來分析下思路。
這樣一個房子,其實也是由幾個幾何體堆起來的:

具體有這么些幾何體:
地板就是個平面,用 PlaneGeometry(平面幾何體) 就可以畫,貼上個紋理貼圖就行。

兩個側(cè)面的墻,是一個不規(guī)則的形狀,這個可以用 ExtrudeGeometry(擠壓幾何體),它支持用畫筆畫一個 2D 的路徑,然后加厚變成 3D 的。

同理,后面的墻也很簡單,可以是 BoxGeometry(立方體)來畫,也可以是 ExtrudeGeometry(擠壓結(jié)合體)先畫個形狀,然后變成 3D 的。

前面的墻稍微復(fù)雜些,它也是不規(guī)則的,可以用 ExtrudeGeometry(擠壓幾何體)來畫出形狀,然后變成 3D 的,只不過它多了兩個洞,需要畫兩個洞加到形狀里面去。

門框、窗框也是形狀里扣個洞,用 ExtrudeGeometry 變成 3D 的。

那房頂呢?房頂也沒什么特殊的,只是立方體旋轉(zhuǎn)一定的角度就行,用 BoxGeometry(立方體) 就可以畫。

接下來,給墻和房頂、地板貼上不同的圖,設(shè)置好不同的位置,就可以組裝成一個房子了。
那么床呢?

Three.js 提供了很多的幾何體,可以畫一些簡單的物體,但復(fù)雜的物體就很難畫出來了,這類物體一般會用專業(yè)的 3D 建模軟件來畫,導(dǎo)出 FPX 或者 OBJ 格式的文件由 Three.js 加載并渲染出來。
我們在網(wǎng)上找一個床的 3D 模型,我找了一個 FBX 格式的,然后用 Three.js 的 FBXLoader 加載就行。
還剩下一個草地,這個也是一個平面,用 PlaneGeometry(平面幾何體)畫,只不過就是長寬比較大,看不到盡頭而已。

看起來還有霧?
沒錯,確實設(shè)置了霧(Fog),Three.js 在場景中設(shè)置霧的效果,指定顏色和霧的遠(yuǎn)近范圍就行。為了有種模糊的感覺,我就在場景中加入了霧。
全部的物體都畫完了,接下來就可以在 3D 場景中漫游了,通過鼠標(biāo)和鍵盤可以改變方向和前后左右移動,這種交互使用 FirstPersonControls(第一人稱控制器) 來實現(xiàn)。
一般我們常用的是 OrbitsControls(軌道控制器),它支持圍繞物體轉(zhuǎn)動相機,就像衛(wèi)星一樣。但我們這里不是想繞著轉(zhuǎn),而是想鍵盤和鼠標(biāo)控制的前后左右的隨意移動。
我們簡單小結(jié)下:
Three.js 是在三維的坐標(biāo)系中添加各種物體,組裝成不同的 3D 場景。其中簡單的物體可以畫,復(fù)雜的物體會用建模軟件畫,然后加載到場景中。我們可以用不同的控制器來控制相機移動,達(dá)到不同的交互效果,比如軌道控制器、第一人稱控制器等。
房子的墻、地板、房頂都可以用 BoxGeometry(立方體)、ExtrudeGeometry(擠壓幾何體)畫出來,但是床這種復(fù)雜的就不行了,會直接加載模型文件。
通過 FistPersonControls(第一人稱控制器)來控制交互,就能達(dá)到 3D 游戲的那種感覺。
思路理清了,接下來我們具體寫下代碼:
代碼實現(xiàn)
先畫草地,也就是一個大的平面,貼上草地的貼圖。
三維的物體(Mesh) 是由幾何體(Geometry),加上材質(zhì)(Material)構(gòu)成的。我們創(chuàng)建平面幾何體(PlaneGeometry),長和寬制定一個很大的值,比如 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è)置兩個方向都重復(fù),重復(fù)的次數(shù)是 100 次。
然后草地的平面要旋轉(zhuǎn)一下。

加點霧,讓天際模糊一些:
scene.fog?=?new?THREE.Fog(0xffffff,?10,?1500);
分別指定顏色為白色,霧的遠(yuǎn)近范圍為 10 到 1500。

接下來是創(chuàng)建房子,房子由地板、兩側(cè)的墻、前面的墻、后面的墻、門框窗框、房頂、床構(gòu)成,要分別創(chuàng)建每一部分,我們把它們放到單獨的 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(擠壓幾何體)來畫,也就是先畫出一個 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;
}
兩個側(cè)墻只是位置不同,修改下 z 軸位置就行:
const?sideWall?=?createSideWall();
const?sideWall2?=?createSideWall();
sideWall2.position.z?=?300;

對了,如果對位置拿不準(zhǔn),可以在場景中加個坐標(biāo)系輔助工具(AxisHelper)。
const?axisHelper?=?new?THREE.AxisHelper(2000);
scene.add(axisHelper);

然后是后面的墻,這個形狀簡單一些,就是個矩形:
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);
}

接下來是前面的墻,這個除了要畫出形狀外,還要摳出兩個洞:
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);
}
只是形狀上多了兩個洞,畫起來復(fù)雜些,其余的紋理、材質(zhì),還有位置等設(shè)置方式都一樣。

門窗也是畫一個形狀,摳一個洞,然后加點厚度變成 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);
}

接下來是房頂,就是兩個立方體(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ù)牧鶄€面的材質(zhì)不同,一個面放瓦片的貼圖,其余的面設(shè)置成灰色就行,模擬水泥的效果。其中,瓦片的紋理要做下旋轉(zhuǎn),設(shè)置下兩個方向的重復(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;
}

接下來的床就簡單了,因為不用自己畫,直接加載一個已有的模型就行,這種復(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)境光,也就是每個方向的光照強度都一樣。
const?light?=?new?THREE.AmbientLight(0xCCCCCC);
scene.add(light);
創(chuàng)建相機,使用透視相機,也就是近大遠(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 場景中漫游,這個也不用自己做,Three.js 貼心的提供了很多控制器,各自有不同的交互效果,其中有個第一人稱控制器(FirstPersonControls),就是玩游戲時那種交互,通過 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,移動的速度 movementSpeed,禁止了縱向的轉(zhuǎn)動。
然后每一幀都要更新一下看到的畫面,通過時鐘 Clock 獲取到過去了多久,然后更新下控制器。
const?clock?=?new?THREE.Clock();
function?render()?{
????const?delta?=?clock.getDelta();
????controls.update(delta);
????renderer.render(scene,?camera);
????requestAnimationFrame(render)
}
看下最終的效果:
全部代碼上傳到了 github:
代碼地址:https://github.com/QuarkGluonPlasma/threejs-exercize
總結(jié)
本文寫了 Three.js 畫 3D 房子的實現(xiàn)原理。
Three.js 通過場景 Scene 管理各種物體,物體之間可以分組。物體由幾何體(Geometry)和材質(zhì)(Material)兩部分構(gòu)成,房子就是由立方體(BoxGeometry)、擠壓幾何體(ExtrudeGeometry)等各種幾何體構(gòu)成的,設(shè)置不同的貼圖紋理,還有位置、旋轉(zhuǎn)角度。
其中比較特殊的是 ExtrudeGeometry(擠壓幾何體),它是通過在二維平面畫一個形狀,然后“擠壓”成 三維的形式,形狀中還可以扣個洞。
房子中放了一張床,這種復(fù)雜的物體用 Three.js 手畫就比較難了,這種一般都是由專業(yè)建模軟件,比如 blender 來畫好,然后用 Three.js 加載并渲染的。
視角的改變其實就是相機位置和朝向的改變,Three.js 提供了各種控制器,比如 OrbitsControls(軌道控制器)、FirstPersonControls(第一人稱控制器)等。
我們這里要的通過鍵盤控制前后左右,通過鼠標(biāo)控制轉(zhuǎn)向的交互就可以用 FirstPersonControls。
Three.js 還是挺好玩的,業(yè)務(wù)上可能主要用于可視化、游戲,但工作之余也可以用它來做些有趣的東西。
