深入理解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开发库(注:你也可以直接把<script src="http://wow.techbrood.com/libs/three.r73.js"></script>拷贝到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(); } |
点击菜单栏中的[运行]菜单(),或者按快捷键:CTRL+R,来运行该代码,你将看到一个旋转的蓝色立方体.
我们接下来要做的就是把这个立方体变成一个游戏里常见的木箱子.
为此我们需要一张箱子表面的图像,并用这张图像映射到立方体对象的材料中去,
这里我们直接使用在线图片https://wow.techbrood.com/uploads/1702/crate.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( 'https://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。
相应的,我们把材料构造代码修改为:
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张图拼装成如下的一张图:https://wow.techbrood.com/uploads/160801/texture-atlas.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)表示右上角。
子图像的坐标是根据贴图中百分比来定义。比如下面这个砖头子图像:
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。
最后,我们替换如下代码:
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在线实例。
- 相关文章
3D感知和建模关键硬件技术:双目、3D结构光和TOF
无论VR、AR和3D打印,其核心技术包含3D成像和建模。而3D建模属于劳动密集型的工作,耗时耗力,凡这类工作都会是被新技术革命的地方,自动3D建模技术就是为了解决...
增强现实引擎ARToolKit工作原理简介
ARToolkit是一个基于CV(计算机视觉)和Marker(标识)的开源增强现实引擎。其具备如下功能特性:A. 鲁棒跟踪,包括基于标记的跟踪与基于特征的跟踪;
html5跨平台实战-第一周-水平测验-新闻列表页面
这是一个DIV+CSS布局页面的一个实例,主要介绍POSITION定位、导航UL LI的制作、利用浮动原理对页面分栏、分列的页面布局。新闻页面的效果图
A-Frame WebVR(网页虚拟现实)快速开发入门教程
WebVR和WebGL应用程序接口使得我们已经可以在浏览器上创建虚拟现实(VR)体验,但从工程化的角度而言,开发社区还需要更多方便强大的开发库来简化编程,Mozilla的
创建非矩形网页页面元素的常用技术和实例代码
非矩形设计正在变成一种时尚,比如波浪形、菱形、三角形等:而随着技术发展,这种设计在技术实现上也变得更容易。本文以最简单的三角形为例,演示使用5种方法来...
CSS3属性选择器特性使用详解
CSS3除了引入动画、滤镜(用于特效)以及新的布局技术外,在选择器(selector)方面也有增强。属性选择器根据元素的属性(attributes)来匹配。这可以是一个单独...
ES6小知识:动态对象键(Dynamic Object Keys)语法简介
在ES5,对象的键(key)总是被解释为字符串。ES6允许我们使用计算的值作为对象的键,使用方括号:[myKey]const
深入理解JS和CSS3动画性能问题和技术选择
本文对比了JS及其框架和CSS3的动画性能,并深入剖析了其内在原因。技术结论大致如下:1. jQuery出于设计原因,在动画性能上表现最差2. CSS3由于把动画逻辑推给了...
WebGL 纹理映射模式以及WRAP_S | WRAP_T参数详解
我们在纹理滤镜一文中已经说明了2个重要的纹理参数,用来定义对象缩放时纹理的处理方式:GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER本文讲解其余几个纹理参数...
如何使用WebGL实现空气高温热变形动画特效
我们在炎炎夏日,或者在火堆旁,经常会观察到热源周围空气的不稳定波动现象。本文将讲解如何通过WebGL来实现这个特效。该效果可用于热变形、波浪、水面波光等场...
浏览器控制台报JS脚本执行错误:Module is not defined
现在JS分成了两个分支,一部分在服务器端发展如NodeJS,一部分是传统的浏览器运行环境。
有些插件在编写JS代码时,是针对Node编写的,所以直接在浏览器中使...使用top/left/margin和CSS3 translate两种方法实现标题居中的性能差异详解
要实现标题全屏居中(同时在垂直和水平方向居中),有若干种方法,包括使用弹性布局、表格单元、绝对定位、自动外边距和CSS3平移变换等。你可能已经使用了这些方...
Three.js 开发基础知识 - 绘制3D对象
Three.js是一个用来简化WebGL开发的JavaScript库,比如绘制一个三维立方体,使用WebGL需要100多行,那Three.js只要10几行就能够完成。本文通过创建一个立方体来...
Processing.js和P5.js的功能简介和区别
什么是ProcessingProcessing是关于数字艺术的编程语言,支持跨平台,语言本身是一个类Java语言,程序文件的后缀为.pde。
什么是Processing.js为了能让Proce... 更多...