Three.js 之通过 vite + ts 渲染一个旋转的立方体

363 阅读6分钟

之前我们学习了一段时间关于 WebGL 的使用,而在实际工作中,多数情况下都是直接使用 Three.js 这个库,一个运行在浏览器的 3D 引擎,来帮我们更方便地操作使用 WebGL 去渲染 3D 场景。本篇文章就介绍下如何使用 threejs 去创建一个旋转的立方体。

使用 vite 构建项目

我们使用 vite5 来构建项目,先通过 pnpm 给项目安装上 vite:

pnpm init
pnpm add vite -D

然后安装 threejs:

pnpm add three

在项目根目录准备 index.html 如下:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>threejs 渲染旋转立方体</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <script type="module" src="/main.ts"></script>
  </body>
</html>

可以看到,在 <script> 标签内,我直接引入的是 ts 文件,为了在编写代码时获取更好的提示,我们还需要安装 threejs 的类型声明文件:

pnpm add @types/three -D

本项目使用的各个依赖包版本如下:

2024-08-12_112217.png

渲染三维对象

我们先在 main.ts 中引入 threejs:

import * as THREE from 'three'

执行 console.log(THREE) ,可以看到获取的 THREE 是一个对象,结果如下:

2.png

创建场景

通过 new THREE.Scene() 可以获取一个场景的实例对象 scene

const scene = new THREE.Scene()

scene 有很多方法和属性,后文会在用到时介绍 。

创建相机

通过 new THREE.PerspectiveCamera() 创建透视投影相机,它其实就是我们之前推导出的透视投影矩阵,传入的参数也一样,分别是视角(角度,而不是弧度)、宽高比和视点距离近、远平面的距离:

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
)

在 webgl 中,控制相机的移动旋转我们使用的是视图矩阵,并说到视图矩阵有 3 大要素——视点、目标点和上方向。threejs 创建的相机,其视点位置默认位于原点,而之后我们会创建一个立方体,它的中心点默认也位于原点,为了能看到立方体,我们需要更改下相机的视点位置,将它的中心点 z 轴坐标改为 10:

// 单个赋值
camera.position.z = 10
// 或者通过 set 方法赋值
camera.position.set(0, 0, 10)

修改相机的目标点则是通过相机的 lookAt() 方法,传入的参数可以是代表目标点的世界坐标系的位置的 x、y和 z 的分量,或者直接是一个向量:

camera.lookAt(3, 3, 0)

修改上方向则通过 up 属性:

// 单个赋值
camera.up.x = 2
// 通过 set 方法赋值
camera.up.set(2, 1, 0)

要使上方向的修改有效,需在设置 lookAt() 之前修改 up 属性。

创建渲染器

使用 new THREE.WebGLRenderer() 创建渲染器:

// 创建渲染器
const renderer = new THREE.WebGLRenderer()
// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

注意,在先前创建的 index.html 中,我们并没有定义 <canvas> 元素,因为此处 threejs 会帮我们自动创建,创建好的 canvas 对象会放在 renderer.domElement。通过 renderer.setSize(window.innerWidth, window.innerHeight) 自定义画布的尺寸为浏览器窗口的视口尺寸,最后添加到页面中。

参数

WebGLRenderer() 中可以传入一个参数对象,其有很多属性,用以定义渲染器的行为。下面介绍其中几个:

  • canvas:我们可以指定某个供渲染器绘制其输出的 canvas,比如 index.html 页面中已经准备好了一个 canvas:
<body>
  <canvas id="canvas" width="400" height="400"></canvas>
</body>

那么就可以直接通过 canvas 属性指定,这样就不需要再通过 renderer.setSize() 设置 canvas 的尺寸,也不需要再 document.body.appendChild(renderer.domElement) 把默认生成的 canvas 插入 body 中了:

const canvas = document.getElementById('canvas') as HTMLCanvasElement
const renderer = new THREE.WebGLRenderer({ canvas })
  • antialias:是否执行抗锯齿。如果不传,默认为 false,则渲染出来的立方体可以看到有明显的锯齿存在:

1.png

创建立方体

创建立方体时先 new THREE.BoxGeometry() 创建立方体对象,传入的参数代表 x、y、z 轴上的长度;
然后new THREE.MeshBasicMateria() 创建基础材质,传入的参数是一个对象,可以通过 color 属性指定颜色,值为 16 进制的颜色值;
最后通过网格 new THREE.Mesh() 传入立方体对象和材质生成立方体,模拟生活中的物体:

const cubeGeometry = new THREE.BoxGeometry(2, 2, 2) // 几何体
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x666666 }) // 材质
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial) // 物体

创建好的立方体,需要使用 scene.add() 添加到场景中:

scene.add(cube)

我们可以通过给 cubename 属性赋上一个唯一值,然后将该值传给 scene.getObjectByName(),来获取到创建的立方体:

cube.name = 'myCube'
console.log(scene.getObjectByName('myCube'))

打印结果如下:

3.png

如果想移除创建的立方体,可以使用 sceneremove 方法将其从场景移除:

scene.remove(cube)

添加动画并渲染

将创建的场景 scene 和相机 camera 传入渲染器 rendererrender 方法中渲染立方体,再通过单独改变 cube 的旋转弧度给立方体添加旋转动画:

const animation = () => {
  requestAnimationFrame(animation)
  cube.rotation.x += 0.01
  cube.rotation.y += 0.01
  // console.log(cube.matrix.elements)
  // 渲染
  renderer.render(scene, camera)
}
animation()

我们可以执行 console.log(cube.matrix.elements) 打印查看立方体的矩阵(matrix),该对象存储着立方体的位置,旋转和比例,所以旋转时打印的值在不断变换:

4.png

现在,就可以在命令行输入 npx vite 来查看效果了。演示效果如下:

此时渲染的立方体是纯色的,如果它静止不动,那几乎都看不出其是个 3 维物体。我们可以给场景添加上灯光,让物体具有明暗变化,从而显示出立体感。

添加灯光

聚光灯

使用 new THREE.SpotLight() 创建聚光灯,比如手电筒就可以认为是聚光灯光源,传入的第 1 个参数为 16 进制的颜色值,用于定义灯光的颜色,默认为白色(0xffffff);第 2 参数为灯光的强度值(intensity),默认为 1。

通过 position.set() 设置灯光的位置;intensity 可单独设置灯光的强度;最后使用 scene.add() 把灯光添加到场景中:

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

之前创建立方体时,使用的材质是 MeshBasicMaterial,是基础网格材质,这种材质不受光照的影响,所以我们需要把材质改为朗伯材质 MeshLambertMaterial,该材质会考虑光照,创建的是较为暗淡的物体,然后才能看到灯光的效果。

环境光

环境光是对整个场景内的对象都生效的,所以无需像之前设置聚光灯那样需要定义光源的位置,而是直接生成实例添加到场景中即可,环境光的颜色默认也是白色 0xffffff,强度(intensity)默认为 1:

const ambientLight = new THREE.AmbientLight(0xffffff)
scene.add(ambientLight)

环境光一般都是配合其它光源一起使用,以模拟更加真实的现实环境。 现在渲染效果如下:

添加阴影效果

在 threejs 中,有些类型的光,比如我们使用的聚光灯,还有点光源等,会产生阴影;而环境光 、半球光,则不会产生阴影。为了显示出阴影效果,我们先在立方体的后面创建一个 30 * 30 的平面,用于接收灯光透过立方体产生的阴影。步骤和创建立方体类似,不过是方法名和传参不同而已,plane.position.z = -20 是让平面位于立方体的后面,也就是屏幕的更深处:

const planeGeometry = new THREE.PlaneGeometry(30, 30) 
const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xefefef })
const plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.position.z = -20
scene.add(plane)

要开启阴影效果,需要让灯光、立方体、平面和渲染器的相关属性为 true

spotLight.castShadow = true
cube.castShadow = true
plane.receiveShadow = true
renderer.shadowMap.enabled = true

效果如下:

感谢.gif 点赞.png