上次我们用three.js写了一个下雨的动画,主要是用粒子。这次是用three.js搭建了一个小场景。
项目地址依然是:https://github.com/magicsoso/threejs-tutorial.git ,后面three.js的练习demo都放在这里。
作为练习的小项目,这个小场景可以练习:
- 各种几何体、贴图的运用
- 导入外部模型
- 场景与用户交互
- 简单的动画
- 调试three.js项目
虽然很简单,但完整地写下来还是挺有收获。
草地场景
场景搭建复用了在下雨动画中的场景模板Template。
首先是在草地上搭建一个房子,草地的实现是一个很大的平面几何体,然后用草地材质的贴图不断重复,铺满整个平面。
const groundGeometry = new PlaneGeometry( 20000, 20000 ) //草地平面几何体
const groundTexture = new TextureLoader().load('/images/room/grass.jpg') //加载草地材质
groundTexture.wrapS = groundTexture.wrapT = RepeatWrapping //设置重复贴图
groundTexture.repeat.set( 50, 50 )
groundTexture.anisotropy = 16
const groundMaterial = new MeshLambertMaterial({ //生成贴图的材质
map: groundTexture
})
const ground = new Mesh( groundGeometry, groundMaterial ) //生成草地
为了显得不突兀,我们用天空色作为画布的背景色,然后在远处加上雾化的效果。
this.rendererColor = new Color(0xcce0ff) //设置画布的背景色
//renderer.setClearColor(this.rendererColor)
this.scene.fog = new Fog( 0xcce0ff, 2500, 10000) //加上雾化的效果
在这个过程中,要不断调整相机的位置和视野范围,到一个合适的视野。我们最终选择位置和近面距离和远面距离是:
this.PCamera.far = 10000
this.PCamera.near = 1
this.cameraPostion = new Vector3(1000, 600, 1500)
盖房子
我们的房子是建在原点左右的,这样方便坐标的设置。
为了方便调试,我们在场景中加入AxesHelper
,它用于简单模拟场景中的3个坐标轴。红色代表 X 轴,绿色代表 Y 轴,蓝色代表 Z 轴。
const axesHelper = new AxesHelper( 700 ) //创建AxesHelper,700是三条线的长度
this.scene.add( axesHelper ) //将AxesHelper加入到场景中
有了AxesHelper
,我们在坐标系中设置位置,旋转等信息就方便多了。
- 首先,创建一个地面,和上面草地的创建一样,
PlaneGeometry
和贴图。
const floorGeometry = new PlaneGeometry( 800, 1000 )
const floorTexture = new TextureLoader().load('/images/room/floor.png')
floorTexture.wrapS = floorTexture.wrapT = RepeatWrapping
floorTexture.repeat.set( 25, 25 )
floorTexture.anisotropy = 16
const floorMaterial = new MeshLambertMaterial({
map: floorTexture
})
const floor = new Mesh( floorGeometry, floorMaterial )
- 接下来就是墙体。墙体按形状主要分为:前墙、后墙和侧墙。后墙最简单,就是普通的立方体,侧墙是不规则立方体,前墙是立方体上面挖了门和窗两个洞。
后墙我们直接用BoxGeometry
.
const boxGeometry = new BoxGeometry( ...arguments )
const boxMaterial = new MeshLambertMaterial({
color: 0xe5d890
})
const box = new Mesh( boxGeometry, boxMaterial )
侧墙和前墙用ExtrudeGeometry
。ExtrudeGeometry
可以从一个二维图形创建出一个三维图形,我们可以先画一个二维的形状,ExtrudeGeometry
会将这个二维形状不断 “加厚”,得到一个柱体。类比从一个平面圆到一个圆柱体。
以侧墙为例,我们要先画一个如下的形状,然后把它“加厚”:
function drawShape () {
const shape = new Shape() //用Shape类绘制二维形状
shape.moveTo(-400, 0) //绘制方法类似canvas中的绘制方法
shape.lineTo(400, 0)
shape.lineTo(400,400)
shape.lineTo(0,500)
shape.lineTo(-400,400)
const extrudeSettings = { //Extrude配置,具体可以修改参数调试各种效果
amount: 8,
bevelSegments: 2,
steps: 2,
bevelSize: 1,
bevelThickness: 1
}
//根据二维形状和Extrude配置生成ExtrudeGeometry
const geometry = new ExtrudeGeometry( shape, extrudeSettings )
}
const wallGeometry = drawShape()
const wallMaterial = new MeshLambertMaterial({
color: 0xe5d890
})
const wall = new Mesh( wallGeometry, wallMaterial )
前墙的绘制和侧墙类似,只是要“挖”门和窗两个洞。其实也是在二维图形上“挖”。
drawShape () {
const shape = new Shape() //绘制整体形状
shape.moveTo(-500, 0)
shape.lineTo(500, 0)
shape.lineTo(500,400)
shape.lineTo(-500,400)
const window = new Path() //用Path类绘制窗户形状
window.moveTo(100,100)
window.lineTo(100,250)
window.lineTo(300,250)
window.lineTo(300,100)
shape.holes.push(window) //将窗户形状加入到shape.holes数组,就会从当前形状减去窗户形状。
const door = new Path() //用Path类绘制门的形状
door.moveTo(-330,30)
door.lineTo(-330, 250)
door.lineTo(-210, 250)
door.lineTo(-210, 30)
shape.holes.push(door) //将门的形状加入到shape.holes数组
const extrudeSettings = {
amount: 8,
bevelSegments: 2,
steps: 2,
bevelSize: 1,
bevelThickness: 5
}
const geometry = new ExtrudeGeometry( shape, extrudeSettings )
return geometry
}
这样,四面墙就绘制完成了,ExtrudeGeometry
可以实现各种形状的镂空柱体,后面的门框和窗框也是基于它实现的。
- 最后就是搭上屋顶。屋顶是用两个
BoxGeometry
,设置合适的位置和旋转角度实现的,每一个BoxGeometry
的其中一面用贴图,剩下的五个面使用纯色。
const roofGeometry = new BoxGeometry( 500, 1300, 10 ) //创建几何体
const roofTexture = new TextureLoader().load('/images/room/roof.png') //导入贴图
roofTexture.wrapS = roofTexture.wrapT = RepeatWrapping
roofTexture.repeat.set( 2, 2 )
const materials = [] //创建一个6项的材质数组,three.js会自动将每一项贴一个面
const colorMaterial = new MeshLambertMaterial({ color: 'grey' })
const textureMaterial = new MeshLambertMaterial({ map: roofTexture })
for(let i=0; i<6; i++){
materials.push(colorMaterial)
}
materials[5] = textureMaterial //将其中一个面的设为图片材质,而其他五个面是纯色材质
const roof = new Mesh( roofGeometry, materials )
然后就是调整它的位置,还有倾角,让屋顶和侧墙的斜角切合。
加入门窗
门分为门板和门框,它们形状和材质都不同,但是它们又是一个整体。同样,窗户和窗框也是这样的。
在three.js中,我们用Group类来管理一组物体。
const group = new Group() //创建Group
group.add( this.frame ) //往Group加入门框
group.add( this.door ) //往Group加入门板
这样的一个好处,就是门板和门框可以作为一个整体来设置位置和旋转方向等等。比如要调整一下门的位置、朝向什么的,我们就只需要移动和旋转group,不用分别操作门板和门框。
当然,门板和门框也有相对于group的位移和旋转,比如开关门动画。
initFrame () {
const frameGeometry = this.drawShape() //门框的形状是用`ExtrudeGeometry`实现的
const frameMaterial = new MeshLambertMaterial({ //门框材质
color: 0x8d7159
})
const frame = new Mesh( frameGeometry, frameMaterial )
this.frame = frame
}
initDoor () {
const doorGeometry = new BoxGeometry(100,210,4) //门的形状
const doorTexture = new TextureLoader().load('/images/room/wood.jpg')
const doorMaterial = new MeshLambertMaterial({ map: doorTexture }) //门的材质
const door = new Mesh(doorGeometry, doorMaterial) //BoxGeometry的材质不是数组时,每个面都会贴这个材质
this.param = {
positionX : 60,
positionZ: 0,
rotationY: 0
}
door.position.set(this.param.positionX, 105, this.param.positionZ) //门相对于group的位移和旋转,开关门动画会用到。
door.rotation.y = this.param.rotationY
this.door = door
this.status = 'closed'
}
开关门动画
判断鼠标是否点击了某个物体,将鼠标点击位置转换成三维空间中的位置,从摄像机的位置向点击转化后的三维空间位置发射射线,判断物体是否在这条射线上,如果在,就意味着点击了该物体。
window.addEventListener('click', onm ouseDown) //给window绑定点击事件
function onm ouseDown (event) {
let vector = new Vector3( // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0.5
)
vector = vector.unproject(this.camera)
const raycaster = new Raycaster( // 通过摄像机和鼠标位置更新射线
this.camera.position,
vector.sub(this.camera.position).normalize()
)
// 计算物体和射线的交点
const intersects = raycaster.intersectObjects([this.doorSet.door])
if(intersects.length > 0){
this.doorSet.animate()
}
}
在门被点击后,判断门的状态是开还是关,根据状态设置下一个状态的位置和旋转(相对于group)。
animate () {
if(this.status === 'closed'){
this.param.positionX = 10
this.param.positionZ = 50
this.param.rotationY = -Math.PI/2
this.status= 'open'
}else{
this.param.positionX = 60
this.param.positionZ = 0
this.param.rotationY = 0
this.status= 'closed'
}
this.onUpdate(this.param)
}
onUpdate (param) {
this.door.position.x = param.positionX
this.door.position.z = param.positionZ
this.door.rotation.y = param.rotationY
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QajAeoOm-1617153479720)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/71abb2243c374a569ae8f2c098ef3dfa~tplv-k3u1fbpfcp-watermark.image)]
绘制窗户
窗框的绘制和前墙的绘制一样的操作,都是用ExtrudeGeometry
。
窗户是用BoxGeometry
, 材质是MeshPhysicalMaterial
,设置了一定的透明度,模拟玻璃的效果。
const windowGeometry = new BoxGeometry( 150, 200, 4 )
const windowMaterial = new MeshPhysicalMaterial( {
map: null,
color: 0xcfcfcf,
metalness: 0,
roughness: 0,
opacity: 0.45,
transparent: true,
envMapIntensity: 10,
premultipliedAlpha: true
} )
const window = new Mesh( windowGeometry, windowMaterial )
和门一样,窗户和窗框也添加到一个group
中。
导入桌子和花
桌子和花是导入的外部模型。对于复杂的模型,直接用three.js搭建挺麻烦的,我们可以用专门的建模软件建模,然后导出模型。
three.js支持多种3d模型的导入,这里我们用的OBJ。
OBJ和MTL是相互配合的两种格式,经常一起使用。OBJ文件定义几何体,而MTL文件定义所用的材质。它们的导入都是借助响应的Loader完成。以导入桌子为例:
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader" //引入OBJLoader
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader" //引入MTLLoader
function addTable (scene) {
const mtlLoader = new MTLLoader()
const objLoader = new OBJLoader()
mtlLoader.load( '../../images/room/table/table.mtl', ( material ) => { //导入材质
objLoader.setMaterials( material ) //为objLoader设置材质
objLoader.load( '../../images/room/table/table.obj', ( object ) => { //导入形状
object.position.set(600,0,0) //设置形状的位置
scene.add( object ) //将形状加入到场景中
} );
})
}
addTable(this.scene)
orbitControls
Three.js提供了一些摄像机控件,使用这些控件,你可以控制场景中的摄像机。下面是几个最常用的控件。
我们这里用的是轨道控制器(OrbitControls
),它可以用于控制场景中的对象围绕场景中心旋转和平移。使用方法很简单:
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
function addOrbitControls (camera, el) {
const controls = new OrbitControls( camera, el ) //参数是将要被控制的相机和用于事件监听的HTML元素(通常是renderer.domElement)
controls.maxPolarAngle = Math.PI * 0.45 //垂直旋转的角度的上限
controls.enablePan = false //禁止平移
}
addOrbitControls(this.camera, this.renderer.domElement)
OrbitControls
允许我们按住鼠标左键旋转画面,按住右键平移画面,用鼠标滚轮放缩画面。这些都是可配置的,这里我们就禁止了平移,并设置了垂直旋转的角度的上限,以防止画面移到草地外。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHi4SLpQ-1617153479723)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f2b9c3898fca48d1bac47dba033673d4~tplv-k3u1fbpfcp-watermark.image)]
dat.gui
dat.gui可以很容易地创建出能够改变代码变量的界面组件,可以简化three.js的调试,在three的官方案例中,我们常常可以看到dat.gui的使用。
使用方法可以参考这篇文章,手把手教你使用dat.gui ,讲得很详细。
我们这里用它来控制坐标轴线和屋顶的显示和隐藏。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULBqamMQ-1617153479724)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/14e90f9484ae4deaa7da1047627e4d67~tplv-k3u1fbpfcp-watermark.image)]
import { GUI } from 'dat.gui'
export function Gui () { //初始化GUI,添加要控制的变量
const controls = new function () {
this.showAxes = false
this.showRoof = true
}
const gui = new GUI()
gui.add(controls, 'showAxes')
gui.add(controls, 'showRoof')
return controls
}
//director.js
import { Gui } from "../tools/dat.gui"
this.Controls = Gui()
//在循环渲染中,根据当前Controls中的值判断是否显示axesHelper和roof
animate () {
if(this.Controls.showAxes){
this.scene.add( this.axesHelper )
}else{
this.scene.remove( this.axesHelper )
}
if(this.Controls.showRoof){
this.scene.add( this.roof_1 )
this.scene.add( this.roof_2 )
}else{
this.scene.remove( this.roof_1 )
this.scene.remove( this.roof_2 )
}
this.renderer.render(this.scene, this.camera)
requestAnimationFrame(this.animate.bind(this))
}
今天就到这里,后面在github上的threejs-tutorial项目会持续更新各种three案例,欢迎大家关注哦~~
https://github.com/magicsoso/threejs-tutorial.git