之前我们学习了一段时间关于 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
本项目使用的各个依赖包版本如下:
渲染三维对象
我们先在 main.ts 中引入 threejs:
import * as THREE from 'three'
执行 console.log(THREE)
,可以看到获取的 THREE
是一个对象,结果如下:
创建场景
通过 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
,则渲染出来的立方体可以看到有明显的锯齿存在:
创建立方体
创建立方体时先 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)
我们可以通过给 cube
的 name
属性赋上一个唯一值,然后将该值传给 scene.getObjectByName()
,来获取到创建的立方体:
cube.name = 'myCube'
console.log(scene.getObjectByName('myCube'))
打印结果如下:
如果想移除创建的立方体,可以使用 scene
的 remove
方法将其从场景移除:
scene.remove(cube)
添加动画并渲染
将创建的场景 scene
和相机 camera
传入渲染器 renderer
的 render
方法中渲染立方体,再通过单独改变 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
),该对象存储着立方体的位置,旋转和比例,所以旋转时打印的值在不断变换:
现在,就可以在命令行输入 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
效果如下: