Three.js基础---一文了解Three.js(1)

626 阅读1小时+

前言

如果你想了解一下3D开发的话,那么你肯定需要接触到这方面的内容了,比如THREE.js或者是WebGL,WebGL是一个JavaScript API,它能通过GPU进行快速的渲染,但是图形的每个点的数据需要使用着色器来进行控制,这也是原生WebGL很困难的原因,Three.js是MIT许可下的JavaScript 库,工作在 WebGL 之上。他在渲染的时候并不需要去提供着色器,其文档也随着社区的努力越来越完善,这也是它流行起来的原因。

注意:

  • 简单点,可以使用vite直接生成项目

  • 如果在代码中看到// ...类似这样的,就表示直接使用之前的代码

一、第一个简单场景案例

(1)文件的基本结构

说明: 在生成的项目中,主要文件就是一个index.html文件来存放画布以及一个script.js文件存放自己的Three.js代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <!-- 创建一个用于WebGL渲染的画布元素 -->
    <canvas class="webgl" ></canvas>

    <!-- 因为需要使用import导入,所以需要设置一下type的类型 -->
    <script type="module" src="./src/main.js"></script>
</body>
</html>
// 使用npm i three,然后直接导入就可以了
import * as THREE from 'three'

// 然后可以运行一下项目看看有没有报错,这样就证明安装成功
console.log(THREE);

image.png

(2)场景的基础组成

说明: 指三部分,分别是场景、相机以及渲染器,这里可以简单了解一下...

  • 场景: 场景是一个容器,主要用于保存、跟踪所要渲染的物体和使用的光源。如果没有这个对象,那么Three.js就无法渲染任何物体

  • 相机: 摄像机决定了能够在场景看到什么

  • 渲染器: 渲染器对象,该对象会基于摄像机的角度来计算场景对象在浏览器中会渲染成什么样子,然后使用显卡进行渲染

import * as THREE from 'three'

// 获取画布
const canvas = document.getElementById('webgl')

// 一、场景
const scene = new THREE.Scene()

// 往场景里面添加物体
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00
})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

// 二、摄像机
const size = {
    width: window.innerWidth,
    height: window.innerHeight
}

const camera = new THREE.PerspectiveCamera(75, size.width / size.height)
// 由于默认所有物体都在原点,所以需要调整摄像机的位置
camera.position.z = 3
scene.add(camera)

// 三、渲染器
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
// 确保渲染出的画面能够适应不同的屏幕尺寸
renderer.setSize(size.width, size.height)
renderer.render(scene, camera)

image.png

(3)物体的基础变换

说明: 这里变换有四种,分别是position、scale、以及rotation,分别代表移动、缩放以及旋转

==> 轴辅助器 <==

说明: 在空间中定位物体可能是一项真正的挑战。了解每个轴的方向非常复杂,尤其是当我们开始移动相机时。一个好的解决方案是使用AxesHelper,它将显示与轴相对应的3条线,每条线从场景的中心开始并朝着相应的方向延伸;它存在唯一的参数就是线的长度

// 这里由于相机在z轴上,所以看不见z轴的辅助线
const axesHelper = new THREE.AxesHelper(2)
scene.add(axesHelper)

image.png

==> position <==

说明: position有三个属性,分别是x、y、z对应三条轴,默认情况下,x轴控制左右(正方向),y轴控制上(正方向)下,z轴控制里外(正方向);不过这些会根据相机的方向不同而有所改变。

// 向右
mesh.position.x = 0.7
// 向下
mesh.position.y = -0.6
// 向外(越靠近相机,物体越大)
mesh.position.z = 1

image.png

// 在渲染器之后移动物体会发现物体没有什么变化
renderer.render(scene, camera)
mesh.position.x = 0.7
mesh.position.y = -0.6
mesh.position.z = 1

image.png

注意: 使用position移动物体需要在创建这个物体之后,渲染器之前移动,因为需要先移动后渲染

<== 实质与常用方法 ==>

实质: position属性其实它Vector3类的一个实例。虽然此类具有x、 y和z属性,但它也有许多有用的方法,比如下面这些常用的

// 获取从场景中心(0,0,0)到position(x,y,z)之间的举例
mesh.position.length()

// 获取两个物体间的距离(比如这里获取相机和物体之间的距离)
mesh.position.distanceTo(camera.position)

// 将一个向量变为单位向量(长度为1,但保留方向)
mesh.position.normalize()
// 上面移动的时候也可以直接使用set方法进行简写
mesh.position.set(0.6,-0.7,1)

==> scale <==

说明: scale属性也是Vector3的实例。默认情况下,x、y和z的默认值为1,这意味着对象没有应用缩放。如果你将0.5其作为值,则对象在此轴上的大小将为原来的一半,如果你将其2作为值,则对象在此轴上的大小将为原始大小的两倍。

mesh.scale.x = 0.5
mesh.scale.y = 0.5
mesh.scale.z = 1

image.png

注意: 虽然可以使用负值,但这可能会在以后产生错误,因为轴不会按照逻辑方向定向。尽量避免这样做

==> rotation <==

注意: 旋转有两种方法,一种是rotation,另一种是quaternion,好在这两种方式threejs都支持,更新其中一个属性会自动更新另一个属性

说明: rotation但它不是Vector3,而是Euler,因为它不存在方向;此外,旋转都以旋转轴转动,默认情况下,这个轴可以理解为从轴线的方向向物体中央插入一根棍子,然后绕这个棍子进行旋转

// 绕x轴旋转1/8圈
mesh.rotation.x = Math.PI * 0.25

image.png

注意: 旋转的值用弧度表示,一般使用Math.PI * 2表示一圈

<== 万向节锁行为 ==>

说明: 简单来说就是以某一条轴旋转之后,另外两条轴就不是默认的方向了,就好像只有旋转的这一条轴锁定了一样,看下面的例子就明白了

mesh.rotation.x = Math.PI * 0.25
// 此时y轴的旋转方向就不是垂直向上的了,因为x轴转动了y轴
mesh.rotation.y = Math.PI * 0.25

image.png

// 用大写字母表示轴,以此规定其旋转的顺序
mesh.rotation.reorder('YXZ')
mesh.rotation.x = Math.PI * 0.25
mesh.rotation.y = Math.PI * 0.25

image.png

注意: 如果需要规定轴旋转的方向,可以使用reorder()

(4)动画的基础制作

说明: 动画的工作原理类似于定格动画。移动对象,然后进行渲染。然后将对象再移动一点,然后进行另一次渲染。等等。在渲染之间移动对象的次数越多,它们看起来移动得就越快,这里的快慢会与屏幕的帧速率有关,大多数屏幕以每秒60帧的速度运行。这意味着每16毫秒大约一帧,有些时候,计算机也会有一定的限制,比如游戏本这种比普通的笔记本就要好很多。对于最简单的动画来说,可以执行一个函数,该函数将移动物体并在每一帧上进行渲染,而不管帧速率如何

==> requestAnimationFrame <==

对于定时器: 对于动画来说,你可能第一时间想到使用定时器,但是定时器有一个缺点就是不管浏览器在做什么东西,定时器中的函数都会隔特定的时间触发一次,此外它还与屏幕的刷新时间不同步,这将会导致较高的CPU使用率和性能不良

使用requestAnimationFrame: 这个函数为稳定而连续的渲染场景提供了良好的解决方案,通过这个函数,你可以向浏览器提供一个回调函数。你无须定义回调间隔,浏览器将自行决定最佳回调时机。你需要做的是在这个回调函数里完成一帧绘制操作,然后将剩下的工作交给浏览器,它负责使场景绘制尽量高效和平顺地进行

// 定义一个tick函数,用于每一帧的渲染
const tick = () => {
    // 然后就可以在此处进行想要的动画效果了,比如绕x轴旋转物体
    // mesh.rotation.x += 0.1

    // 每一次渲染,都需要使用渲染器渲染场景和相机,这样可以确保场景能够实时响应玩家的交互
    renderer.render(scene, camera)

    // 在下一帧继续调用tick函数,实现动画的连续播放
    window.requestAnimationFrame(tick)
}

// 启动tick函数,开始渲染动画
tick()

==> 帧率问题 <==

说明: 上面这种写法在高帧率的电脑上测试此代码,立方体会旋转得更快,而如果你在较低的帧率上测试,立方体会旋转得更慢;此时可以使用经过的时间来进行处理,最简单的方式就是使用threejs中的Clock变量进行处理,其解决的方式就是虽然你的帧速率高,函数执行了多次,但是我经过的时间是一致的,这样你我看到的效果也是一致的

// 需要实例化
const clock = new THREE.Clock()

const tick = () => {
    // 获取经过的时间
    const elapsedTime = clock.getElapsedTime()

    // 更新物体
    mesh.rotation.y = elapsedTime

    // ...
}

tick()

(5)场景的全屏变换

说明: 这里做的功能就是画布是全屏的,然后当窗口变化的时候画布也跟着变化,以此适应屏幕的大小,这样就拥有更好的体验了

==> 去除默认样式 <==

说明: 在上面的代码中可以看到四周存在滚动条以及一些白色的空白部分,可以使用下面的样式进行修复

image.png

* {
    margin: 0;
    padding: 0;
}

html,
body {
    overflow: hidden;
}

.webgl {
    position: fixed;
    top: 0;
    left: 0;
    outline: none;
}

image.png

==> 尺寸调整函数 <==

说明: 就是直接在window上面监听一个resize事件,窗口一发生变化就进行更改操作

window.addEventListener('resize', () => {
    // 更新相机的尺寸
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // 更新相机的宽高比
    camera.aspect = sizes.width / sizes.height
    // 宽高比的改变会影响到透视投影的形状,所以需要调用更新投影矩阵,
    // 确保渲染的场景能够正确地适应新的窗口尺寸
    camera.updateProjectionMatrix()

    // 更新画布
    renderer.setSize(sizes.width, sizes.height)
})

==> 处理像素比 <==

说明: 可能你会看见你的物体边缘存在类似锯齿状的东西,那么你的屏幕的像素比大于1;可以这么理解,像素比为1的屏幕就是屏幕上一个像素的位置只能存放一个像素,像素比为2就是一个像素的位置存放4个像素,像素比为3就是存放9个像素...以此类推,所以需要把你屏幕的像素比发送给渲染器,让它来处理,屏幕的像素比可以通过window.devicePixelRatio来获取,通过渲染器的setPixelRatio方法来设置

注意: 像素比大于2的屏幕跟像素比为2的屏幕人的眼睛是看不出来区别的,当像素比到3会产生性能问题并更快耗尽电池电量,所以需要做一下限制

window.addEventListener('resize', () => {
    // ... 前面的内容

    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
})
// ... 渲染器

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

==> 双击全屏 <==

// 直接用就好
window.addEventListener('dblclick', () => {
    const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement

    if (!fullscreenElement) {
        // 请求全屏
        if (canvas.requestFullscreen) {
            canvas.requestFullscreen()
        } else if (canvas.webkitRequestFullscreen) {
            canvas.webkitRequestFullscreen()
        }
    } else {
        // 退出全屏
        if (document.exitFullscreen) {
            document.exitFullscreen()
        } else if (document.webkitExitFullscreen) {
            document.webkitExitFullscreen()
        }
    }
})

(6)工具的简单推荐

说明: 这里推荐一个lil-gui库让调试变得更加简单,完整的用法可以直接去看一下文档,这里介绍其基本的使用

import * as dat from 'lil-gui'

const gui = new dat.GUI()

// 支持链式调用
gui
    .add(mesh.position, 'y')
    .min(-3)
    .max(3)
    .step(0.01)
    .name('elevation')

image.png

二、了解Three.js应用的基本组件

(1)场景

说明: 从上面的例子可以看到,如果想要一个场景显示任何东西,那么这个场景里面最少需要对象、摄像机以及渲染器这三部分,当然,可以放在场景里面的东西还有很多,比如后面所说的光源这些;如果你去看文档,会发现这个对象本身没有过多的选项和函数,因此这里介绍它常用的几个属性和方法

==> add <==

说明: 在场景中添加对象就需要使用add()方法,比如前面添加对象和相机到场景中去,这些内容会被添加到scene.children属性上面去

// 创建场景
const scene = new THREE.Scene()

// ...
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

// ...
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
scene.add(camera)

// 打印一下场景,看看里面有什么
console.log(scene)

image.png

==> children <==

说明: 将添加到场景中的对象以数组的形式保存起来,因此,可以对其进行数组的操作比如循环,使用length属性等

for(let i = 0; i < scene.children.length; i++) {
    console.log(scene.children[i]);
    console.log(scene.children.length);
}

image.png

==> remove <==

说明: 既然有添加对象,那么肯定就存在删除对象,删除需要使用remove方法

// ...
scene.remove(mesh)
console.log(scene);

image.png

==> getObjectByName <==

说明: 在说getObjectByName()这个方法之前,先了解一下name属性,这个可以理解为给对象起一个名字,默认是没有的,也就是name的值是空字符串,如果场景中存在很多相同的物体,比如相机什么的,就很难区分了,其实name属性就有用武之地了;然后如果想获取具体name对象的信息,就可以使用getObjectByName('name')来实现效果

image.png

// ...
// 设置name属性
mesh.name = 'mesh';
camera.name = 'camera';

console.log(scene);

image.png

console.log(scene.getObjectByName('mesh'));

image.png

==> traverse <==

说明: 可以将一个方法作为参数传递给traverse()方法,这个传递来的方法将会在每一个子对象上执行,而这个方法的参数就是当前遍历到的子对象对象。由于THREE.Scene对象存储的是对象树,所以如果子对象本身还有子对象,traverse()方法会在所有的子对象上执行,直到遍历完场景树中的所有对象为止

// ...
scene.traverse((child) => {
    console.log(child);
})

image.png

==> fog <==

说明: 让场景中的物体离摄像机越远就会变得越模糊,也就是添加雾化效果;这里添加有两种添加方式,分别是使用Fog()和FogExp2()

注意: 上面的代码中还不能通过鼠标控制照相机,等到照相机那里添加一个控制器就好了

// 雾化颜色、雾化开始的地方(near)、雾化结束的地方(far)
const scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0xcccccc, 10, 15 );
// 雾化颜色、浓度
const scene = new THREE.Scene(); 
scene.fog = new THREE.FogExp2( 0xcccccc, 0.002 );

区别: 使用THREE.Fog创建的对象,雾的浓度是线性增长的;而使用THREE.FogExp2创建的对象是呈指数增长的

==> overrideMaterial <==

说明: 当设置了overrideMaterial属性后,场景中所有的物体都会使用该属性指向的材质,即使物体本身也设置了材质。当某一个场景中所有物体都共享同一个材质时,使用该属性可以通过减少Three.js管理的材质数量来提高运行效率,但是实际应用中,该属性通常并不非常实用。

// 之前设置的物体材质
const material = new THREE.MeshBasicMaterial({
    color: 'skyblue'
})

image.png

// 使用overrideMaterial属性进行设置(只更改颜色)
scene.overrideMaterial = new THREE.MeshBasicMaterial({
    color: 0xffffff
})

image.png

(2)照相机与控制器

说明: 相机可以理解为你的眼睛,它看到啥就会在渲染器中渲染啥;在threejs中,相机种类很多,这里只简单了解透视相机和正交相机,其次这两种相机里面透视相机使用的最多。

==> 透视相机 <==

说明: 可以模拟人眼观察世界的视觉效果,产生近大远小的透视效果

参数:

  • fov(必): 视场决定了摄像机的视野范围,即摄像机可以看到场景的宽广程度,推荐角度设置为45-75之间,因为过大会显得很近,过小会显得很远

  • aspect(必): 它通过将摄像机的宽度除以高度来计算。推荐设置为 sizes.width / sizes.height,这个比例对于保持图像的正确显示非常重要

  • near: 这个值定义了摄像机能够看到的最近物体的距离。任何比这个距离更近的物体都不会被渲染,默认值是0.1

  • far: 这个值定义了摄像机能够看到的最远物体的距离。任何比这个距离更远的物体都不会被渲染,默认值是2000

参数图示:

image.png

==> 正交相机 <==

说明: 用于创建正交投影的相机。正交投影是一种平行投影方式,它不会产生透视效果,即物体的大小不会随着距离的变化而变化,这使得正交投影相机非常适合用于创建2D图形和动画,因为你可以精确地控制物体的尺寸和位置,而不必担心透视失真;但是平时使用并不多

参数:

  • left(必):相机视口的左边界。
  • right(必):相机视口的右边界。
  • top(必):相机视口的上边界。
  • bottom(必):相机视口的下边界。
  • near:相机的近裁剪面距离。
  • far:相机的远裁剪面距离。

参数图示:

image.png

==> 控制器 <==

说明: 上面的物体是不能够移动的,因为我们没有移动照相机,对于照相机的移动,除了使用position属性移动外,还可以使用控制器,控制器是一个照相机的控件,它能让你很容易就移动照相机;控制器有很多种类,不同的控制器其效果也不一样,这里以OrbitControls控制器为例

<== 简单使用 ==>

说明: 将这样简单的导入设置之后你会发现你能够使用鼠标来转动物体了

// ...
// 导入
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

// ...
// 实例化
const controls = new OrbitControls(camera, canvas);
// 启用阻尼,让你滑动屏幕的效果更加平滑
controls.enableDamping = true;

// ...
// 在动画函数中更新控制器
const tick = () => {
    // ! 在控制器使用了阻尼的时候,不要忘记在渲染器之前更新控制器
    controls.update()

    // ...
}
<== 简单模拟 ==>

说明: 这里模拟的效果只是你能够四处翻转物体而已,像滚轮能够放大物体的这种效果没有实现

// ...
// 跟踪鼠标位置,方便后续进行更改
const cursor = {
    x: 0,
    y: 0
}

// 监听鼠标移动事件,更新cursor的x和y值(归一化坐标通常为[-1,1])
window.addEventListener('mousemove', (event) => {
    // 计算鼠标在窗口宽度上的归一化横坐标
    cursor.x = event.clientX / sizes.width - 0.5; 
    // 计算鼠标在窗口高度上的归一化纵坐标,并取反以适应坐标
    // 系,这里翻转是因为在three.js中,y轴是向上的,而屏幕坐
    // 标系中y轴是向下的,所以需要取反
    cursor.y = - (event.clientY / sizes.height - 0.5); 
});

// ...
const tick = () => {
    // ...

    // 使用sin和cos组合并使用相同的角度时,能够将物体放在圆圈上,
    // 也就是旋转物体的效果;其次要进行完整的旋转,该角度的振幅
    // 必须是 π 的 2 倍
    camera.position.x = Math.sin(cursor.x * Math.PI * 2) * 2
    camera.position.z = Math.cos(cursor.x * Math.PI * 2) * 2
    camera.position.y = cursor.y * 3
    // camera.lookAt可以让相机看向某一个位置,默认是场景中心,
    // 也就是(0,0,0)这个点,这里如果不更改相机所看的位置,
    // 那么旋转的效果就是正方体绕着这个中心点进行转动了,
    // 而不是正方体转动,所以需要相机看向正方体,这样改变相机
    // 的位置才是旋转的看物体
    camera.lookAt(mesh.position)

    // ...
}

(3)自定义几何体

==> 几何体的构成 <==

说明: 以最简单的正方体BoxGeometry为例,它由顶点以及面构成;每个顶点可以使用x、y和z来定义其位置,然后将顶点依次连接所构成的三角形会形成面,对于正方体来说,它的每一个面都会有两个三角形组成,至于为什么是三角形,因为它的渲染效率要高一些

// ...
const geometry = new THREE.BoxGeometry(1, 1, 1)

const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    // 启用线框,也就是只能看见每个面所构成的三角形
    wireframe: true
})

const mesh = new THREE.Mesh(geometry, material)

scene.add(mesh)

// ...

image.png

==> 自定义几何体 <==

说明: 这里自定义的几何体是那种简单的,因为复杂的几何体你可以直接使用建模软件制作然后导入就好了,这样会方便很多

简单举例:

// ...
// 创建场景
const scene = new THREE.Scene()

// 创建BufferGeometry(默认生成三角形)
const geometry = new THREE.BufferGeometry()

// 定义顶点数量
const count = 50

// 创建顶点位置数组,只能使用Float32Array这种数组
// (由于每一个坐标都存在x、y、z三个值,所以*3*3,
// 另外它们在数组中存储的方式是[x,y,z,x,y,z,x,y,z...]这样排列的)
const positionsArray = new Float32Array(count * 3 * 3)

// 填充顶点位置数组
for (let i = 0; i < count * 3 * 3; i++) {
    // 这里将顶点坐标的值限制在-2~2之间是为了避免顶点位置超出场景范围
    positionsArray[i] = (Math.random() - 0.5) * 4
}

// 创建BufferAttribute对象(可以理解为每三个值一组,
// 这样就能够得到50个顶点的坐标了)
const positionsAttribute = new THREE.BufferAttribute(positionsArray, 3)

// 将顶点位置属性添加到BufferGeometry(目的是生成小三角形,在生成的时候)
geometry.setAttribute('position', positionsAttribute)

// 创建材质
const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    wireframe: true
})

// 创建Mesh对象
const mesh = new THREE.Mesh(geometry, material)

// 将Mesh对象添加到场景
scene.add(mesh)

// ...

image.png

总结: 使用BufferGeometry进行几何体的自定义,对于几何体的顶点使用Float32Array型的数组来储存,由于每个顶点坐标由x、y和z组成,所以注意3倍的关系,填充完毕后创建BufferAttribute对象然后将其添加到几何体的position属性上就可以了

三、了解光源与阴影

前提: 这里是需要用到的几何体和材质,然后你就能看到这里一样的效果了

// ...
const material = new THREE.MeshStandardMaterial()
material.roughness = 0.4

// 创建几何体并添加到场景
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    material
)
sphere.position.x = - 1.5

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(0.75, 0.75, 0.75),
    material
)

const torus = new THREE.Mesh(
    new THREE.TorusGeometry(0.3, 0.2, 32, 64),
    material
)
torus.position.x = 1.5

const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    material
)
plane.rotation.x = - Math.PI * 0.5
plane.position.y = - 0.65

scene.add(sphere, cube, torus, plane)

注意:

  • 灯光很棒,如果使用得当,可以很逼真。问题是,灯光在性能方面会消耗很多资源。GPU 必须进行许多计算,例如面部与灯光之间的距离、面部朝向灯光的程度、面部是否在聚光灯锥体内等。尝试添加尽可能少的灯光,并尝试使用成本较低的灯光

  • 由于定位和调整灯光方向很难,所以可以使用灯光助手来确认灯光的具体位置,以此达到帮助调试的目的

(1)基础光源

==> THREE.AmbientLight <==

作用: [ 低成本 ]环境光可以为物体提供均匀的照明,使它们看起来更加自然,通常用于填充场景中的阴影区域,使其看起来更加柔和

<== 使用 ==>

说明: 在创建THREE.AmbientLight时,颜色将会应用到全局。该光源并没有特别的来源方向,并且THREE.AmbientLight不会生成阴影。通常,不能将THREE.AmbientLight作为场景中唯一的光源,因为它会将场景中的所有物体渲染为相同的颜色,而不管是什么形状,所以只设置环境光你会看到下面的现象

// ...

const ambientLight = new THREE.AmbientLight()
// 颜色
ambientLight.color = new THREE.Color(0xffffff)
// 强度
ambientLight.intensity = 0.5
scene.add(ambientLight)

// ...

image.png

光源图示:

image.png

注意:

  • 对于颜色: 用色应该尽量保守,如果指定的颜色过于明亮,就会发现画面的颜色过于饱和

  • 对于强度: 如果将该参数调小,则光源对颜色的影响会很微弱。如果将该值调大,则整个场景会变得过于明亮

// ...

// 添加调试组件选择最合适的参数
gui.add(ambientLight, 'intensity')
   .min(0)
   .max(1)
   .step(0.01)
   
gui.addColor(ambientLight, 'color').onChange((e) => {
    ambientLight.color.set(e)
})

// ...

image.png

==> THREE.SpotLight <==

作用: [ 成本高 ]用于模拟现实世界中的聚光灯效果,能够为场景中的特定区域提供照明效果,使该区域内的物体产生阴影和高光等明暗变化,从而增强场景的真实感和立体感,通常用于模拟舞台灯光、手电筒等定向光源的效果,为场景中的特定区域提供重点照明,使场景更加生动和有趣

<== 使用 ==>

说明: 这种光源会从特定的一点以锥形发射光线,所以它存在方向的,因此照射在物体上面也会产生阴影的

// ...

const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.5, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)

// ...

image.png

光源图示:

image.png

注意: 对于参数,需要注意第三个和第四个,也就是距离和角度;angle属性定义了光锥的角度,而distance属性则可以用来设置光锥的长度,所以发出光线的区域由这两个值决定

image.png

<== 对于target属性 ==>

说明: 这个很好理解,它跟camera.lookAt类似,就是聚光灯看向哪一个地方,默认都是看场景中心(0,0,0)的,如果要改为除默认值之外的其他位置,该位置必须被添加到场景中去

// ...

const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)

// ...

image.png

// ...

const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)

spotLight.target.position.x = - 0.75
scene.add(spotLight.target)

// ...

image.png

<== 灯光助手 ==>
// ...

const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)

// ...

image.png

==> THREE.PointLight <==

作用: [ 消耗适中 ]用于模拟现实世界中的点状光源,为场景中的物体提供照明效果,使物体产生阴影和高光等明暗变化,从而增强场景的真实感

<== 使用 ==>

说明: 这是一种单点发光、照射所有方向的光源,它同样存在方向和产生阴影

// ...

const pointLight = new THREE.PointLight(0xff9000, 0.5, 10, 2)
pointLight.position.set(1, - 0.5, 1)
scene.add(pointLight)

// ...

image.png

光源图示:

image.png

<== 灯光助手 ==>
// ...
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
scene.add(pointLightHelper)

// ...

image.png

==> THREE.DirectionalLight <==

作用: [ 消耗适中 ]是一种模拟来自特定方向的平行光的灯光类型。它通常用于模拟太阳光或其他远距离光源的效果;默认情况下光线来自正上方

<== 使用 ==>

说明: 这种类型的光可以看作是距离很远的光。它发出的所有光线都是相互平行的,平行光不像聚光灯那样离目标越远越暗淡。被平行光照亮的整个区域接收到的光强是一样的

// ...

const directionalLight = new THREE.DirectionalLight(0x00fffc, 0.3)
directionalLight.position.set(1, 0.25, 0)
scene.add(directionalLight)

// ...

image.png

光源图示:

image.png

<== 灯光助手 ==>
// ...
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.2)
scene.add(directionalLightHelper)

// ...

image.png

(2)特殊光源

==> THREE.HemisphereLight <==

作用: [ 低成本 ]是一种模拟天空光照效果的灯光类型。它由一个半球形的漫反射分量和一个顶部的聚光分量组成,它可以模拟从天空照射下来的光线,为场景提供柔和的全局照明;默认情况下可以简单理解为物体的上面是一种颜色,物体的下面也是一种颜色,中间部分就是过渡色了

<== 使用 ==>

说明: 这种光源可以创建出更加贴近自然的户外光照效果

// ...

const hemisphereLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6)
scene.add(hemisphereLight)

// ...

image.png

<== 灯光助手 ==>
// ...

const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)

// ...

image.png

==> THREE.RectAreaLight <==

作用: [ 成本高 ]矩形区域光源具有较大的照射范围,并且可以通过其宽度和高度属性来控制照射范围的大小。此外,矩形区域光源还可以通过其颜色和强度属性来控制光照的颜色和亮度,通常用于模拟大面积的光照效果,如室内场景中的天花板灯、路灯等。通过合理地设置矩形区域光源的属性和位置,可以使场景中的物体获得更加自然和真实的照明效果

<== 使用 ==>
// ...

const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1)
rectAreaLight.position.set(- 1.5, 0, 1.5)
scene.add(rectAreaLight)

// ...

image.png

<== 灯光助手 ==>
// ...

// 就这个助手需要导入一下
import {
    RectAreaLightHelper
} from 'three/examples/jsm/helpers/RectAreaLightHelper.js'

const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)

// ...

image.png

(3)阴影

说明: 阴影是一种视觉效果,它模拟了现实世界中光源照射物体时产生的暗区,不过对于实时的3D渲染,它一直是一个挑战,因为会存在性能的问题

  • 照相机每进行一次渲染的时候,threejs将为每个支持阴影的灯光进行一次渲染,这些光的渲染会模拟光所看到的内容,在渲染的时候,材质会替换成MeshDepthMaterial这种材质,然后将渲染的结果储存在纹理中,这种纹理称为阴影贴图,这种贴图只是光可以看到的纹理,最后将这些阴影贴图用于每种应该接收阴影并投影到的材质上

准备工作:

// ...

// 创建一个环境光源,提供均匀的弱光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5) 
scene.add(ambientLight)

// 创建一个方向光,设置其颜色和强度
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(2, 2, -1)
scene.add(directionalLight)


// 创建一个MeshStandardMaterial对象,用于定义物体表面的材质
const material = new THREE.MeshStandardMaterial()

// 创建一个球体Mesh对象
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    material
)


// 创建一个平面Mesh对象
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    material
)

// 设置平面的旋转角度和位置
plane.rotation.x = - Math.PI * 0.5
plane.position.y = - 0.5

scene.add(sphere, plane)

// ...

image.png

==> 基础使用 <==

举例:

// ...

// 允许光线投射阴影
directionalLight.castShadow = true

// 让球体可以投射阴影
sphere.castShadow = true

// 让平面可以接收阴影
plane.receiveShadow = true

// 告诉渲染器处理阴影贴图
renderer.shadowMap.enabled = true

// ...

注意: 对于产生阴影的光源只有三种,分别是PointLight、SpotLight和DirectionalLight,这里以DirectionalLight光源为例来说明

image.png

<== 分辨率优化 ==>

说明: 可以看见上面的阴影四周存在明显的锯齿状的东西,这个可以通过shadow.mapSize来进行修复,这个属性决定了阴影贴图的分辨率,贴图的分辨率越高,阴影的细节就越丰富,但同时也会增加渲染的计算量

// ...

directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024

// ...

image.png

<== 远截面和近截面的优化 ==>

说明: Three.js 使用相机进行阴影贴图渲染。这些相机具有与我们已使用的相机相同的属性。这意味着我们必须定义 anear和 a far。它不会真正改善阴影的质量,但它可能会修复您看不到阴影或阴影突然被裁剪的错误。为了帮助我们调试相机和预览near和far,CameraHelper和位于属性中的用于阴影贴图的相机directionalLight.shadow.camera

// ...

directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6

const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)

// ...

image.png

  • 如果far或者near的值设置不正确,可能得到的阴影会被裁剪或者得不到阴影,此时就需要重新去调试得到自己满意的值;如果对调试的值满意的话,可以将CameraHelper的visible的值设置为true将其隐藏起来
<== 相机振幅优化 ==>

说明: 通过刚才添加的相机助手,可以看见相机的振幅也就是范围太大了,一般来说,范围越大,开销越大,得到的阴影也就会越模糊,所以需要根据自己的情况而定,对于更改振幅,直接去设置相机的参数就好了

注意: 相机的参数需要根据相机来确定,不要一味的使用top、right、bottom、left这些值

// ...

directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = - 2
directionalLight.shadow.camera.left = - 2

// ...

image.png

image.png

<== 阴影模糊 ==>

说明: 可以使用radius属性来模糊阴影,它的优点是比较廉价,缺点是没有利用相机与物体之间的距离,阴影的每个地方模糊程度是一样的

// ...

directionalLight.shadow.radius = 10

// ...

image.png

<== 阴影贴图算法 ==>

说明: 这种算法有如下几种,默认使用的是THREE.PCFShadowMap,如果你想更改它,可以更改type属性来进行操作,需要注意的是radius属性不适用于THREE.PCFSoftShadowMap

  • THREE.BasicShadowMap: 性能很好,但质量很差

  • THREE.PCFShadowMap: 性能较差但边缘更平滑

  • THREE.PCFSoftShadowMap: 性能较差,但边缘更柔和

  • THREE.VSMShadowMap: 性能较差,限制较多,可能会产生意想不到的结果

==> 烘焙阴影 <==

说明: 烘焙阴影是一种预计算阴影的技术,它通过在离线阶段预先生成阴影贴图来提高实时渲染的性能。这种技术特别适用于静态场景或者场景中只有部分物体移动的情况,可以简单理解为假阴影

使用图片:

bakedShadow.jpg

准备工作:

// ...

// directionalLight.castShadow = false
// spotLight.castShadow = false
// pointLight.castShadow = false
// renderer.shadowMap.enabled = false

// ...

举例:

// ...

const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')

// ...

const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    new THREE.MeshBasicMaterial({
        map: bakedShadow
    })
)

// ...

image.png

<== 替代方案 ==>

说明: 上面的阴影是不会根据物体的运动而变化的,这里提供一个不太现实但更具动态的解决方案是在球体下方和平面略上方使用更简单的阴影,看下面的例子就明白了

使用图片:

simpleShadow.jpg

举例:

// ...

// 加载所需纹理图片
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')

// ...

const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    material
)

// 创建的一个阴影平面
const sphereShadow = new THREE.Mesh(
    new THREE.PlaneGeometry(1.5, 1.5),
    new THREE.MeshBasicMaterial({
        color: 0x000000,
        // 使用透明度这个值就需要设置为true
        transparent: true,
        // 由于纹理是黑白色的,想要白色的部分显示,
        // 黑色的部分隐藏就需要使用alphaMap
        alphaMap: simpleShadow
    })
)
sphereShadow.rotation.x = - Math.PI * 0.5
// 两个平面不能够重叠,否则GPU就不能够确定哪一个平面在上面,
// 就会出现闪烁的现象
sphereShadow.position.y = plane.position.y + 0.01

scene.add(sphere, sphereShadow, plane)

// ...

const clock = new THREE.Clock()

const tick = () => {
    const elapsedTime = clock.getElapsedTime()

    sphere.position.x = Math.cos(elapsedTime) * 1.5
    sphere.position.z = Math.sin(elapsedTime) * 1.5
    sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))

    sphereShadow.position.x = sphere.position.x
    sphereShadow.position.z = sphere.position.z
    sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3

    // ...
}

tick()

动态阴影.gif

四、材质

(1)材质的共有属性

说明: 共有属性存放在THREE.Material这个类上面,它列出来所有的共有属性,这些共有属性分为三类,分别是基础属性、融合属性以及高级属性

  • 基础属性: 这些属性是最常用的。通过这些属性,可以控制物体的不透明度、是否可见以及如何被引用(通过ID或是自定义名称)。

  • 融合属性: 每个物体都有一系列的融合属性。这些属性决定了物体如何与背景融合

  • 高级属性: 有一些高级属性可以控制底层WebGL上下文对象渲染物体的方式。大多数情况下是不需要使用这些属性的

==> 基础属性 <==

属性描述
id用来识别材质,并在材质创建时赋值,第一个材质的值从0开始,每新加一个材质,这个值+1
uuid生成的唯一ID,在内部使用
name通过这个属性赋予材质名称,达到调试的目的
opacity定义物体的透明度,不过需要和transparent属性一起使用;取值范围为0-1
transparent如果值为true,Three.js会使用指定的不透明度渲染物体,如果设置为false,这个物体就不透明,只是着色更明亮一些;如果使用alpha通道的纹理,该属性应该设置为true
visible定义材质是否可见,如果为false那么在场景中就看不到该物体
side通过这个属性,可以定义几何体的那一面应用材质,默认值THREE.FrontSide,这样可以将材质应用到物体的前面,也可以将其设置为THREE.BackSide,这样可以将材质应用到物体的后面,或者也可以将其设置为THREE.DoubleSide,可以将材质应用到物体的两侧
needsUpdate对于材质的某些更改,你需要告诉THREE.js材质已经改变了,如果该属性设置为true,THREE.js会使用新的材质属性更新它的缓存
colorWrite如果该属性值为false,则具有该材质的物体不会真正渲染到场景中,实际效果是该物体本身是不可见的,但其它物体被它挡住的部分也是不可见的
premultipliedAlpha该属性控制半透明表面的颜色混合方式,默认值为false
dithering是否启用颜色抖动模式,该模式可以在一定程度上减轻颜色不均匀的问题
shadowSide这个属性与上面的side类似,但它控制的是物体哪一个面会投射阴影,默认值为null,当该属性值为null时,投射阴影的面会与side相反,比如side是前面,那么shadowSide就是后面了...

==> 融合属性 <==

属性描述
blending该属性决定物体上的材质如何与背景融合,默认的值是three.normalBlending,也就是只显示材质的上层
blendSrc这个属性定义了物体上的材质如何与背景融合,默认值是three.SrcAlhaFactor,也就是使用alpha通道进行融合;前提条件是将材质的blending设置为CustomBlending才能生效
blendSrcAlpha该属性为blendSrc指定透明度,默认值为null
blendDst这个属性定义了融合时如何使用背景,默认值为three.OneMinusSrcAlphaFactor,其含义是目标也使用源的alpha通道进行融合,只是使用的值是1;前提条件是将材质的blending设置为CustomBlending才能生效
blendDstAlpha为blendDst指定透明度,默认值为null
blendEquation定义了如何使用blendSrc和blendDst的值,默认值会使它们相加,使用这三个属性,可以创建自定义的混合模式;前提条件是将材质的blending设置为CustomBlending才能生效

==> 高级属性 <==

属性描述
depthTest这是一个高级的WebGL属性,使用这个属性可以打开或者关闭GL_DEPTH_TEST参数,此参数控制是否使用像素深度来计算新像素的值
depthWrite这是另外一个内部属性,这个属性可以用来决定这个材质是否影响WebGL的深度缓存,如果你将一个物体应用二维贴图,应该将这个属性设置为false
depthFunc指定所使用的深度测试算法
polygonOffset、polygonOffsetFactor和polygonOffsetUnits通过这几个属性可以控制WebGL的POLYGON_OFFSET_FILL特性
alphaTest如果某个像素的alpha值小于该值,那么该像素不会显示出来,可以用这个属性移除一些与透明度相关的毛边
precision设置当前材质计算的精度,可使用WebGL的参数值有highp, mediump或lowp。默认值为null。

(2)基础的网格材质

注意: 对于材质属性的设置,有两种方法,一种是在构造函数中通过参数对象的方式传入参数,另一种是创建一个实例,并分别设置属性;比如下面这样的

// 通过构造函数
let material = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    Wireframe: true
    ...
})

// 通过实例
let material = new THREE.MeshBasicMaterial()
material.opacity = 0.5
material.wireframe = true
...

==> THREE.MeshBasicMaterial <==

说明: 这是一种非常简单的材质,这种材质不考虑场景中光照的影响。使用这种材质的网格会被渲染成简单的平面多边形,而且也可以显示几何体的线框,一般来说下面这几个属性比较常用

属性描述
color设置材质的颜色
map在几何的表面应用纹理
alphaMap在transparent属性为true的时候,可以使用这个属性通过纹理来控制透明度
wireframe将材质渲染为线框,非常适用于测试
wireframeLinewidth如果已经打开了wireframe,这个属性定义线框中线的宽度
wireframeLinecap这个属性定义了线框模式下顶点间线段的端点如何显示,可选值为round, bevel和miter。默认值为round;在实际的使用过程中,这个属性的修改结果很难看出来
wireframeLinejoin这个属性定义了线段的连接点如何显示,可选值为butt,round和square。默认为round;如果在一个使用低透明度和wireframeLinewidth值很大的例子中靠近观察,就可以看到这个属性的效果

简单使用:

// ...

const geometry = new THREE.BoxGeometry(1,1,1)
const material = new THREE.MeshBasicMaterial({
    wireframe: true,
    // 紫色
    color: 0x7777ff
})

const mesh = new THREE.Mesh(geometry,material)
scene.add(mesh)

// ...

image.png

==> THREE.MeshDepthMaterial <==

说明: 使用这种材质的物体,其外观不是由光照或某个材质属性决定的,而是由物体到摄像机的距离决定的,最近会显示白色,最远会显示黑色;这种材质只有两个控制线框显示的属性,一个是wireframe和wireframeLinewidth

注意:

  • 由于相机的near和far属性决定了场景的可视区域,那么如果这个区域很大,这种材质的物体远离摄像机只会消失一点点,如果这个距离非常小,那么物体消失的效果会非常明显

  • 这种材质不能设置颜色

简单使用:

// ...

const geometry = new THREE.BoxGeometry(1,1,1)
const material = new THREE.MeshDepthMaterial({})
const mesh = new THREE.Mesh(geometry,material)
scene.add(mesh)

// ...

image.png

==> 联合材质 <==

说明: 简单来说就是将几个材质合成为一个材质使用

// ...

const geometry = new THREE.BoxGeometry(1,1,1)
const cubeMaterial = new THREE.MeshDepthMaterial()
const colorMaterial = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    // 将transparent属性设置为true,Three.js就会检查blending属性,
    // 以查看这个绿色的THREE.MeshBasicMaterial材质如何与背景相互作用。
    // 这里所说的背景是用THREE.MeshDepthMaterial材质渲染的方块,
    // 如果不设置,就只能得到一个纯绿色的物体
    transparent: true,
    blending: THREE.MultiplyBlending
})

// 这个可以理解为一个材质,但是这个材质有两个子材质,一个是深度材质,
// 一个是颜色材质,只不过融合在一起来;当调用这个方法后,几何体会被复制,
// 返回一个网格组,里面的两个网格完全相同
const cube = SceneUtils.createMultiMaterialObject(geometry, [cubeMaterial, colorMaterial])
scene.add(cube)

// ...

image.png

image.png

注意: 当渲染的物体有一个在别的物体上,并且有一个物体是透明的,渲染的时候就会出现闪烁的现象,此时可以稍微把一个物体缩放一点点就可以

==> THREE.MeshNormalMaterial <==

说明: 这种材质简单来说就是物体的每一面的颜色是确定的,就算你把另外一面旋转到这里,被旋转的这一面会逐渐变成这里之前的颜色;这是因为每一面的颜色是由从该面向外指的法向量计算得到的,所谓法向量是指与面垂直的向量

// ...

const scene = new THREE.Scene()

const geometry = new THREE.BoxGeometry(1,1,1)
const material = new THREE.MeshNormalMaterial()
const mesh = new THREE.Mesh(geometry, material)

scene.add(mesh)

// ...

image.png

image.png

==> 将多种材质应用到一个几何体上 <==

说明: 以BoxGeometry为例,在控制台打印你会看见其实几何体的每一个面都具有一个materialIndex属性。该属性指定了该面将使用哪一个具体的材质,然后可以将需要应用的材质当在一个数组中然后应用到物体上面就可以了

image.png

举例: 制作一个3 * 3的魔方

// ...

const scene = new THREE.Scene()

const group = new THREE.Mesh

// 魔方每一个面的颜色
const materials = [
    new THREE.MeshBasicMaterial({
        color: 0x009e60
    }),

    new THREE.MeshBasicMaterial({
        color: 0x0051ba
    }),

    new THREE.MeshBasicMaterial({
        color: 0xffd500
    }),

    new THREE.MeshBasicMaterial({
        color: 0xff5800
    }),

    new THREE.MeshBasicMaterial({
        color: 0xc41e3a
    }),

    new THREE.MeshBasicMaterial({
        color: 0xffffff
    }),
]

// 创建3 * 3 * 3的方块
for (let x = 0; x < 3; x++) {
    for (let y = 0; y < 3; y++) {
        for (let z = 0; z < 3; z++) {
            const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9)
            const cube = new THREE.Mesh(geometry, materials)
            // 这里设置了位移比上面方块的大小大0.05,
            // 多出来的部分就是魔方中间的间隙了
            cube.position.set(x - 0.95, y - 0.95, z - 0.95)
            group.add(cube)
        }
    }
}

scene.add(group)

// ...

image.png

(3)对光源产生反应的材质

准备工作: 这里给出需要的灯光以及物体

// ...

const scene = new THREE.Scene()

// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 点光源
const light = new THREE.PointLight(0xffffff, 0.5);
light.position.x = 2;
light.position.y = 3;
light.position.z = 4;
scene.add(light);

// 等下只修改这里的材质就可以了
const material = new THREE.MeshLambertMaterial({})

// 创建一个球体,使用给定的材质
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 64, 64),
    material
)
sphere.position.x = - 1.5

// 创建一个平面,使用给定的材质
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1, 100, 100),
    material
)

// 创建一个环面,使用给定的材质
const torus = new THREE.Mesh(
    new THREE.TorusGeometry(0.3, 0.2, 64, 128),
    material
)
torus.position.x = 1.5

// 将球体、平面和环面添加到场景中
scene.add(sphere, plane, torus)

// ...

image.png

==> THREE.MeshLambertMaterial <==

注意: 由于这种材质的表面存在一些奇怪的线条并且物体表面接收到的光线比较平均,所以不会出现明显的高光部分,因此比较适合制作暗淡并不光亮的表面(如上图)

==> THREE.MeshPhongMaterial <==

说明: 这种材质与上面哪一种材质很相似,其明显区别就是高光部分(如下图),缺点是性能上不如上一种材质

// ...

const material = new THREE.MeshPhongMaterial({})

// ...

image.png

image.png

==> THREE.MeshToonMaterial <==

说明: 这种材质具有卡通风格,默认情况下,您只能获得两部分着色,一部分用于阴影,一部分用于光

// ...

const material = new THREE.MeshToonMaterial({})

// ...

image.png

==> THREE.MeshStandardMaterial <==

说明: 这种材质使用更加正确的物理计算来决定物体表面如何与场景中的光源互动。这种材质不但能够更好地表现塑料质感和金属质感的表面,还提供metalness和roughness属性来给你控制

  • metalness: 控制物体表面金属感程度;非金属材质,如木材或石材,使用0.0,金属使用1.0,通常没有中间值。 默认值为0.0。0.0到1.0之间的值可用于生锈金属的外观。如果还提供了metalnessMap,则两个值相乘

  • roughness: 控制物体表面的粗糙程度;在视觉上,它决定表面对入射光的漫反射程度,默认值为1.0,表示产生完全漫反射的效果,如果为0,那就产生镜面反射的效果

// ...

const material = new THREE.MeshStandardMaterial({})

// ...

image.png

(4)线段几何体材质

说明: 这个就是用在线段几何体上面的,对于线段来说,它由顶点组成,不包含任何面

==> THREE.LineBasicMaterial <==

说明: 这种材质很简单,在文档中也没有几个属性,直接看例子就能理解了

// 这是一种填充二维空间的简单算法,可以理解为创造一天曲线
function gosper(a, b) {

    var turtle = [0, 0, 0];
    var points = [];
    var count = 0;

    rg(a, b, turtle);


    return points;

    function rt(x) {
        turtle[2] += x;
    }

    function lt(x) {
        turtle[2] -= x;
    }

    function fd(dist) {
        //                ctx.beginPath();
        points.push({
            x: turtle[0],
            y: turtle[1],
            z: Math.sin(count) * 5
        });
        //                ctx.moveTo(turtle[0], turtle[1]);

        var dir = turtle[2] * (Math.PI / 180);
        turtle[0] += Math.cos(dir) * dist;
        turtle[1] += Math.sin(dir) * dist;

        points.push({
            x: turtle[0],
            y: turtle[1],
            z: Math.sin(count) * 5
        });
        //                ctx.lineTo(turtle[0], turtle[1]);
        //                ctx.stroke();

    }

    function rg(st, ln, turtle) {

        st--;
        ln = ln / 2.6457;
        if (st > 0) {
            //                    ctx.strokeStyle = '#111';
            rg(st, ln, turtle);
            rt(60);
            gl(st, ln, turtle);
            rt(120);
            gl(st, ln, turtle);
            lt(60);
            rg(st, ln, turtle);
            lt(120);
            rg(st, ln, turtle);
            rg(st, ln, turtle);
            lt(60);
            gl(st, ln, turtle);
            rt(60);
        }
        if (st == 0) {
            fd(ln);
            rt(60);
            fd(ln);
            rt(120);
            fd(ln);
            lt(60);
            fd(ln);
            lt(120);
            fd(ln);
            fd(ln);
            lt(60);
            fd(ln);
            rt(60)
        }
    }

    function gl(st, ln, turtle) {
        st--;
        ln = ln / 2.6457;
        if (st > 0) {
            //                    ctx.strokeStyle = '#555';
            lt(60);
            rg(st, ln, turtle);
            rt(60);
            gl(st, ln, turtle);
            gl(st, ln, turtle);
            rt(120);
            gl(st, ln, turtle);
            rt(60);
            rg(st, ln, turtle);
            lt(120);
            rg(st, ln, turtle);
            lt(60);
            gl(st, ln, turtle);
        }
        if (st == 0) {
            lt(60);
            fd(ln);
            rt(60);
            fd(ln);
            fd(ln);
            rt(120);
            fd(ln);
            rt(60);
            fd(ln);
            lt(120);
            fd(ln);
            lt(60);
            fd(ln);
        }
    }
}
// ...

const scene = new THREE.Scene()
// 生成曲线
var points = gosper(4, 60);

var lines = new THREE.BufferGeometry();
const position = []

// 然后循环曲线的点,将其装换成坐标并用数组存储起来
points.forEach(function (e) {
    position.push(new THREE.Vector3(e.x, e.z, e.y));
});
// 把这些点设置到创建的几何体上面
lines.setFromPoints(position)

var material = new THREE.LineBasicMaterial();

var line = new THREE.Line(lines, material);

scene.add(line)

// ...

image.png

==> THREE.LineDashedMaterial <==

说明: 这种材质可以在让实线有断断续续的感觉,直接看例子就明白了

// ...
var material = new THREE.LineDashedMaterial({
    color: 0xffffff,
	scale: 0.1,
	dashSize: 1,
	gapSize: 1,
});

var line = new THREE.Line(lines, material);
// 这里必须调用这个方法,如果不这么做,间隔就不会正确地显示
line.computeLineDistances()

// ...

image.png

五、基础几何体

(1)二维几何体

说明: 也就是只有两个维度,并且看上去是扁平的

==> THREE.PlaneGeometry <==

说明: 可以用来创建一个非常简单的二维矩形

参数:

  • width(必): 指定矩形的宽度
  • height(必): 指定矩形的高度
  • widthSegments: 矩形的宽度应该分成几段,默认值为1
  • heightSegments: 矩形的高度应该分成几段,默认值为1
// ...

const scene = new THREE.Scene()

const geometry = new THREE.PlaneGeometry(1, 1)
const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wirefarme: true
})

const mesh = new THREE.Mesh(geometry, material)

scene.add(mesh)

// ...

image.png

注意: 在几何体创建后,可以使用parameters属性来访问几何体的属性

// ...

const geometry = new THREE.PlaneGeometry(1, 1)
console.log(geometry.parameters);

// ...

image.png

==> THREE.CircleGeometry <==

说明: 可以用来创建一个二维的圆或者部分圆

参数:

  • radius: 决定了圆的大小,半径是指圆心到圆弧的距离,默认值为1

  • segments: 将圆分为几个部分,最小值为3,默认值32

  • thetaStart: 定义了从哪里开始画圆,范围是0 - 2 * Math.PI,默认值是0

  • thetaLength: 决定圆的形状,默认值是 2 * Pi,表示画一整个圆,如果是0.5 * Pi,就是画0.25个圆

// ...

const geometry = new THREE.CircleGeometry(1)

// ...

image.png

==> THREE.RingGeometry <==

说明: 可以生成一个二维的圆环

参数:

  • innerRadius: 圆环的内半径,它定义了中心圆环的尺寸,如果该属性为0,则表示不显示圆环,默认值是0.5

  • outerRadius: 圆环的外半径,它定义了圆环的尺寸,这个半径是指从圆心到圆弧的距离,默认值为1

  • thetaSegments: 圆环会被分成几段,这个值越大,圆环越光滑,最小值为3,默认值为32

  • phiSegments: 圆环的半径被分成几段,它不会影响圆环的光滑程度,但是会影响面的数量,最小值和默认值都为1

  • thetaStart: 从哪里开始画,范围为0 - 2 * Pi,默认值为0

  • thetaLength: 用于定义圆的形状,默认值是 Math.PI * 2,表示一整个圆

// ...

const geometry = new THREE.RingGeometry()

// ...

image.png

==> THREE.ShapeGeometry <==

说明: 可以自定义二维图形

参数:

  • shapes: 用来创建THREE.ShapeGeometry的一个或者多个THREE.Shape对象,可以传入单个的THREE.Shape对象,或者是多个的THREE.Shape对象组成的数组

  • curveSegments: 将曲线分为几段,分的越多,曲线越光滑,默认值是12

// ...
const scene = new THREE.Scene()

const x = 0,
      y = 0;

const heartShape = new THREE.Shape();

heartShape.moveTo(x + 5, y + 5);
heartShape.bezierCurveTo(x + 5, y + 5, x + 4, y, x, y);
heartShape.bezierCurveTo(x - 6, y, x - 6, y + 7, x - 6, y + 7);
heartShape.bezierCurveTo(x - 6, y + 11, x - 3, y + 15.4, x + 5, y + 19);
heartShape.bezierCurveTo(x + 12, y + 15.4, x + 16, y + 11, x + 16, y + 7);
heartShape.bezierCurveTo(x + 16, y + 7, x + 16, y, x + 10, y);
heartShape.bezierCurveTo(x + 7, y, x + 5, y + 5, x + 5, y + 5);

const geometry = new THREE.ShapeGeometry(heartShape);
const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// ...

image.png

注意: 对于路径的绘制函数可以参考一个cavansthreejs文档

(2)三维几何体

==> THREE.BoxGeometry <==

说明: 这是一种非常简单的三维几何体,只需要指定宽度、高度和深度就可以创建一个长方体

参数:

  • width(必): 长方体的宽度,也就是长方体沿着x轴的长度;其默认值为1

  • height(必): 长方体的高度,也就是长方体沿着y轴的长度;其默认值为1

  • depth(必): 长方体的深度,也就是长方体沿着z轴的长度;其默认值为1

  • widthSegments: 沿x轴方向上的面分成几份,默认值为1

  • heightSegments: 沿y轴方向上的面分成几份,默认值为1

  • depthSegments: 沿z轴方向上的面分成几份,默认值为1

// ...

const scene = new THREE.Scene()

const geometry = new THREE.BoxGeometry(1,1,1);
const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// ...

image.png

==> THREE.SphereGeometry <==

说明: 创建一个三维球体,它可以用来创建所有跟球体相关的几何体

参数:

  • radius: 球体的半径,默认值是1

  • widthSegments: 水平方向上分几段,分的越多越光滑,默认值是32,最小值是3

  • heightSegments: 垂直方向上分几段,分的越多越光滑,默认值是16,最小值是2

  • phiStart: x轴从什么地方开始绘制球体,范围是 0 - 2 * Pi,默认值是0

  • phiLength: 决定x轴上画的形状,默认值是 2 * Pi,表示在x轴上是整个球

  • thetaStart: y轴从什么地方开始绘制球体,范围是 0 - 2 * Pi,默认值是0

  • thetaLength: 决定y轴上画的形状,默认值是 2 * Pi,表示在y轴上是整个球

// ...

const geometry = new THREE.SphereGeometry();

// ...

image.png

==> THREE.CylinderGeometry <==

说明: 这个可以创建圆柱或者类似圆柱的物体

参数:

  • radiusTop: 圆柱顶部的半径,默认值是1

  • radiusBottom: 圆柱底部的半径,默认值是1

  • height: 圆柱的高度,默认值是1

  • radialSegments: 圆柱侧面分成几部分,分的越多,圆柱的顶部和底部越像圆,默认值是32

  • heightSegments: 原著党额高度分成几部分,这个只会影响圆柱侧面面的数量,默认值是1

  • openEnded: 一个布尔值,表示圆柱的顶部和底部不是封闭的,默认值是false,表示是封闭的

  • thetaStart: 圆柱在x轴上从那里开始绘制,取值范围为 0 -2 * PI,默认值是0

  • thetaLength: 决定圆柱的形状,默认值是 2 * PI,表示一个完整的圆柱

// ...

const geometry = new THREE.CylinderGeometry();

// ...

image.png

==> THREE.ConeGeometry <==

说明: 可以创建圆锥几何体,它拥有圆柱的大部分属性,只是没有顶半径而已,其它的属性都一致

// ...

const geometry = new THREE.ConeGeometry();

// ...

image.png

==> THREE.TorusGeometry <==

说明: 可以创建一个圆环。类似甜甜圈一样的图形

参数:

  • radius: 整个圆环的半径,默认值是1

  • tube: 外面圆环的半径,默认值是0.4

  • radialSegments: 可以理解为圆环在厚度上面分几段,默认值是12

  • tubularSegments: 可以理解为圆环平面上分几段,默认值是48

  • arc: 定义圆环的形状,比如默认值2 * PI就表示一整个圆环,然后以此类推

// ...

const geometry = new THREE.TorusGeometry();

// ...

image.png

==> THREE.TorusKnotGeometry <==

说明: 可以创建一个环状扭结。环状扭结是一种比较特别的结,看起来就像一根管子绕自己转了几圈

参数:

  • radius: 整个形状的半径,默认值是1

  • tube: 环状部分的半径,默认值是0.4

  • tubularSegments: 环状部分表面分成几部分,默认值是64

  • radialSegments: 环状部分的厚度分为几个部分,默认值是8

  • p: 如果一个圆环平放在水平面上,那么p的值表示在y轴的方向上扭曲多少次,默认值是2

  • q: 如果一个圆环平放在水平面上,那么q的值表示在y轴的方向上扭曲多少次,默认值是3

// ...

const geometry = new THREE.TorusKnotBufferGeometry();

// ...

image.png

==> THREE.PolyhedronGeometry <==

说明: 可以很容易地创建多面体。多面体是只有平面和直边的几何体;在使用的时候需要指定各个顶点以及面

参数:

  • vertices(必): 设置构成多面体的顶点

  • indices(必): 设置构成多面体的面

  • radius: 设置多面体的大小

  • detail: 给多面体添加额外的细节,如果是1,那么多面体上的每个三角形都会分成4个小三角形,如果是2,那么前面分成的四个小三角形会继续分成四个小三角形;以此类推

// ...

const verticesOfCube = [
    -1,-1,-1,    1,-1,-1,    1, 1,-1,    -1, 1,-1,
    -1,-1, 1,    1,-1, 1,    1, 1, 1,    -1, 1, 1,
];

const indicesOfFaces = [
    2,1,0,    0,3,2,
    0,4,7,    7,3,0,
    0,1,5,    5,4,0,
    1,2,6,    6,5,1,
    2,3,7,    7,6,2,
    4,5,6,    6,7,4
];

const geometry = new THREE.PolyhedronGeometry( verticesOfCube, indicesOfFaces, 6, 2 );

// ...

image.png

注意: 一般来说,不会使用这个几何体,threejs提供了几种特殊的多面体,比如THREE.TetrahedronGeometry四面体、THREE.OctahedronGeometry八面体、THREE.DodecahedronGeometry十二面体等

六、高级几何体

(1)THREE.ConvexGeometry

说明: 可以通过传入的一组点生成凸包,凸包就是使用这些点所能够形成的最小凸形多边形或者凸形多面体

// ...

import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js';

const scene = new THREE.Scene()

let points = [];

for (let i = 0; i < 20; i++) {
    let randomX = -15 + Math.round(Math.random() * 30);
    let randomY = -15 + Math.round(Math.random() * 30);
    let randomZ = -15 + Math.round(Math.random() * 30);

    points.push(new THREE.Vector3(randomX, randomY, randomZ));
}

const convexGeometry = new ConvexGeometry(points);

const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true
});

const mesh = new THREE.Mesh(convexGeometry, material);

scene.add(mesh);

// ...

image.png

(2)THREE.LatheGeometry

说明: 允许你从一条光滑曲线创建图形。此曲线是由多个点(也称为节点)定义的,通常称作样条曲线。这条样条曲线绕物体的中心z轴旋转,得到类似花瓶或铃铛的图形

参数:

  • points(必): 指定构造样条曲线的点,然后基于这条曲线生成图形

  • segments: 创建图形时的分段数目,值越高,得到的物体越光滑,其默认值是12

  • phiStart: 从圆的那个地方开始创建图形,取值范围是0 - 2 * PI,默认值是0

  • phiLength: 指定创建的图形有多完整,默认值是 2 * PI,表示旋转一周得到一个完整的图形

// ...

const scene = new THREE.Scene()

const points = [];
const height = 5;
const count = 30;
for (let i = 0; i < count; i++) {
    points.push(
        new THREE.Vector2(
            (Math.sin(i * 0.2) + Math.cos(i * 0.3)) * height + 12,
            (i - count) + count / 2
        )
    );
}

const convexGeometry = new THREE.LatheGeometry(points);

const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true
});
const mesh = new THREE.Mesh(convexGeometry, material);
scene.add(mesh);

// ...

image.png

(3)通过拉伸创建几何体

说明: 拉伸就是沿着z轴拉伸二维物体,给它一个厚度,让它有三维图形的感觉

==> THREE.ExtrudeGeometry <==

说明: 给定一条形状路径,使其拉伸为一个三维图形

参数:

  • shape(必): 需要提供一个shape对象或者包含shape的数组来进行拉伸

  • options: 配置对象

    • [options] curveSegments: 指定拉伸图形时曲线分成多少段,分的越多,曲线越光滑
    • [options] steps: 指定拉伸图形在厚度上分成多少段,值越大,分的面越多;其默认值为1
    • [options] depth: 几何体的厚度需要拉伸多少,值越大,物体越厚;其默认值为1
    • [options] bevelEnabled: 如果为true,就会存在斜角;默认值是false
    • [options] bevelThickness: 指定斜角的深度;斜角是前后面与拉伸体之间的倒角,该值定义了斜角进入图形的深度,默认值是0.2
    • [options] bevelSize: 指定斜角的高度,这个高度会加到图形的正常高度上;默认值为bevelThickness-0.1
    • [options] bevelOffset: 斜面开始处与形状轮廓之间的距离;默认值是0
    • [options] bevelSegments: 定义斜角的分段数,分的越多,斜角越光滑;默认值是3
    • [options] extrudePath: 指定图形沿着什么路径拉伸(THREE. CurvePath),如果没有指定则会沿着z轴拉伸
    • [options] UVGenerator: 当给材质使用纹理的时候,UV映射确定纹理的那一部分用于特定的面,使用这个属性,可以传入自己的对象,该对象将为传入的图形创建的面创建UV设置
// ...
const scene = new THREE.Scene()

const length = 12,
      width = 8;

const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(0, width);
shape.lineTo(length, width);
shape.lineTo(length, 0);
shape.lineTo(0, 0);

const extrudeSettings = {
    steps: 2,
    depth: 16,
    bevelEnabled: true,
    bevelThickness: 1,
    bevelSize: 1,
    bevelOffset: 0,
    bevelSegments: 1
};

const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// ...

image.png

==> THREE.TubeGeometry <==

说明: 可以将一条三维的样条曲线拉伸为一条管状结构

参数:

  • path: 该属性用一个THREE.Curve对象来指定管应当遵循的路径

  • tubularSegments: 整个管道的分段数,分的越多,管道弯曲的地方就会越光滑;其默认值是64

  • radius: 指定管的半径,默认值是1

  • radialSegments: 管道的横截面的分段数,这个值越大,管道的表面就越光滑;其默认值是8

  • closed: 管道的两端是否是闭合状态

// ...

const scene = new THREE.Scene()

class CustomSinCurve extends THREE.Curve {

    constructor(scale = 1) {

        super();

        this.scale = scale;

    }

    getPoint(t, optionalTarget = new THREE.Vector3()) {

        const tx = t * 3 - 1.5;
        const ty = Math.sin(2 * Math.PI * t);
        const tz = 0;

        return optionalTarget.set(tx, ty, tz).multiplyScalar(this.scale);

    }

}

const path = new CustomSinCurve(10);
const geometry = new THREE.TubeGeometry(path, 20, 2, 8, false);
const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true,
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// ...

image.png

(4)THREE.ParametricGeometry

说明: 用于定义参数化几何体,也就是用一个函数去定义几何体的表面

参数:

  • (u, v, target) => {}: 用于定义几何体的表面

    • [params] u 和 v:这两个参数通常用于在定义的范围内遍历几何体的表面。它们可以是角度、比例或其他任何可以用来计算空间位置的数值
    • [params] target:这是一个THREE.Vector3对象,用于存储计算出的点的坐标。函数执行完毕后,target对象将被更新为新的点坐标
  • slices: u方向上的分段数;增加分段数可以使几何体的表面更加平滑和精细,但同时也会增加渲染所需的计算量。减少分段数则会使几何体看起来更加粗糙,但可以提高渲染性能

  • stacks: v方向上的分段数;增加分段数可以使几何体的表面更加平滑和精细,但同时也会增加渲染所需的计算量。减少分段数则会使几何体看起来更加粗糙,但可以提高渲染性能

// ...

import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';

const scene = new THREE.Scene()

// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 点光源
const light = new THREE.PointLight(0xffffff, 0.5);
light.position.x = 2;
light.position.y = 3;
light.position.z = 4;

scene.add(light);

const material = new THREE.MeshPhysicalMaterial()

const geometry = new ParametricGeometry((u, v, target) => {
    // 定义u和v的范围
    u *= Math.PI * 2;
    v = (v * Math.PI * 2) - Math.PI / 2;

    // 设置点的x, y, z坐标
    target.x = Math.cos(u) * Math.cos(v);
    target.y = Math.sin(u) * Math.cos(v);
    target.z = Math.sin(v);

    return target;
}, 10, 10);

// 创建一个球体,使用给定的材质
const sphere = new THREE.Mesh(
    geometry,
    material
)

scene.add(sphere)

// ...

image.png

image.png

(5)THREE.TextGeometry

说明: 这个可以用来创建3D文本,使用起来很简单,看下面的例子就明白了

参数:

  • text: 需要展示的文本

  • options: 配置对象

    • [ options ] font(必): 该属性要用哪种字体
    • [ options ] size: 字体大小;默认值100
    • [ options ] depth: 字体的厚度;默认为50
    • [ options ] curveSegments: 拉伸图形时,曲线分成多少段,默认值是12
    • [ options ] bevelEnabled: 是否开启斜角,默认为false
    • [ options ] bevelThickness: 文本上斜角的深度,默认值为20
    • [ options ] bevelSize: 斜角与原始文本轮廓之间的延伸距离。默认值为8
    • [ options ] bevelSegments: 斜角的分段数。默认值为3
// ...

// 用于加载字体
import {
    FontLoader
} from 'three/examples/jsm/loaders/FontLoader.js'
// 用于创建3D文本
import {
    TextGeometry
} from 'three/examples/jsm/geometries/TextGeometry.js'

// 创建字体加载器实例
const fontLoader = new FontLoader()
// 加载字体文件
fontLoader.load(
    // 字体的路径
    '/fonts/helvetiker_regular.typeface.json',
    // 加载完毕之后这个回调函数会执行,就可以使用参数font去访问字体了
    (font) => {
        // 这里就直接理解为使用了纹理为了好看一点(纹理后面会说)
        const material = new THREE.MeshMatcapMaterial({
            matcap: matcapTexture
        })

        // 创建文本几何体
        const textGeometry = new TextGeometry(
            'Hello Three.js', {
                font: font,
                size: 0.5,
                height: 0.2,
                curveSegments: 12,
                bevelEnabled: true,
                bevelThickness: 0.03,
                bevelSize: 0.02,
                bevelOffset: 0,
                bevelSegments: 5
            }
        )
        // 将文本几何体居中
        textGeometry.center()
        // 创建文本网格对象
        const text = new THREE.Mesh(textGeometry, material)
        // 将文本添加到场景中
        scene.add(text)
    }
)

// ...

image.png

注意: 如果要渲染二维字体,使用HTML5中的画布要更好一些,因为渲染开销少一些

七、粒子与精灵

(1)粒子与精灵

==> 了解粒子 <==

说明: 粒子通常指单个的点,用于创建粒子系统,一般用来模拟火、烟、雾、爆炸等效果;它使用THREE.Points类来创建,并且每个粒子都是一个THREE.Vector3对象

  • 跟THREE.Mesh一样,THREE.Sprite对象也是 THREE.Object3D对象的扩展。也就是说THREE.Mesh的大部分属性和函数都可以用于THREE.Sprite;比如position、scale、translate等

  • 在创建的时候几何体需要使用BufferGeometry,对于使用的材质,并没有什么要求,但是推荐使用PointsMaterial

  • 通过THREE.Points,Three.js不再需要管理大量单个的对象,而只需管理THREE.Points实例,自然它的性能也就要好一些了

THREE.PointsMaterial常用属性:

  • color: 指定粒子系统中所有的颜色,默认值是0xFFFF;如果vertexColors属性值为true并设置了颜色属性,那么几何体顶点的颜色将乘以此值作为最终的颜色

  • map: 可以在粒子上应用某种材质

  • size: 指定粒子的大小,默认值为1.0

  • sizeAttenuation: 如果该属性值为false,那么所有粒子都将拥有相同的尺寸,无论距离摄像机有多远;如果属性值为true,粒子的大小取决于其距离摄像机的远近,默认值为true

  • opacity: 该属性与transparent属性一起使用,用来设置粒子的不透明度,默认值是1

  • transparent: 如果该属性设置为true,那么粒子在渲染时会根据opacity属性的值来确定其透明度,默认值为false

  • blending: 指定渲染粒子时的融合模式

  • fog: 决定粒子是否受场景中的雾化效果影响,默认值为true

// ...

const particlesGeometry = new THREE.BufferGeometry()
// 粒子数量
const count = 50000 

// 初始化粒子的位置数组
const positions = new Float32Array(count * 3)

// 随机生成粒子的位置
for (let i = 0; i < count * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 10 // 位置范围在-5到5之间
}

// 将位置数据添加到几何体中
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))

const particlesMaterial = new THREE.PointsMaterial({
    size: 0.1, // 粒子大小
    sizeAttenuation: true, // 粒子大小随距离衰减
})

// 创建粒子点对象并添加到场景
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)

// ...

image.png

==> 了解精灵 <==

说明: 精灵是一个总是面朝着摄像机的平面,默认情况下会被渲染成白色的二维图形;它使用THREE.Sprite类来创建

  • 如果想在控制器中移动它,需要将它变成三维图形,也就是将position的值从(x,y)变成(x,y,z),如果设置为(0,0,0)还是不会生效的

  • THREE.Sprite类只接受一个材质参数,并且材质只能够使用SpriteMaterial这一种

  • 每个THREE.Sprite对象之间是相互独立的状态,所以使用大量的THREE.Sprite对象,会很快遇到性能问题,因为每个对象需要分别由Three.js进行管理

// ...

const spriteMeterial = new THREE.SpriteMaterial({
    color: 0xff00ff
})

const sprite = new THREE.Sprite(spriteMeterial)
sprite.position.set(1,1,0)
scene.add(sprite)

// ...

chrome-capture-2024-9-11.gif

(2)使用纹理样式化粒子

粒子.gif

说明: 对于上面创建的粒子,你会发现它的形状是一个正方形,但这形状并不符合我们的预期,对于这种形状的更改,最简单的方式就是使用纹理加载图像了,然后将加载好的纹理赋值给图形的材质就好,比如将粒子使用下面的黑色背景图像,其好处如下;对于这种纹理的选择,可以点击这里

1.png

  • 易于融合: 黑色背景的纹理在与不同颜色的表面结合时,可以更容易地融合和匹配,因为它不会引入额外的颜色偏差。

  • 突出主体: 在视觉上,黑色背景可以使纹理中的细节更加突出,因为它减少了背景的干扰,让观察者的注意力集中在纹理本身。

  • 节省资源: 在某些情况下,使用黑色背景可以减少纹理图像的大小,因为它包含的信息较少,这有助于节省存储空间和加载时间。

  • 便于处理: 在图像编辑和处理时,黑色背景可以简化操作,例如在进行颜色校正或应用滤镜时,黑色背景不会影响最终结果。

  • 通用性: 黑色背景的纹理具有较高的通用性,因为它们可以很容易地与其他颜色和材质结合,而不需要额外的调整。

==> 对于alphaMap和map的选择 <==

对于alphaMap:

  • 当你需要控制纹理的透明度时

  • 如果纹理图像本身包含透明度信息(例如PNG格式的图像),你可以使用alphaMap属性将这个透明度信息应用到材质上

对于map:

  • 当你需要将一张纹理图像映射到三维对象的表面时

  • map属性通常用于给模型添加颜色、细节和质感,比如将木纹纹理应用到立方体上,使其看起来像木头

  • 如果纹理图像包含颜色信息,map属性可以帮助你将这些颜色应用到模型上

说明: 这里就需要使用alphaMap了,因为要去除黑色背景带来的影响,由于使用了透明度,所以需要将transparent属性设置为true

// ...

const particlesMaterial = new THREE.PointsMaterial({
    size: 0.1, // 粒子大小
    sizeAttenuation: true, // 粒子大小随距离衰减
    color: 0xff88cc, // 粒子颜色
    transparent: true, // 材质透明
    alphaMap: particleTexture, // 使用纹理作为透明度贴图
})

// ...

image.png

image.png

==> depthWrite与深度缓冲区 <==

说明: 可以看到上面有的粒子是正常的,有的粒子还是会被其它粒子所遮挡,这是因为粒子的绘制顺序与创建顺序相同,这个就与depthWrite属性相关了,这个属性用于实现遮挡关系,即离相机近的物体遮挡住离相机远的物体,它的取值有两种,这里设置为false就好了

  • true: 当前材质渲染的像素会将其深度值写入深度缓冲区。这意味着,如果后续渲染的物体像素的深度值大于已写入的深度值,那么这些像素将被丢弃,不会显示在屏幕上,从而实现了正确的遮挡效果

  • false: 当前材质渲染的像素不会更新深度缓冲区。这通常用于创建半透明效果或者需要忽略深度测试的场景

// ...

const particlesMaterial = new THREE.PointsMaterial({
    size: 0.1, // 粒子大小
    sizeAttenuation: true, // 粒子大小随距离衰减
    color: 0xff88cc, // 粒子颜色
    transparent: true, // 材质透明
    alphaMap: particleTexture, // 使用纹理作为透明度贴图
    depthWrite: false, // 不写入深度缓冲
})

// ...

image.png

==> blending与混合模式 <==

说明: 这个就是将当前像素的颜色与已经渲染到屏幕上的颜色混合,这样就能产生不同的视觉效果;比如这里设置了材质的混合模式为加法混合,这意味着当渲染这个材质时,Three.js会使用加法来混合当前像素的颜色和已经存在于屏幕上的颜色,从而可能产生发光或者光效的效果

// ...

const particlesMaterial = new THREE.PointsMaterial({
    size: 0.1, // 粒子大小
    sizeAttenuation: true, // 粒子大小随距离衰减
    color: 0xff88cc, // 粒子颜色
    transparent: true, // 材质透明
    alphaMap: particleTexture, // 使用纹理作为透明度贴图
    depthWrite: false, // 不写入深度缓冲
    blending: THREE.AdditiveBlending, // 使用加法混合
})

// ...

blending.gif

==> vertexColors与顶点颜色 <==

说明: 为了让粒子更加好看,可以给它上一点颜色,设置的方法跟BufferGeometry设置position属性的方法一样,只不过这里设置的是color属性,设置完毕之后需要将vertexColors属性设置为true用于激活顶点颜色

// ...

const colors = new Float32Array(count * 3)

// 随机生成粒子的位置和颜色
for (let i = 0; i < count * 3; i++) {
    // ...
    
    colors[i] = Math.random() // 颜色随机
}

// 将位置和颜色数据添加到几何体中
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))

const particlesMaterial = new THREE.PointsMaterial({
    size: 0.1, // 粒子大小
    sizeAttenuation: true, // 粒子大小随距离衰减
    color: 0xff88cc, // 粒子颜色
    transparent: true, // 材质透明
    alphaMap: particleTexture, // 使用纹理作为透明度贴图
    depthWrite: false, // 不写入深度缓冲
    blending: THREE.AdditiveBlending, // 使用加法混合
    vertexColors: true // 使用顶点颜色
})

// ...

image.png

八、模型

说明: Three.js允许您创建许多原始几何图形,但是当涉及到更复杂的形状时,最好使用专用的3D软件来制作,然后导入使用,这样会更加方便;这里会以gltf这种格式的模型为例,因为这种格式正在逐渐成为标准并且它适用于大部分的场景...

Three.js编辑器: 这个是测试模型是否正常工作的好东西。不过要小心;你只能测试由一个文件组成的模型,而不是一个文件夹

(1)GLTF

解释: GLTF是一种开放标准文件格式,用于高效地传输和存储3D场景和模型数据。它被设计为易于解析,并且可以在不同的应用程序和网络环境中使用。GLTF支持多种编码方式,包括二进制和JSON文本格式,以及嵌入纹理和动画等复杂特性;在WebGL和Three.js等3D图形库中,GLTF格式被广泛用于加载和显示3D模型。它提供了一种标准化的方式来表示3D内容,使得开发者可以更容易地在不同的平台和工具之间共享和使用3D资产;如果想寻找一些别人做好的模型,可以点击此处

相关格式: 在下载完模型后,你会发现文件夹中存在以下几种格式

  • glTF: 默认格式

    • 后缀为gltf的文件是一个JSON,可以在编辑器中打开它,它包含各种信息,比如相机、灯光、场景、材质、对象变换,但是不包含集合图形和纹理
    • 后缀为bin的文件是二进制文件,无法像JSON文件那样被读取,它通常包含几何图形等数据以及与顶点相关的所有信息,如 UV 坐标、法线、顶点颜色等
    • 后缀为png的文件只包含模型的纹理
  • glTF-Binary: 这种格式仅由一个文件组成。它包含我们在glTF默认格式中讨论的所有数据。这是一个二进制文件,您无法直接在代码编辑器中打开它来查看里面的内容

    • 优点是可能更轻量,加载起来更方便
    • 缺点是只有一个文件,无法轻易更改其数据
  • glTF-Draco: 这种格式类似于glTF 默认格式,但缓冲区数据(通常是几何图形)使用Draco 算法进行压缩。如果比较bin文件大小,您会发现它要小得多。虽然此格式有一个单独的文件夹,但您可以将 Draco 压缩应用于其他格式

  • glTF-Embedded: 这种格式类似于glTF-Binary格式,因为它只有一个文件,但该文件实际上是一个可以在编辑器中打开的 JSON。这种格式的唯一好处是只有一个易于编辑的文件

image.png

准备工作: 也就是下面会用到这些几何图形和灯光

// ...

// 创建一个平面几何体作为地面
const floor = new THREE.Mesh(
    new THREE.PlaneGeometry(10, 10),
    // 设置地面的材质
    new THREE.MeshStandardMaterial({
        color: '#444444',
        metalness: 0,
        roughness: 0.5
    })
)
// 使地面接收阴影
floor.receiveShadow = true
// 旋转地面使其水平
floor.rotation.x = - Math.PI * 0.5
// 将地面添加到场景
scene.add(floor)

/**
 * 设置灯光
 */
// 创建环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
// 将环境光添加到场景
scene.add(ambientLight)

// 创建方向光并设置阴影属性
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.camera.far = 15
directionalLight.shadow.camera.left = - 7
directionalLight.shadow.camera.top = 7
directionalLight.shadow.camera.right = 7
directionalLight.shadow.camera.bottom = - 7
// 设置方向光位置
directionalLight.position.set(- 5, 5, 0)
// 将方向光添加到场景
scene.add(directionalLight)

// ...

image.png

(2)GLTFLoader

说明: 要在Three.js中加载GLTF文件,我们必须使用 GLTFLoader,默认情况下,该类在变量THREE中是不可用的,需要进行导入才可以;对于模型的加载,只需要使用load方法就可以了。

// ...

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'

const gltfLoader = new GLTFLoader()

// load参数:模型的路径、加载成功函数、正在加载函数和加载失败函数
gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) => {
        console.log(gltf);
        
        // 模型添加到场景中最简单的方法就是将模型的sence添加进去就好了
        scene.add(gltf.scene)
    }
)

// ...

image.png

image.png

(3)DRACOLoader

说明: 如果你想使用GLTFLoader去加载压缩过的GLTF的文件的话,你应该会看到下面这类似的警告,它的含义是需要为我们的GLTFLoader提供一个DRACOLoader实例,以便它可以加载压缩文件

// ...

gltfLoader.load(
    '/models/Duck/glTF-Draco/Duck.gltf',
    (gltf) => {}
)

// ...

image.png

解码器: 只提供DRACOLoader还远远不够,因为它自身没有解压缩的能力,还需要为它提供解码器,好在threejs内置了解码器,解码器存放的位置会根据版本的不同而不一样,可以根据它提供的例子去寻找,比如下面这样

image.png

// ...

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'

// 初始化DRACO加载器并设置解码路径
const dracoLoader = new DRACOLoader()
// 这个是我将解码器复制到static文件夹中再使用的
dracoLoader.setDecoderPath('/draco/')

// 初始化GLTF加载器并关联DRACO加载器
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

// ...

image.png

(4)THREE.AnimationMixer

image.png

说明: glTF模型还支持动画并且Three.js可以处理这些动画,在查看已经加载好的gltf对象中,会看到名为属性animations包含多个AnimationClip片段,这些AnimationClip片段不能轻易使用,需要使用AnimationMixer播放器对AnimationClip片段进行处理;在实例化一个播放器后,调用播放器的clipAction方法将AnimationClip片段添加进去,这个方法会返回一个AnimationAction,先调用它的play方法,然后告诉播放器在每一帧使用update方法更新自身就可以了

// ...

// 用于存储动画混合器
let mixer = null

// 加载模型并添加到场景中,同时播放动画
gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) =>
    {
        // ...

        // 创建动画混合器并播放指定动画
        mixer = new THREE.AnimationMixer(gltf.scene)
        const action = mixer.clipAction(gltf.animations[2])
        action.play()
    }
)

// 动画循环函数
const tick = () =>
{
    // 获取当前时间
    const elapsedTime = clock.getElapsedTime()
    // 计算时间差
    const deltaTime = elapsedTime - previousTime
    // 更新上一帧时间
    previousTime = elapsedTime

    // 更新模型动画
    if(mixer) {
        mixer.update(deltaTime)
    }

    // ...
}

// 开始动画循环
tick()

// ...

动画.gif