计算机图形学:(八)纹理映射
在提到,Three.js中创建一个网格Mesh时,需要传入两个组件:一个几何体和一个材质。几何体定义了网格的形状,材料定义了网格的各种表面属性,特别是它对光照的反应方式。当我们渲染场景时,几何体和材质以及影响网格的任何光线和阴影都控制着网格的外观。本章我们先不讨论光照、材质中的粗糙度、金属度、不透明度等对模拟一个现实世界中的几何体的重要性,而是单纯讨论一下纹理在几何体材质中的应用。纹理映射的核心思
# 前言
在计算机图形学:(三)MVP变换扩展提到,Three.js中创建一个网格Mesh时,需要传入两个组件:一个几何体和一个材质。

几何体定义了网格的形状,材料定义了网格的各种表面属性,特别是它对光照的反应方式。当我们渲染场景时,几何体和材质以及影响网格的任何光线和阴影都控制着网格的外观。
本章我们先不讨论光照、材质中的粗糙度、金属度、不透明度等对模拟一个现实世界中的几何体的重要性,而是单纯讨论一下纹理在几何体材质中的应用。
纹理映射的核心思想是将一个二维图像映射到一个三维物体的表面,我们将以这种方式使用的图像称为纹理(texture)。组成纹理图像的像素又被称为纹素(texels,texture elements),每一个纹素的颜色都使用RGB或RGBA 格式编码。
# 基础理论
纹理的2D坐标系如下图所示,其中(0,0)在左下角和(1,1)在右上角。因为我们已经使用了字母X、Y和Z作为我们的3D坐标系,我们将使用字母U和V来指代2D纹理坐标系,这就是UV映射名称的由来。

UV映射中使用的公式:
![]()

① 拉伸方式
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?
(1)重复拉伸
- GL_REPEAT(默认)
重复拉伸方式在很多大场景地形的纹理贴图中有很大作用,如将大块地面重复铺满草皮纹理、将大片水面重复铺满水波纹理等。如果没有设置重复拉伸方式,则开发人员只能将大块面积切割为一块块的小面积,对每一块矩形单独设置0.0~1.0内的纹理坐标。

(2)镜像重复
- GL_MIRRORED_REPEAT

(3)截取拉伸
- GL_CLAMP_TO_EDGE
当纹理坐标值大于1时都看作1,因此会产生边缘被拉伸的效果.

② 纹理采样
将较小的纹理图映射到较大的图元或将较大的纹理图映射到较小的图元时,通过纹理坐标并不一定能找到与之完全对应的像素,这时候就需要采用一些策略使纹理采样可以顺利进行下去。
- GL_NEAREST(默认)
最近点采样是最简单的一种采样算法,在各种采样算法中其速度也是最快的,原理即:选择中心点最接近纹理坐标的那个像素的颜色值为采样值。
缺点:那就是若把较小的纹理图映射到较大的图元上,则容易产生很明显的锯齿。需要注意的是,将较大的纹理图映射到较小的图元时,也会有锯齿产生,但由于图元整体较小,所以视觉上就不那么明显了。

- GL_LINEAR
线性采样时结果颜色并不一定仅来自纹理图中的一个像素,在采样时它会考虑与片元对应的纹理坐标点附近的几个像素。
由于采样是对采样范围内的多个像素进行了加权平均,因此在将较小的纹理图映射到较大的图元上时,将不再出现锯齿现象,而是平滑过渡的,平滑过渡解决了锯齿的问题,但有时线条边缘会很模糊。
③ Mipmap(多级纹理贴图)
当需要处理的场景很大时(如一大片铺满相同纹理的丘陵地形),若不采用一些技术手段,则可能会出现远处地形在视觉上更清楚,近处地形更模糊的反真实现象。这主要是由于透视投影中有近大远小的效果,远处地形投影到屏幕上的尺寸比较小,近处投影到屏幕上尺寸比较大,而整个场景使用的是同一幅纹理图。因此对远处的山体而言,纹理图被缩小进行映射,自然很清楚(甚至会产生由于过分缩小而大量纹理元素对应到同一个片元,同时纹理采样率不足造成失真的锯齿现象);而对于近处的山体,可能纹理图需要被拉大进行映射,自然就发虚。
Mipmap的基本思想:应该对远处的地形采用尺寸较小且分辨率低的纹理,近处的采用尺寸较大且分辨率高的纹理。

若要开发根据场景视觉大小自动选择恰当分辨率的纹理进行映射,那么这会非常复杂。幸运的是,Mipmap仅需要在加载纹理时进行一些处理,然后根据所需纹理采样的细节级别(lod)进行纹理采样即可,其他工作是由渲染管线自动完成的。
④ 多重纹理、过程纹理
对同一个图元采用多幅纹理图,这种技术称为多重纹理。
在多重纹理变化的边界根据某种规则进行平滑过渡,这种技术称为过程纹理。这种平滑过渡在很多情况下都会用到,如本案例中的白天纹理与黑夜纹理的过渡,丘陵地形中根据海拔不同进行的纹理过渡等。
⑤ 压缩纹理
所谓纹理压缩是指在应用开发的准备阶段将各种格式的(png、jpg等)纹理图采用特定的工具转化为特殊的压缩纹理格式,然后在应用程序运行时直接将压缩格式的纹理数据送入纹理缓冲以供纹理采样使用。
# 示例
首先需要加载一张纹理图片,可以使用 THREE.TextureLoader 类来实现。
const textureLoader = new THREE.TextureLoader();
const textureMap = textureLoader.load('./images/smile.jpeg');
可以对纹理进行一些变换操作:
// 纹理偏移
textureMap.offset.set(0.1, -0.5);
// 纹理旋转
textureMap.center.set(0.5, 0.5); // 设置旋转中心点
textureMap.rotation = 45 * Math.PI / 180; // 以弧度为单位
-
纹理贴图
这里用一张笑脸图来对立方体进行贴图:

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { createMultiMaterialObject } from 'three/examples/jsm/utils/SceneUtils';
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.up.set(0, 1, 0);
camera.lookAt(new THREE.Vector3(0, 0, 0));
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x99CCFF, 1); // 渲染器的背景色(浅蓝色)
document.body.appendChild(renderer.domElement);
//————————————————————————————————————————————————————————————————————————————————————
// 一、加载纹理图片
const textureLoader = new THREE.TextureLoader();
const textureMap = textureLoader.load('http://localhost:83/images/smile.jpeg'); // 避免跨域
// 二、创建材质:使用加载好的纹理创建一个材质
var boxMaterial = new THREE.MeshBasicMaterial({ // MeshBasicMaterial基础材质,不受光照影响。可设置纹理、颜色、线框显示、透明度等属性。
map: textureMap, // 纹理贴图
// side: THREE.DoubleSide, // 正反面都贴图
});
// 三、应用材质:将创建好的材质应用到一个几何体上
var boxGeometry = new THREE.BoxGeometry(3, 3, 3);
var box = new THREE.Mesh(boxGeometry, boxMaterial);
// 四、渲染场景
scene.add(box);
renderer.render(scene, camera);
//————————————————————————————————————————————————————————————————————————————————————
const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', function () {
renderer.render(scene, camera); //执行渲染操作
});
</script>
效果图:

-
不同面不同贴图
const materials = [];
for (let i = 0; i < boxGeometry.groups.length; i++)
{
const texture = textureLoader.setCrossOrigin('anonymous').load(
`http://localhost:83/images/${i + 1}.jpeg`
);
materials.push(
new THREE.MeshBasicMaterial({
map: texture,
})
);
}
var box = new THREE.Mesh(boxGeometry, materials);

-
纹理拉伸
通常来说S轴和T轴的纹理坐标都在0.0~1.0的范围内,但在特定情况下,也可以设置大于1的纹理坐标。当纹理坐标大于1以后,设置的拉伸方式才会起作用。
// 重复拉伸
THREE.RepeatWrapping
// 镜像重复
THREE.MirroredRepeatWrapping
// 边缘拉伸
THREE.ClampToEdgeWrapping
在开发中S与T两个轴的拉伸方式是独立设置的,在一般情况下两个轴都会设置同样的选项。
const textureLoader = new THREE.TextureLoader();
const textureMap = textureLoader.load('http://localhost:83/smile.jpeg');
textureMap.wrapS = textureMap.wrapT = THREE.RepeatWrapping;
// 设置重复纹理的次数
textureMap.repeat.set(5, 5); // (水平重复次数, 垂直重复次数)
这里利用GUI切换不同拉伸类型查看效果:
const gui = new dat.GUI();
const wrapModes = {
RepeatWrapping: THREE.RepeatWrapping,
ClampToEdgeWrapping: THREE.ClampToEdgeWrapping,
MirroredRepeatWrapping: THREE.MirroredRepeatWrapping
};
function updateTexture()
{
textureMap.needsUpdate = true;
}
class StringToNumberHelper {
constructor(obj, prop) {
this.obj = obj
this.prop = prop
}
get value() {
return this.obj[this.prop]
}
set value(v) {
this.obj[this.prop] = parseFloat(v)
}
}
gui
.add(new StringToNumberHelper(textureMap, 'wrapS'), 'value', wrapModes)
.name('texture.wrapS')
.onChange(updateTexture)
gui
.add(new StringToNumberHelper(textureMap, 'wrapT'), 'value', wrapModes)
.name('texture.wrapT')
.onChange(updateTexture)
gui.add(textureMap.repeat, 'x', 0, 5, 0.01).name('texture.repeat.x');
gui.add(textureMap.repeat, 'y', 0, 5, 0.01).name('texture.repeat.y');
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();

# 参考
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)