我是如何用 Three.js 在三维世界建房子的(详细教程)
这两天用 Three.js 画了一个 3D 的何用房子,放了一个床进去,世界可以用鼠标和键盘控制移动,建房有种 3D 游戏的细教即视感。
这篇文章就来讲下实现原理。何用
代码地址:https://github.com/QuarkGluonPlasma/threejs-exercize
思路分析
我们先不着急写代码,世界先来分析下思路。建房
这样一个房子,细教其实也是何用由几个几何体堆起来的:
具体有这么些几何体:
地板就是个平面,用 PlaneGeometry(平面几何体) 就可以画,世界贴上个纹理贴图就行。建房
两个侧面的细教墙,是何用一个不规则的形状,这个可以用 ExtrudeGeometry(挤压几何体),世界它支持用画笔画一个 2D 的建房路径,然后加厚变成 3D 的。
同理,后面的墙也很简单,可以是 BoxGeometry(立方体)来画,也可以是 ExtrudeGeometry(挤压结合体)先画个形状,然后变成 3D 的云南idc服务商。
前面的墙稍微复杂些,它也是不规则的,可以用 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) }总结
本文写了 Three.js 画 3D 房子的实现原理。
Three.js 通过场景 Scene 管理各种物体,物体之间可以分组。物体由几何体(Geometry)和材质(Material)两部分构成,房子就是由立方体(BoxGeometry)、挤压几何体(ExtrudeGeometry)等各种几何体构成的,设置不同的贴图纹理,还有位置、旋转角度。
其中比较特殊的是 ExtrudeGeometry(挤压几何体),它是通过在二维平面画一个形状,然后“挤压”成 三维的形式,形状中还可以扣个洞。
房子中放了一张床,这种复杂的物体用 Three.js 手画就比较难了,这种一般都是由专业建模软件,比如 blender 来画好,然后用 Three.js 加载并渲染的。
视角的改变其实就是相机位置和朝向的改变,Three.js 提供了各种控制器,比如 OrbitsControls(轨道控制器)、FirstPersonControls(第一人称控制器)等。
我们这里要的通过键盘控制前后左右,通过鼠标控制转向的交互就可以用 FirstPersonControls。
Three.js 还是挺好玩的,业务上可能主要用于可视化、游戏,但工作之余也可以用它来做些有趣的东西。