ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

用three.js写一个小场景

2021-03-31 09:30:02  阅读:283  来源: 互联网

标签:场景 const door three param js shape new


上次我们用three.js写了一个下雨的动画,主要是用粒子。这次是用three.js搭建了一个小场景。

项目地址依然是:https://github.com/magicsoso/threejs-tutorial.git ,后面three.js的练习demo都放在这里。

作为练习的小项目,这个小场景可以练习:

  1. 各种几何体、贴图的运用
  2. 导入外部模型
  3. 场景与用户交互
  4. 简单的动画
  5. 调试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 )   //生成草地

image.png
为了显得不突兀,我们用天空色作为画布的背景色,然后在远处加上雾化的效果。

this.rendererColor = new Color(0xcce0ff)   //设置画布的背景色
//renderer.setClearColor(this.rendererColor)

this.scene.fog = new Fog( 0xcce0ff, 2500, 10000)   //加上雾化的效果

image.png
在这个过程中,要不断调整相机的位置和视野范围,到一个合适的视野。我们最终选择位置和近面距离和远面距离是:

this.PCamera.far = 10000
this.PCamera.near = 1
this.cameraPostion = new Vector3(1000, 600, 1500)

盖房子

我们的房子是建在原点左右的,这样方便坐标的设置。

为了方便调试,我们在场景中加入AxesHelper,它用于简单模拟场景中的3个坐标轴。红色代表 X 轴,绿色代表 Y 轴,蓝色代表 Z 轴。

image.png

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 )

image.png

  • 接下来就是墙体。墙体按形状主要分为:前墙、后墙和侧墙。后墙最简单,就是普通的立方体,侧墙是不规则立方体,前墙是立方体上面挖了门和窗两个洞。

后墙我们直接用BoxGeometry.

const boxGeometry = new BoxGeometry( ...arguments )
const boxMaterial = new MeshLambertMaterial({ 
  color: 0xe5d890 
})
const box = new Mesh( boxGeometry, boxMaterial )

侧墙和前墙用ExtrudeGeometryExtrudeGeometry可以从一个二维图形创建出一个三维图形,我们可以先画一个二维的形状,ExtrudeGeometry会将这个二维形状不断 “加厚”,得到一个柱体。类比从一个平面圆到一个圆柱体。

以侧墙为例,我们要先画一个如下的形状,然后把它“加厚”:
image.png

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可以实现各种形状的镂空柱体,后面的门框和窗框也是基于它实现的。

image.png

  • 最后就是搭上屋顶。屋顶是用两个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 )

然后就是调整它的位置,还有倾角,让屋顶和侧墙的斜角切合。

image.png

加入门窗

门分为门板和门框,它们形状和材质都不同,但是它们又是一个整体。同样,窗户和窗框也是这样的。

在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中。

image.png

导入桌子和花

桌子和花是导入的外部模型。对于复杂的模型,直接用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提供了一些摄像机控件,使用这些控件,你可以控制场景中的摄像机。下面是几个最常用的控件。
image.png
我们这里用的是轨道控制器(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的使用。

image.png

使用方法可以参考这篇文章,手把手教你使用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

标签:场景,const,door,three,param,js,shape,new
来源: https://blog.csdn.net/ynweiy/article/details/115342271

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有