Three.js 纹理贴图的实现
在线工具推荐:3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎
纹理贴图简介
当我们创建一个网格时,比如我们不起眼的立方体,我们传入两个组件:几何体和材质。
网格需要两个子组件:几何体和材料
const mesh = new Mesh(geometry, material);
几何形状定义了网格的形状,材料定义了网格的各种表面属性,特别是它对光的反应。几何体和材质,以及影响网格的任何光影,在渲染场景时控制网格的外观。目前,我们的场景包含一个网格,其形状由 a 定义,曲面由 a 定义,颜色参数设置为紫色。这由单个 照亮,当我们渲染场景时,结果就是这个简单的紫色框。BoxBufferGeometryMeshStandardMaterialDirectionalLight
将其与现实世界中的混凝土盒子进行比较 - 或木箱,或金属箱,或由几乎任何物质制成的盒子,除了光滑的塑料,我们可以立即看到我们的3D盒子根本不现实。现实世界中的物体通常是划伤、破损和肮脏的。但是,应用于我们盒子的材料看起来不是这样的。相反,它由平滑地应用于网格的整个表面的单一颜色组成。除非我们希望我们所有的作品看起来像全新的塑料,否则这是行不通的。
除了颜色之外,材质还有许多参数,我们可以用这些参数来调整物体表面的各种属性,如粗糙度、金属度、不透明度等。但是,就像颜色参数一样,这些参数均匀地应用于网格的整个表面。例如,如果我们增加材料的属性,物体的整个表面将变得更加粗糙。如果我们将 设置为红色,则整个对象将变为红色。.roughness.color
相比之下,大多数真实世界对象的表面属性从一个点到另一个点会发生变化。考虑一个代表人脸的网格。再一次,它由几何形状和材料组成,就像我们的立方体网格一样。大比例特征(如眼睛、鼻子、耳朵、颈部和下巴)由几何图形定义。然而,要创建逼真的面孔,不仅仅是精心制作的几何图形。仔细观察皮肤,我们可以看到有很多小肿块、皱纹和毛孔,更不用说眉毛、嘴唇和轻微的胡须了。在创建像人脸这样的复杂模型时,艺术家必须决定使用几何图形表示模型的哪些部分,以及在材质级别表示哪些部分,请记住,使用材料表示事物通常比几何图形便宜。当模型必须在移动设备上运行时,这是一个特别重要的考虑因素,因为高性能至关重要。例如,虽然可以在几何形状上对眉毛上的每根头发进行建模,但这样做会使该模型不适合在除最强大的设备之外的所有设备上实时使用。相反,我们必须在材质级别表示头发等小特征,并为眼睛、鼻子和耳朵等大比例特征保留几何图形。
另请注意,此面由单个几何图形组成。我们通常希望避免将几何体拆分得过多,因为每个网格只能有一个几何体,因此每个单独的几何体都对应于场景中的新网格。场景中的对象越少,性能越好,开发人员和 3D 美术师也更容易使用。换句话说,我们不想被迫为耳朵和眼睛创造不同的几何形状。无论如何,这是不切实际的。仔细观察嘴唇,我们可以看到嘴唇的红色和下巴的肤色之间没有明显的区别。这意味着我们需要某种方法来修改材料属性,以便它们可以在物体表面上平滑地变化。我们需要能够这样说:
- 构成嘴唇的几何形状部分是红色的
- 构成下巴的几何形状部分是肤色,上面覆盖着轻微的胡须
- 构成眉毛的几何形状部分是头发颜色的
...等等。这不仅适用于颜色。例如,皮肤比头发和嘴唇更有光泽。因此,我们还需要能够指定其他属性(如粗糙度)如何在几何体中从一个点变化到下一个点。
此颜色纹理使用 UV 映射映射到面部几何体上这就是纹理映射的用武之地。用最简单的术语来说,纹理映射意味着拍摄图像并将其拉伸到 3D 对象的表面上。我们将以这种方式使用的图像称为纹理,我们可以使用纹理来表示颜色、粗糙度和不透明度等材质属性。例如,要更改几何体某个区域的颜色,我们将位于顶部的纹理区域的颜色更改为颜色,如附着在面部模型上的颜色纹理所示。
虽然获取 2D 纹理并将其拉伸到像立方体这样的规则形状上很容易,但对于像脸这样的不规则几何体来说,要做到这一点要困难得多,多年来,已经开发了许多纹理映射技术。也许最简单的技术是投影映射,它将纹理投射到物体(或场景)上,就好像它通过电影放映机照射一样。想象一下,把你的手放在电影放映机前,看到图像投射到你的皮肤上。
将 UV 坐标显式写入纹理的测试纹理。虽然投影映射和其他技术仍然广泛用于创建阴影(或模拟投影仪)等操作,但这不适用于将面部的颜色纹理附加到面部几何体上。取而代之的是,我们使用一种称为UV映射的技术,它允许我们在几何体上的点和面上的点之间创建连接。使用UV贴图,我们将纹理划分为带有点的2D网格(0,0)在左下角和点(1,1)在右上角。然后,重点(0. 5,0.5)将位于图像的确切中心。同样,几何体中的每个点在网格的 3D 局部空间中都有一个位置。因此,UV 贴图是将纹理中的 2D 点分配给几何体中的 3D 点的过程。例如,假设面部模型中的嘴唇位于该点(0,0,0 ).我们可以看到纹理中的嘴唇靠近中心,在周围(0. 5,0.5).因此,我们将创建一个映射:
(0. 5,0.5)⟶(0,0,0 )
现在,当我们将纹理指定为材质中的颜色映射表时,纹理的中心将映射到嘴唇上。接下来,我们必须对几何体中的许多其他点执行相同的操作,将耳朵、眼睛、眉毛、鼻子和下巴分配给纹理的适当点。如果这听起来像是一个令人生畏的过程,请不要担心,因为手动执行此操作很少见。对于此模型,UV 贴图是在外部程序中创建的,通常,这是创建 UV 贴图的推荐方法。
表示UV贴图的数据存储在几何体上。像 这样的Three.js几何体已经设置了UV贴图,在大多数情况下,当您加载在外部程序中创建的模型(如面)时,它也将准备好UV贴图以供使用。在本章的后面,我们将更详细地探讨盒子几何体的UV贴图,并将黑白测试纹理分配给盒子网格。BoxBufferGeometry
一旦我们有了带有UV贴图的几何体,我们就可以获取任何纹理并将其应用于几何体,它就会立即起作用。但是,可能很难找到其他纹理在面部模型中看起来不错,因为必须仔细协调UV贴图才能将纹理与面部上的正确点相匹配,而做好这项工作是熟练的3D艺术家的工作。但是,对于像立方体这样的简单形状,我们可以使用几乎任何图像作为纹理,将盒子变成木箱、混凝土箱或板条箱,等等。
可以存储在纹理中的数据类型
在本章中,我们将重点介绍如何使用纹理来表示颜色。我们将获取 uv-test-bw.png 纹理,您可以在编辑器的 /assets/textures/ 文件夹中找到它,并将其拉伸到我们的立方体上。当我们这样做时,默认情况下,three.js 会在立方体的每个面上拉伸一个纹理副本,总共六个副本。
在计算机图形学的早期,纹理仅用于存储对象的颜色。然而,如今,纹理可用于存储各种数据,例如颜色、凹凸、不透明度、表面上的小阴影(称为环境光遮蔽)、照明、金属度和粗糙度,仅举几例。例如,不同的材质接受不同种类和组合的纹理,因此 不接受所有相同的纹理。我们将在本书后面更详细地介绍可以存储在纹理中的数据类型。MeshBasicMaterialMeshStandardMaterial
纹理类型
uv-test-bw.png 是一个以 PNG 格式存储的普通 2D 图像文件,下面,我们将使用 加载它,它将返回 Texture
类的实例。您可以以相同的方式使用浏览器支持的任何图像格式,例如 PNG、JPG、GIF、BMP。这是我们将遇到的最常见和最简单的纹理类型:存储在简单 2D 图像文件中的数据。TextureLoader
还有用于 HDR、EXR 和 TGA 等专用图像格式的加载器,它们具有相应的加载器,如 TGALoader
。同样,一旦加载,我们将得到一个实例,我们可以以与加载的PNG或JPG图像大致相同的方式使用它。Texture
除此之外,three.js 还支持许多其他类型的纹理,这些纹理不是简单的 2D 图像,例如视频纹理、3D 纹理、画布纹理、压缩纹理、立方体纹理、等距柱状投影纹理等。同样,我们将在本书后面更详细地探讨这些内容。在本章的其余部分,我们将重点介绍以 PNG 或 JPG 格式存储的 2D 纹理。
Texture类
该类是 HTML 图像元素的包装器,其中包含一些与用作纹理而不是普通图像相关的额外设置。我们可以在 下访问原始图像。它是我们在处理纹理时最常用的类,尽管有几个派生类(如 VideoTexture
或 CubeTexture
)用于处理其他类型的纹理。不过,通常我们不会直接创建一个,因为会自动为我们创建一个,我们将在下面看到。Textureimage.textureTextureTextureLoader
通过纹理类提供的设置示例包括 和 ,它们控制纹理到达边缘时的换行方式(例如,它是重复、简单地停止,还是将纹理的边缘拉伸到网格的边缘?我们还可以指定各种过滤(using 和 )来控制在远处或近距离看到纹理时的过滤方式。换句话说,这些设置控制用于放大或缩小图像的算法。.wrapS.wrapT.minFilter.magFilter
还有几个属性,如 ,,允许我们控制纹理的位置。另外两个重要设置是 ,它沿.offsetcenter.rotation.flipY
Y-axis(用于与在某些外部程序中创建的模型兼容)和属性,正如我们稍后将看到的,必须正确设置该属性才能获得最佳结果。.encoding
花几分钟时间浏览文档页面,并查看使用纹理时可用的选项。我们将在本书后面更详细地探讨其中的大多数。
创建纹理
有许多方法可以准备图像以用作纹理,但最简单的方法是拍摄对象的照片。例如,如果您拍摄了一张砖墙的照片并将其分配给材质的颜色槽,您将在场景中看到与 3D 墙的相似性。我们可以通过使用原始图像为其他材质属性(如凹凸或粗糙度)创建额外的纹理来改进这一点。在 freepbr.com 上查看这组纹理,以作个例子(选择虚幻引擎版本用于Three.js,请注意反照率是颜色的另一个术语)。我们将在本书的后面部分探索使用一组这样的纹理来创建逼真的材质。
虽然拍摄平坦墙壁的照片是一件简单的事情,但像脸、树或兔子这样的曲面会带来更大的挑战。对于这样的表面,艺术家必须拼合照片,并将拼合图像中的每个点连接到 3D 模型上的相应点,再次使用 UV 贴图。这通常是在外部建模程序中完成的,而不是在 three.js 中完成的。
对于砖墙和木地板等常见表面,您可以在网络上找到高质量的纹理集(如上图),其中许多是免费的。在本书中,我们将使用来自Three.js存储库和 freepbr.com 或 Quixel megascan 等站点的纹理。
纹理术语
在继续加载纹理并将其应用于立方体之前,让我们回顾一下在处理纹理时将使用的所有技术术语。
图像和纹理有什么区别?
您会在计算机图形学文献中看到很多术语纹理和图像。这些甚至通常以相同的格式存储,例如 PNG 或 JPG。有什么区别?
- 图像是设计供人类查看的 2D 图片。
- 纹理是专门准备的数据,用于 3D 图形中的各种目的。
构成图像的各个像素表示颜色。另一种看待这个问题的方式是,图像是一个二维的颜色数组。在计算机图形学的早期,纹理也是如此,但随着时间的推移,纹理的用途越来越多,现在更正确的说法是纹理是数据的二维数组。此数据可以表示任何内容。如今,甚至可以将几何图形或动画存储在纹理中。
当纹理以 PNG 或 JPG 等图像格式存储时,我们可以在任何图像查看器中打开它。在本章中,我们将加载的纹理表示颜色数据,因此如果我们在查看器中打开它,它将看起来像图像。但是,用于其他目的的纹理(例如凹凸贴图、不透明度贴图、光照贴图等)通常看起来不像任何特别的东西,直到它们被应用于材质并由渲染器解释。
纹理贴图
虽然在技术上不正确,但纹理通常也被称为贴图,甚至是纹理贴图,尽管贴图在为材质分配纹理时最常用。当使用纹理来表示颜色时,我们会说我们正在将纹理分配给材质上的颜色映射表槽。下面,我们将向您展示如何将 uv-test-bw.png 纹理分配给 .MeshStandardMaterial
Pixel 和 Texel
数字图像是像素的 2D 数组,每个像素都是一个包含单一颜色的小点。我们的屏幕也由一个由小点组成的 2D 数组组成,每个小点都显示一种颜色,我们也称这些像素为像素。然而,构成屏幕的像素是实际的物理对象,LED或OLED或其他一些高科技设备,而构成图像的像素只是存储在文件中的数字。
为了避免混淆,我们将继续将构成屏幕像素的点称为,但我们将构成纹理的点称为纹素。
UV映射
UV 贴图是一种将 2 维纹理映射到 3 维几何体上的方法。想象一下纹理顶部的 2D 坐标系,其中(0,0)在左下角和(1,1)在右上角。由于我们已经使用了字母X,Y和Z对于我们的 3D 坐标,我们将使用字母来指代 2D 纹理坐标U和V.这就是UV贴图这个名字的由来。
以下是UV贴图中使用的公式:
( u,v)⟶(x,y,z)
(u,v)表示纹理上的一个点,并且(x,y,z)表示几何体上的一个点,在局部空间中定义。从技术上讲,几何体上的点称为顶点。
UV 将纹理映射到BoxBufferGeometry
在上图中,纹理的左上角已映射到具有坐标的立方体角上的顶点(−1,1,1):
( 0,1)⟶(−1,1,1)
对立方体的其他五个面进行了类似的映射,从而在立方体的六个面中的每一个面上都生成一个完整的纹理副本:
请注意,该点没有映射(0. 5,0.5),纹理的中心。只有纹理的角被映射到立方体的八个角上,其余的点都是从这些角中“猜测”出来的。相比之下,像人脸这样的复杂模型必须定义更多的UV坐标,才能将表示鼻子、耳朵、眼睛、嘴唇等的纹理部分映射到几何体的正确点。
一旦我们更深入地了解了几何体的工作原理,我们将在本书的后面回到UV贴图。幸运的是,我们很少需要手动设置 UV 贴图,因为所有Three.js 几何体(包括 )都内置了 UV 贴图。我们只需要加载纹理并将其应用到我们的材料上,一切都会起作用。BoxBufferGeometry
在本章的其余部分,我们将向您展示如何做到这一点。
重要提示:从现在开始,如果您在本地工作,则需要设置一个 Web 服务器,否则,由于浏览器安全限制,您将无法加载纹理。
对于使用内联代码编辑器的每个人来说,一切照旧。让我们继续前进。
班级Texture
该类是 HTML 图像元素的包装器,其中包含一些与用作纹理而不是普通图像相关的额外设置。Texture
加载纹理
现在我们已经掌握了所有的理论,加载纹理并将其应用于我们的立方体很简单。我们在本章中添加的所有代码都将进入 cube.js 模块。我们将使用 three.js TextureLoader
类来加载纹理,因此请添加到 cube.js 顶部的导入列表中:TextureLoader
cube.js:导入TextureLoader
import {
BoxBufferGeometry,
MathUtils,
Mesh,
MeshStandardMaterial,
TextureLoader,
} from 'three';
将材料设置移动到单独的功能中
为了防止函数变得太大,让我们将材质创建移动到一个新函数中:createCube
cube.js:将材质设置移动到新函数中
function createMaterial() {
// create a "standard" material
const material = new MeshStandardMaterial({ color: 'purple' });
return material;
}
function createCube() {
const geometry = new BoxBufferGeometry(2, 2, 2);
const material = createMaterial();
const cube = new Mesh(geometry, material);
...
}
创建实例TextureLoader
接下来,在新函数的顶部创建一个新实例:TextureLoadercreateMaterial
cube.js:创建纹理加载器实例
function createMaterial() {
// create a texture loader.
const textureLoader = new TextureLoader();
// create a "standard" material using
const material = new MeshStandardMaterial({ color: 'purple' });
return material;
}
用于加载纹理TextureLoader.load
TextureLoader.load
方法可以加载任何标准图像格式的纹理,例如 PNG、JPEG、GIF、BMP 等。在这里,我们将从 assets/textures 文件夹中加载 uv-test-bw.png 文件:
cube.js:加载纹理
function createMaterial() {
// create a texture loader.
const textureLoader = new TextureLoader();
// load a texture
const texture = textureLoader.load(
'/assets/textures/uv-test-bw.png',
);
// create a "standard" material using
const material = new MeshStandardMaterial({ color: 'purple' });
return material;
}
当我们调用时,会发生一些有趣的事情。即使加载纹理需要一些时间(可能几百毫秒),也会立即返回 Texture
类的空实例。上面,我们将其存储在一个名为 ..loadTextureLoadertexture
我们可以立即使用这个空,甚至在图像完成加载之前。但是,在图像数据完全加载之前,纹理将显示为黑色。换句话说,如果我们将此纹理分配给材质的颜色映射表插槽,则该材质将在场景中显示为黑色,直到纹理完成加载。texture
加载完成后,将自动插入正确的图像,并且材料的颜色将从黑色变为图像中的任何颜色。如果互联网连接速度较慢,此过程将尤为明显。如果使用内联编辑器更新场景,则可能会看到这种情况发生,尽管图像数据应在几分之一秒内加载。您可能希望避免在场景中显示黑色网格,在这种情况下,您可以等到所有纹理加载完毕后再渲染场景。我们将在本书的后面部分探讨您的选择。TextureLoader
将纹理分配给材质的色彩映射表插槽
以前,我们使用 .color
属性设置材质的颜色。在这里,我们将 material.map 属性分配给 material.map
属性,该属性描述了颜色在对象表面上的变化情况。 应该命名,但是,由于它经常使用,因此为了方便起见,因此将其缩短。texture.map.colorMap
通常,我们设置 .color
或 .map
,但不能同时设置两者。如果我们同时设置了两者,纹理中的颜色将乘以属性。例如,如果我们保持紫色,这种黑白纹理将获得紫色调。这里的一个常见用例是将颜色设置为灰色阴影以使纹理变暗。由于白色是默认颜色,因此设置为白色不会对纹理产生影响。因此,无法用于减轻质地。你只能让它变暗。.color.color.color
与 color 参数一样,我们可以将纹理传递到材质的构造函数中:
在构造函数中将纹理分配给材质
const material = new MeshStandardMaterial({
map: texture,
});
或者,我们可以在创建材质后设置:.map
创建材质后分配纹理
const material = new MeshStandardMaterial();
material.map = texture;
我们将在这里使用第一种方法。再次更新:createMaterial
cube.js:将纹理分配给材质的颜色映射表插槽
function createMaterial() {
// create a texture loader.
const textureLoader = new TextureLoader();
// load a texture
const texture = textureLoader.load(
'/assets/textures/uv-test-bw.png',
);
// create a "standard" material using
// the texture we just loaded as a color map
const material = new MeshStandardMaterial({
map: texture,
});
return material;
}
现在,您的场景将更新,您应该看到纹理映射到立方体的六个面中的每一个面上。
纹理有六个副本,立方体的每个面一个。特别注意拐角处发生的事情。
在上面的场景中,您可以使用鼠标或触摸旋转立方体。实际上,移动的是摄像机,而不是立方体,因为我们在这个场景中添加了一个摄像机控制插件。这个插件允许您平移、旋转和缩放/推车相机以从任何角度查看场景,这非常适合我们设置场景并想要仔细观察所有内容。在下一章中,我们将把这个插件添加到我们的应用程序中。