前言
我们已经学习了足够的基础知识来创建一些内容。在本节中,我们将基于自己的名字创建一个3D文字展示的小网页。
这个小网页可以综合应用我们在前面小节中学到的各种基础知识,并且它看起来很酷。
在Three.js
里使用TextGeometry
类就可以创建3D文字,但必须用一种名为typeface
的特定json格式的字体文件。
如何获得typeface格式的字体
首先,我们需要找到想使用的ttf
格式的字体文件。
有很多字体网站可以下载ttf
格式的字体文件,但在商业项目中使用字体时需要注意版权问题,最好的办法是使用可免费商用的字体。本小节中我们使用可免费商用的阿里巴巴普惠体AlibabaPuHuiTi-2-95-ExtraBold.ttf
。
下载好了字体文件后,我们打开https://gero3.github.io/facetype.js/ 将ttf
文件格式的字体转换为typeface
格式的json
文件。
中文字体本来就不小,一款阿里巴巴普惠字体有8.1MB
,转换成typeface
的json
格式后更大了,足足有29.4MB
。
字体子集化
中文字体文件量大的原因是主要是汉字数量太庞大了,常用字有7000多个。但我们如果仅仅只使用几个汉字,加载如此大的字库实属浪费,这就需要我们使用字体子集化的手段来缩减字库大小。能实现字体子集化的工具不少,这里我推荐fontmin
。
接着我们使用http://ecomfe.github.io/fontmin/ 将阿里巴巴普惠体
子集化,仅保留大帅老猿
四个汉字。
生成后的字体文件只有3KB
!
加载字体
要加载typeface
格式的json
字体文件,我们要用到FontLoader
这个类,和前面小节中学习到的镜头控制类一样,它也不是three.js
中内置的类,但我们可以在three.js
仓库examples/js/loaders/FontLoader.js
路径下找到它。
将其复制粘贴到我们的项目中,依然通过<script>
标签来引入它:
<script src="./FontLoader.js"></script>
FontLoader
的用法和前面我们学过的TextureLoader
类似:
/**
* Fonts
*/
const fontLoader = new THREE.FontLoader()
fontLoader.load(
'./fonts/ali-puhui-extrabold.json',
(font) =>
{
console.log('loaded')
}
)
不出意外的话,我们应该可以在调试面板中看到loaded
输出,在这个回调函数里返回的font
就是我们接下来要用于创建几何体的字体了。
创建文字几何体
如前言中提到的,我们要使用TextGeometry
来创建文字几何体,和FontLoader一样,它也需要我们手动引导到项目中。
首先在three.js
仓库的examples/js/geometries/TextGeometry.js
路径下找到它,并复制粘贴到我们的项目中,使用<script>
标签来引入
<script src="./TextGeometry.js"></script>
然后我们创建TextGeometry
对象,并设置一些初始必备的参数,例如size
(字号),height
(深度)等。
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
{
const textGeometry = new THREE.TextGeometry(
'大帅老猿',
{
font: font,
size: 0.5,
height: 0.2
}
)
const textMaterial = new THREE.MeshBasicMaterial({color:0xffffff})
const text = new THREE.Mesh(textGeometry, textMaterial)
scene.add(text)
}
)
现在我们能看到立体的大帅老猿
四个汉字了吧!
如果我们想让它看起来更酷,可以给材质加入wireframe: true
属性
const textMaterial = new THREE.MeshBasicMaterial({ wireframe: true })
在创建TextGeometry时,我们也可以加入一些斜角的参数来丰富字体的表现:
const textGeometry = new THREE.TextGeometry(
'大帅老猿',
{
font: font,
size: 0.5,
height: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
}
)
如你所见,我们的文字几何体拥有了斜角
等更多细节,这也会带来更多的性能消耗。
文本居中
要将文本居中首先必须要拿到几何体的边界
大小,有立方体和球体两种边界算法。
边界并不能用眼睛观察到,但它可以帮助Three.js轻松计算对象是否在屏幕上,如果不在屏幕上,几何体甚至不会被渲染。
Three.js
中默认使用的是球体边界。但在此例中我们想要一个立方体的边界,我们可以通过computeBoundingBox()
来计算:
textGeometry.computeBoundingBox()
console.log(textGeometry.boundingBox)
计算结果是一个Box3
类型的对象,它具有min属性和max属性。
max属性中的x,y,z分别代表了三个轴上的尺寸,有了这个尺寸,居中就很简单了。也有两种方式来做,结果从视觉上看是一致的,区别在于Mesh对象的坐标是否改变。
- 通过移动网格Mesh对象的坐标
text.position.set(
- textGeometry.boundingBox.max.x * 0.5,
- textGeometry.boundingBox.max.y * 0.5,
- textGeometry.boundingBox.max.z * 0.5
)
- 通过移动几何体相对Mesh的坐标
textGeometry.translate(
- textGeometry.boundingBox.max.x * 0.5,
- textGeometry.boundingBox.max.y * 0.5,
- textGeometry.boundingBox.max.z * 0.5
)
我们的文本几何体已经居中了,但其实并不是特别精准,因为没有计算斜角。如果想要非常精确的话,也可以将斜角的值加入计算:
textGeometry.translate(
- (textGeometry.boundingBox.max.x - 0.02) * 0.5, // Subtract bevel size
- (textGeometry.boundingBox.max.y - 0.02) * 0.5, // Subtract bevel size
- (textGeometry.boundingBox.max.z - 0.03) * 0.5 // Subtract bevel thickness
)
上面就是将文字居中的计算方法。
不过,Three.js
的几何体Geometry
对象提供了一个内置方法来帮我们更快速的实现居中。
它就是.center()
!
textGeometry.center()
你不早说-_-!
因为我想让大家通过自己实现居中来了解边界的概念。
加入材质
现在我们来复习一下前面学习过的材质,让我们的3D文字看起来更酷!
让我们使用MeshMatcapMaterial
来快速加入材质,这种材质类型有着很好的视觉表现,并且性能消耗不大。
首先,我们可以在 https://github.com/nidorx/matcaps 仓库找到自己想要的matcap
纹理贴图,然后使用TextureLoader
来加载它。
const matcapTexture = new THREE.TextureLoader.load('./textures/matcap1.png')
接着我们使用 MeshMatcapMaterial
,看上去真的不错吧~
const textMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
再换一个matcap
纹理贴图试试看
const matcapTexture = new THREE.TextureLoader.load('./textures/matcap2.png')
恭喜,现在我们拥有了土豪金 :P
加入漂浮装饰几何体
接下来,我们在文字周围加入一些漂浮的几何体,让这个小网页看起来更加富有动感。
现在我们加入一个for循环,批量创建100个TorusGeometry(甜甜圈)几何体,和咱们的文字设置为一样的材质
for(let i = 0; i < 100; i++)
{
const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
const donut = new THREE.Mesh(donutGeometry, donutMaterial)
scene.add(donut)
}
现在这100个甜甜圈的位置坐标都是一样的,所以看上去只有一个。
现在为它们分别加入一些随机坐标:
donut.position.x = (Math.random() - 0.5) * 10
donut.position.y = (Math.random() - 0.5) * 10
donut.position.z = (Math.random() - 0.5) * 10
我们有了100个甜甜圈在不同的坐标!
为了让它们看上去更加自然,我们给每个甜甜圈再加入一些随机的旋转角度
donut.rotation.x = Math.random() * Math.PI
donut.rotation.y = Math.random() * Math.PI
看上去已经很酷了,但还不够好,我们再给每个甜甜圈加入一些随机的大小
const scale = Math.random()
donut.scale.set(scale, scale, scale)
性能优化
上面的代码其实有一些性能问题的,在for循环中,我们创建了100个甜甜圈几何体,100个材质,100个网格对象。但其实,由于每个甜甜圈都长得一样,所以几何体和材质我们只需要创建一个就可以了。
现在我们把donutGeometry
和 donutMaterial
移到循环外部:
const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
for(let i = 0; i < 100; i++)
{
// ...
}
结果看上去完全一样!但我们少创建了99个几何体和材质对象。
在后面的课程中我们还会有一个专门关于性能优化的小节。
动起来!
根据前面学过的动画章节,我们给这个3D世界加入一些动感,比如旋转的甜甜圈。
首先我们需要创建一个名为donutsArray
的空数组
let donutsArray = [];
注意,我们需要在后面的tick函数中使用这个变量,所以请确保在可用的作用域里定义变量。
接着我们在for循环创建甜甜圈后将甜甜圈对象添加到数组中
for(let i = 0; i < 100; i++)
{
const donut = new THREE.Mesh(donutGeometry, donutMaterial)
//...
donutsArray.push(donut);
}
然后在tick函数中用for遍历donutsArray,并且让每一个甜甜圈几何体持续旋转:
const tick = () =>{
for(let i = 0;i<donutsArray.length;i++){
let donut = donutsArray[i];
donut.rotation.x += 0.01;
donut.rotation.y += 0.01;
}
//...
![](https://secure-bigfile.wostatic.cn/static/siFxLg4ybzTQUCK58Gtjzf/Jul-12-2022 08-49-52.gif?auth_key=1711661893-qo3gfV9AGBb89HemGMEGsB-0-32d146d90f1c1292e2f543fa6e0a54c8)
我们的甜甜圈动起来了!