乐趣区

关于three.js:用threejs写一个下雨动画

最近看了《Three.js 开发指南》,粗浅地意识到光看不练跟没看差不多,所以就练习写了这个小动画。

我的项目地址:https://github.com/alasolala/…

前置常识

WebGL 让咱们能在浏览器开发 3D 利用,然而间接应用 WebGL 编程还是挺简单的,开发者须要晓得 WebGL 的底层细节,并且学习简单的着色语言来取得 WebGL 的大部分性能。Three.js 提供了一系列很简略的对于 WebGL 个性的 JavaScript API,使开发者能够很不便地创作出难看的 3D 图形。在 Three.js 官网,就有很多酷炫 3D 成果。

应用 Three.js 开发 3D 利用,通常要包含渲染器(Renderer)、场景(Scene)、照相机(Camera),以及你在场景中创立的物体,光照。

构想一下照相的状况,咱们须要一个场景(Scene),在这个场景中摆好要拍摄的物体,设置光照环境,摆放好照相机(Camera)的地位和朝向,而后就能够拍照了。渲染器(Renderer)可能和摄影师比拟像吧,负责下命令拍摄,并且生成图像(照片)。

将上面的代码的复制并运行,就能够失去一个很简略的 3D 场景。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>room</title>
</head>
<body>
  <div id="webgl-output"></div>
  <script src="https://unpkg.com/three@0.119.0/build/three.js"></script>
  <script>
    function init () {const scene = new THREE.Scene()

      const camera = new THREE.PerspectiveCamera(45, 
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      )
      camera.position.set(-30, 40, 30)
      camera.lookAt(0,0,0)
      scene.add(camera) 

      const planeGeometry = new THREE.PlaneGeometry(60,20)
      const planeMaterial = new THREE.MeshLambertMaterial({color: 0xAAAAAA})  
      const plane = new THREE.Mesh(planeGeometry, planeMaterial)
      plane.rotation.x = -Math.PI / 2
      plane.position.set(15, 0, 0)
      scene.add(plane)

      const sphereGeometry = new THREE.SphereGeometry(4, 20, 20)
      const sphereMaterial = new THREE.MeshLambertMaterial({color: 0xffff00})
      const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
      sphere.position.set(20, 4, 2)
      scene.add(sphere)

      const spotLight = new THREE.SpotLight(0xffffff)
      spotLight.position.set(-20, 30, -15)
      scene.add(spotLight)

      const renderer = new THREE.WebGLRenderer()
      renderer.setClearColor(new THREE.Color(0x000000))
      renderer.setSize(window.innerWidth, window.innerHeight)
      document.getElementById('webgl-output').appendChild(renderer.domElement)

      renderer.render(scene, camera)
    }

    init()
</script>
</body>
</html>

场景(Scene)

THREE.Scene 对象是所有不同对象的容器,但这个对象自身没有很简单的操作,咱们通常在程序最开始的时候实例化一个场景,而后将照相机、物体、光源增加到场景中。

const scene = new THREE.Scene()
scene.add(camera)        // 增加照相机
scene.add(plane)         // 增加灰色立体
scene.add(sphere)        // 增加黄色球体
scene.add(spotLight)     // 增加光源

照相机(Camera)

Three.js 库提供了两种不同的照相机:透视投影照相机和正交投影照相机。

透视投影照相机的成果相似人眼在真实世界中看到的场景,有 "近大远小" 的成果,垂直视立体的平行线在远方会相交。正交投影照相机的成果相似咱们在数学几何学课上老师教咱们画的成果,在三维空间内平行的线,在屏幕上永远不会相交。

咱们这里用的是透视投影照相机,就次要探讨它,正交投影照相机前面用到再说。

const camera = new THREE.PerspectiveCamera(
  45, 
  window.innerWidth / window.innerHeight,
  0.1,
  1000
)
camera.position.set(-30, 40, 30)
camera.lookAt(0,0,0)
scene.add(camera) 

设置一个照相机分三步:确定视线范畴,确定照相机坐标,确定照相机聚焦点

咱们在 new THREE.PerspectiveCamera 的时候确定照相机的视线范畴,对应上图,45 是 fov,就是视线高低边缘之间的夹角。window.innerWidth / window.innerHeight是视线程度方向和竖直方向长度的比值,0.1(near)和 1000(far)别离是照相机到视景体最近、最远的间隔,这些参数决定了要显示的三维空间的范畴,也就是上图中的灰色区域。

camera.position.set(-30, 40, 30)确定了照相机在空间中的坐标。

camera.lookAt(0,0,0)确定了照相机聚焦点,该点和照相机坐标的连线就是拍摄方向。

上图中的灰色区域在屏幕上的显示成果,也就是将三维空间的坐标投影到屏幕二维坐标是 webgl 实现的,咱们只须要关怀三维空间的坐标。

坐标系

与咱们之前讲到的 CSS 的 3D 坐标系不同,webgl 坐标系是右手坐标系,X 轴向右,Y 轴向上,Z 轴是指向“本人”的。

伸出右手,让拇指和食指成 "L" 形,大拇指向右,食指向上。其余的手指指向本人,这样就建设了一个右手坐标系。其中,拇指、食指和其余手指别离代表 x,y,z 轴的正方向

在空间中定位、平移都比拟好了解,这里看一下旋转。

有时,咱们会这样设置物体的旋转:object.rotation.x = -Math.PI / 2,示意的是绕 X 轴旋转 -90 度。具体是怎么旋转,就要对照下面坐标系,开展右手,拇指指向 x 轴正方向,其余手指的蜿蜒方向就是旋转的正方向;拇指指向 x 轴负方向,其余手指的蜿蜒方向就是旋转的负方向。y 轴和 z 轴旋转方向的判断同理。

物体

在 three.js 中,创立一个物体须要两个参数:几何形态(Geometry)和 材质(Material)。艰深的讲,几何形态决定物体的形态,材质决定物体外表的色彩、纹理贴图、对光照的反馈等等。

// 创立一个平面几何体,参数是沿 X 方向的 Width 和沿 Y 方向的 height
const planeGeometry = new THREE.PlaneGeometry(60,20)  

// 创立一种材质,MeshLambertMaterial 是一种思考漫反射而不思考镜面反射的材质
const planeMaterial = new THREE.MeshLambertMaterial({color: 0xAAAAAA})  

// 依据几何形态和材质创立物体
const plane = new THREE.Mesh(planeGeometry, planeMaterial)

// 设置物体的地位和旋转,并将物体加到场景(scene)中
plane.rotation.x = -Math.PI / 2
plane.position.set(15, 0, 0)
scene.add(plane)

光照

没有光源,渲染的场景将不可见(除非你应用根底材质或线框材质,当然,在构建 3D 利用时,简直不怎么用根底材质和线框材质)。

WebGL 自身并不反对光源。如果不应用 Three.js,则须要本人写 WebGL 着色程序来模仿光源。Three.js 让光源的应用变得简略。

const spotLight = new THREE.SpotLight(0xffffff)
spotLight.position.set(0, 0, 100)
scene.add(spotLight)

如上所示,咱们只须要创立一个光源,并将它退出到场景中就能够了。three.js 会依据光源的类型、地位等信息计算出场景中各个物体的展现成果。

最罕用的几种光源是 AmbientLight、PointLight、SpotLight、DirectionalLight。

渲染器(Renderer)

当场景中的照相机、物体、光照等准备就绪,就该渲染器上场了。

在下面那个小例子中,咱们是这样应用渲染器的:

//new 一个渲染器
const renderer = new THREE.WebGLRenderer()

// 设置画布背景色,也就是画布中没有物体的中央的显示色彩
renderer.setClearColor(new THREE.Color(0x000000))

// 设置画布大小
renderer.setSize(window.innerWidth, window.innerHeight)

// 将画布元素(即 renderer.domElement,它是一个 canvas 元素)挂载到一个 dom 节点
document.getElementById('webgl-output').appendChild(renderer.domElement)

// 执行渲染操作,参数是下面定义的场景(scene)和照相机(camera)renderer.render(scene, camera)

能够看出,应用 Three.js 开发 3D 利用,咱们只须要关怀场景中物体、照相机、光照等在三维空间中的布局,以及静止,具体怎么渲染都由 Three.js 去实现。当然,懂一些 webgl 的基本原理会更好,毕竟有一些利用会简单到 three.js 的 API 满足不了要求。

实现下雨动画

初始化场景

因为每个 3D 利用的初始化都有 scene、camera、render,所以咱们把这三者的初始化封装成一个类 Template,前面的利用初始化能够通过子类继承这个类,以便疾速搭建框架。

import {
  Scene,
  PerspectiveCamera,
  WebGLRenderer,
  Vector3,
  Color
} from 'three'

export default class Template {constructor () {               // 各种默认选项
    this.el = document.body
    this.PCamera = {
      fov: 45,
      aspect: window.innerWidth / window.innerHeight,
      near: 1,
      far: 1000
    }
    this.cameraPostion = new Vector3(0, 0, 1)
    this.cameraLookAt = new Vector3(0,0,0)
    this.rendererColor = new Color(0x000000)
    this.rendererWidth = window.innerWidth
    this.rendererHeight = window.innerHeight
  }

  initPerspectiveCamera () {     // 初始化相机,这里是透视相机
    const camera = new PerspectiveCamera(
      this.PCamera.fov,
      this.PCamera.aspect,
      this.PCamera.near,
      this.PCamera.far,
    )
    camera.position.copy(this.cameraPostion)
    camera.lookAt(this.cameraLookAt)
    this.camera = camera
    this.scene.add(camera)
  }

  initScene () {                // 初始化场景
    this.scene = new Scene()}

  initRenderer () {             // 初始化渲染器
    const renderer = new WebGLRenderer()
    renderer.setClearColor(this.rendererColor)
    renderer.setSize(this.rendererWidth, this.rendererHeight)
    this.el.appendChild(renderer.domElement)
    this.renderer = renderer
  }

  init () {this.initScene()
    this.initPerspectiveCamera()
    this.initRenderer()}
}

在咱们的下雨动画中,创立一个 Director 类治理动画,它继承自 Template 类。能够看出,它要做的事很清晰:初始化框架、批改父类的默认配置、增加物体(云层和雨滴)、增加光照(闪电也是光照造成的)、增加雾化成果、循环渲染。

//director.js
export default class Director extends Template{constructor () {super()

    //set params
    //camera
    this.PCamera.fov = 60       // 批改照相机的默认视场 fov

    //init camera/scene/render
    this.init()
    this.camera.rotation.x = 1.16   // 设置照相机的旋转角度(望向天空)this.camera.rotation.y = -0.12
    this.camera.rotation.z = 0.27

    //add object
    this.addCloud()                  // 增加云层和雨滴
    this.addRainDrop()

    //add light
    this.initLight()                // 增加光照,用 PointLight 模仿闪电
    this.addLightning()
    
    //add fog
    this.addFog()                   // 增加雾,在相机左近视线清晰,间隔相机越远,雾的浓度越高

    //animate
    this.animate()                 //requestAnimationFrame 实现动画}
}

创立一直变换的云层

咱们首先创立一个立体,将一小朵云做为材质,失去一个云朵物体。而后将很多云朵物体进行叠加,失去一团云。

//Cloud.js
const texture = new TextureLoader().load('/images/smoke.png')  // 加载云朵素材
const cloudGeo = new PlaneBufferGeometry(564, 300)   // 创立平面几何体
const cloudMaterial = new MeshLambertMaterial({   // 图像作为纹理贴图,生成材质
  map: texture,
  transparent: true
})
export default class Cloud {constructor () {const cloud = new Mesh(cloudGeo, cloudMaterial)   // 生成云朵物体
    cloud.material.opacity = 0.6
    this.instance = cloud
  }

  setPosition (x,y,z) {this.instance.position.set(x,y,z)
  }

  setRotation (x,y,z) {
    this.instance.rotation.x = x
    this.instance.rotation.y = y
    this.instance.rotation.z = z
  }

  animate () {this.instance.rotation.z -= 0.003            // 云朵的静止是一直绕着 z 轴旋转}
}

在 Director 类中,生成 30 个云朵物体,随机设置它们的地位和旋转,造成铺开和层叠的成果。在循环渲染时调用云朵物体的 animate 办法。

//director.js
addCloud () {this.clouds = []
  for(let i = 0; i < 30; i++){const cloud = new Cloud()
    this.clouds.push(cloud)
    cloud.setPosition(Math.random() * 1000 - 460, 600, Math.random() * 500 - 400)
    cloud.setRotation(1.16, -0.12, Math.random() * 360)
    this.scene.add(cloud.instance)
  }
}
animate () {
    //cloud move
    this.clouds.forEach((cloud) => {  // 调用每个云朵物体的 animate 办法,造成整个云层的一直变换成果
      cloud.animate()})
    ...
    this.renderer.render(this.scene, this.camera)
    requestAnimationFrame(this.animate.bind(this))
  }

环境光和闪电

同时应用了 AmbientLight 和 DirectionalLight 作为整个场景的稳固光源,加强对事实场景的模仿。

//director.js
initLight () {const ambientLight = new AmbientLight(0x555555)
  this.scene.add(ambientLight)

  const directionLight = new DirectionalLight(0xffeedd)
  directionLight.position.set(0,0,1)
  this.scene.add(directionLight)
}

用 PointLight 模仿闪电,首先是初始一个 PointLight。

//director.js
addLightning () {const lightning = new PointLight(0x062d89, 30, 500, 1.7)
  lightning.position.set(200, 300, 100)
  this.lightning = lightning
  this.scene.add(lightning)
}

在循环渲染时,一直随机扭转点光源 PointLight 的强度(power),造成闪动的成果,当强度较小,即光线暗下来时,” 轻轻 ” 扭转点光源的地位,这样就能不突兀使闪电随机地呈现在云层地各个地位。

//director.js
animate () {
  ...
  //lightning
  if(Math.random() > 0.93 || this.lightning.power > 100){if(this.lightning.power < 100){
      this.lightning.position.set(Math.random() * 400,
        300 + Math.random() * 200,
        100
      )
    }
    this.lightning.power = 50 + Math.random() * 500}

  this.renderer.render(this.scene, this.camera)
  requestAnimationFrame(this.animate.bind(this))
}

创立雨滴

创立雨滴用到的粒子成果。创立一组粒子,直观的办法是,创立一个粒子物体,而后复制 N 个,别离定义它们的地位和旋转。

当你应用大量的对象时,这很无效,然而当你想应用大量的 THREE.Sprite 对象时,你会很快遇到性能问题,因为每个对象须要别离由 Three.js 进行治理。

Three.js 提供了另一种形式来解决大量的粒子,这须要应用 THREE.Points。通过 THREE.Points,Three.js 不再须要治理大量单个的 THREE.Sprite 对象,而只需治理 THREE.Points 实例。

应用 THREE.Points,能够非常容易地创立很多细小的物体,用来模仿雨滴、雪花、烟和其余乏味的成果。

THREE.Points 的核心思想,就是先申明一个几何体 geom,而后确定几何体各个顶点的地位,这些顶点的地位将会是各个粒子的地位。通过 PointsMaterial 确定顶点的材质 material,而后 new Points(geom, material),依据传入的几何体和顶点材质生成一个粒子系统。

粒子的挪动:粒子的地位坐标是由一组数字确定const positions = this.geom.attributes.position.array,这组数字,每三个数确定一个坐标点(x\y\z),所以要扭转粒子的 X 坐标,就扭转positions[3n] (n 是粒子序数);同理,Y 坐标对应的是positions[3n+1],Z 坐标对应的是positions[3n+2]

//RainDrop.js
export default class RainDrop {constructor () {const texture = new TextureLoader().load('/images/rain-drop.png')
    const material = new PointsMaterial({    // 用图片初始化顶点材质
      size: 0.8,
      map: texture,
      transparent: true
    })
    
    const positions = []

    this.drops = 8000
    this.geom = new BufferGeometry()
    this.velocityY = []

    for(let i = 0; i < this.drops; i++){positions.push( Math.random() * 400 - 200 )
      positions.push(Math.random() * 500 - 250 )
      positions.push(Math.random() * 400 - 200 )
      this.velocityY.push(0.5 + Math.random() / 2)  // 初始化每个粒子的坐标和粒子在 Y 方向的速度
    }
    
    // 确定各个顶点的地位坐标
    this.geom.setAttribute('position', new Float32BufferAttribute( positions, 3) )  
    this.instance = new Points(this.geom, material)  // 初始化粒子系统
  }

  animate () {
    const positions = this.geom.attributes.position.array;
    
    for(let i=0; i<this.drops * 3; i+=3){    // 扭转 Y 坐标,加速运动
      this.velocityY[i/3] += Math.random() * 0.05
      positions[i + 1] -=  this.velocityY[i/3]
      if(positions[ i + 1] < -200){positions[ i + 1] =  200
        this.velocityY[i/3] = 0.5 + Math.random() / 2}                                     
    }
    this.instance.rotation.y += 0.002    
    this.geom.attributes.position.needsUpdate = true
  }
}

将雨滴粒子增加到场景中,并在循环渲染时,调用 RainDrop 的 animate 办法:

//director.js
addRainDrop () {this.rainDrop = new RainDrop()
  this.scene.add(this.rainDrop.instance)
}
animate () {
  //rain drop move
  this.rainDrop.animate() 
  ...
  this.renderer.render(this.scene, this.camera)
  requestAnimationFrame(this.animate.bind(this))
}

结语

感激你的浏览,如果感觉还不错,欢送点赞、评论、珍藏哦❤️❤️!

更多技术交换欢送关注我的公众号:Alasolala

退出移动版