【Three.js】第十一章 Textures 纹理

11.Textures 纹理

介绍

对你的红色立方体感到厌烦了吗?是时候添加一些纹理了。
但首先,什么是纹理,我们可以用它们做什么?

什么是纹理?

您可能知道,纹理是覆盖几何体表面的图像。许多类型的纹理会对几何体的外观产生不同的影响。这不仅仅是关于颜色。
以下是最常见的纹理类型,使用了João Paulo著名的门纹理。如果您喜欢他的作品,请给他买一个Ko-fi或成为Patreon

颜色(或反照率)

反照率纹理是最简单的一种。它只会获取纹理的像素并将它们应用于几何体。

Alpha

alpha 纹理是灰度图像,其中白色可见,黑色则不可见。

高度

高度纹理是一个灰度图像,它将移动顶点以创建一些浮雕。如果你想看到它,你需要添加细分。

普通的

普通纹理会添加小细节。它不会移动顶点,但会引诱光线认为面部的方向不同。法线纹理对于添加具有良好性能的细节非常有用,因为您不需要细分几何体。

环境光遮蔽

环境遮挡纹理是一个灰度图像,可以在表面的缝隙中伪造阴影。虽然它在物理上不准确,但它肯定有助于形成对比。

金属度

金属度纹理是一个灰度图像,它将指定哪个部分是金属(白色)和非金属(黑色)。这些信息将有助于产生反思。

粗糙度

粗糙度是带有金属感的灰度图像,它将指定哪些部分是粗糙的(白色),哪些部分是光滑的(黑色)。此信息将有助于驱散光。地毯很凹凸不平,看不到反光,而水面很光滑,可以看到反光。在这里,木材是均匀的,因为上面有一层透明涂层。

光子反应器

这些纹理(尤其是金属感和粗糙度)遵循我们所说的 PBR 原则。PBR 代表基于物理的渲染。它重新组合了许多倾向于遵循现实生活方向以获得真实结果的技术。
虽然还有许多其他技术,但 PBR 正在成为真实渲染的标准,许多软件、引擎和库都在使用它。
现在,我们将只关注如何加载纹理、如何使用它们、我们可以应用哪些转换以及如何优化它们。我们将在后面的课程中看到更多关于 PBR 的信息,但如果您好奇,可以在这里了解更多信息:

如何加载纹理

获取图片的URL

要加载纹理,我们需要图像文件的 URL。
因为我们使用的是构建工具,所以有两种获取方式。
您可以将图像纹理放在/src/文件夹中并像导入 JavaScript 依赖项一样导入它:

import imageSource from './image.png'

console.log(imageSource)

或者您可以将该图像放在/static/文件夹中,只需将图像的路径(不带/static)添加到 URL 即可访问它:

const imageSource = '/image.png'

console.log(imageSource)

注意,这个/static/文件夹只是因为 Vite 模板的配置才有效。如果您使用其他类型的脚手架,您可能需要调整您的项目。
我们将在课程的其余部分使用/static/文件夹操作。

加载图像

您可以在文件夹中找到我们刚刚看到的门纹理/static/,并且有多种加载方式。

使用原生 JavaScript

使用原生 JavaScript,首先,您必须创建一个Image实例,监听load事件,然后更改其src属性以开始加载图像:

const image = new Image()
image.onload = () =>
{
    
    
    console.log('image loaded')
}
image.src = '/textures/door/color.jpg'

您应该会看到'image loaded'出现在控制台中。如您所见,我们将图片的src设置为路径'/textures/door/color.jpg',但是文件夹/static中没有。
我们不能直接使用该图像。我们需要先从该图像创建一个纹理。
这是因为 WebGL 需要一种非常特殊的格式,可以由 GPU 访问,还因为一些更改将应用于纹理,如 mipmapping,我们稍后会看到更多相关信息。
使用Texture类创建纹理:

const image = new Image()
image.addEventListener('load', () =>
{
    
    
    const texture = new THREE.Texture(image)
})
image.src = '/textures/door/color.jpg'

我们现在需要做的是texture常量在material中使用. 不幸的是,该texture变量已在函数中声明,我们无法在该函数之外访问它。这是一个 JavaScript 的 作用域 限制。
我们可以在函数内部创建网格,但有一个更好的解决方案needsUpdateneedsUpdate可以在函数外部创建纹理,然后通过将纹理属性设置为true那么就会在加载图像后更新纹理:

const image = new Image()
const texture = new THREE.Texture(image)
image.addEventListener('load', () =>
{
    
    
    texture.needsUpdate = true
})
image.src = '/textures/door/color.jpg'

执行此操作时,您可以立即立即使用该texture变量,图像在加载之前将是透明的。
要查看立方体上的纹理,请将color属性替换为map并使用textureas 值:

const material = new THREE.MeshBasicMaterial({
    
     map: texture })


您应该在立方体的每一侧都看到门纹理。

使用 TextureLoader

原生 JavaScript 技术并没有那么复杂,但是有一种更直接的方法使用TextureLoader
使用TextureLoader类实例化变量并使用其.load(...)方法创建纹理:

const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')

在内部,Three.js 将执行操作之前来加载图像并在准备就绪后更新纹理。
您可以只使用一个TextureLoader实例加载任意数量的纹理。
您可以在路径后发送 3 个函数。他们将被要求参加以下周期:

  • load图片加载成功时
  • progress当加载正在进行时
  • error如果出了什么问题
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

最后,如果您有多个图像要加载,并且想要共享事件,例如在加载所有图像时收到通知,您可以使用LoadingManager
创建LoadingManager类的实例并将其传递给TextureLoader

const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(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')

正如您在此处看到的,我们重命名了texture变量colorTexture,所以不要忘记更改它material

const material = new THREE.MeshBasicMaterial({
    
     map: colorTexture })

如果您想要显示一个加载程序并仅在加载所有资产时将其隐藏,则LoadingManager非常有用。正如我们将在以后的课程中看到的,您还可以将它与其他类型的加载器一起使用。

UV unwrapping 紫外线展开

虽然如何在立方体上放置纹理是合乎逻辑的,但对于其他几何体来说,事情可能有点棘手。
尝试用其他几何图形替换您的 BoxGeometry:

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

// Or
const geometry = new THREE.SphereGeometry(1, 32, 32)

// Or
const geometry = new THREE.ConeGeometry(1, 1, 32)

// Or
const geometry = new THREE.TorusGeometry(1, 0.35, 32, 100)

如您所见,纹理以不同的方式被拉伸或挤压以覆盖几何体。
这就是所谓的UV 展开。您可以想象这就像打开折纸或糖果包装纸使其变平一样。每个顶点在平面(通常是正方形)上都有一个二维坐标。

您实际上可以在属性中看到那些 UV 2D 坐标geometry.attributes.uv

console.log(geometry.attributes.uv)

当您使用图元时,这些 UV 坐标由 Three.js 生成。如果您创建自己的几何体并想对其应用纹理,则必须指定 UV 坐标。
如果您使用 3D 软件制作几何体,您还必须进行 UV 展开。

不用担心; 大多数 3D 软件也有自动展开功能,应该可以解决问题。

Transforming the texture 变换纹理

让我们回到具有一个纹理的立方体,看看我们可以对该纹理应用什么样的变换。

重复

您可以使用Vector2repeat属性重复纹理,这意味着它具有xy属性。
尝试更改这些属性:

const colorTexture = textureLoader.load('/textures/door/color.jpg')
colorTexture.repeat.x = 2
colorTexture.repeat.y = 3


如您所见,纹理没有重复,但更小,最后一个像素似乎被拉伸了。
这是由于默认情况下未将纹理设置为重复自身。要更改它,您必须使用THREE.RepeatWrapping常量更新wrapSwrapT属性。

  • wrapS是为x
  • wrapT是为y
colorTexture.wrapS = THREE.RepeatWrapping
colorTexture.wrapT = THREE.RepeatWrapping


你也可以改变方向THREE.MirroredRepeatWrapping

colorTexture.wrapS = THREE.MirroredRepeatWrapping
colorTexture.wrapT = THREE.MirroredRepeatWrapping

Offset 抵消

您可以使用属性来偏移纹理,offset该属性也是具有和属性的Vector2。更改这些将简单地偏移 UV 坐标:x``y

colorTexture.offset.x = 0.5
colorTexture.offset.y = 0.5

rotation 回转

您可以使用rotation属性旋转纹理,这是一个简单的数字,对应于以弧度表示的角度:

colorTexture.rotation = Math.PI * 0.25


如果删除offsetrepeat属性,您会看到旋转发生在立方体面的左下角:

也就是说,实际上UV 坐标是0, 0。如果要更改该旋转的轴心,可以使用也是Vector2的属性center来实现:

colorTexture.rotation = Math.PI * 0.25
colorTexture.center.x = 0.5
colorTexture.center.y = 0.5

纹理现在将在其中心旋转。

过滤和映射

如果您查看立方体的顶面,而该面几乎被隐藏,您会看到非常模糊的纹理。

这是由于过滤和 mipmapping。
Mipmapping(或带空格的“mip mapping”)是一种技术,包括反复创建纹理的一半较小版本,直到获得 1x1 纹理。所有这些纹理变化都被发送到 GPU,GPU 将选择最合适的纹理版本。
Three.js 和 GPU 已经处理了所有这些,您只需设置要使用的过滤算法即可。有两种类型的过滤器算法:缩小过滤器和放大过滤器。

缩小过滤器

当纹理像素小于渲染像素时,会发生缩小过滤器。换句话说,纹理对于表面来说太大了,它覆盖了。
您可以使用minFilter该属性更改纹理的缩小过滤器。
有 6 个可能的值:

  • THREE.NearestFilter
  • THREE.LinearFilter
  • THREE.NearestMipmapNearestFilter
  • THREE.NearestMipmapLinearFilter
  • THREE.LinearMipmapNearestFilter
  • THREE.LinearMipmapLinearFilter

默认值为THREE.LinearMipmapLinearFilter。如果您对纹理的外观不满意,您应该尝试其他滤镜。
我们不会看到每一个,但我们将测试THREE.NearestFilter,它有一个非常不同的结果:

colorTexture.minFilter = THREE.NearestFilter

如果您使用的设备像素比高于 1,则不会有太大差异。如果没有,将相机放在这张脸几乎被隐藏的地方,你应该会得到更多的细节和奇怪的伪像。
checkerboard-1024x1024.png如果您使用文件夹中的纹理进行测试/static/textures/,您会更清楚地看到这些人工制品:

const colorTexture = textureLoader.load('/textures/checkerboard-1024x1024.png')

您看到的人工制品称为波纹图案,通常希望你避免使用它们。

放大滤镜 !可以让图像清晰非常重要

放大滤镜的工作方式与缩小滤镜类似,但纹理像素大于渲染像素。换句话说,纹理对于它覆盖的表面来说太小了。
checkerboard-8x8.png您可以使用同样位于文件夹中的纹理查看结果static/textures/

const colorTexture = textureLoader.load('/textures/checkerboard-8x8.png')


纹理变得模糊,因为它是一个非常大的表面上的非常小的纹理。
虽然您可能认为这看起来很糟糕,但它可能是最好的。如果效果不是太夸张,用户可能甚至不会注意到它。
您可以使用属性更改纹理的放大过滤器magFilter
只有两个可能的值:

  • THREE.NearestFilter
  • THREE.LinearFilter

默认值为THREE.LinearFilter
如果你测试THREE.NearestFilter,你会看到基本图像被保留,你会得到一个像素化的纹理:

colorTexture.magFilter = THREE.NearestFilter


如果您想要具有像素化纹理的 Minecraft 风格,这可能是有利的。
您可以使用位于static/textures/文件夹中的minecraft.png纹理查看结果:

const colorTexture = textureLoader.load('/textures/minecraft.png')


关于所有这些过滤器的最后一句话是,THREE.NearestFilter它比其他过滤器方便,而且您在使用它时应该会获得更好的性能。
仅对mipmap属性使用 minFilter。如果您正在使用THREE.NearestFilter,则不需要 mipmap,您可以使用以下命令停用它们colorTexture.generateMipmaps = false

colorTexture.generateMipmaps = false
colorTexture.minFilter = THREE.NearestFilter

这将稍微卸载 GPU 的负担。

纹理格式和优化

准备纹理时,必须牢记 3 个关键要素:

  • weight 重量
  • size 尺寸(或resolution分辨率)
  • data 数据

**weight **重量

不要忘记访问您网站的用户必须下载这些纹理。您可以使用我们在网络上使用的大多数图像类型,例如.jpg(有损压缩但通常较轻)或.png(无损压缩但通常较重)。
尝试应用优化的方法来获得用户可接受的图像大小,但尽可能让图片存储体积缩小。您可以使用压缩图片的网站,如TinyPNG(也适用于 jpg)或任何软件。

size 尺寸

无论图像的权重如何,您使用的纹理的每个像素都必须存储在 GPU 上。和您的硬盘一样,GPU 也有存储限制。更糟糕的是,自动生成的 mipmapping 增加了必须存储的像素数量。
尽量减小图像的大小。
如果您还记得我们所说的关于 mipmapping 的内容,Three.js 将重复生成纹理的一半小版本,直到它获得 1x1 纹理。因此,您的纹理宽度和高度必须是 2 的幂。这是强制性的,以便 Three.js 可以将纹理的大小除以 2。
一些例子:512x512,1024x1024或512x2048
512,1024并且2048可以除以 2 直到达到 1。
如果您使用的纹理的宽度或高度不同于 2 的幂值,Three.js 将尝试将其拉伸到最接近的 2 的幂数,这可能会导致视觉效果不佳,并且您还会收到警告在控制台中。

data 数据

我们还没有测试它,因为我们有其他东西要先看,但是纹理支持透明。您可能知道,jpg 文件没有 alpha 通道,因此您可能更喜欢使用 png(透明)。
或者您可以使用 alpha 贴图,我们将在以后的课程中看到。
如果您使用的是普通纹理(紫色纹理),您可能希望每个像素的红色、绿色和蓝色通道具有准确的值,否则最终可能会出现视觉故障。为此,您需要使用 png,因为它的无损压缩将保留好像素值。

在哪里可以找到纹理

不幸的是,总是很难找到完美的纹理。有很多纹理素材的网站,但纹理并不都是很好用的,您可能还需要付费。
从网络搜索开始可能是个好主意。以下是我经常访问的一些网站。

如果不是供个人使用,请始终确保您有权使用该纹理。
您还可以使用照片和 2D 软件(如 Photoshop)或什至使用Substance Designer等软件创建自己的程序纹理。

猜你喜欢

转载自blog.csdn.net/m0_68324632/article/details/130997884