我是如何用 Three-js 在三維世界建房子的(詳細教程)

這兩天用 Three.js 畫了一個 3D 的房子,放了一個牀進去,可以用鼠標和鍵盤控制移動,有種 3D 遊戲的即視感。

我是如何用 Three.js 在三維世界建房子的(詳細教程)

這篇文章就來講下實現原理。

代碼地址:https://github.com/QuarkGluonPlasma/threejs-exercize

思路分析

我們先不着急寫代碼,先來分析下思路。

這樣一個房子,其實也是由幾個幾何體堆起來的:

具體有這麼些幾何體:

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

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

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

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

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

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

接下來,給牆和房頂、地板貼上不同的圖,設置好不同的位置,就可以組裝成一個房子了。

那麼牀呢?

Three.js 提供了很多的幾何體,可以畫一些簡單的物體,但複雜的物體就很難畫出來了,這類物體一般會用專業的 3D 建模軟件來畫,導出 FPX 或者 OBJ 格式的文件由 Three.js 加載並渲染出來。

我們在網上找一個牀的 3D 模型,我找了一個 FBX 格式的,然後用 Three.js 的 FBXLoader 加載就行。

還剩下一個草地,這個也是一個平面,用 PlaneGeometry(平面幾何體)畫,只不過就是長寬比較大,看不到盡頭而已。

看起來還有霧?

沒錯,確實設置了霧(Fog),Three.js 在場景中設置霧的效果,指定顏色和霧的遠近範圍就行。爲了有種模糊的感覺,我就在場景中加入了霧。

全部的物體都畫完了,接下來就可以在 3D 場景中漫遊了,通過鼠標和鍵盤可以改變方向和前後左右移動,這種交互使用 FirstPersonControls(第一人稱控制器) 來實現。

一般我們常用的是 OrbitsControls(軌道控制器),它支持圍繞物體轉動相機,就像衛星一樣。但我們這裏不是想繞着轉,而是想鍵盤和鼠標控制的前後左右的隨意移動。

我們簡單小結下:

Three.js 是在三維的座標系中添加各種物體,組裝成不同的 3D 場景。其中簡單的物體可以畫,複雜的物體會用建模軟件畫,然後加載到場景中。我們可以用不同的控制器來控制相機移動,達到不同的交互效果,比如軌道控制器、第一人稱控制器等。

房子的牆、地板、房頂都可以用 BoxGeometry(立方體)、ExtrudeGeometry(擠壓幾何體)畫出來,但是牀這種複雜的就不行了,會直接加載模型文件。

通過 FistPersonControls(第一人稱控制器)來控制交互,就能達到 3D 遊戲的那種感覺。

思路理清了,接下來我們具體寫下代碼:

代碼實現

先畫草地,也就是一個大的平面,貼上草地的貼圖。

三維的物體(Mesh) 是由幾何體(Geometry),加上材質(Material)構成的。我們創建平面幾何體(PlaneGeometry),長和寬制定一個很大的值,比如 10000,然後加載草地的圖片作爲紋理(Texture),構成材質。之後就可以創建出草地了。

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 );
}

紋理貼圖要設置兩個方向都重複,重複的次數是 100 次。

然後草地的平面要旋轉一下。

加點霧,讓天際模糊一些:

scene.fog = new THREE.Fog(0xffffff, 10, 1500);

分別指定顏色爲白色,霧的遠近範圍爲 10 到 1500。

接下來是創建房子,房子由地板、兩側的牆、前面的牆、後面的牆、門框窗框、房頂、牀構成,要分別創建每一部分,我們把它們放到單獨的 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();
}

創建地板也是平面幾何體(PlaneGeometry),貼上木材的圖就行,然後設置下位置:

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);
}

創建側面的牆,要用 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;
}

兩個側牆只是位置不同,修改下 z 軸位置就行:

const sideWall = createSideWall();
const sideWall2 = createSideWall();
sideWall2.position.z = 300;

對了,如果對位置拿不準,可以在場景中加個座標系輔助工具(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);
}

只是形狀上多了兩個洞,畫起來複雜些,其餘的紋理、材質,還有位置等設置方式都一樣。

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

顏色設置爲銀白色。

門框也是一樣:

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),做下旋轉:

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;

房頂的六個面的材質不同,一個面放瓦片的貼圖,其餘的面設置成灰色就行,模擬水泥的效果。其中,瓦片的紋理要做下旋轉,設置下兩個方向的重複次數。

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;
}

接下來的牀就簡單了,因爲不用自己畫,直接加載一個已有的模型就行,這種複雜的模型一般都是專業建模軟件畫的。

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 );
    } );
}

再就是燈光設置爲環境光,也就是每個方向的光照強度都一樣。

const light = new THREE.AmbientLight(0xCCCCCC);
scene.add(light);

創建相機,使用透視相機,也就是近大遠小的那種透視效果:

const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);

指定看的角度爲 60 度,寬高比,遠近範圍 0.1 到 1000。

創建渲染器,並用 requestAnimationFrame 一幀幀渲染就行了:

const renderer = new THREE.WebGLRenderer();
function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render)
}

接下來還要支持在 3D 場景中漫遊,這個也不用自己做,Three.js 貼心的提供了很多控制器,各自有不同的交互效果,其中有個第一人稱控制器(FirstPersonControls),就是玩遊戲時那種交互,通過 W、S、A、D 鍵控制前後左右,通過鼠標控制方向。

const controls = new THREE.FirstPersonControls(camera);
controls.lookSpeed = 0.05;
controls.movementSpeed = 100;
controls.lookVertical = false;

我們指定了轉換方向的速度 lookSpeed,移動的速度 movementSpeed,禁止了縱向的轉動。

然後每一幀都要更新一下看到的畫面,通過時鐘 Clock 獲取到過去了多久,然後更新下控制器。

const clock = new THREE.Clock();

function render() {
    const delta = clock.getDelta();
    controls.update(delta);

    renderer.render(scene, camera);
    requestAnimationFrame(render)
}

看下最終的效果:

您的瀏覽器不支持 video 標籤

全部代碼上傳到了 github:

代碼地址:https://github.com/QuarkGluonPlasma/threejs-exercize

總結

本文寫了 Three.js 畫 3D 房子的實現原理。

Three.js 通過場景 Scene 管理各種物體,物體之間可以分組。物體由幾何體(Geometry)和材質(Material)兩部分構成,房子就是由立方體(BoxGeometry)、擠壓幾何體(ExtrudeGeometry)等各種幾何體構成的,設置不同的貼圖紋理,還有位置、旋轉角度。

其中比較特殊的是 ExtrudeGeometry(擠壓幾何體),它是通過在二維平面畫一個形狀,然後 “擠壓” 成 三維的形式,形狀中還可以扣個洞。

房子中放了一張牀,這種複雜的物體用 Three.js 手畫就比較難了,這種一般都是由專業建模軟件,比如 blender 來畫好,然後用 Three.js 加載並渲染的。

視角的改變其實就是相機位置和朝向的改變,Three.js 提供了各種控制器,比如 OrbitsControls(軌道控制器)、FirstPersonControls(第一人稱控制器)等。

我們這裏要的通過鍵盤控制前後左右,通過鼠標控制轉向的交互就可以用 FirstPersonControls。

Three.js 還是挺好玩的,業務上可能主要用於可視化、遊戲,但工作之餘也可以用它來做些有趣的東西。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/k3Z-3if05pVv-I_VAcFihw