前言
前面的课程我们都只是在摆弄一个红色的立方体~ 是时候加入一点纹理
了。
那究竟什么是纹理,以及我们能用纹理做到哪些事呢?
什么是纹理贴图?
纹理就是覆盖几何体表面的图像,比如我们已经看腻了的红色立方体,加上纹理,立刻就可以变成一个快递包装盒子。
但纹理的作用远不止取代颜色。
有很多不同类型,不同用途的纹理贴图,让我们来逐一了解下:
Map :(ColorMap) 颜色贴图是最简单的贴图方式,将图片上的每一个像素应用到几何体上。
Emissive Map:自发光纹理贴图,使用一种有不同色阶灰色的图(灰度图),用于描述材质上哪些像素具有自发光特性。需要配合材质的emissive
属性使用。
Alpha Map:透明纹理贴图也是使用灰度图,越白则透明度越低,越黑则透明度越高。需要配合材质的transparent
属性使用。
Bump Map 、Normal Map 、 Displace Map:
BumpMap凹凸贴图
,Normal Map 法线贴图
, Displacement Map置换贴图
都是用于创建更丰富细节的一种技术。
凹凸贴图几乎已被淘汰,取代它的正是法线贴图,因为法线贴图可以存储比凹凸贴图更多的信息。但法线贴图并不会真正的改变模型,它只是和光发生反应,形成虚假的立体感细节,所以法线贴图在强化细节的同时不会产生额外的性能负担。
置换贴图则会通过改变模型结构来创建真正的细节,不过,这种方式会产生额外的性能消耗。
Rough. Map:粗糙度纹理贴图也是使用灰度图,它用于标识材质表面的粗糙度,颜色越白则越粗糙,越黑则越光滑。
Metal. Map :金属度纹理贴图也是使用灰度图,它将标识材质的哪个部分是金属 (白色) 和非金属 (黑色)。这些信息有助于创造反射。
Env Map:环境反射贴图,用于创建在材质表面反射环境细节的效果。比如金属球可以像镜子一样反射出周围环境的细节。
Light Map:光照贴图,用于来模拟物体和灯光均处于静止状态下的光照的效果。比如不会运动的墙面和吊灯射灯。
AO Map :全称是Ambient Occlusion环境闭塞贴图也是使用灰度图,但它用来模拟更加真实的阴影效果。
扩充知识:PBR
这些纹理 (尤其是金属度和粗糙度) 遵循我们所说的PBR原理。PBR(Physically Based Rendering)
是一种着色和渲染技术,它基于真实世界的物理特性来渲染三维场景,能够更精确的描述光如何与物体表面互动。
接下来,我们将学习如何加载纹理贴图,如何使用纹理贴图,以及我们可以应用哪些转换和如何优化它们。
如何使用纹理贴图
加载纹理图片
可以在assets
文件夹中找到我们刚刚看到的纹理图片,并且有多种加载方式。
使用原生JS
我们可以使用原生的Javascript来创建一个image对象并监听其onload事件,对这个对象的src属性赋值即可完成图片的加载。
const image = new Image()
image.onload = () =>
{
console.log('image loaded')
}
image.src = '/textures/door/color.jpg'
不过,我们并不能直接使用image对象。我们需要用它来创建一个Texture纹理
类型的对象。
const image = new Image()
image.addEventListener('load', () =>
{
const texture = new THREE.Texture(image)
})
image.src = '/textures/door/color.jpg'
创建完Texure后,我们就可以在创建Material
(材质)时使用这个纹理了。
const material = new THREE.MeshBasicMaterial({ map: texture })
有的时候,我们可能不等加载结束,需要先创建Texture
对象并赋值,这样的话就需要在加载完成之后将needsUpdate
属性设置为true
来通知纹理刷新变量。
const image = new Image()
const texture = new THREE.Texture(image)
image.addEventListener('load', () =>
{
texture.needsUpdate = true
})
image.src = '/textures/door/color.jpg'
这样的好处是,不用等待图片加载结束我们就可以使用纹理变量了,加载完成后再刷新渲染即可。
现在这个立方体终于变样了吧~
使用 TextureLoader
了解了原生JavaScript如何加载纹理图片后,我们再来学习Three.js中提供的一种更简单的方法TextureLoader
。
创建TextureLoader
,并使用其load(...)
方法加载纹理图片,
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
只需要两行代码就可以,当然你也可以像这样写成一行。
const texture = new THREE.TextureLoader().load('/textures/door/color.jpg')
load
方法还可以添加三个回调函数作为参数,
onload
当图片加载完成时
onprogress
当图片加载中,可以拿到加载进度
onerror
当图片加载遇到问题时
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
'/textures/door/color.jpg',
() => {
console.log('loading finished')
},
() => {
console.log('loading progressing')
},
() => {
console.log('loading error')
}
)
通过这几个回调函数可以监听到图片文件的加载状况,用于创建加载进度条或是提示加载出错。
使用LoadingManager
在商业项目中,我们经常需要加载很多纹理图片,3D模型等等。我们需要建立一个整体的加载管理器,以确保用户加载完了所有必要资源后才使用我们的网站。这个时候就可以使用LoadingManager
。
创建LoadingManager
类的实例,并将其传递给TextureLoader
:
const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(loadingManager)
我们可以对loadingManager
添加onStart
, onLoad
, onProgress
和 onError
的监听来了解全部资源的加载情况
const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = () =>
{
console.log('loading started')
}
loadingManager.onLoad = () =>
{
console.log('loading finished')
}
loadingManager.onProgress = () =>
{
console.log('loading progressing')
}
loadingManager.onError = () =>
{
console.log('loading error')
}
const textureLoader = new THREE.TextureLoader(loadingManager)
接着我们可以一次性添加所有的纹理贴图资源并管理他们的加载进度
// ...
const colorTexture = textureLoader.load('/textures/door/color.jpg')
const alphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const heightTexture = textureLoader.load('/textures/door/height.jpg')
const normalTexture = textureLoader.load('/textures/door/normal.jpg')
const ambientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const metalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const roughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
LoadingManager
非常有用,除了TextureLoader
,在Three.js
中其他内置的Loader都可以使用LoadingManager
来统一管理。
UV 展开
假如我们把3D立方体的面一一展开,立方体上每一个顶点其实都对应了图片上的一个二维坐标
为了和三维空间的XYZ区分,所以使用UV来代表XY。
立方体比较好理解,但所有几何体都是由面构成的,我们都可以将他们的面展开到一个矩形平面上。
这叫做UV展开。我们可以从几何体的attributes.UV属性中看到这些2D坐标:
console.log(geometry.attributes.uv)
当我们使用Three.js中预置的几何体时,这些UV坐标由Three.js生成。如果我们要创建自己的3D模型并希望使用纹理,则必须对模型进行UV展开,指定UV坐标。不过不用担心,3D建模软件都有这个功能。
转换纹理贴图
现在回到我们的立方体,使用一张纹理贴图来看看我们可以对纹理贴图进行哪些转换操作
Repeat重复
我们可以使用repeat
属性来使贴图重复,repeat的x,y属性分别代表了在x轴和y轴上的重复次数。
const colorTexture = textureLoader.load('/textures/door/color.jpg')
colorTexture.repeat.x = 2
colorTexture.repeat.y = 3
直接这么设置的话,纹理并没有重复,但变小了,最后一个像素进行了拉伸。
这是由于除了设置轴向上的重复次数外,我们还应该设置两个轴向上重复的模式
wrapS
代表x轴上的重复模式
wrapT
代表y轴上的重复模式
colorTexture.wrapS = THREE.RepeatWrapping
colorTexture.wrapT = THREE.RepeatWrapping
我们还可以将其设置为THREE.MirroredRepeatWrapping
,看出区别了吗?
colorTexture.wrapS = THREE.MirroredRepeatWrapping
colorTexture.wrapT = THREE.MirroredRepeatWrapping
Offset偏移
使用偏移属性可以改变纹理贴图的起始位置
colorTexture.offset.x = 0.5
colorTexture.offset.y = 0.5
Rotation旋转
使用rotation
属性来改变纹理的旋转角度
colorTexture.rotation = Math.PI * 0.25
这个旋转后的结果看上去有些奇怪,如果现在我们删除偏移和重复属性,会看到旋转的圆心在立方体的面的左下角:
这起始就是UV坐标的原点0,0
。我们可以使用center属性来改变旋转的圆心
colorTexture.rotation = Math.PI * 0.25
colorTexture.center.x = 0.5
colorTexture.center.y = 0.5
现在贴图基于中心点来旋转了
纹理过滤和纹理分级细化
当我们把立方体旋转到几乎看不见立方体顶面的角度时,仔细看,此时顶面的纹理是非常模糊的。
这是由于我们显示纹理贴图的面的尺寸与纹理图片的尺寸并不一样导致的。在Three.js中已经预置好了纹理过滤器(filter)和纹理分级细化(Mipmapping)相关的算法来帮我们优化这个问题。
纹理分级细化,就是将一张纹理图不断以2的倍数缩小直到像素为1时不可再细分,在使用时,根据贴图的面的大小自动选择相近等级的纹理图。
缩小过滤器
当纹理图片对于立方体的面来说更大时候,则会启用缩小过滤器。
有以下6中类别的缩小过滤器算法
THREE.NearestFilter
THREE.LinearFilter
THREE.NearestMipmapNearestFilter
THREE.NearestMipmapLinearFilter
THREE.LinearMipmapNearestFilter
THREE.LinearMipmapLinearFilter
默认使用的是THREE.LinearMipmapLinearFilter
,我们试着将它设置为THREE.NearestFilter
看看效果。
colorTexture.minFilter = THREE.NearestFilter
我们不会过多地讲解这些过滤器及其算法,大家可以自行多尝试一下。
放大过滤器
放大过滤器的工作原理和缩小过滤器是一样的,不同的是它处理的是纹理图片对于立方体的面的尺寸更小的情况。
我们现在将纹理图片换成checkerboard-8x8.png
const colorTexture = textureLoader.load('/textures/checkerboard-8x8.png')
看上去很模糊,这是因为纹理图片的尺寸只有8x8像素,而立方体的一个面要比这个尺寸大很多。
这个时候我们就可以改变放大过滤器magFilter
的算法来优化显示效果
THREE.NearestFilter
THREE.LinearFilter
默认使用的是 THREE.LinearFilter
,我们将其改成THREE.NearestFilter
再看看
colorTexture.magFilter = THREE.NearestFilter
每一个像素都无比清晰,这就很适合用来建立类似Minecraft风格的纹理。
const colorTexture = textureLoader.load('/textures/minecraft.png')
注意,当我们的缩小过滤器使用THREE.NearestFilter的时候,我们应该将纹理的Generatempmaps属性设置为false,这样可以稍微减轻一些GPU的负担。
colorTexture.generateMipmaps = false
colorTexture.minFilter = THREE.NearestFilter
纹理图片的格式和优化
当我们需要准备纹理贴图的图片文件时,我们需要注意3个关键因素
文件大小
我们可以使用大多数浏览器支持的图片格式来作为纹理贴图,主要是两种:
- jpg (有损压缩,但通常文件较小)
- png (无损压缩,但通常文件较大)
有一些图片压缩的解决方案,可以帮我们在尽可能不降低品质的同时获得更小的文件量,比如TinyPNG。
图片尺寸
请尽可能减小图片的尺寸,还记得前面说的图像分级细化吗?Three.js
将反复产生一半大小的纹理直到像素为1,所以我们常常看到贴图文件的尺寸是512x512
,1024x1024
或者512x2048
....等这些图片尺寸的宽高都是2的倍数,我们可以一直除以2直到最后结果为1。
如果我们使用的纹理图片的宽度或高度不等于2的倍数时,Three.js
将尝试把它们拉伸到最接近一个2的倍数值。这种拉伸的方式自然是无法获得最佳效果,并且我们还会在控制台中收到警告信息。
文件格式
最好是使用png
格式,这种格式虽然可能文件量较大,但它将非常完整的保留所有颜色值,包括透明度。