linuv与linux关系,深入理解Three.js(WebGL)贴图(纹理映射)和UV映射

本文将详细描述如何使用Three.js给3D对象添加贴图(Texture Map,也译作纹理映射,“贴图”的翻译要更直观,而“纹理映射”更准确。)。为了能够查看在线演示效果,你需要有一个兼容WebGL的现代浏览器(最好是Chrome/FireFox/Safari/Edge/IE11+)。

本文的在线演示结果和代码请点击这里:Three.js贴图实例。

什么是贴图(Texture Mapping)

贴图是通过将图像应用到对象的一个或多个面,来为3D对象添加细节的一种方法。

这使我们能够添加表面细节,而无需将这些细节建模到我们的3D对象中,从而大大精简3D模型的多边形边数,提高模型渲染性能。

开始吧

这里方便起见,我们使用踏得网在线开发工具来一步步边学边操作。

请点击新建作品,在第三方库中选择Three.js 80版本,这将自动加载对应版本的Three.js开发库(注:你也可以直接把拷贝到HTML代码面板中去)。

首先我们创建一个立方体,在JavaScript面板中编写代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

var camera;

var scene;

var renderer;

var mesh;

init();

animate();

function init() {

scene =new THREE.Scene();

camera =new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000);

var light =new THREE.DirectionalLight( 0xffffff );

light.position.set( 0, 1, 1 ).normalize();

scene.add(light);

var geometry =new THREE.CubeGeometry( 10, 10, 10);

var material =new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } );

mesh =new THREE.Mesh(geometry, material );

mesh.position.z = -50;

scene.add( mesh );

renderer =new THREE.WebGLRenderer();

renderer.setSize( window.innerWidth, window.innerHeight );

document.body.appendChild( renderer.domElement );

window.addEventListener('resize', onWindowResize,false );

render();

}

function animate() {

mesh.rotation.x += .04;

mesh.rotation.y += .02;

render();

requestAnimationFrame( animate );

}

function render() {

renderer.render( scene, camera );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;

camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

render();

}

点击菜单栏中的[运行]菜单(

f24e79a3c6935ab6049c8740869351ef.png),或者按快捷键:CTRL+R,来运行该代码,你将看到一个旋转的蓝色立方体:

dD0xNDA1NDUwMTEzMDAw.jpg

我们接下来要做的就是把这个立方体变成一个游戏里常见的木箱子,如下图所示:

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi9jdWJlMi5wbmcvNjhiNjU5ZjMtZWVmYy00NjNkLWJkMjUtMjQxZmYxNGVhYzkyP3Q9MTQwNTQ1MDExNDAwMA==.jpg

为此我们需要一张箱子表面的图像,并用这张图像映射到立方体对象的材料中去,

这里我们直接使用在线图片http://wow.techbrood.com/uploads/1702/crate.jpg.

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi9jcmF0ZS5qcGcvNGM4YjA2YjUtYmI3Mi00YzllLWEwMDktMTMxMWE2ZWUxMzEzP3Q9MTQwNTQ1MDExMzAwMA==.jpg

JS代码中修改之前的材料(material)创建代码:

1

var material =new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } );

为使用贴图:

1

var material =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('http://wow.techbrood.com/uploads/1702/crate.jpg') } );

再运行下(按[运行]菜单或CTRL+R快捷键),你会看到一个旋转的板条箱,而不是一个普通的蓝色立方体。

在构造我们的材质时,我们指定了texture属性并将其值设置为木箱图像,Three.js然后会加载纹理图像并映射到立方体各个面上。

那么,问题是如果我们想给不同的面添加不同的纹理贴图,该怎么办呢?

一种方法是使用材料数组,我们创建6个新材料,每一个使用不同的纹理贴图:bricks.jpg,clouds.jpg,stone-wall.jpg,water.jpg,wood-floor.jpg以及上面的crate.jpg。

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi9icmlja3MuanBnLzRmMmY4MjRiLWQxMWUtNGYzOS04ZWZkLTRjMDUzMDMyZDg1Nj90PTE0MDU0NTAxMTIwMDA=.jpg

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi9jbG91ZHMuanBnL2Q2NTAxYTI2LTEzMzItNDFmOS1hMmM0LTg4M2NiMTA5YmE1YT90PTE0MDU0NTAxMTIwMDA=.jpg

dD0xNDA1NDUwMTA5MDAw.jpg

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi93YXRlci5qcGcvMWQ2NzYxZDctYzlmOS00YTZmLWJmYmUtN2MxZGJmMzU4N2Y0P3Q9MTQwNTQ1MDExMTAwMA==.jpg

dD0xNDA1NDUwMTExMDAw.jpg

相应的,我们把材料构造代码修改为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

var material1 =new THREE.MeshPhongMaterial( {

map: THREE.ImageUtils.loadTexture('/uploads/1702/crate.jpg') } );

var material2 =new THREE.MeshPhongMaterial( {

map: THREE.ImageUtils.loadTexture('/uploads/1702/bricks.jpg') } );

var material3 =new THREE.MeshPhongMaterial( {

map: THREE.ImageUtils.loadTexture('/uploads/1702/clouds.jpg') } );

var material4 =new THREE.MeshPhongMaterial( {

map: THREE.ImageUtils.loadTexture('/uploads/1702/stone-wall.jpg') } );

var material5 =new THREE.MeshPhongMaterial( {

map: THREE.ImageUtils.loadTexture('/uploads/1702/water.jpg') } );

var material6 =new THREE.MeshPhongMaterial( {

map: THREE.ImageUtils.loadTexture('/uploads/1702/wood-floor.jpg') } );

var materials = [material1, material2, material3, material4, material5, material6];

var meshFaceMaterial =new THREE.MeshFaceMaterial( materials );

上述代码,我们先分别创建了6个材料,组成了一个材料数组,并使用这个数组创建一个MeshFaceMaterial对象。

最后,我们需要告诉我们的3D模型来使用这个新的组合“面材料”,修改下面的代码:

1

mesh =new THREE.Mesh(geometry, material );

为:

1

mesh =new THREE.Mesh(geometry,  meshFaceMaterial);

再运行下(按[运行]菜单或CTRL+R快捷键),你就将看到立方体的各个表面使用了不同的贴图。

这很酷,Three.js会自动把数组中的这些材料应用到不同的面上去。

但问题又来了,随着3D模型的面的增长,为每个面创建贴图是不现实的。

这就是为什么我们需要另外一种更为普遍的解决方法:UV映射的原因。

UV映射(UV Mapping)

UV映射最典型的例子就是把一张地图映射到3D球体的地球仪上去。其本质上就是把平面图像的不同区块映射到3D模型的不同面上去。我们把之前的6张图拼装成如下的一张图:http://wow.techbrood.com/uploads/160801/texture-atlas.jpg.

dD0xNDA1NDUwMTEwMDAw.jpg

修改如下代码:

1

2

3

4

5

6

var material1 =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/crate.jpg') } );

var material2 =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/bricks.jpg') } );

var material3 =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/clouds.jpg') } );

var material4 =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/stone-wall.jpg') } );

var material5 =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/water.jpg') } );

var material6 =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/wood-floor.jpg') } );

为:

1

var material =new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/texture-atlas.jpg') } );

我们又把代码给改回来使用一张贴图了,接下来我们需要把贴图的不同位置映射到立方体不同的面上去。

首先我们创建贴图的6个子图,在创建完材料的代码后面添加如下几行:

1

2

3

4

5

6

var bricks = [new THREE.Vector2(0, .666),new THREE.Vector2(.5, .666),new THREE.Vector2(.5, 1),new THREE.Vector2(0, 1)];

var clouds = [new THREE.Vector2(.5, .666),new THREE.Vector2(1, .666),new THREE.Vector2(1, 1),new THREE.Vector2(.5, 1)];

var crate = [new THREE.Vector2(0, .333),new THREE.Vector2(.5, .333),new THREE.Vector2(.5, .666),new THREE.Vector2(0, .666)];

var stone = [new THREE.Vector2(.5, .333),new THREE.Vector2(1, .333),new THREE.Vector2(1, .666),new THREE.Vector2(.5, .666)];

var water = [new THREE.Vector2(0, 0),new THREE.Vector2(.5, 0),new THREE.Vector2(.5, .333),new THREE.Vector2(0, .333)];

var wood = [new THREE.Vector2(.5, 0),new THREE.Vector2(1, 0),new THREE.Vector2(1, .333),new THREE.Vector2(.5, .333)];

上面的代码创建了六个数组,每一个对应于纹理贴图中的每个子图像。每个数组包含4个点,定义子图像的边界。坐标的范围值是0到1,(0,0)表示左下角,(1,1)表示右上角。

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi9jdWJlMTAucG5nLzljZWI2MTg5LTI3MjEtNDQ3Yi05YjlmLTk4ZjNhM2E5ZDIxZj90PTE0MDU0NTAxMTUwMDA=.jpg

子图像的坐标是根据贴图中百分比来定义。比如下面这个砖头子图像:

1

2

3

4

5

6

var bricks = [

new THREE.Vector2(0, .666),

new THREE.Vector2(.5, .666),

new THREE.Vector2(.5, 1),

new THREE.Vector2(0, 1)

];

在贴图中的位置在左上角(占据横向1/2,竖向1/3的位置),以逆时针方向来定义顶点坐标,从该子图像较低的左下角开始。

左下角:

0 - 最左边

.666 - 底部向上2/3处

右下角:

.5 - 中间线

.666 - 底部向上2/3处

右上角:

.5 - 中间线

1 - 顶边

右上角:

0 - 最左边

1 - 顶边

定义好子图像后,我们现在需要把它们映射到立方体的各个面上去。首先添加如下代码:

1

geometry.faceVertexUvs[0] = [];

上述代码清除现有的UV映射,接着我们添加如下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

geometry.faceVertexUvs[0][0] = [ bricks[0], bricks[1], bricks[3] ];

geometry.faceVertexUvs[0][1] = [ bricks[1], bricks[2], bricks[3] ];

geometry.faceVertexUvs[0][2] = [ clouds[0], clouds[1], clouds[3] ];

geometry.faceVertexUvs[0][3] = [ clouds[1], clouds[2], clouds[3] ];

geometry.faceVertexUvs[0][4] = [ crate[0], crate[1], crate[3] ];

geometry.faceVertexUvs[0][5] = [ crate[1], crate[2], crate[3] ];

geometry.faceVertexUvs[0][6] = [ stone[0], stone[1], stone[3] ];

geometry.faceVertexUvs[0][7] = [ stone[1], stone[2], stone[3] ];

geometry.faceVertexUvs[0][8] = [ water[0], water[1], water[3] ];

geometry.faceVertexUvs[0][9] = [ water[1], water[2], water[3] ];

geometry.faceVertexUvs[0][10] = [ wood[0], wood[1], wood[3] ];

geometry.faceVertexUvs[0][11] = [ wood[1], wood[2], wood[3] ];

geometry对象的faceVertexUvs属性包含该geometry各个面的坐标映射。既然我们映射到一个多维数据集,你可能会疑惑为什么数组中有12个面。原因是在ThreeJS模型中,立方体的每个面实际上是由2个三角形组成的。所以我们必须单独映射每个三角形。上述场景中,ThreeJS将为我们加载单一材料贴图,自动分拆成三角形并映射到每个面。

这里要注意每个面的顶点坐标的定义顺序必须遵循逆时针方向。为了映射底部三角形,我们需要使用的顶点指数0,1和3,而要映射顶部三角形,我们需要使用索引1,2,和顶点的3。

L3Byb3h5L2h0dHAvc29sdXRpb25kZXNpZ24uY29tL2RvY3VtZW50cy8xMDI4Mi8yMTU2Mi9jdWJlMTEucG5nLzY1NmRiMmYwLTY4YjAtNDVlMi05ZjlhLTMwYTNjZWZkOGY2Mz90PTE0MDU0NTAxMTUwMDA=.jpg

最后,我们替换如下代码:

1

2

var meshFaceMaterial =new THREE.MeshFaceMaterial( materials );

mesh =new THREE.Mesh(geometry,  meshFaceMaterial);

为:

1

mesh =new THREE.Mesh(geometry,  material);

我们再运行下代码(按[运行]菜单或CTRL+R快捷键),将看到各个面使用不同贴图的旋转立方体。

当然对于复杂的对象,我们还可以在建模的时候建立好模型贴图,并导出为ThreeJS所支持的模型格式,然后在场景中直接加载。

这个超出本文范围,请自行搜索本站Three.js在线实例。

编注:原文在线演示和源代码链接不可用,已重新建立在WOW上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值